1005 lines
32 KiB
HTML
1005 lines
32 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Woke - Alarm Clock</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--color-bg: #1a1a1a;
|
|
--color-surface: #2a2a2a;
|
|
--color-clock: #00ff88;
|
|
--color-alarm: #ff4444;
|
|
--color-text: #e0e0e0;
|
|
--color-text-dim: #888;
|
|
--color-border: #404040;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
line-height: 1.6;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 15px;
|
|
}
|
|
|
|
/* Clock Display */
|
|
.clock-container {
|
|
text-align: center;
|
|
padding: 20px 15px;
|
|
background: var(--color-surface);
|
|
border-radius: 8px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.big-clock {
|
|
font-family: monospace;
|
|
font-size: 42px;
|
|
font-weight: bold;
|
|
color: var(--color-clock);
|
|
line-height: 1.2;
|
|
white-space: pre;
|
|
display: inline-block;
|
|
text-shadow: 0 0 15px rgba(0, 255, 136, 0.4);
|
|
}
|
|
|
|
.big-clock.firing {
|
|
color: var(--color-alarm);
|
|
animation: pulse 1s infinite;
|
|
text-shadow: 0 0 30px rgba(255, 68, 68, 0.8);
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.7; transform: scale(1.05); }
|
|
}
|
|
|
|
.current-date {
|
|
font-size: 14px;
|
|
color: var(--color-text-dim);
|
|
margin-top: 8px;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
background: var(--color-clock);
|
|
color: var(--color-bg);
|
|
border: none;
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: 'Courier New', monospace;
|
|
transition: all 0.2s;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.4);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--color-alarm);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
box-shadow: 0 4px 12px rgba(255, 68, 68, 0.4);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--color-surface);
|
|
color: var(--color-text);
|
|
border: 2px solid var(--color-border);
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Alarm List */
|
|
.alarms-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: var(--color-clock);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.alarm-list {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.alarm-card {
|
|
background: var(--color-surface);
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
border-left: 3px solid var(--color-border);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.alarm-card.enabled {
|
|
border-left-color: var(--color-clock);
|
|
}
|
|
|
|
.alarm-card.disabled {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.alarm-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.alarm-time-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.alarm-time {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
color: var(--color-clock);
|
|
}
|
|
|
|
.alarm-card.disabled .alarm-time {
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
.alarm-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.alarm-info {
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.alarm-name {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.alarm-desc {
|
|
color: var(--color-text-dim);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.alarm-trigger {
|
|
display: inline-block;
|
|
padding: 3px 8px;
|
|
background: var(--color-bg);
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
color: var(--color-text-dim);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Form */
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 1000;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal.active {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.modal-content {
|
|
background: var(--color-surface);
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 100%;
|
|
border: 2px solid var(--color-clock);
|
|
}
|
|
|
|
.modal-header {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: var(--color-clock);
|
|
margin-bottom: 15px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-weight: bold;
|
|
color: var(--color-text);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.form-input, .form-select, .form-textarea {
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: var(--color-bg);
|
|
border: 2px solid var(--color-border);
|
|
border-radius: 4px;
|
|
color: var(--color-text);
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
|
outline: none;
|
|
border-color: var(--color-clock);
|
|
}
|
|
|
|
.form-textarea {
|
|
resize: vertical;
|
|
min-height: 60px;
|
|
}
|
|
|
|
.form-error {
|
|
color: var(--color-alarm);
|
|
font-size: 12px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
justify-content: flex-end;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: 12px;
|
|
color: var(--color-text-dim);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.checkbox-group input[type="checkbox"] {
|
|
width: 20px;
|
|
height: 20px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
|
|
/* Firing Alarm Overlay */
|
|
.firing-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.95);
|
|
z-index: 2000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.firing-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.firing-content {
|
|
text-align: center;
|
|
}
|
|
|
|
.firing-time {
|
|
font-size: 64px;
|
|
font-weight: bold;
|
|
color: var(--color-alarm);
|
|
margin-bottom: 15px;
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
.firing-name {
|
|
font-size: 32px;
|
|
margin-bottom: 8px;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.firing-desc {
|
|
font-size: 16px;
|
|
color: var(--color-text-dim);
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.firing-actions {
|
|
display: flex;
|
|
gap: 15px;
|
|
justify-content: center;
|
|
}
|
|
|
|
.firing-actions .btn {
|
|
font-size: 18px;
|
|
padding: 15px 30px;
|
|
}
|
|
|
|
/* Status Message */
|
|
.status-bar {
|
|
position: fixed;
|
|
bottom: 15px;
|
|
right: 15px;
|
|
background: var(--color-surface);
|
|
padding: 12px 18px;
|
|
border-radius: 6px;
|
|
border-left: 3px solid var(--color-clock);
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
|
|
z-index: 1500;
|
|
display: none;
|
|
max-width: 350px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.status-bar.active {
|
|
display: block;
|
|
animation: slideIn 0.3s ease-out;
|
|
}
|
|
|
|
.status-bar.error {
|
|
border-left-color: var(--color-alarm);
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(100%);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Loading State */
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 40px 20px;
|
|
color: var(--color-text-dim);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* Toggle Switch */
|
|
.toggle-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
width: 44px;
|
|
height: 24px;
|
|
}
|
|
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: var(--color-border);
|
|
transition: 0.3s;
|
|
border-radius: 28px;
|
|
}
|
|
|
|
.toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 18px;
|
|
width: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: 0.3s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
input:checked + .toggle-slider {
|
|
background-color: var(--color-clock);
|
|
}
|
|
|
|
input:checked + .toggle-slider:before {
|
|
transform: translateX(20px);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.big-clock {
|
|
font-size: 36px;
|
|
}
|
|
|
|
.alarm-time {
|
|
font-size: 22px;
|
|
}
|
|
|
|
.firing-time {
|
|
font-size: 52px;
|
|
}
|
|
|
|
.firing-name {
|
|
font-size: 26px;
|
|
}
|
|
|
|
.settings-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.container {
|
|
padding: 10px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Clock Display -->
|
|
<div class="clock-container">
|
|
<div id="bigClock" class="big-clock"></div>
|
|
<div class="current-date" id="currentDate"></div>
|
|
</div>
|
|
|
|
<!-- Alarms Section -->
|
|
<div class="alarms-section">
|
|
<div class="section-header">
|
|
<div class="section-title">Alarms</div>
|
|
<button class="btn" onclick="app.openAddModal()">+ Add Alarm</button>
|
|
</div>
|
|
<div id="alarmList" class="alarm-list">
|
|
<div class="loading">Loading alarms...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Modal -->
|
|
<div id="alarmModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header" id="modalTitle">Add Alarm</div>
|
|
<form id="alarmForm">
|
|
<input type="hidden" id="alarmId">
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Name *</label>
|
|
<input type="text" id="alarmName" class="form-input" required>
|
|
<div class="form-error" id="nameError"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Description</label>
|
|
<textarea id="alarmDescription" class="form-textarea"></textarea>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Time (HH:MM) *</label>
|
|
<input type="text" id="alarmTime" class="form-input" placeholder="07:30" required pattern="[0-2][0-9]:[0-5][0-9]">
|
|
<div class="form-hint">24-hour format</div>
|
|
<div class="form-error" id="timeError"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Trigger Type *</label>
|
|
<select id="alarmTrigger" class="form-select" onchange="app.updateTriggerHelp()">
|
|
<option value="once">Once (one-time alarm)</option>
|
|
<option value="cron">Cron (repeating)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" id="cronGroup" style="display: none;">
|
|
<label class="form-label">Cron Expression</label>
|
|
<input type="text" id="cronExpression" class="form-input" placeholder="0 7 * * 1-5">
|
|
<div class="form-hint" id="cronHint">
|
|
Format: MIN HOUR DAY MONTH WEEKDAY<br>
|
|
Example: "0 7 * * 1-5" = 7:00 AM weekdays
|
|
</div>
|
|
<div class="form-error" id="cronError"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Sound Path</label>
|
|
<input type="text" id="alarmSound" class="form-input" placeholder="default">
|
|
<div class="form-hint">Leave as "default" for system sound</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="checkbox-group">
|
|
<input type="checkbox" id="alarmEnabled" checked>
|
|
<span class="form-label" style="margin: 0;">Enabled</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="app.closeModal()">Cancel</button>
|
|
<button type="submit" class="btn">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Firing Alarm Overlay -->
|
|
<div id="firingOverlay" class="firing-overlay">
|
|
<div class="firing-content">
|
|
<div class="firing-time" id="firingTime"></div>
|
|
<div class="firing-name" id="firingName"></div>
|
|
<div class="firing-desc" id="firingDesc"></div>
|
|
<div class="firing-actions">
|
|
<button class="btn btn-danger" onclick="app.dismissAlarm()">Dismiss</button>
|
|
<button class="btn" onclick="app.snoozeAlarm()">Snooze (5 min)</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<div id="statusBar" class="status-bar">
|
|
<div id="statusMessage"></div>
|
|
</div>
|
|
|
|
<!-- Audio element for alarm sound -->
|
|
<audio id="alarmAudio" loop></audio>
|
|
|
|
<script>
|
|
// ============================================================
|
|
// CONFIG - Edit these values to customize your setup
|
|
// ============================================================
|
|
const CONFIG = {
|
|
SERVER_URL: 'http://localhost:9119', // API server URL
|
|
API_PASSWORD: 'test', // Leave empty if no auth
|
|
SHOW_SECONDS: true, // Show seconds on clock
|
|
SOUND_URL: '', // URL to alarm sound (leave empty for silent)
|
|
};
|
|
// ============================================================
|
|
|
|
// Main Application
|
|
const app = {
|
|
apiUrl: CONFIG.SERVER_URL,
|
|
apiPassword: CONFIG.API_PASSWORD,
|
|
alarms: [],
|
|
currentAlarm: null,
|
|
firingAlarm: null,
|
|
audio: null,
|
|
clockInterval: null,
|
|
pollInterval: null,
|
|
showSeconds: CONFIG.SHOW_SECONDS,
|
|
|
|
init() {
|
|
this.audio = document.getElementById('alarmAudio');
|
|
this.startClock();
|
|
this.loadAlarms();
|
|
this.startPolling();
|
|
this.setupEventListeners();
|
|
},
|
|
|
|
setupEventListeners() {
|
|
// Form submission
|
|
document.getElementById('alarmForm').addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
this.saveAlarm();
|
|
});
|
|
|
|
// Close modal on outside click
|
|
document.getElementById('alarmModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'alarmModal') {
|
|
this.closeModal();
|
|
}
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
this.closeModal();
|
|
}
|
|
});
|
|
},
|
|
|
|
async apiRequest(endpoint, options = {}) {
|
|
const url = `${this.apiUrl}${endpoint}`;
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (this.apiPassword) {
|
|
headers['Authorization'] = `Bearer ${this.apiPassword}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: { ...headers, ...options.headers },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
throw new Error(error.error || 'Request failed');
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return null;
|
|
}
|
|
|
|
return response.json();
|
|
},
|
|
|
|
async loadAlarms() {
|
|
try {
|
|
const alarms = await this.apiRequest('/api/alarms');
|
|
this.alarms = alarms;
|
|
this.renderAlarms();
|
|
this.checkAlarms();
|
|
} catch (error) {
|
|
console.error('Failed to load alarms:', error);
|
|
this.showStatus('Failed to load alarms: ' + error.message, true);
|
|
}
|
|
},
|
|
|
|
renderAlarms() {
|
|
const container = document.getElementById('alarmList');
|
|
|
|
if (this.alarms.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⏰</div>
|
|
<div>No alarms yet. Click "Add Alarm" to create one.</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = this.alarms.map(alarm => `
|
|
<div class="alarm-card ${alarm.enabled ? 'enabled' : 'disabled'}">
|
|
<div class="alarm-header">
|
|
<div class="alarm-time-row">
|
|
<div class="alarm-time">${alarm.time || '—'}</div>
|
|
<span class="alarm-trigger">${alarm.trigger === 'once' ? 'One-time' : alarm.trigger}</span>
|
|
</div>
|
|
<div class="alarm-actions">
|
|
<label class="toggle-switch">
|
|
<input type="checkbox" ${alarm.enabled ? 'checked' : ''}
|
|
onchange="app.toggleAlarm(${alarm.id})">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<button class="btn btn-small btn-secondary" onclick="app.openEditModal(${alarm.id})">Edit</button>
|
|
<button class="btn btn-small btn-danger" onclick="app.deleteAlarm(${alarm.id})">Delete</button>
|
|
</div>
|
|
</div>
|
|
<div class="alarm-info">
|
|
<div class="alarm-name">${this.escapeHtml(alarm.name)}</div>
|
|
${alarm.description ? `<div class="alarm-desc">${this.escapeHtml(alarm.description)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
},
|
|
|
|
openAddModal() {
|
|
document.getElementById('modalTitle').textContent = 'Add Alarm';
|
|
document.getElementById('alarmId').value = '';
|
|
document.getElementById('alarmForm').reset();
|
|
document.getElementById('alarmEnabled').checked = true;
|
|
document.getElementById('alarmSound').value = 'default';
|
|
document.getElementById('alarmTrigger').value = 'once';
|
|
this.updateTriggerHelp();
|
|
document.getElementById('alarmModal').classList.add('active');
|
|
},
|
|
|
|
openEditModal(id) {
|
|
const alarm = this.alarms.find(a => a.id === id);
|
|
if (!alarm) return;
|
|
|
|
document.getElementById('modalTitle').textContent = 'Edit Alarm';
|
|
document.getElementById('alarmId').value = alarm.id;
|
|
document.getElementById('alarmName').value = alarm.name;
|
|
document.getElementById('alarmDescription').value = alarm.description || '';
|
|
document.getElementById('alarmTime').value = alarm.time || '';
|
|
document.getElementById('alarmSound').value = alarm.sound_path || 'default';
|
|
document.getElementById('alarmEnabled').checked = alarm.enabled;
|
|
|
|
if (alarm.trigger === 'once') {
|
|
document.getElementById('alarmTrigger').value = 'once';
|
|
} else {
|
|
document.getElementById('alarmTrigger').value = 'cron';
|
|
document.getElementById('cronExpression').value = alarm.trigger;
|
|
}
|
|
|
|
this.updateTriggerHelp();
|
|
document.getElementById('alarmModal').classList.add('active');
|
|
},
|
|
|
|
closeModal() {
|
|
document.getElementById('alarmModal').classList.remove('active');
|
|
this.clearFormErrors();
|
|
},
|
|
|
|
updateTriggerHelp() {
|
|
const trigger = document.getElementById('alarmTrigger').value;
|
|
const cronGroup = document.getElementById('cronGroup');
|
|
cronGroup.style.display = trigger === 'cron' ? 'block' : 'none';
|
|
},
|
|
|
|
clearFormErrors() {
|
|
['nameError', 'timeError', 'cronError'].forEach(id => {
|
|
document.getElementById(id).textContent = '';
|
|
});
|
|
},
|
|
|
|
validateForm() {
|
|
this.clearFormErrors();
|
|
let valid = true;
|
|
|
|
const name = document.getElementById('alarmName').value.trim();
|
|
if (!name) {
|
|
document.getElementById('nameError').textContent = 'Name is required';
|
|
valid = false;
|
|
}
|
|
|
|
const time = document.getElementById('alarmTime').value.trim();
|
|
if (!time.match(/^[0-2][0-9]:[0-5][0-9]$/)) {
|
|
document.getElementById('timeError').textContent = 'Invalid time format (use HH:MM)';
|
|
valid = false;
|
|
}
|
|
|
|
const trigger = document.getElementById('alarmTrigger').value;
|
|
if (trigger === 'cron') {
|
|
const cron = document.getElementById('cronExpression').value.trim();
|
|
if (!cron) {
|
|
document.getElementById('cronError').textContent = 'Cron expression required';
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
return valid;
|
|
},
|
|
|
|
async saveAlarm() {
|
|
if (!this.validateForm()) {
|
|
return;
|
|
}
|
|
|
|
const id = document.getElementById('alarmId').value;
|
|
const trigger = document.getElementById('alarmTrigger').value;
|
|
|
|
const data = {
|
|
name: document.getElementById('alarmName').value.trim(),
|
|
description: document.getElementById('alarmDescription').value.trim(),
|
|
time: document.getElementById('alarmTime').value.trim(),
|
|
trigger: trigger === 'once' ? 'once' : document.getElementById('cronExpression').value.trim(),
|
|
sound_path: document.getElementById('alarmSound').value.trim() || 'default',
|
|
enabled: document.getElementById('alarmEnabled').checked,
|
|
};
|
|
|
|
try {
|
|
if (id) {
|
|
await this.apiRequest(`/api/alarms/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
this.showStatus('Alarm updated');
|
|
} else {
|
|
await this.apiRequest('/api/alarms', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
this.showStatus('Alarm created');
|
|
}
|
|
|
|
this.closeModal();
|
|
await this.loadAlarms();
|
|
} catch (error) {
|
|
this.showStatus('Failed to save alarm: ' + error.message, true);
|
|
}
|
|
},
|
|
|
|
async toggleAlarm(id) {
|
|
try {
|
|
await this.apiRequest(`/api/alarms/${id}/toggle`, {
|
|
method: 'PATCH',
|
|
});
|
|
await this.loadAlarms();
|
|
} catch (error) {
|
|
this.showStatus('Failed to toggle alarm: ' + error.message, true);
|
|
}
|
|
},
|
|
|
|
async deleteAlarm(id) {
|
|
if (!confirm('Delete this alarm?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.apiRequest(`/api/alarms/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
this.showStatus('Alarm deleted');
|
|
await this.loadAlarms();
|
|
} catch (error) {
|
|
this.showStatus('Failed to delete alarm: ' + error.message, true);
|
|
}
|
|
},
|
|
|
|
startClock() {
|
|
const updateClock = () => {
|
|
const now = new Date();
|
|
const h = String(now.getHours()).padStart(2, '0');
|
|
const m = String(now.getMinutes()).padStart(2, '0');
|
|
const s = String(now.getSeconds()).padStart(2, '0');
|
|
|
|
const clockElem = document.getElementById('bigClock');
|
|
const colonVisible = Math.floor(now.getTime() / 1000) % 2 === 0;
|
|
|
|
if (this.showSeconds) {
|
|
clockElem.textContent = `${h}${colonVisible ? ':' : ' '}${m}${colonVisible ? ':' : ' '}${s}`;
|
|
} else {
|
|
clockElem.textContent = `${h}${colonVisible ? ':' : ' '}${m}`;
|
|
}
|
|
|
|
// Update date
|
|
const dateStr = now.toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
document.getElementById('currentDate').textContent = dateStr;
|
|
};
|
|
|
|
updateClock();
|
|
this.clockInterval = setInterval(updateClock, 100);
|
|
},
|
|
|
|
startPolling() {
|
|
// Poll for alarm updates every 5 seconds
|
|
this.pollInterval = setInterval(() => {
|
|
this.loadAlarms();
|
|
}, 5000);
|
|
},
|
|
|
|
checkAlarms() {
|
|
const now = new Date();
|
|
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
|
|
for (const alarm of this.alarms) {
|
|
if (alarm.enabled && alarm.time === currentTime && alarm.trigger === 'once') {
|
|
// Check if we haven't fired this alarm in the last minute
|
|
if (!alarm.last_triggered || new Date(alarm.last_triggered) < new Date(now - 60000)) {
|
|
this.fireAlarm(alarm);
|
|
break; // Only fire one alarm at a time
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
fireAlarm(alarm) {
|
|
this.firingAlarm = alarm;
|
|
|
|
document.getElementById('firingTime').textContent = alarm.time;
|
|
document.getElementById('firingName').textContent = alarm.name;
|
|
document.getElementById('firingDesc').textContent = alarm.description || '';
|
|
document.getElementById('firingOverlay').classList.add('active');
|
|
document.getElementById('bigClock').classList.add('firing');
|
|
|
|
// Play sound
|
|
if (CONFIG.SOUND_URL) {
|
|
this.audio.src = CONFIG.SOUND_URL;
|
|
this.audio.play().catch(err => {
|
|
console.error('Failed to play sound:', err);
|
|
});
|
|
}
|
|
},
|
|
|
|
dismissAlarm() {
|
|
this.stopFiring();
|
|
this.showStatus('Alarm dismissed');
|
|
this.loadAlarms(); // Reload to update status
|
|
},
|
|
|
|
snoozeAlarm() {
|
|
this.stopFiring();
|
|
this.showStatus('Alarm snoozed for 5 minutes');
|
|
|
|
// Set a timer to fire again in 5 minutes
|
|
setTimeout(() => {
|
|
if (this.firingAlarm) {
|
|
this.fireAlarm(this.firingAlarm);
|
|
}
|
|
}, 5 * 60 * 1000);
|
|
},
|
|
|
|
stopFiring() {
|
|
document.getElementById('firingOverlay').classList.remove('active');
|
|
document.getElementById('bigClock').classList.remove('firing');
|
|
this.audio.pause();
|
|
this.audio.currentTime = 0;
|
|
this.firingAlarm = null;
|
|
},
|
|
|
|
showStatus(message, isError = false) {
|
|
const statusBar = document.getElementById('statusBar');
|
|
const statusMessage = document.getElementById('statusMessage');
|
|
|
|
statusMessage.textContent = message;
|
|
statusBar.classList.toggle('error', isError);
|
|
statusBar.classList.add('active');
|
|
|
|
setTimeout(() => {
|
|
statusBar.classList.remove('active');
|
|
}, 3000);
|
|
},
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
};
|
|
|
|
// Initialize app when DOM is ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', () => app.init());
|
|
} else {
|
|
app.init();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|