- agent/skywatch-agent.sh: bash daemon that collects CPU, memory, disk,
network, load, and uptime, then POSTs to /api/agents/report every 60 s
- agent/install-agent.sh: systemd installer; registers agent on first run
- server/database.js: adds `agents` and `agent_metrics` tables with indexes
- server/index.js: adds agent API routes (register, report, list, history,
delete, script/installer download); optional AGENT_REGISTRATION_KEY guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ensures gcc/g++/python3 are present before npm install so
better-sqlite3 can compile its native binding on a fresh Ubuntu server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server fixes:
- database.js: auto-create data/ directory on startup — was crashing on
fresh clone because the gitignored directory didn't exist
- better-sqlite3 upgraded to latest (13.x) for Node 26 compatibility
Network discovery:
- discoverNetwork() accepts optional subnet param; scans that range instead
of auto-detecting when provided
- New GET /api/network-subnets endpoint returns detected local interfaces
so UI can offer quick-select buttons
- Discovery modal: subnet text input + Scan button + auto-detect quick-select;
local subnet buttons populate once the modal is open
- server/index.js: pass subnet from request body to discoverNetwork()
Infra:
- React client runs on PORT=3456 in dev to avoid port conflicts
- .claude/launch.json added for one-command preview via Express (serves
the pre-built client, no port conflict with the preview tool)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
- tags field was missing from INSERT/UPDATE SQL — tags never persisted
- is_public: `0 !== false` is true in JS, so private resources (is_public=0)
were silently flipped to public on every edit; fixed with explicit 0/false check
Global setting inheritance:
- One-time DB migration nulls out check_interval and timeout on all existing
resources so they inherit from global monitoring settings going forward
- Scheduler reads global check_interval and timeout from DB each cycle and
uses them as the fallback when per-resource values are null
- Create and edit modals: empty field = use global; placeholder shows the
current global value so it's clear what will be used
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes the consecutive-slow-checks requirement as a configurable setting
instead of hardcoding 3. Saved under key 'slow_alert_consecutive' in the
DB, readable via SLOW_ALERT_CONSECUTIVE env var. UI appears in Settings →
Monitoring alongside the existing Consecutive Failures Threshold field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously a single check above 2× average response time fired an alert.
This caused false positives from normal network variance.
- scheduler.js: track consecutive slow checks per resource; only alert after
3 in a row (resets to 0 when response time normalises)
- monitorService.js: calculateResponseTimeBaseline now returns P95 instead
of mean, making 2× baseline a much more realistic slow threshold
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract getServerTimezone() helper in timeUtils.js; apply it in
formatChartTime and formatLocalTimeOnly (previously ignored TZ)
- Status.js: remove local formatLocalTime (was bare toLocaleString),
import the shared util that reads server timezone
- SettingsWizard: immediately update localStorage when timezone is saved
so new value takes effect without waiting for cache to expire
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The POST /api/settings handler previously wrote ALL settings on every
save — so saving just the Email section would overwrite timezone,
check interval, ntfy topic, etc. with defaults.
Now:
- DB: only UPSERT the keys that are in this specific request body
- process.env: only update env vars for the keys in this request
- .env file: read → patch touched keys → write (not a full rewrite)
Section-scoped saves from the UI no longer clobber unrelated settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sending emoji in HTTP headers is not valid ASCII and causes errors.
Switching to ntfy's JSON body API (POST to /topic with {topic, title,
message, priority, tags}) allows full UTF-8 including emoji.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTTP headers must be ASCII-only. Emoji in the Title header caused
"Invalid character in header content" errors. ntfy.sh accepts
URL-encoded header values, so encodeURIComponent() fixes it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bugs fixed:
- Status page: autoRefresh checkbox now actually pauses the interval
- Dashboard edit modal: hide URL field for heartbeat resources (was always shown as required)
- Dashboard: apply items_per_page, default_sort, and refresh_interval from settings on mount
- Dashboard: call loadResources() immediately on mount (was waiting 15s for fallback)
- Observability: listen for WS 'metrics' messages so tab updates live, not just on poll
UX improvements:
- Dashboard: replaced window.confirm() with inline Confirm/Cancel buttons for single and bulk delete
- Dashboard: skeleton rows shown while initial data loads (no more flash of empty state)
- Notifications: WebSocket moved from window global to useRef; exponential backoff on reconnect
- Notifications: replaced window.confirm() with inline Confirm/Cancel for Clear All
- ResourceDetail: initial API calls run in parallel via Promise.all
New features:
- ntfy.sh push notification channel: enable in Settings, enter topic, get mobile push alerts
- Network Discovery: Actions → Discover Network scans LAN for reachable hosts, probes HTTP ports, and lets you add any found host to monitoring in one click
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix changed code defaults to dark but the DB still had
theme='light' stored explicitly — a truthy value that bypassed all
fallbacks. On every restart the server was returning theme:'light',
which the client applied via data-theme="light", making the app white.
- Delete the 'theme' key from DB on startup (one-time migration)
- Always return theme:'dark' from buildSettingsFromDb (not from DB)
- Stop writing theme to DB/env/process.env on settings save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The app is a dark-mode design but all theme defaults were 'light'.
Once the theme setting was wired to data-theme, it applied light mode
overrides that turned the app background white while panels stayed dark.
- Changed all server-side theme defaults from 'light' to 'dark'
- Added data-theme="dark" to index.html so the correct theme applies
before React loads (eliminates any white flash on first render)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- maintenance_mode and hasActiveIncident are SQLite integers (0/1), not JS booleans.
{0 && <span>} evaluates to {0} which React renders as the text "0" in the DOM.
Fixed with !! coercion on both conditions in renderResourceRow.
- avgResponseTime can be NaN for resources with no checks (e.g. icmp with no data).
Added || 0 guard in the aggregate average and —/ms conditional in the row display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hard-coded white/light colors with dark theme palette matching the rest
of the app. Key changes: dark navy background (#1e293b/#0f172a), high-contrast
text (#f1f5f9 titles, #94a3b8 body), teal accent (#14b8a6) for active filter and
unread left-border, red (#f87171) clear button, dark scrollbar, subtle hover
states on items and action buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HTTP method + request body: GET/POST/PUT/PATCH/DELETE/HEAD selector and body textarea for http/health checks; stored in http_method/http_body columns; monitorService sends correct axios method+data
- Alert escalation: incidents open longer than escalation_hours get forwarded to fallback_webhook and marked escalated=1; hourly cron in scheduler
- Baseline-relative slow alerts: effectiveThreshold = response_time_threshold || (baseline * 2) — adaptive threshold without manual config
- Notification grouping: pendingAlerts collected per check cycle; ≥2 alerts flush as one grouped email+webhook; individual in-app entries still saved per resource
- Rate limiting: in-memory per-IP limiter (20/min heartbeat, 60/min status-page); no external package
- App.js split: Dashboard.js (monitors list + modals) and ResourceDetail.js extracted; App.js reduced from ~1987 to ~80 lines
- formatDuration moved to utils/timeUtils.js; imported by both new components
- DB migrations: http_method, http_body, incidents.escalated columns
- 18 passing API/unit tests (node:test, zero dependencies)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Heartbeat type: cron jobs POST to /api/heartbeat/:token to stay alive; scheduler marks
them down if no ping is received within the configured timeout window
- Alert rules: per-resource consecutive_failures_threshold now respected in handleIncident,
overriding the global setting; GET /api/resources/:id/alert-rules endpoint added
- SSL cert expiry badge: dashboard rows surface days_remaining for TLS monitors, turning
orange at ≤30 days and red at ≤7 days
- Webhook payload templates: {{name}}/{{status}}/{{url}}/{{type}}/{{message}}/{{timestamp}}
variables interpolated before POST; Slack-compatible text wrapping as fallback
- Status page visibility: is_public column per resource; GET /api/status-page returns only
public resources; Status.js updated to use the new endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add proper defaults when writing settings to .env
- Prevents 'undefined' string from appearing in environment variables
- Ensures fallback values are used when settings are partially updated
- Settings were being cached by browser, causing stale values on refresh
- Added no-store, no-cache, must-revalidate headers
- Ensures fresh settings are always fetched from database
- Added incident_failure_threshold to POST /api/settings destructuring
- Added incident_failure_threshold to .env file generation
- Added incident_failure_threshold to process.env updates
- Added incident_failure_threshold to settingsTable for database storage
- Added incident_failure_threshold to GET /api/settings response (both success and fallback)
- All 27 settings now persist correctly to database and are retrieved properly
- Settings survive server restart and page reload
- Add in-app notification system with database storage
- Implement bell icon component with unread count badge in dashboard header
- Add full Notifications page tab with pagination and filtering (All/Unread/Read)
- Implement real-time WebSocket push notifications to all connected clients
- Add notification API endpoints: GET, PUT /read, DELETE, POST /clear
- Fix critical bug in notificationService where 'status' variable was undefined
- Add Notifications route to App.js routing
- Enhanced UI with notification popup in header and dedicated full-page view
- Auto-reconnect WebSocket with 5s timeout on disconnect
- Support rich notification content with resource details and timestamps
- Refactored SettingsWizard to allow saving each section separately (Email, Webhook, Monitoring)
- Each section now has its own Save button
- Removed global Save button - sections save independently
- No longer need to re-enter email passwords when changing other settings
- Added success/error status messages that appear for 3-5 seconds
- Status messages appear as toast notifications in top-right corner
- Updated CSS with save-status styling and slideIn animation
- handleSaveSection() only POSTs the relevant fields for that section
- Add description and failed_check_count fields to incidents table
- Update handleIncident() to use configurable threshold (default 10 checks)
- Auto-generate incident description with:
- Number of consecutive failures
- Last error message/reason
- First failure timestamp
- Add API endpoints: GET/POST /api/settings/incident-threshold
- Add UI settings field to configure incident failure threshold
- Server: Get threshold from database settings (persistent across restarts)
- Default threshold: 10 consecutive failed checks per resource
- Allows users to enter multiple email addresses separated by commas
- Updated both Add Resource and Edit Resource forms
- Improved placeholder text to show example of comma-separated format
- notificationService.js: Parse comma-separated emails from resource.email_to or global config; validate and send to each recipient
- index.js: Add PATCH /api/resources/:id/group endpoint to move resource between groups with validation and cache invalidation
- App.js: Add group selector to edit modal; wire group change to PATCH endpoint; add help text for multi-email format (comma-separated)
- notificationService: Parse comma-separated emails from resource.email_to or global config; validate and send to each recipient
- API: Add PATCH /api/resources/:id/group endpoint to move resource between groups with validation and cache invalidation
- UI: Add group selector to edit modal; wire group change to PATCH endpoint; add help text for multi-email format