source_key was minute-stamped (health:wd_restart:YYYYMMDDHHMM), so the same restart event seen across two 5-min cron runs within the logs 6-min lookback window got two different keys -> two alert rows + two emails, and the key never matched a clear_cond call so these rows stayed resolved=0 forever, accumulating in the active-alerts view.
Fixed to a stable key (health:wd_restart), matching the pattern used by the other three checks in this script: raise() now dedups via the existing COUNT..resolved=0 check, and clear_cond() runs when no recent restart line is found.
Verified live: injected a fake watchdog restart log line, ran the script twice -> exactly 1 alert row + 1 email (not 2). Removed the line -> alert auto-resolved (resolved=1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jarvis-backup.sh now also archives /etc/jarvis-arc (reactor.env), /etc/jarvis (db.env), the jarvis-arc.service.d systemd drop-ins, and /usr/local/bin/jarvis-*.sh. Previously a restore would have come back with no API keys or DB password since those moved outside /var/www/jarvis + /opt/jarvis-arc during the secrets sweep.
Verified: ran a real backup (45M) and confirmed all new paths are present in the archive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
deploy/jarvis-health.sh (cron */5): detects silent failures the service watchdog cannot — stalled crons (cron.log >10min stale = 2+ missed runs), Arc jobs stuck running >30min, disk >85%, and watchdog service restarts. Writes auto-resolving rows to the alerts table (shown in admin panel) and emails myronblair@gmail.com on any NEW finding via the reactor Gmail SMTP creds.
Verified live end-to-end: injected a fake stuck job -> alert raised + email sent; cleared it -> alert auto-resolved. Installed in root crontab.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Untracked from git and added to .gitignore. The file stays on disk at public_html/admin/downloads/ (served behind admin auth) and in the jarvis-private + VM110 copies — it is no longer pushed to GitHub going forward.
NOTE: prior revisions remain in GitHub history; a git filter-repo purge + force-push is still pending user sign-off, as are rotations of the secrets that were in it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- login.php: Redis-backed per-IP rate limit (10 fails / 15 min lockout), keyed off CF-Connecting-IP/X-Forwarded-For so it sees the real client behind NPM; fails open if Redis is down
- login.php: session_regenerate_id(true) on successful auth (prevents session fixation)
- php.ini: session.use_strict_mode = 1 (reject unknown/attacker-supplied session IDs)
- netscan.php: constant-time hash_equals for the registration-key check (matches agent.php)
Cookie flags already HttpOnly + SameSite=Lax (verified live). Agent auth verified: missing/bad X-Agent-Key -> 401 on every machine action.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removed live secret literals from git-tracked code (all were on GitHub):
- deploy/reactor.py: Claude/Groq API keys, DB pass, Gmail/iCloud app passwords now from os.environ (loaded via systemd EnvironmentFile=/etc/jarvis-arc/reactor.env, root:www-data 0640)
- public_html/login.php: used a private hardcoded PDO connection; now uses config.php DB_* constants
- deploy/jarvis-backup.sh (runs via cron), jarvis-deploy.sh, jarvis-watchdog.sh: DB pass now sourced from /etc/jarvis/db.env (root:root 0600)
- removed dead agent/jarvis-arc-reactor.py (unreferenced old duplicate leaking an old Groq key + stale Ollama IP)
- added deploy/reactor.env.example and deploy/db.env.example templates
Verified live: reactor restarted with all 21 handlers + DB poller (job round-trip OK), login works, mysqldump auth via env OK.
NOTE: these keys remain in GitHub history and should be rotated (Claude/Groq/Gmail/iCloud/DB). Separate decision needed on INFRASTRUCTURE-REFERENCE.md (full cred doc still tracked) + history purge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- install.sh, install-agent.sh: require JARVIS_REG_KEY env var or interactive prompt instead of baked-in key (matches install-mac.sh/install-windows.ps1 behavior)
- netscan.php: reuse AGENT_REGISTRATION_KEY constant instead of a duplicate literal
- agent.php + api.php: add session-authed "regkey" action so the admin install modal fetches the current key at runtime
- jarvis-agents.js: fetch reg key via /api/agent/regkey instead of hardcoding it; pass JARVIS_REG_KEY in the Linux install one-liner
- INFRASTRUCTURE-REFERENCE.md: scrub old key literal (rotated; real value lives only in api/config.php on VM211)
Key rotated on the box + rolled out to all 11 agents (verified all re-register online). New value is in the gitignored api/config.php only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- config.example.php: error_reporting(0) -> E_ALL (live config.php matches); add JELLYFIN_URL/JELLYFIN_API_KEY placeholders
- history.php, jellyfin.php: drop require of nonexistent includes/auth.php + AuthMiddleware call (router enforces auth centrally) — both endpoints were fataling on every request
- remove stale kb_intent_generator .bak files from deployed tree
DB (not in repo): kb_facts.fact_value TEXT -> MEDIUMTEXT; ha/entity_map had failed every write since Jul 2 at >64KB
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The installer wrote the same service list (ollama, homeassistant, mysql,
mariadb, nginx, apache2, docker) onto every host regardless of what it
actually runs, causing false "Service Down" alerts on hosts that never
installed those services. Default is now empty; watch_services should be
set per-host to match what's actually running there.
- Fix 1 (ORDER BY): use table alias t.id to force integer PK ordering;
topic_id alias was causing alphabetical sort, breaking rotation offsets
- Fix 2 (PDOException): wrap INSERT/UPDATE in try/catch in kb_topic_save;
duplicate topic_id now returns clean JSON error instead of HTTP 500
- Fix 3 (XSS/onclick): DEL button passes only t.id; tmDelete looks up
name from _topicsData, removing JSON.stringify from onclick attribute
- Fix 4 (validation): topic_id must contain at least one [a-z0-9] after
sanitization; "@@@" to "___" no longer passes required-field check
- Fix 5 (stale logs): guard comment and log messages updated from 20h to
4h to match the actual 14400s threshold
- Fix 6 (dedup): tmEsc() replaced with existing esc() throughout; removed
duplicate function definition
- Fix 7 (toggle rollback): tmToggle uses inline fetch to revert el.checked
on API failure instead of leaving the UI in wrong state
- Fix 8 (confirm): tmDelete uses openModal confirmation instead of
confirm() dialog, consistent with project live-popup convention
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
- Create kb_generator_topics table (140 topics migrated from hardcoded array)
- kb_intent_generator.php now loads active topics from DB instead of $BATCHES array
- Admin panel: MANAGE TOPICS button opens full topic manager modal
- Browse all 140 topics with search, category, and active/disabled filters
- Inline active toggle per topic (checkbox)
- Add new topic form (topic_id, category, name, description)
- Edit existing topics (all fields)
- Delete topics with confirmation
- Run count display
- Backend API cases: kb_topics, kb_topic_save, kb_topic_delete, kb_topic_toggle
- Topics can now be added/managed without any code changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
Rich topic library across mathematics (12), sciences (13), history (10),
government/economics, literature, life skills (13), technology (5),
Texas/local (4), national US (3), world affairs (5), human sexuality (2),
astronomy (3), space (2), culture/arts (8), sports (8), food/drink (4),
home/DIY (3), wellness (4), tech continued (3), national continued (2),
world continued (2), medicine (3), math continued (2), communication (2).
Each topic has detailed multi-subtopic descriptions (~200 chars each)
vs prior 8-word descriptions — significantly richer Groq prompts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
- Expanded $BATCHES from 25 to 122 topics across: life skills, personal
finance, Texas/local, US national, world geopolitics, human sexuality
(educational), deep astronomy (15 topics), and space exploration (15 topics)
- Rotation engine: 25 topics per run cycling through all 122 in order;
wraps to topic 1 on cycle complete (~30h full cycle at 6h interval)
- batch_offset tracked in kb_facts for cross-run persistence
- Cron changed from 0 3 * * * to 0 */6 * * * (every 6 hours)
- 20h skip guard reduced to 4h so 6h cron isn't blocked
- max_tokens bumped 3500 → 5000 to prevent JSON truncation errors
- JSON extraction: partial recovery + raw snippet logged on failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
Token budget was too low for 40 intents — responses were truncated mid-JSON,
causing "No JSON array found" on ~88% of batches. Fixes 880 errors/run.
Also adds partial-JSON truncation recovery and raw response debug logging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW
- Always runs when triggered from admin (JARVIS_FORCE_RUN=1 bypasses 20h guard)
- Closing popup stops UI polling but server job continues; toast confirms background run
- History panel at top of popup shows last success time/count + failures from last 7 days
- New intent_gen_history backend case parses cron.log for status summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014p87VFec84hNaf2WpvmLrW