<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://ryanjosephkamp.github.io/resmon/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ryanjosephkamp.github.io/resmon/" rel="alternate" type="text/html" /><updated>2026-05-05T20:26:40+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/feed.xml</id><title type="html">resmon</title><subtitle>Updates, release notes, and announcements for the resmon research-monitoring app.</subtitle><author><name>Ryan Kamp</name></author><entry><title type="html">resmon Update 4 — May 5, 2026</title><link href="https://ryanjosephkamp.github.io/resmon/2026/05/05/resmon-update-4/" rel="alternate" type="text/html" title="resmon Update 4 — May 5, 2026" /><published>2026-05-05T16:00:00+00:00</published><updated>2026-05-05T16:00:00+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/2026/05/05/resmon-update-4</id><content type="html" xml:base="https://ryanjosephkamp.github.io/resmon/2026/05/05/resmon-update-4/"><![CDATA[<h1 id="update-4--background-routine-reliability-scheduler--jobstore-lifecycle-daemon-attach-race-and-advanced-tab-honesty">Update 4 — Background Routine Reliability: Scheduler / Jobstore Lifecycle, Daemon-Attach Race, and Advanced-Tab Honesty</h1>

<h2 id="metadata">Metadata</h2>

<ul>
  <li><strong>Update number:</strong> 4</li>
  <li><strong>Update type:</strong> bugfix</li>
  <li><strong>Date:</strong> 2026-05-05</li>
  <li><strong>Author:</strong> Ryan Kamp</li>
  <li><strong>Commit hash:</strong> 9bb710562208ab053536b4bd6f49411b2174a595</li>
  <li><strong>Commit timestamp:</strong> 2026-05-05T15:59:55-04:00</li>
  <li><strong>GitHub push timestamp:</strong> 2026-05-05T20:00:06Z</li>
  <li><strong>App version:</strong> 1.2.0 → 1.2.1</li>
</ul>

<h2 id="summary">Summary</h2>

<p>This update is a reliability patch for scheduled Routines firing while the resmon window is closed. It closes three coupled defects: (i) routine ↔ APScheduler-jobstore lifecycle integrity (deleted routines no longer leave ghost <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> rows, daemon startup reconciles any pre-existing ghosts, and routine jobs are registered with a 1-hour <code class="language-plaintext highlighter-rouge">misfire_grace_time</code> so a fire whose nominal moment briefly passed still runs); (ii) the dual-backend race in which the Electron main process raced a launchd-bootstrapping daemon and silently spawned a competing backend with its own scheduler against the shared SQLite jobstore (now: <code class="language-plaintext highlighter-rouge">pingHealth</code> waits ~3 s with retry/backoff, lock-file presence forces a wait rather than a spawn, and even a legitimate fallback spawn honors <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER=1</code> so the renderer-spawned backend never owns a scheduler); and (iii) Advanced-tab honesty — the “Run resmon in the background” status block now reads <code class="language-plaintext highlighter-rouge">daemon.lock</code> and probes the daemon’s actual port through a new <code class="language-plaintext highlighter-rouge">GET /api/service/daemon-status</code> route, so the displayed pid / version / <code class="language-plaintext highlighter-rouge">last_started</code> reflect the real daemon and any future dual-backend race surfaces immediately rather than being masked. App version bumps <code class="language-plaintext highlighter-rouge">1.2.0 → 1.2.1</code>.</p>

<h2 id="motivation">Motivation</h2>

<p>All five fixes originated in a diagnostic session after the user observed in Update 3’s tutorial-recording flow that, having used the Settings → Advanced Danger Zone to wipe data and re-enable “Run resmon in the background,” scheduled Routines no longer fired when the app window was closed even though the Advanced tab reported the daemon as Installed and “up.” The full diagnosis lives in <a href="./resmon_update_4_overview.md"><code class="language-plaintext highlighter-rouge">resmon_update_4_overview.md</code></a>; the change inventory and batching plan live in <a href="./update_5_5_26_workflow.md"><code class="language-plaintext highlighter-rouge">update_5_5_26_workflow.md</code></a>. The five items map back to the workflow doc as Fixes A–E:</p>

<ul>
  <li><strong>Fix A — Routine ↔ jobstore lifecycle integrity</strong> (workflow item #1, Batch 1). <code class="language-plaintext highlighter-rouge">delete_routine</code> did not remove the matching <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> row; the dispatcher kept trying to fire ghost jobs whose owning routine was gone, and a Danger-Zone wipe could leave ghosts behind that no UI surface could reach. Coupled with Fix B in Batch 1 because both touch <code class="language-plaintext highlighter-rouge">scheduler.py</code> / <code class="language-plaintext highlighter-rouge">database.py</code> / the FastAPI startup wiring and are exercised by the same scheduler verification suite.</li>
  <li><strong>Fix B — Default <code class="language-plaintext highlighter-rouge">misfire_grace_time</code> for routine jobs</strong> (workflow item #2, Batch 1). APScheduler’s default <code class="language-plaintext highlighter-rouge">misfire_grace_time</code> of 1 second silently dropped any fire whose nominal moment had passed by even a brief window — exactly the failure mode produced by a daemon restart, a scheduler reattach, or the app’s own scheduler dying without a clean <code class="language-plaintext highlighter-rouge">shutdown()</code>.</li>
  <li><strong>Fix C — Robust daemon attach in the Electron main process</strong> (workflow item #3, Batch 2). The 500 ms <code class="language-plaintext highlighter-rouge">/api/health</code> timeout returned <code class="language-plaintext highlighter-rouge">false</code> during the launchd daemon’s bootstrap window; the renderer then silently spawned its own backend with its own scheduler against the same SQLite jobstore, and once the spawned backend died at app close every queued fire was marked “missed by N hours.”</li>
  <li><strong>Fix D — Renderer-spawn never owns a scheduler</strong> (workflow item #4, Batch 2). Defense in depth for Fix C: even when a renderer-spawned fallback is legitimate (no daemon installed at all), the spawned backend must not start a <code class="language-plaintext highlighter-rouge">ResmonScheduler</code> against the shared jobstore. Implemented as a <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER=1</code> env var that <code class="language-plaintext highlighter-rouge">main.ts</code> sets on the spawned child and the FastAPI startup hook honors.</li>
  <li><strong>Fix E — Advanced tab daemon-status truthfulness</strong> (workflow item #5, Batch 3). The Advanced tab populated its daemon-status line from <code class="language-plaintext highlighter-rouge">GET /api/health</code> against whichever backend the renderer was attached to, so a renderer-spawned fallback masqueraded as the daemon and the dual-backend race was invisible to the user. The fix reads <code class="language-plaintext highlighter-rouge">daemon.lock</code> server-side and probes the daemon’s actual port through a new endpoint.</li>
</ul>

<h2 id="changes">Changes</h2>

<h3 id="batch-1--fixes-a--b-scheduler--jobstore-lifecycle-cluster">Batch 1 — Fixes A + B (scheduler / jobstore lifecycle cluster)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/scheduler.py</code> — <code class="language-plaintext highlighter-rouge">add_routine</code> registers each routine job with <code class="language-plaintext highlighter-rouge">misfire_grace_time=3600</code>; new <code class="language-plaintext highlighter-rouge">reconcile_jobstore_with_routines</code> method drops every <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> row whose id has no matching <code class="language-plaintext highlighter-rouge">routines.id</code> with <code class="language-plaintext highlighter-rouge">is_active=1</code>; called once from the FastAPI startup hook before active routines are re-registered. (Fix B + Fix A reconciliation.)</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/database.py</code> — <code class="language-plaintext highlighter-rouge">delete_routine</code> removes the matching <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> row in the same transaction as the <code class="language-plaintext highlighter-rouge">routines</code> delete (idempotent against APScheduler-side removals; deletion-time job removal is tolerant of an already-gone row). (Fix A cascade.)</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — startup hook calls <code class="language-plaintext highlighter-rouge">scheduler.reconcile_jobstore_with_routines()</code> before re-registering active routines so any pre-existing ghosts are dropped on first boot of the patched daemon. (Fix A startup wiring.)</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_scheduler_reconciliation.py</code> <em>(new)</em> — regression: seeds an orphan <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> row, runs reconciliation, asserts removal; seeds a job whose owning routine is <code class="language-plaintext highlighter-rouge">is_active=0</code>, asserts removal; seeds a job whose owning routine is active, asserts retention.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_routine_scheduler_sync.py</code> <em>(new)</em> — regression: inserts a routine, registers its job, calls <code class="language-plaintext highlighter-rouge">delete_routine</code>, asserts no orphan <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code> row remains.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_scheduler_lifecycle.py</code> <em>(new)</em> — regression: asserts new routine jobs are added with <code class="language-plaintext highlighter-rouge">misfire_grace_time=3600</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_scheduler_wiring.py</code> <em>(new)</em> — verifies the FastAPI startup hook invokes <code class="language-plaintext highlighter-rouge">reconcile_jobstore_with_routines</code> before re-registration.</li>
</ul>

<h3 id="batch-2--fixes-c--d-dual-backend-race-cluster">Batch 2 — Fixes C + D (dual-backend race cluster)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/electron/main.ts</code> — <code class="language-plaintext highlighter-rouge">pingHealth</code> AbortController timeout raised from 500 ms to ~3 s; the lock-file → health-probe sequence is wrapped in a 2–3 attempt retry loop with brief backoff (hard ceiling ~4.5 s wall); when <code class="language-plaintext highlighter-rouge">read_lock()</code> returns a payload but the health probe has not yet responded, the main process waits for the daemon rather than falling through to the spawn branch; the spawn branch sets <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER=1</code> on the child env so a legitimate renderer-spawned fallback never owns a scheduler.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — FastAPI startup hook gates <code class="language-plaintext highlighter-rouge">ResmonScheduler</code> instantiation and <code class="language-plaintext highlighter-rouge">set_dispatcher</code> on <code class="language-plaintext highlighter-rouge">os.environ.get("RESMON_DISABLE_SCHEDULER")</code> being unset / falsy; default behavior preserved for direct <code class="language-plaintext highlighter-rouge">python resmon.py &lt;port&gt;</code> invocations and for <code class="language-plaintext highlighter-rouge">create_app()</code> test calls.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/dist/electron/main.js</code> — rebuilt from <code class="language-plaintext highlighter-rouge">main.ts</code> via <code class="language-plaintext highlighter-rouge">tsc --project tsconfig.electron.json</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_scheduler_disable_env.py</code> <em>(new)</em> — regression: with <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER=1</code> set, <code class="language-plaintext highlighter-rouge">create_app()</code> startup must not instantiate <code class="language-plaintext highlighter-rouge">ResmonScheduler</code> and must not call <code class="language-plaintext highlighter-rouge">set_dispatcher</code>; with the env var unset / falsy, scheduler startup proceeds normally.</li>
</ul>

<h3 id="batch-3--fix-e-advanced-tab-daemon-status-truthfulness">Batch 3 — Fix E (Advanced tab daemon-status truthfulness)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — new <code class="language-plaintext highlighter-rouge">GET /api/service/daemon-status</code> route that calls <code class="language-plaintext highlighter-rouge">daemon.read_lock()</code>, probes <code class="language-plaintext highlighter-rouge">http://127.0.0.1:&lt;lock_port&gt;/api/health</code> via <code class="language-plaintext highlighter-rouge">httpx</code> (1.5 s timeout), and returns <code class="language-plaintext highlighter-rouge">{ lock_present, running, pid, port, version, started_at, lock_pid, lock_port, lock_version, error, is_self }</code>; the <code class="language-plaintext highlighter-rouge">is_self</code> flag is set when the probed pid equals the current process pid so the renderer can render the distinction between “daemon is the current backend” and “daemon is a separate process.”</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AdvancedSettings.tsx</code> — added <code class="language-plaintext highlighter-rouge">DaemonStatusResponse</code> type; <code class="language-plaintext highlighter-rouge">refresh()</code> polls <code class="language-plaintext highlighter-rouge">/api/service/daemon-status</code> alongside the existing <code class="language-plaintext highlighter-rouge">/api/service/status</code> and <code class="language-plaintext highlighter-rouge">/api/health</code> calls; status block rewritten to render three explicit states (“daemon up” with pid/version and an <code class="language-plaintext highlighter-rouge">, this process</code> tag when <code class="language-plaintext highlighter-rouge">is_self</code>; “lock present but unreachable” with the diagnostic error; “no daemon running”); the renderer-attached backend’s identity is shown separately as <code class="language-plaintext highlighter-rouge">· this window → pid …, v…</code> so any divergence between the two is immediately visible. <code class="language-plaintext highlighter-rouge">Last started</code> is now sourced from the daemon-status response rather than <code class="language-plaintext highlighter-rouge">/api/health</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/dist/bundle.js</code> — rebuilt from the renderer source via <code class="language-plaintext highlighter-rouge">webpack --mode production</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_daemon_status_endpoint.py</code> <em>(new)</em> — three regression tests: (a) no lock file → <code class="language-plaintext highlighter-rouge">lock_present=False, running=False</code>; (b) stale lock pointing at a closed port → <code class="language-plaintext highlighter-rouge">lock_present=True, running=False, error</code> populated; (c) valid lock + monkey-patched <code class="language-plaintext highlighter-rouge">httpx.Client</code> returning a healthy response → <code class="language-plaintext highlighter-rouge">running=True</code> with daemon identity surfaced.</li>
</ul>

<h3 id="t-upd-3-documentation-pass-this-step">T-UPD-3 documentation pass (this step)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/config.py</code> — <code class="language-plaintext highlighter-rouge">APP_VERSION = "1.2.0"</code> → <code class="language-plaintext highlighter-rouge">"1.2.1"</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/package.json</code> — <code class="language-plaintext highlighter-rouge">"version": "1.2.0"</code> → <code class="language-plaintext highlighter-rouge">"1.2.1"</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/AboutAppTab.tsx</code> — <code class="language-plaintext highlighter-rouge">backendVersion</code> initial fallback bumped <code class="language-plaintext highlighter-rouge">1.0.0</code> → <code class="language-plaintext highlighter-rouge">1.2.1</code>; “Recent Update” card rewritten to summarize Update 4 (replacing the Update 3 copy).</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/settings_info.md</code> — Advanced panel description now documents the new <code class="language-plaintext highlighter-rouge">GET /api/service/daemon-status</code> endpoint and the three-state status block; endpoint table gains the new route.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/system_info.md</code> — “headless daemon split” paragraph now documents the raised <code class="language-plaintext highlighter-rouge">pingHealth</code> timeout / retry+backoff, the lock-file-aware wait, and the <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER</code> env gate that ensures only the daemon ever owns a scheduler.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/routines_info.md</code> — Delete-a-routine flow now documents the <code class="language-plaintext highlighter-rouge">delete_routine</code> cascade into <code class="language-plaintext highlighter-rouge">apscheduler_jobs</code>, the daemon-startup reconciliation, and the <code class="language-plaintext highlighter-rouge">misfire_grace_time=3600</code> registration default.</li>
  <li><code class="language-plaintext highlighter-rouge">.ai/updates/update_5_5_26/update_5_5_26.md</code> — this log.</li>
  <li><code class="language-plaintext highlighter-rouge">.ai/updates/updates.csv</code> — appended Update 4 row (commit_hash / commit_timestamp / github_push_timestamp = pending; T-UPD-4 will overwrite).</li>
</ul>

<h2 id="verification">Verification</h2>

<p>Per-batch test command and results (verification suite at <code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/</code>):</p>

<ul>
  <li><strong>Batch 1</strong> — <code class="language-plaintext highlighter-rouge">python -m pytest resmon_scripts/verification_scripts/test_scheduler_disable_env.py resmon_scripts/verification_scripts/test_scheduler_lifecycle.py resmon_scripts/verification_scripts/test_scheduler_wiring.py resmon_scripts/verification_scripts/test_scheduler_reconciliation.py resmon_scripts/verification_scripts/test_routine_scheduler_sync.py resmon_scripts/verification_scripts/test_daemon.py</code> — all passing (this superset is the same command used for the post-Batch-3 sanity run; per-batch counts were captured at the time of each batch report).</li>
  <li><strong>Batch 2</strong> — Python regression <code class="language-plaintext highlighter-rouge">test_scheduler_disable_env.py</code> covers Fix D’s <code class="language-plaintext highlighter-rouge">RESMON_DISABLE_SCHEDULER</code> env gate; Fix C’s Electron main-process retry/backoff is exercised by manual UI verification (the project does not currently host a JS-side Electron main test harness — same posture documented in Updates 2 and 3).</li>
  <li><strong>Batch 3</strong> — <code class="language-plaintext highlighter-rouge">python -m pytest resmon_scripts/verification_scripts/test_daemon_status_endpoint.py resmon_scripts/verification_scripts/test_daemon.py resmon_scripts/verification_scripts/test_service_units.py</code> → <strong>21/21 passing, 0 skipped</strong>.</li>
  <li><strong>Frontend build</strong> — <code class="language-plaintext highlighter-rouge">cd resmon_scripts/frontend &amp;&amp; npm run build</code> succeeded after the Batch 2 / Batch 3 edits (webpack production renderer bundle clean; <code class="language-plaintext highlighter-rouge">tsc --project tsconfig.electron.json</code> for <code class="language-plaintext highlighter-rouge">dist/electron/main.js</code> clean).</li>
  <li><strong>Manual UI smoke</strong> — daemon was restarted via <code class="language-plaintext highlighter-rouge">launchctl kickstart -k gui/$UID/com.resmon.daemon</code> to reload the patched Python; <code class="language-plaintext highlighter-rouge">resmon.app</code> launched and attached to the daemon on port 8742 (no fallback backend spawned); <code class="language-plaintext highlighter-rouge">GET /api/service/daemon-status</code> returns the documented shape with <code class="language-plaintext highlighter-rouge">is_self=true</code> when the renderer is attached to the daemon.</li>
</ul>

<p>Concatenated counts string (for the CSV row): <code class="language-plaintext highlighter-rouge">backend: 21/21 passing, 0 skipped on the targeted batch-3 suite (test_daemon_status_endpoint.py + test_daemon.py + test_service_units.py); batch-1 suite (test_scheduler_disable_env.py + test_scheduler_lifecycle.py + test_scheduler_wiring.py + test_scheduler_reconciliation.py + test_routine_scheduler_sync.py + test_daemon.py) all passing; platform-gated Linux systemd --user and Windows Task Scheduler tests remain skipped on macOS via their existing skipif guards (no daemon / service-unit code path on those platforms changed in this update); frontend: webpack production build clean after every batch; manual UI smoke confirmed daemon attach via launchd, no fallback backend spawn, /api/service/daemon-status three-state rendering, and that scheduled Routines now fire from the daemon's scheduler with the new misfire grace</code>.</p>

<h2 id="follow-ups">Follow-ups</h2>

<ul>
  <li><strong>Stale-daemon caveat for upgrading users.</strong> As with Updates 2 and 3, an existing <code class="language-plaintext highlighter-rouge">resmon-daemon</code> started before this update is still running the old <code class="language-plaintext highlighter-rouge">scheduler.py</code> / <code class="language-plaintext highlighter-rouge">database.py</code> / <code class="language-plaintext highlighter-rouge">resmon.py</code> and therefore still has the 1-second misfire grace, the <code class="language-plaintext highlighter-rouge">delete_routine</code> cascade gap, and the missing <code class="language-plaintext highlighter-rouge">/api/service/daemon-status</code> route. Users upgrading from 1.2.0 should restart the daemon (<code class="language-plaintext highlighter-rouge">launchctl kickstart -k gui/$UID/com.resmon.daemon</code> on macOS, <code class="language-plaintext highlighter-rouge">systemctl --user restart resmon-daemon</code> on Linux, or unregister/re-register the Task Scheduler entry on Windows) so the renderer attaches to a fresh backend.</li>
  <li><strong>Electron main-process test harness.</strong> Fix C’s retry/backoff / lock-file-aware wait is currently covered only by manual UI verification. A future update could introduce a focused JS-side test harness for <code class="language-plaintext highlighter-rouge">frontend/electron/main.ts</code> (mocking <code class="language-plaintext highlighter-rouge">fetch</code> and the lock-file reader) so Fix C’s branches are regression-protected the same way Fix D is.</li>
  <li><strong>Scheduler-diagnostics surface.</strong> The Advanced tab’s <code class="language-plaintext highlighter-rouge">/api/scheduler/jobs</code> panel now reflects the daemon’s jobstore truthfully (since only the daemon owns a scheduler). A small follow-up could surface <code class="language-plaintext highlighter-rouge">misfire_grace_time</code> and the most-recent reconciliation pass timestamp on each row for operator transparency.</li>
</ul>]]></content><author><name>Ryan Kamp</name></author><category term="updates" /><summary type="html"><![CDATA[Update 4 — Background Routine Reliability: Scheduler / Jobstore Lifecycle, Daemon-Attach Race, and Advanced-Tab Honesty]]></summary></entry><entry><title type="html">resmon Update 3 — April 27, 2026</title><link href="https://ryanjosephkamp.github.io/resmon/2026/04/27/resmon-update-3/" rel="alternate" type="text/html" title="resmon Update 3 — April 27, 2026" /><published>2026-04-27T16:00:00+00:00</published><updated>2026-04-27T16:00:00+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/2026/04/27/resmon-update-3</id><content type="html" xml:base="https://ryanjosephkamp.github.io/resmon/2026/04/27/resmon-update-3/"><![CDATA[<h1 id="update-3--calendar-bug-cluster-ai-key-deep-link-and-the-new-about-resmon-page-tutorials-issues-blog-about-app">Update 3 — Calendar Bug Cluster, AI-Key Deep-Link, and the New About resmon Page (Tutorials, Issues, Blog, About App)</h1>

<h2 id="metadata">Metadata</h2>

<ul>
  <li><strong>Update number:</strong> 3</li>
  <li><strong>Update type:</strong> mixed (bugfix + feature)</li>
  <li><strong>Date:</strong> 2026-04-27</li>
  <li><strong>Author:</strong> Ryan Kamp</li>
  <li><strong>Commit hash:</strong> 0570bc5ac416b42b774b3e1609e1602d6c8adc27</li>
  <li><strong>Commit timestamp:</strong> 2026-04-30T17:35:25-04:00</li>
  <li><strong>GitHub push timestamp:</strong> 2026-04-30T21:35:39Z</li>
  <li><strong>App version:</strong> 1.1.0 → 1.2.0</li>
</ul>

<h2 id="summary">Summary</h2>

<p>This update lands two Calendar bugs, one small deep-link feature, and one large feature — a brand-new top-level <strong>About resmon</strong> page — together with eleven out-of-band additions that emerged mid-flight and were folded into the same change set per the <code class="language-plaintext highlighter-rouge">T-UPD-ADD</code> rule. The Calendar’s scheduled-routine times no longer drift by ~4 hours and Custom-cadence first-fire / interval anomalies (every-N-months, every-5-hours, every-5-days, every-3-weeks, every-1-year) all expand correctly; the 30-minute “orange-bar” cosmetic bug is fixed; and the expansion window now extends to a full 12 months with a user-facing notice when the FullCalendar viewport is navigated past the horizon. The Repositories &amp; API Keys page gains a “Looking for AI API key settings?” deep-link button to <code class="language-plaintext highlighter-rouge">Settings → AI</code>. A new top-level <strong>About resmon</strong> page hosts four tabs — <strong>Tutorials</strong> (eighteen embedded YouTube walk-throughs covering the full app, every page, and every Settings sub-tab), <strong>Issues</strong> (a credentials-free <code class="language-plaintext highlighter-rouge">mailto:</code> + GitHub-issue-deep-link form), <strong>Blog</strong> (an in-app reader fed by the new GitHub Pages site at <code class="language-plaintext highlighter-rouge">https://ryanjosephkamp.github.io/resmon/</code>), and <strong>About App</strong> (relocated out of Settings); a shared <code class="language-plaintext highlighter-rouge">TutorialLinkButton</code> is rendered next to every page header and every Settings sub-panel header so any user can deep-link straight into the matching tutorial section. App version bumps <code class="language-plaintext highlighter-rouge">1.1.0 → 1.2.0</code>.</p>

<h2 id="motivation">Motivation</h2>

<p>All items in this update originated in the user’s third-after-release feedback session and map back to the change inventory in <a href="../update_4_27_26/update_4_27_26_workflow.md"><code class="language-plaintext highlighter-rouge">.ai/updates/update_4_27_26/update_4_27_26_workflow.md</code></a>.</p>

<ul>
  <li><strong>Bug 1 — Calendar scheduled-routine times incorrect</strong> (workflow item #1, batch 1). Daily / weekday / weekly / monthly cron expansions on the Calendar page rendered 4 hours earlier than they actually fired (UTC vs. local-time mismatch), and several Custom cadences exhibited first-fire / interval anomalies. The Calendar’s 90-day expansion window also silently truncated upcoming fires for high-frequency cadences with no user-facing notice.</li>
  <li><strong>Bug 2 — Cosmetic 30-minute routines render as multi-day orange bars</strong> (workflow item #2, batch 1). Same Calendar subsystem; FullCalendar event mapping was treating short-duration routine fires as all-day events.</li>
  <li><strong>Feature 3 — “Looking for AI API key settings?” deep-link button</strong> (workflow item #3, batch 2). Users repeatedly looked for AI provider keys (OpenAI, Anthropic, etc.) on the Repositories &amp; API Keys page even though those keys live on <code class="language-plaintext highlighter-rouge">Settings → AI</code>. A small inline button between the page intro paragraph and the scope selector now routes them directly to <code class="language-plaintext highlighter-rouge">/settings/ai</code>.</li>
  <li><strong>Feature 4 — New About resmon top-level page</strong> (workflow item #4, batch 3). Users had no in-app place to learn how each page worked, no credentials-free way to file a bug or request a feature, and no surfaced channel for per-update release notes. The About App tab was also crowding the Settings page. Update 3 introduces the new top-level page with <strong>Tutorials</strong>, <strong>Issues</strong>, <strong>Blog</strong>, and the relocated <strong>About App</strong> tab, plus a shared <code class="language-plaintext highlighter-rouge">TutorialLinkButton</code> next to every page and every Settings sub-panel header.</li>
  <li><strong>Out-of-band additions (T-UPD-ADD).</strong> Eleven user-requested additions emerged after T-UPD-1 was scaffolded and during T-UPD-2 execution; they were folded into the active batches per the workflow’s T-UPD-ADD rule. They are listed under <a href="#changes">Changes</a> below and motivated individually in <code class="language-plaintext highlighter-rouge">update_4_27_26_workflow.md</code> (OOB-1 … OOB-11).</li>
</ul>

<h2 id="changes">Changes</h2>

<h3 id="batch-1--calendar-bug-cluster-bug-1--bug-2--window-extension--horizon-notice">Batch 1 — Calendar bug cluster (Bug 1 + Bug 2 + window extension + horizon notice)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — <code class="language-plaintext highlighter-rouge">/api/calendar/events</code> cron expansion now resolves trigger fire times in the local timezone rather than UTC (fixes the ~4-hour shift); Custom-cadence first-fire arithmetic is reworked to honor <code class="language-plaintext highlighter-rouge">IntervalTrigger</code> start-date semantics for every-N-hours / every-N-days / every-N-weeks / every-N-months / every-N-years schedules so first-fires and intervals are correct; the expansion window is extended from 90 days to a full 12 months and <code class="language-plaintext highlighter-rouge">MAX_PER_ROUTINE</code> is raised proportionally so high-frequency cadences (<code class="language-plaintext highlighter-rouge">every 5 hours</code>) are not truncated; the response now carries a <code class="language-plaintext highlighter-rouge">horizon_end</code> timestamp the frontend can compare against the FullCalendar viewport.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — FullCalendar event mapping now sets <code class="language-plaintext highlighter-rouge">allDay: false</code> and a real <code class="language-plaintext highlighter-rouge">end</code> derived from the routine duration so 30-minute routines render as a tight bar rather than a full-width orange band; renders a user-facing notice when the FullCalendar viewport is navigated past the 12-month horizon explaining that the calendar only projects scheduled fires up to 12 months ahead.</li>
</ul>

<h3 id="batch-2--repositories--api-keys-deep-link-button">Batch 2 — Repositories &amp; API Keys deep-link button</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RepositoriesPage.tsx</code> — adds a “Looking for AI API key settings?” inline button between the page intro paragraph and the scope selector that routes to <code class="language-plaintext highlighter-rouge">/settings/ai</code>.</li>
</ul>

<h3 id="batch-3--about-resmon-page--tutorial-deep-link-buttons--settings--about-app-relocation">Batch 3 — About resmon page + tutorial deep-link buttons + Settings → About App relocation</h3>

<ul>
  <li><strong>New top-level page and routes.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/AboutResmonPage.tsx</code> <em>(new)</em> — top-level page with the tab strip and nested router; final tab order <strong>Tutorials → Issues → Blog → About App</strong> (Issues and Blog were added under OOB-5 / OOB-6).</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/TutorialsTab.tsx</code> <em>(new)</em> — eighteen tutorial sections (one full-app overview + ten page sections + seven Settings sub-tab sections), each with an embedded <code class="language-plaintext highlighter-rouge">youtube-nocookie.com/embed/&lt;id&gt;</code> iframe, a TOC, and prev / next navigation; deep-link hash drives smooth-scroll into view.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/AboutAppTab.tsx</code> <em>(new — relocated content)</em> — the previous Settings → About App panel content, now mounted under <code class="language-plaintext highlighter-rouge">/about-resmon/about-app</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/App.tsx</code> — adds the <code class="language-plaintext highlighter-rouge">/about-resmon/*</code> route and the sidebar nav entry appended after Settings.</li>
    </ul>
  </li>
  <li><strong>Shared <code class="language-plaintext highlighter-rouge">TutorialLinkButton</code> next to every page header and every Settings sub-panel header.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/TutorialLinkButton.tsx</code> <em>(new)</em> — single shared component navigating to <code class="language-plaintext highlighter-rouge">/about-resmon/tutorials#&lt;anchor&gt;</code>.</li>
      <li>All ten page components (<code class="language-plaintext highlighter-rouge">DashboardPage</code>, <code class="language-plaintext highlighter-rouge">DeepDivePage</code>, <code class="language-plaintext highlighter-rouge">DeepSweepPage</code>, <code class="language-plaintext highlighter-rouge">RoutinesPage</code>, <code class="language-plaintext highlighter-rouge">CalendarPage</code>, <code class="language-plaintext highlighter-rouge">ResultsPage</code>, <code class="language-plaintext highlighter-rouge">ConfigurationsPage</code>, <code class="language-plaintext highlighter-rouge">MonitorPage</code>, <code class="language-plaintext highlighter-rouge">RepositoriesPage</code>, <code class="language-plaintext highlighter-rouge">SettingsPage</code>) plus all seven Settings sub-panels (<code class="language-plaintext highlighter-rouge">EmailSettings</code>, <code class="language-plaintext highlighter-rouge">CloudAccountSettings</code>, <code class="language-plaintext highlighter-rouge">CloudSettings</code>, <code class="language-plaintext highlighter-rouge">AISettings</code>, <code class="language-plaintext highlighter-rouge">StorageSettings</code>, <code class="language-plaintext highlighter-rouge">NotificationSettings</code>, <code class="language-plaintext highlighter-rouge">AdvancedSettings</code>) render <code class="language-plaintext highlighter-rouge">TutorialLinkButton</code> next to their <code class="language-plaintext highlighter-rouge">&lt;h1&gt;</code> / <code class="language-plaintext highlighter-rouge">&lt;h2&gt;</code> headers.</li>
    </ul>
  </li>
  <li><strong>Settings → About App removal.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/SettingsPage.tsx</code> — removes the <code class="language-plaintext highlighter-rouge">About App</code> <code class="language-plaintext highlighter-rouge">NavLink</code> and nested <code class="language-plaintext highlighter-rouge">&lt;Route&gt;</code>; removes the <code class="language-plaintext highlighter-rouge">AboutAppSettings</code> import; tab count goes 8 → 7. The <code class="language-plaintext highlighter-rouge">Settings</code> index redirect to <code class="language-plaintext highlighter-rouge">/settings/email</code> is unchanged.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AboutAppSettings.tsx</code> — deleted (content relocated to <code class="language-plaintext highlighter-rouge">AboutResmon/AboutAppTab.tsx</code>).</li>
    </ul>
  </li>
</ul>

<h3 id="out-of-band-additions-t-upd-add">Out-of-band additions (T-UPD-ADD)</h3>

<ul>
  <li><strong>OOB-1 — Calendar <code class="language-plaintext highlighter-rouge">Routines: x of y</code> dropdown filters to active-only.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — <code class="language-plaintext highlighter-rouge">fetchData</code> seeds <code class="language-plaintext highlighter-rouge">visibleRoutines</code> from the active subset and a derived <code class="language-plaintext highlighter-rouge">activeRoutines</code> constant drives the <code class="language-plaintext highlighter-rouge">Select all</code> action and the rendered checkbox list.</li>
  <li><strong>OOB-2 — Calendar event popover shows <code class="language-plaintext highlighter-rouge">Name:</code> and <code class="language-plaintext highlighter-rouge">Cron Schedule:</code> info lines.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — popover renders two new <code class="language-plaintext highlighter-rouge">event-popover-meta</code> lines for the matching active routine.</li>
  <li><strong>OOB-3 — <code class="language-plaintext highlighter-rouge">Edit Routine</code> button on Calendar popover + shared <code class="language-plaintext highlighter-rouge">RoutineEditModal</code> + cross-page sync.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/lib/routinesBus.ts</code> <em>(new)</em> — pub/sub bus mirroring <code class="language-plaintext highlighter-rouge">configurationsBus.ts</code> with <code class="language-plaintext highlighter-rouge">notifyRoutinesChanged()</code> + <code class="language-plaintext highlighter-rouge">useRoutinesVersion()</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Routines/RoutineEditModal.tsx</code> <em>(new)</em> — extracted reusable create/edit modal that broadcasts on both <code class="language-plaintext highlighter-rouge">routinesBus</code> and <code class="language-plaintext highlighter-rouge">configurationsBus</code> on save.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RoutinesPage.tsx</code> — refactored to consume the shared modal; subscribes to <code class="language-plaintext highlighter-rouge">useRoutinesVersion()</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — adds the <code class="language-plaintext highlighter-rouge">Edit Routine</code> popover button; subscribes to <code class="language-plaintext highlighter-rouge">useRoutinesVersion()</code> and re-runs <code class="language-plaintext highlighter-rouge">fetchData()</code> on save without closing the popover.</li>
    </ul>
  </li>
  <li><strong>OOB-4 — <code class="language-plaintext highlighter-rouge">.modal-overlay</code> z-index above the Calendar event popover.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/styles/global.css</code> — bumps <code class="language-plaintext highlighter-rouge">.modal-overlay</code> from <code class="language-plaintext highlighter-rouge">z-index: 1000</code> → <code class="language-plaintext highlighter-rouge">1100</code>.</li>
  <li><strong>OOB-5 — <code class="language-plaintext highlighter-rouge">About resmon → Issues</code> tab + GitHub issue templates.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/IssuesTab.tsx</code> <em>(new)</em> — credentials-free form with two read-only submit paths (<code class="language-plaintext highlighter-rouge">mailto:</code> link + GitHub-issue deep link); both routed through <code class="language-plaintext highlighter-rouge">window.resmonAPI.openPath</code> (falls back to <code class="language-plaintext highlighter-rouge">window.location.href</code> / <code class="language-plaintext highlighter-rouge">window.open</code> when the preload bridge is unavailable).</li>
      <li><code class="language-plaintext highlighter-rouge">.github/ISSUE_TEMPLATE/bug.yml</code>, <code class="language-plaintext highlighter-rouge">feature.yml</code>, <code class="language-plaintext highlighter-rouge">question.yml</code>, <code class="language-plaintext highlighter-rouge">config.yml</code> <em>(new)</em> — typed GitHub issue forms whose IDs match the slugs the Issues tab generates.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/AboutResmonPage.tsx</code> — mounts the new tab between Tutorials and About App.</li>
    </ul>
  </li>
  <li><strong>OOB-6 — <code class="language-plaintext highlighter-rouge">About resmon → Blog</code> tab + GitHub Pages publishing infrastructure.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/BlogTab.tsx</code> <em>(new)</em> — fetches the Atom feed at <code class="language-plaintext highlighter-rouge">https://ryanjosephkamp.github.io/resmon/feed.xml</code>, parses client-side via <code class="language-plaintext highlighter-rouge">DOMParser</code>, renders a two-pane layout with an origin-locked Electron <code class="language-plaintext highlighter-rouge">&lt;webview&gt;</code>; off-origin links open in the user’s default browser.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/electron/main.ts</code> — enables <code class="language-plaintext highlighter-rouge">webPreferences.webviewTag: true</code> and adds a <code class="language-plaintext highlighter-rouge">will-attach-webview</code> hardening hook that scrubs <code class="language-plaintext highlighter-rouge">nodeIntegration</code>, <code class="language-plaintext highlighter-rouge">preload</code>, and any non-https <code class="language-plaintext highlighter-rouge">src</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/index.html</code> — widens the CSP <code class="language-plaintext highlighter-rouge">&lt;meta&gt;</code> to permit <code class="language-plaintext highlighter-rouge">https://ryanjosephkamp.github.io</code> for <code class="language-plaintext highlighter-rouge">frame-src</code> / <code class="language-plaintext highlighter-rouge">child-src</code> / <code class="language-plaintext highlighter-rouge">connect-src</code>, plus <code class="language-plaintext highlighter-rouge">https://www.youtube-nocookie.com</code> and <code class="language-plaintext highlighter-rouge">https://www.youtube.com</code> for the Tutorials embeds.</li>
      <li><code class="language-plaintext highlighter-rouge">docs/_config.yml</code>, <code class="language-plaintext highlighter-rouge">docs/Gemfile</code>, <code class="language-plaintext highlighter-rouge">docs/index.md</code>, <code class="language-plaintext highlighter-rouge">docs/README.md</code>, <code class="language-plaintext highlighter-rouge">docs/_posts/2026-04-23-welcome-to-the-resmon-blog.md</code> <em>(new — Jekyll site scaffolding)</em>. The two republished update posts (<code class="language-plaintext highlighter-rouge">docs/_posts/2026-04-24-resmon-update-1.md</code>, <code class="language-plaintext highlighter-rouge">docs/_posts/2026-04-25-resmon-update-2.md</code>) shipped in a separate <code class="language-plaintext highlighter-rouge">7f1ea24</code> commit on 2026-04-29 ahead of T-UPD-4 and are noted here for the record; they are not staged as part of the Update 3 commit.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/AboutResmonPage.tsx</code> — mounts the new tab between Issues and About App.</li>
    </ul>
  </li>
  <li><strong>OOB-7 — New T-UPD-5 prompt template in <code class="language-plaintext highlighter-rouge">update_prompts.md</code>.</strong> <code class="language-plaintext highlighter-rouge">.ai/updates/update_prompts.md</code> — adds a <strong>T-UPD-5 — Publish Update as Blog Post</strong> prompt template formalizing the seven-step per-post procedure, bumps the document to version 1.3, extends the usage / TOC / hard rules to reflect the new five-prompt sequence, and adds a “Push before publish” hard rule.</li>
  <li><strong>OOB-8 — <code class="language-plaintext highlighter-rouge">saved_configuration_id</code> linkage on the <code class="language-plaintext highlighter-rouge">executions</code> table.</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/database.py</code> — adds a nullable <code class="language-plaintext highlighter-rouge">saved_configuration_id INTEGER REFERENCES saved_configurations(id) ON DELETE SET NULL</code> column to the <code class="language-plaintext highlighter-rouge">executions</code> table via an idempotent <code class="language-plaintext highlighter-rouge">ALTER TABLE … ADD COLUMN</code> migration.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — <code class="language-plaintext highlighter-rouge">_enrich_execution_row</code> <code class="language-plaintext highlighter-rouge">LEFT JOIN</code>s <code class="language-plaintext highlighter-rouge">saved_configurations</code> and projects <code class="language-plaintext highlighter-rouge">saved_configuration_id</code> + <code class="language-plaintext highlighter-rouge">saved_configuration_name</code> onto every execution-returning endpoint; extends <code class="language-plaintext highlighter-rouge">DiveRequest</code> and <code class="language-plaintext highlighter-rouge">SweepRequest</code> with an optional <code class="language-plaintext highlighter-rouge">saved_configuration_id</code>; adds <code class="language-plaintext highlighter-rouge">PATCH /api/executions/{id}</code> accepting <code class="language-plaintext highlighter-rouge">{ saved_configuration_id: int }</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepDivePage.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepSweepPage.tsx</code> — track the loaded id in a <code class="language-plaintext highlighter-rouge">loadedConfigIdRef</code> populated by <code class="language-plaintext highlighter-rouge">ConfigLoader.applyConfig</code>; user edits clear the ref via a <code class="language-plaintext highlighter-rouge">dirtyRef</code> guard.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Save/SaveConfigButton.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — <code class="language-plaintext highlighter-rouge">PATCH</code> the originating execution row with the new id after a successful Save Config.</li>
    </ul>
  </li>
  <li><strong>OOB-9 — <code class="language-plaintext highlighter-rouge">Saved as &lt;name&gt;</code> badge + <code class="language-plaintext highlighter-rouge">Name</code> column wiring.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DashboardPage.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Results/ResultsList.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Save/SaveConfigButton.tsx</code> — adds a <code class="language-plaintext highlighter-rouge">Name</code> column (with <code class="language-plaintext highlighter-rouge">saved_configuration_name → routine_name → Execution #{id}</code> fallback chain) and a <code class="language-plaintext highlighter-rouge">Saved as &lt;name&gt;</code> badge that reconciles in place after a save broadcast.</li>
  <li><strong>OOB-10 — Configurations page <code class="language-plaintext highlighter-rouge">View JSON</code> per-row read-only modal.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/ConfigurationsPage.tsx</code> — adds a per-row <code class="language-plaintext highlighter-rouge">View JSON</code> button next to <code class="language-plaintext highlighter-rouge">Edit</code> that opens a read-only modal with a Copy-to-clipboard button.</li>
  <li><strong>OOB-11 — Settings → Advanced **Danger Zone</strong> (16 destructive actions, two-tier confirmation).**
    <ul>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/ConfirmDangerModal.tsx</code> <em>(new)</em> — typed-<code class="language-plaintext highlighter-rouge">CONFIRM</code> confirmation gate for the six destructive data/settings actions.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AdvancedSettings.tsx</code> — appends a Danger Zone section with two columns (Local / Cloud) of 8 buttons each; broadcasts on <code class="language-plaintext highlighter-rouge">configurationsBus</code>, <code class="language-plaintext highlighter-rouge">routinesBus</code>, and the <code class="language-plaintext highlighter-rouge">resmon:execution-completed</code> window event on success; the cloud column is rendered disabled with a <code class="language-plaintext highlighter-rouge">Coming soon</code> muted note.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — adds 8 admin route handlers under <code class="language-plaintext highlighter-rouge">POST /api/admin/...</code>; the two API-key wipes accept an empty body, the six data/settings destructions require <code class="language-plaintext highlighter-rouge">{ "confirm": "CONFIRM" }</code>.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/database.py</code> — helpers for whole-table truncates with FK cascade.</li>
      <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/credential_manager.py</code> — bulk-wipe helpers for the API-key categories.</li>
    </ul>
  </li>
</ul>

<h3 id="documentation-version-and-about-app-tab">Documentation, version, and About App tab</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/about_resmon_info.md</code>, <code class="language-plaintext highlighter-rouge">settings_info.md</code>, <code class="language-plaintext highlighter-rouge">calendar_info.md</code>, <code class="language-plaintext highlighter-rouge">configs_info.md</code>, <code class="language-plaintext highlighter-rouge">dashboard_info.md</code>, <code class="language-plaintext highlighter-rouge">results_and_logs_info.md</code>, <code class="language-plaintext highlighter-rouge">repos_and_api_keys_info.md</code>, <code class="language-plaintext highlighter-rouge">system_info.md</code>, <code class="language-plaintext highlighter-rouge">deep_dive_info.md</code>, <code class="language-plaintext highlighter-rouge">deep_sweep_info.md</code> — refreshed in T-UPD-3 sub-tranche 1 to reflect Update 3 changes (<code class="language-plaintext highlighter-rouge">routines_info.md</code> and <code class="language-plaintext highlighter-rouge">monitor_info.md</code> were verified up-to-date and left as-is).</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/config.py</code> — bumps <code class="language-plaintext highlighter-rouge">APP_VERSION</code> from <code class="language-plaintext highlighter-rouge">1.1.0</code> to <code class="language-plaintext highlighter-rouge">1.2.0</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/package.json</code> — bumps <code class="language-plaintext highlighter-rouge">version</code> from <code class="language-plaintext highlighter-rouge">1.1.0</code> to <code class="language-plaintext highlighter-rouge">1.2.0</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AboutResmon/AboutAppTab.tsx</code> — version banner now reads <code class="language-plaintext highlighter-rouge">1.2.0</code> and the Recent Update card is rewritten with this update’s number, name, and summary.</li>
  <li><code class="language-plaintext highlighter-rouge">README.md</code> — adds an <code class="language-plaintext highlighter-rouge">#/about-resmon/*</code> row to the routes table, an <strong>About resmon</strong> bullet under Key Features describing all four tabs, a short <strong>Blog</strong> subsection linking the GitHub Pages site, a note in the existing <strong>Reporting Issues</strong> subsection that issues can be filed via the in-app form, and a brief <strong>Maintenance / Danger Zone</strong> subsection pointing readers at Settings → Advanced.</li>
</ul>

<h2 id="verification">Verification</h2>

<p>Test results from each batch and from the OOB additions, run from the project root with the <code class="language-plaintext highlighter-rouge">.venv</code> virtualenv active and the frontend tooling under <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/</code>.</p>

<ul>
  <li><strong>Batch 1 (Calendar bug cluster):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">pytest resmon_scripts/verification_scripts/ -q</code> → all green; the Calendar timezone-shift and IntervalTrigger first-fire fixes are exercised by the existing scheduler / cron expansion suites.</li>
      <li>Frontend: <code class="language-plaintext highlighter-rouge">webpack 5.106.2 compiled successfully</code>; manual UI verification of every cron expression enumerated in the bug report (8 AM daily, midnight daily, Mon 9 AM, weekday 8 AM, first-of-month, every-2-months, every-5-hours, every-5-days, every-3-weeks, every-1-year, plus a 30-minute routine) on month / week / day views.</li>
    </ul>
  </li>
  <li><strong>Batch 2 (AI-key deep-link button):</strong> <code class="language-plaintext highlighter-rouge">webpack 5.106.2 compiled successfully</code>; manual click-through on the Repositories &amp; API Keys page → lands on <code class="language-plaintext highlighter-rouge">#/settings/ai</code>.</li>
  <li><strong>Batch 3 (About resmon page + tutorial deep-link buttons + Settings relocation):</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">cd resmon_scripts/frontend &amp;&amp; npm run typecheck</code> → clean.</li>
      <li><code class="language-plaintext highlighter-rouge">webpack 5.106.2 compiled successfully</code>; renderer <code class="language-plaintext highlighter-rouge">bundle.js</code> ~1.06 MiB after the YouTube embed pivot.</li>
      <li>Manual nav to <code class="language-plaintext highlighter-rouge">/about-resmon</code> confirmed all four tabs; manual click-through on every <code class="language-plaintext highlighter-rouge">TutorialLinkButton</code> instance (10 page headers + 7 Settings sub-panel headers) confirmed correct deep-link behavior; Settings tab count is 7 with no stranded <code class="language-plaintext highlighter-rouge">/settings/about</code> references.</li>
    </ul>
  </li>
  <li><strong>OOB-1 / OOB-2 / OOB-3 / OOB-4 (Calendar polish + RoutineEditModal + z-index fix):</strong> webpack build clean; manual UI smoke confirmed the Routines dropdown now shows active-only routines, the popover surfaces the <code class="language-plaintext highlighter-rouge">Name:</code> and <code class="language-plaintext highlighter-rouge">Cron Schedule:</code> lines, the <code class="language-plaintext highlighter-rouge">Edit Routine</code> button opens the modal above the popover and updates both Calendar and Routines without a manual reload.</li>
  <li><strong>OOB-5 (Issues tab + GitHub issue templates):</strong> webpack build clean; manual click-through on <strong>Open in Email</strong> opened the default mail client with the correct subject / body / diagnostic block, and <strong>File on GitHub</strong> opened the GitHub new-issue page with the correct template auto-selected and the form fields pre-populated.</li>
  <li><strong>OOB-6 (Blog tab + GitHub Pages):</strong> webpack build clean; the GitHub Pages site is live at <code class="language-plaintext highlighter-rouge">https://ryanjosephkamp.github.io/resmon/</code>; the in-app Blog tab successfully fetches <code class="language-plaintext highlighter-rouge">feed.xml</code>, lists the three published posts, and renders each post in the embedded <code class="language-plaintext highlighter-rouge">&lt;webview&gt;</code>. Off-origin links route to the user’s default browser.</li>
  <li><strong>OOB-7 (T-UPD-5 prompt template):</strong> documentation-only change; no test suite applies. Verified by re-reading the document end-to-end after the edit.</li>
  <li><strong>OOB-8 (<code class="language-plaintext highlighter-rouge">saved_configuration_id</code> linkage):</strong> <code class="language-plaintext highlighter-rouge">pytest resmon_scripts/verification_scripts/ -q</code> → green (idempotent migration covered by the existing schema-migration suite); manual UI verification of the round-trip on Deep Dive → Save Config → Dashboard / Results &amp; Logs / Calendar popover.</li>
  <li><strong>OOB-9 (<code class="language-plaintext highlighter-rouge">Saved as &lt;name&gt;</code> badge + <code class="language-plaintext highlighter-rouge">Name</code> column):</strong> webpack build clean; manual UI verification of the fallback chain (<code class="language-plaintext highlighter-rouge">saved_configuration_name → routine_name → Execution #{id}</code>) and of the in-place reconciliation when a save originates on a different page.</li>
  <li><strong>OOB-10 (View JSON modal):</strong> webpack build clean; manual UI verification of the read-only modal across all three config types (<code class="language-plaintext highlighter-rouge">manual_dive</code>, <code class="language-plaintext highlighter-rouge">manual_sweep</code>, <code class="language-plaintext highlighter-rouge">routine</code>); Copy-to-clipboard succeeded under both <code class="language-plaintext highlighter-rouge">navigator.clipboard.writeText</code> and the <code class="language-plaintext highlighter-rouge">document.execCommand('copy')</code> fallback path.</li>
  <li><strong>OOB-11 (Danger Zone):</strong> <code class="language-plaintext highlighter-rouge">pytest resmon_scripts/verification_scripts/ -q</code> → green for the new admin-route tests (typed-<code class="language-plaintext highlighter-rouge">CONFIRM</code> gate enforced server-side, FK cascades verified). Manual UI verification of all 8 local actions (the two API-key wipes through the simple modal, the six data/settings destructions through the typed-<code class="language-plaintext highlighter-rouge">CONFIRM</code> modal); cloud column rendered disabled with the <code class="language-plaintext highlighter-rouge">Coming soon</code> note as designed.</li>
</ul>

<p>Skipped tests: platform-gated Linux <code class="language-plaintext highlighter-rouge">systemd --user</code> and Windows Task Scheduler tests remain skipped on macOS via their existing <code class="language-plaintext highlighter-rouge">skipif</code> guards (these were exercised under Update 2’s manual verification matrix and are not re-exercised here because no notification-dispatch code changed in this update). No new dependencies; the <code class="language-plaintext highlighter-rouge">executions.saved_configuration_id</code> ALTER is idempotent and forward-compatible with installs that already migrated.</p>

<p>Commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Backend</span>
<span class="nb">source</span> .venv/bin/activate <span class="o">&amp;&amp;</span> pytest resmon_scripts/verification_scripts/ <span class="nt">-q</span>

<span class="c"># Frontend</span>
<span class="nb">cd </span>resmon_scripts/frontend
npm run typecheck
npm run build
npm start  <span class="c"># full Electron + backend smoke</span>
</code></pre></div></div>

<h2 id="follow-ups">Follow-ups</h2>

<ul>
  <li><strong>Tutorials full-app overview video</strong> — the full-app overview entry currently embeds a placeholder video; the user has reserved the final replacement recording for <strong>Update 4</strong> so the rest of the app can be bug-tested first. The placeholder is functional and replaceable by a single <code class="language-plaintext highlighter-rouge">youtubeId</code> change.</li>
  <li><strong>Cloud-side Danger Zone column</strong> — the eight cloud-mirror buttons in the Settings → Advanced Danger Zone are intentionally rendered disabled (<code class="language-plaintext highlighter-rouge">Coming soon</code>). They will activate when the <code class="language-plaintext highlighter-rouge">Cloud Account</code> feature lands and the matching <code class="language-plaintext highlighter-rouge">/api/admin/cloud/...</code> routes are added.</li>
  <li><strong>Stale-daemon caveat (carried forward from Update 2’s settings-allowlist)</strong> — users upgrading should kill any running <code class="language-plaintext highlighter-rouge">resmon-daemon</code> process so the renderer attaches to a fresh backend that has loaded the new admin endpoints and the <code class="language-plaintext highlighter-rouge">executions.saved_configuration_id</code> migration.</li>
</ul>]]></content><author><name>Ryan Kamp</name></author><category term="updates" /><summary type="html"><![CDATA[Update 3 — Calendar Bug Cluster, AI-Key Deep-Link, and the New About resmon Page (Tutorials, Issues, Blog, About App)]]></summary></entry><entry><title type="html">resmon Update 2 — April 25, 2026</title><link href="https://ryanjosephkamp.github.io/resmon/2026/04/25/resmon-update-2/" rel="alternate" type="text/html" title="resmon Update 2 — April 25, 2026" /><published>2026-04-25T16:00:00+00:00</published><updated>2026-04-25T16:00:00+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/2026/04/25/resmon-update-2</id><content type="html" xml:base="https://ryanjosephkamp.github.io/resmon/2026/04/25/resmon-update-2/"><![CDATA[<h1 id="update-2--calendar-readability-cross-platform-desktop-notifications-multi-provider-ai-keys-per-execution-ai-override-parity-and-configurations-lockstep">Update 2 — Calendar Readability, Cross-Platform Desktop Notifications, Multi-Provider AI Keys, Per-Execution AI Override Parity, and Configurations Lockstep</h1>

<h2 id="metadata">Metadata</h2>

<ul>
  <li><strong>Update number:</strong> 2</li>
  <li><strong>Update type:</strong> mixed (bugfix + feature)</li>
  <li><strong>Date:</strong> 2026-04-25</li>
  <li><strong>Author:</strong> Ryan Kamp</li>
  <li><strong>Commit hash:</strong> 023d7abc176f50a5eddac35d04bf86e288ac13a8</li>
  <li><strong>Commit timestamp:</strong> 2026-04-26T18:06:13-04:00</li>
  <li><strong>GitHub push timestamp:</strong> 2026-04-26T22:06:23Z</li>
  <li><strong>App version:</strong> 1.0.0 → 1.1.0</li>
</ul>

<h2 id="summary">Summary</h2>

<p>This update lands two bug fixes and three feature clusters, plus eleven out-of-band additions that emerged mid-update and were folded back into the same change set per the workflow’s T-UPD-ADD rule. The Calendar week/day views are switched to the readable dot+text rendering already used on the month view; desktop completion notifications now fire on macOS, Linux, and Windows for both manual and routine executions (including app-closed routines under the headless-daemon path); the AI summarization stack is reworked to store one API key per provider with transparent migration of any pre-existing global key, and the Deep Dive / Deep Sweep / Routines pages gain a single shared full-parity AI override panel (Provider + Model + Length + Tone + Temperature + Extraction Goals) with per-field merge semantics. The Settings → AI panel is rebuilt around a four-column <strong>Stored API Keys</strong> table backed by a new <code class="language-plaintext highlighter-rouge">ai_default_models</code> per-provider default-model map; the Configurations page gains a per-row <strong>Edit</strong> action with three config-type-aware modal variants and a stale-link 404 fallback; and the configuration-import endpoint now auto-materializes a deactivated routine for every imported routine config, restoring the bidirectional Routines ↔ routine-configs invariant.</p>

<h2 id="motivation">Motivation</h2>

<p>All items in this update originated in the user’s second-after-release feedback session and map back to the change inventory in <a href="../update_4_25_26/update_4_25_26_workflow.md"><code class="language-plaintext highlighter-rouge">.ai/updates/update_4_25_26/update_4_25_26_workflow.md</code></a>.</p>

<ul>
  <li><strong>Bug A — Calendar week/day event readability</strong> (workflow item #1, batch 1). The <code class="language-plaintext highlighter-rouge">timeGridWeek</code> and <code class="language-plaintext highlighter-rouge">timeGridDay</code> views rendered each event as a status-colored background block with type-colored text on top; the two color channels collided and the text was unreadable. The month view already used a status-dot + type-colored-text pattern that worked, so the fix was to extend that pattern down into week and day.</li>
  <li><strong>Bug B — Desktop notifications not firing on any platform</strong> (workflow item #2, batch 2). Notifications enabled in Settings → Notifications were silently dropped for both manual completions and routine fires, on all three supported platforms. Routine notifications additionally needed to fire when the app window was closed under headless-daemon / background-execution mode (the launchd / <code class="language-plaintext highlighter-rouge">systemd --user</code> / Task Scheduler service path). Per the user, cross-platform parity was required inside this same update — not deferred.</li>
  <li><strong>Feature 1 — Multi-provider AI API key storage</strong> (workflow item #3, batch 3). The previous design supported exactly one stored AI key at a time, forcing users to delete one provider’s key to test another. The user requested concurrent per-provider key slots with a transparent one-shot migration of any pre-existing global key on first launch.</li>
  <li><strong>Feature 2 — Full per-execution AI override parity</strong> (workflow item #4, batch 4). The Dive/Sweep override block exposed only Length / Tone / free-text Model and was missing entirely from Routines. The user requested full parity with the Settings → AI panel (Provider + Model + Length + Tone + Temperature + Extraction Goals) on all three pages, wired to the multi-provider credentials introduced in Feature 1, with per-field merge semantics (empty/blank = use app default; populated = override).</li>
  <li><strong>Feature 3 — Settings → AI tooltips</strong> (workflow item #5, batch 4). The Length / Tone / Temperature / Extraction Goals labels on Settings → AI had no in-app explanation. The user requested an <code class="language-plaintext highlighter-rouge">InfoTooltip</code> “?” hover on each of the four labels.</li>
  <li><strong>Out-of-band additions OOB-1..OOB-11.</strong> Eleven follow-on requests landed mid-update after T-UPD-1: a richer Stored API Keys table on Settings → AI (OOB-1, -2, -7, -8); a Model dropdown + Load-models button + inline missing-key entry + Save-as-default-model action on the override panel (OOB-3, -4); a new <code class="language-plaintext highlighter-rouge">ai_default_models</code> per-provider default-model map (OOB-5); race-safe writers in <code class="language-plaintext highlighter-rouge">AISettings.tsx</code> to stop two writers from clobbering the map (OOB-6); a per-row <strong>Edit</strong> action on the Configurations page with three modal variants (OOB-9); a stale <code class="language-plaintext highlighter-rouge">linked_routine_id</code> 404 fallback (OOB-10); and an import-side fix that auto-materializes a deactivated routine for every imported routine config so the bidirectional Routines ↔ routine-configs invariant holds (OOB-11). All eleven are part of this same Update 2 per workflow direction; this log is the single source of truth.</li>
</ul>

<h2 id="changes">Changes</h2>

<h3 id="batch-1--bug-a-calendar-weekday-event-readability">Batch 1 — Bug A: Calendar week/day event readability</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/CalendarPage.tsx</code> — wired the FullCalendar <code class="language-plaintext highlighter-rouge">eventContent</code> / <code class="language-plaintext highlighter-rouge">eventClassNames</code> / <code class="language-plaintext highlighter-rouge">eventDidMount</code> hooks for the <code class="language-plaintext highlighter-rouge">timeGridWeek</code> and <code class="language-plaintext highlighter-rouge">timeGridDay</code> views to the same dot+type-colored-text rendering already used on the <code class="language-plaintext highlighter-rouge">dayGridMonth</code> view; week and day events no longer paint a status-colored background block under type-colored text.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/styles/global.css</code> (or the calendar-scoped styles file actually used) — removed the <code class="language-plaintext highlighter-rouge">background-color</code> rules from the <code class="language-plaintext highlighter-rouge">calendar-type-*</code> / <code class="language-plaintext highlighter-rouge">fc-event-*</code> classes when the active view is <code class="language-plaintext highlighter-rouge">timeGridWeek</code> or <code class="language-plaintext highlighter-rouge">timeGridDay</code>; the status dot is now the sole status channel in those views.</li>
</ul>

<h3 id="batch-2--bug-b-desktop-notifications-across-macos-linux-and-windows">Batch 2 — Bug B: Desktop notifications across macOS, Linux, and Windows</h3>

<ul>
  <li>In-process notification dispatcher (the module reachable via <code class="language-plaintext highlighter-rouge">notify_on_complete</code>) — repaired the dispatch path that swallowed notifications when the renderer was the active window and when the headless daemon fired routines with no attached renderer.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/daemon.py</code> and <code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/service_manager.py</code> — wired the headless-daemon execution path into the notification dispatcher so routine completions raised by the launchd / <code class="language-plaintext highlighter-rouge">systemd --user</code> / Task Scheduler service path emit a desktop notification even when no renderer is attached.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/service_units/</code> (launchd plist, <code class="language-plaintext highlighter-rouge">systemd --user</code> unit, Task Scheduler XML) — verified per-platform notification permissions are not blocked by the service template; documented the per-platform manual-verification checklist where the dev machine could not natively exercise both Linux and Windows.</li>
  <li>No new runtime dependency was added; the existing notifier surface covers all three platforms once the dispatcher is reached.</li>
</ul>

<h3 id="batch-3--feature-1-multi-provider-ai-api-key-storage-with-transparent-migration">Batch 3 — Feature 1: Multi-provider AI API key storage with transparent migration</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/credential_manager.py</code> — added a per-provider credential-naming scheme (<code class="language-plaintext highlighter-rouge">ai_api_key__&lt;provider_slug&gt;</code>) and a one-shot, idempotent migration helper invoked at backend startup that re-keys any pre-existing legacy global AI credential under the per-provider name corresponding to the user’s currently selected provider. The legacy entry is only removed after the new entry is confirmed written.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> — surfaced per-provider presence on <code class="language-plaintext highlighter-rouge">GET /api/credentials</code>, accepted per-provider names on <code class="language-plaintext highlighter-rouge">PUT /api/credentials/{name}</code> and <code class="language-plaintext highlighter-rouge">DELETE /api/credentials/{name}</code>, and updated the AI factory / provider-resolution path so the correct stored key is selected at execution time based on the active provider.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AISettings.tsx</code> — replaced the single-key form with a presence map keyed by provider; this is the foundation OOB-1..OOB-8 build on.</li>
</ul>

<h3 id="batch-4--feature-2--feature-3-per-execution-ai-override-parity-and-settings--ai-tooltips">Batch 4 — Feature 2 + Feature 3: Per-execution AI override parity and Settings → AI tooltips</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AIOverridePanel.tsx</code> <em>(new)</em> — single shared override panel rendering the full Settings → AI control set (Provider + Model + Length + Tone + Temperature + Extraction Goals). Empty values are dropped via a <code class="language-plaintext highlighter-rouge">buildAIOverridePayload</code> helper before posting so they cannot clobber persisted defaults during the backend per-field merge.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepDivePage.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepSweepPage.tsx</code>, <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RoutinesPage.tsx</code> — mounted <code class="language-plaintext highlighter-rouge">AIOverridePanel</code> inside the form / create-edit modal; replaced the previous Length/Tone/free-text-Model trio with the shared panel (Routines previously had no override block at all).</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> (AI execution-settings merge) — extended the per-execution merge to accept Provider + Model + Length + Tone + Temperature + Extraction Goals as per-field overrides; per-execution overrides never write back to the persisted <code class="language-plaintext highlighter-rouge">app_settings</code> row.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AISettings.tsx</code> — added <code class="language-plaintext highlighter-rouge">InfoTooltip</code> “?” hovers next to the <strong>Length</strong>, <strong>Tone</strong>, <strong>Temperature</strong>, and <strong>Extraction Goals</strong> labels (Feature 3).</li>
</ul>

<h3 id="out-of-band-additions">Out-of-Band Additions</h3>

<ul>
  <li><strong>OOB-1, OOB-2, OOB-7, OOB-8 — Settings → AI Stored API Keys table.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AISettings.tsx</code> now renders a four-column <code class="language-plaintext highlighter-rouge">Stored API Keys</code> table (Provider / Status / Default Model / Actions). The Provider name (only — not the row) is the click target for setting the app default; Status carries a <code class="language-plaintext highlighter-rouge">Default</code> badge for the active provider; per-row Actions expose <code class="language-plaintext highlighter-rouge">Clear default model</code> and <code class="language-plaintext highlighter-rouge">Clear API key</code>. The table is lifted out of the <code class="language-plaintext highlighter-rouge">.settings-form</code> 480 px-capped container into its own <code class="language-plaintext highlighter-rouge">min(960px, 100%)</code> wrapper with non-stacking horizontal Actions buttons. (<code class="language-plaintext highlighter-rouge">global.css</code> minor tweak from OOB-1 only; OOB-7/-8 stayed inline so the shared <code class="language-plaintext highlighter-rouge">.settings-form</code> rule is not perturbed.)</li>
  <li><strong>OOB-3, OOB-4 — Override panel: provider-aware Model dropdown + missing-key inline entry + Save-as-default-model.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/AIOverridePanel.tsx</code> replaced the override-panel free-text Model input with a dropdown populated by <code class="language-plaintext highlighter-rouge">POST /api/ai/models</code> for the chosen Provider, behind a <code class="language-plaintext highlighter-rouge">Load models</code> button. When the chosen Provider has no stored key, the panel renders an inline API-key input that POSTs to <code class="language-plaintext highlighter-rouge">/api/credentials/{name}</code> (re-using the Settings → AI endpoint). A <code class="language-plaintext highlighter-rouge">Save as default model</code> button persists the chosen model into <code class="language-plaintext highlighter-rouge">ai_default_models[provider]</code> without leaving the page; after save, the panel emits the live-update event so other surfaces refresh. No backend route additions.</li>
  <li><strong>OOB-5 — <code class="language-plaintext highlighter-rouge">ai_default_models</code> per-provider default-model map.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code> added <code class="language-plaintext highlighter-rouge">ai_default_models</code> (JSON-encoded <code class="language-plaintext highlighter-rouge">{provider: model_id}</code> dict) to <code class="language-plaintext highlighter-rouge">_SETTINGS_GROUPS["ai"]</code>. All read/write paths (Settings → AI panel, override panel, the <code class="language-plaintext highlighter-rouge">AIDefaultsInfo</code> strip on Dive/Sweep/Routines) consult the map keyed by the active provider, falling back to the legacy global <code class="language-plaintext highlighter-rouge">ai_model</code> / <code class="language-plaintext highlighter-rouge">ai_local_model</code> only for the active provider’s row when the map has no entry. Non-breaking, additive — no schema migration. <strong>Operational caveat:</strong> existing daemons started before this allowlist entry existed silently dropped <code class="language-plaintext highlighter-rouge">ai_default_models</code> writes; users upgrading past this update must restart the daemon (kill the existing <code class="language-plaintext highlighter-rouge">resmon-daemon</code> process and let the renderer attach to a fresh one).</li>
  <li><strong>OOB-6 — Race-safe writers in <code class="language-plaintext highlighter-rouge">AISettings.tsx</code>.</strong> <code class="language-plaintext highlighter-rouge">handleClearDefaultModel</code>, <code class="language-plaintext highlighter-rouge">handleSetDefaultProvider</code>, and <code class="language-plaintext highlighter-rouge">handleSave</code> in <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AISettings.tsx</code> each <code class="language-plaintext highlighter-rouge">GET /api/settings/ai</code> immediately before each <code class="language-plaintext highlighter-rouge">PUT</code>, merge the fresh <code class="language-plaintext highlighter-rouge">ai_default_models</code> map with their intended delta, and <code class="language-plaintext highlighter-rouge">PUT</code> only the narrow set of keys they need to change; the override panel already used a similarly narrow PUT. This stops the two writers (Settings panel + override panel) from clobbering each other.</li>
  <li><strong>OOB-9 — Configurations page per-row Edit action.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/ConfigurationsPage.tsx</code> gained an <code class="language-plaintext highlighter-rouge">Edit</code> button on every row’s Actions column with a config-type-aware modal: <code class="language-plaintext highlighter-rouge">routine</code> → full Routines-style editor (cron + multi-repo + date range + keywords + max-results + AI/Email/Email-AI/Notify toggles + Execution-Location radio + shared <code class="language-plaintext highlighter-rouge">AIOverridePanel</code>) saving via <code class="language-plaintext highlighter-rouge">PUT /api/routines/{linked_routine_id}</code>; <code class="language-plaintext highlighter-rouge">manual_sweep</code> → same form minus cron and the routine-only toggles, saving via <code class="language-plaintext highlighter-rouge">PUT /api/configurations/{id}</code>; <code class="language-plaintext highlighter-rouge">manual_dive</code> → same as <code class="language-plaintext highlighter-rouge">manual_sweep</code> but with <code class="language-plaintext highlighter-rouge">RepositorySelector mode="single"</code>. No new backend routes.</li>
  <li><strong>OOB-10 — Stale <code class="language-plaintext highlighter-rouge">linked_routine_id</code> 404 fallback.</strong> <code class="language-plaintext highlighter-rouge">handleEditSave</code> in <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/ConfigurationsPage.tsx</code> now tries <code class="language-plaintext highlighter-rouge">PUT /api/routines/{linked_routine_id}</code> first and, on 404, falls back to <code class="language-plaintext highlighter-rouge">PUT /api/configurations/{id}</code> with the full routine payload (and <code class="language-plaintext highlighter-rouge">linked_routine_id</code> cleared). Defense-in-depth retained for any non-import path that could leave a config row orphaned; for the import path itself, OOB-11 makes the fallback unnecessary by always materializing the routine.</li>
  <li><strong>OOB-11 — Import ↔ Routines bidirectional invariant.</strong> <code class="language-plaintext highlighter-rouge">resmon_scripts/resmon.py</code>’s <code class="language-plaintext highlighter-rouge">import_configurations</code> endpoint post-processes each imported row: for every <code class="language-plaintext highlighter-rouge">config_type == 'routine'</code> config it parses the inner parameters JSON, builds a routine insert payload, <strong>forces <code class="language-plaintext highlighter-rouge">is_active=0</code></strong> (imported routines are deactivated by default so bulk imports never auto-fire), calls <code class="language-plaintext highlighter-rouge">insert_routine</code>, captures the new id, and rewrites the just-imported config row’s parameters JSON so <code class="language-plaintext highlighter-rouge">linked_routine_id</code> points at the new routine and <code class="language-plaintext highlighter-rouge">is_active=False</code>. The response gains a <code class="language-plaintext highlighter-rouge">routines_created</code> counter. <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RoutinesPage.tsx</code> subscribes to <code class="language-plaintext highlighter-rouge">useConfigurationsVersion()</code> so the routines list refetches on import without a page reload. No new dependencies; no schema changes.</li>
</ul>

<h3 id="public-facing-documentation-touch-ups-t-upd-3">Public-Facing Documentation Touch-Ups (T-UPD-3)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">README.md</code> — extended the <strong>AI-powered summarization</strong> Key-Features bullet to mention concurrent per-provider key storage and full per-execution AI override parity across Deep Dive / Deep Sweep / Routines; added a new <strong>Cross-platform desktop notifications</strong> Key-Features bullet covering macOS / Linux / Windows including the headless-daemon path; verified no other section (installation, supported repositories, operational modes, technology stack) required edits.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/settings_info.md</code> — added the new four-column <code class="language-plaintext highlighter-rouge">Stored API Keys</code> table description, click-provider-name-to-set-default semantics, <code class="language-plaintext highlighter-rouge">Default</code> badge in Status, <code class="language-plaintext highlighter-rouge">Clear default model</code> / <code class="language-plaintext highlighter-rouge">Clear API key</code> actions, the <code class="language-plaintext highlighter-rouge">ai_default_models</code> map, and the four <code class="language-plaintext highlighter-rouge">InfoTooltip</code> additions on the AI panel; clarified the Notifications panel covers all three platforms (with the daemon caveat for app-closed routine notifications) and that Advanced → Background Execution is a precondition for app-closed notifications.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/deep_dive_info.md</code>, <code class="language-plaintext highlighter-rouge">deep_sweep_info.md</code>, <code class="language-plaintext highlighter-rouge">routines_info.md</code> — replaced the Length/Tone/free-text-Model override description with the full-parity <code class="language-plaintext highlighter-rouge">AIOverridePanel</code> description (Provider-aware Model dropdown with <code class="language-plaintext highlighter-rouge">Load models</code>, inline API-key entry for missing-key providers, <code class="language-plaintext highlighter-rouge">Save as default model</code>); added the panel description to <code class="language-plaintext highlighter-rouge">routines_info.md</code> (previously absent).</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/configs_info.md</code> — documented the new per-row <strong>Edit</strong> action and its three modal variants (OOB-9), the stale <code class="language-plaintext highlighter-rouge">linked_routine_id</code> 404 fallback (OOB-10), and the import-bidirectional-sync rule from OOB-11; added the <code class="language-plaintext highlighter-rouge">routines_created</code> response field to the <code class="language-plaintext highlighter-rouge">/api/configurations/import</code> description.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/routines_info.md</code> — also noted that routine rows can now appear via Configurations-page imports (deactivated by default) in addition to the Create-modal path, and that the page subscribes to the configurations bus so imports surface without a reload.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_reports/info_docs/calendar_info.md</code> — updated the Event Styling paragraph for week/day views to describe the new dot+type-colored-text rendering.</li>
</ul>

<h3 id="app-version-bump">App Version Bump</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/config.py</code> — <code class="language-plaintext highlighter-rouge">APP_VERSION</code> bumped from <code class="language-plaintext highlighter-rouge">"1.0.0"</code> to <code class="language-plaintext highlighter-rouge">"1.1.0"</code> (minor: new user-visible features under a stable major).</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/package.json</code> — <code class="language-plaintext highlighter-rouge">"version"</code> bumped from <code class="language-plaintext highlighter-rouge">"1.0.0"</code> to <code class="language-plaintext highlighter-rouge">"1.1.0"</code> to match.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Settings/AboutAppSettings.tsx</code> — added a <strong>Recent Update</strong> card to the <code class="language-plaintext highlighter-rouge">about-grid</code> summarizing this update by number, name, and one-paragraph summary; the version line continues to read from <code class="language-plaintext highlighter-rouge">/api/health</code> so it tracks <code class="language-plaintext highlighter-rouge">APP_VERSION</code> automatically.</li>
</ul>

<h2 id="verification">Verification</h2>

<h3 id="test-suites-run">Test Suites Run</h3>

<p>The Python suite (<code class="language-plaintext highlighter-rouge">pytest</code>) was executed from the repository root inside the project’s <code class="language-plaintext highlighter-rouge">.venv</code> after every batch and every OOB change:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">source</span> .venv/bin/activate
pytest resmon_scripts/verification_scripts/ <span class="nt">-q</span>
</code></pre></div></div>

<ul>
  <li><strong>Batch 1 (Calendar):</strong> frontend webpack production build → <code class="language-plaintext highlighter-rouge">webpack 5.106.2 compiled successfully</code>; manual UI smoke confirmed dot+type-colored-text rendering on <code class="language-plaintext highlighter-rouge">dayGridMonth</code>, <code class="language-plaintext highlighter-rouge">timeGridWeek</code>, and <code class="language-plaintext highlighter-rouge">timeGridDay</code>, and that no background fill is applied to week/day events.</li>
  <li><strong>Batch 2 (Notifications):</strong> notification-dispatcher unit tests → green; manual smoke (a) app open, manual Dive completion notification fires; (b) app open, routine completion notification fires; (c) app closed under macOS launchd-managed daemon, routine completion notification fires. Linux (<code class="language-plaintext highlighter-rouge">systemd --user</code>) and Windows (Task Scheduler) verified via the dispatcher entry-point unit tests plus the written manual-verification checklist where the dev machine could not natively exercise both OSes.</li>
  <li><strong>Batch 3 (Multi-provider keys):</strong> <code class="language-plaintext highlighter-rouge">credential_manager</code> unit tests for per-provider naming and migration idempotency → green; <code class="language-plaintext highlighter-rouge">/api/credentials</code> route tests → green; AI factory provider-selection test → green; first-launch migration test against a synthetic legacy keyring entry → green.</li>
  <li><strong>Batch 4 (Override parity + tooltips):</strong> AI execution-settings merge tests (per-execution per-field overrides do not write back to the persisted Settings → AI row) → green; routine-creation tests carrying override settings end-to-end → green; frontend webpack production build → green; one end-to-end smoke run on each of Dive / Sweep / Routine with one non-default override field populated and the others left blank confirmed per-field merge.</li>
  <li><strong>OOB-1..OOB-11:</strong> webpack production build run after each OOB change → <code class="language-plaintext highlighter-rouge">compiled successfully</code> at every step. Backend Python unit tests for the credential-manager / settings paths remained green throughout. Backend <code class="language-plaintext highlighter-rouge">curl</code> round-trip on <code class="language-plaintext highlighter-rouge">PUT /api/settings/ai</code> confirmed <code class="language-plaintext highlighter-rouge">ai_default_models</code> persists across PUT/GET (after the stale-daemon caveat in OOB-5 was applied). Manual UI smoke confirmed: per-provider key storage round-trips; clicking only the Provider name sets the app default and adds the <code class="language-plaintext highlighter-rouge">Default</code> badge; <code class="language-plaintext highlighter-rouge">Clear default model</code> / <code class="language-plaintext highlighter-rouge">Clear API key</code> per-row Actions behave; OOB-6’s race-safe merge accumulates entries across alternating saves from the two surfaces; the override panel’s Load-models / inline-key / Save-as-default flow works on Dive / Sweep / Routines; Configurations Edit modal pre-populates and saves the right endpoint for each of the three config types; the OOB-10 fallback path saves successfully against an imported routine config with a stale <code class="language-plaintext highlighter-rouge">linked_routine_id</code>; and importing a routine config materializes a deactivated routine on the Routines page without reload.</li>
  <li><strong>Final aggregate run</strong> at the end of T-UPD-2: <code class="language-plaintext highlighter-rouge">pytest resmon_scripts/verification_scripts/ -q</code> → all tests green, no skips beyond pre-existing platform-gated tests (Linux-only / Windows-only systemd and Task Scheduler integration paths are skipped on macOS by design and were exercised through the manual checklist instead).</li>
</ul>

<h3 id="skipped-test-rationale">Skipped-Test Rationale</h3>

<ul>
  <li>The platform-gated Linux <code class="language-plaintext highlighter-rouge">systemd --user</code> and Windows Task Scheduler integration tests are skipped on macOS by their existing <code class="language-plaintext highlighter-rouge">pytest.mark.skipif</code> guards. They were exercised manually against the platform-specific service templates per the workflow’s manual-verification checklist; no test was disabled or deleted.</li>
</ul>

<h3 id="build-verification">Build Verification</h3>

<ul>
  <li>Frontend: <code class="language-plaintext highlighter-rouge">npm run build</code> (production webpack) ran clean after each batch and after each OOB change.</li>
  <li>Backend: <code class="language-plaintext highlighter-rouge">python -c "import resmon_scripts.resmon"</code> and the daemon attach path ran clean after every backend touch; <code class="language-plaintext highlighter-rouge">[main] Attached to existing resmon-daemon on port 8742</code> confirmed at the end of T-UPD-2.</li>
</ul>

<h2 id="follow-ups">Follow-ups</h2>

<ul>
  <li><strong>Stale-daemon advisory (OOB-5).</strong> Users upgrading from a build that predates <code class="language-plaintext highlighter-rouge">ai_default_models</code> must restart any long-lived <code class="language-plaintext highlighter-rouge">resmon-daemon</code> process so the new settings-allowlist entry is picked up. The behavior is silent-drop, not error, on the old daemon. Consider adding a startup version check in a future update so the renderer can warn when the attached daemon is older than the renderer.</li>
  <li><strong>Linux / Windows live-OS verification.</strong> Cross-platform parity for desktop notifications is covered by unit tests + the manual checklist; live-OS smoke runs on a Linux box and a Windows box are still recommended in a follow-up update once that hardware is available.</li>
  <li><strong>Configurations-page edit history.</strong> OOB-9’s Edit modal saves immediately; an undo / “revert to last saved” affordance was discussed but deferred — call it out as a candidate enhancement for a future update.</li>
  <li><strong>Per-paper artifact retention</strong> (Settings → Storage PDF / TXT retention policies) remains reserved for a future per-paper artifact-download feature; this update does not exercise that surface.</li>
</ul>]]></content><author><name>Ryan Kamp</name></author><category term="updates" /><summary type="html"><![CDATA[Update 2 — Calendar Readability, Cross-Platform Desktop Notifications, Multi-Provider AI Keys, Per-Execution AI Override Parity, and Configurations Lockstep]]></summary></entry><entry><title type="html">resmon Update 1 — April 24, 2026</title><link href="https://ryanjosephkamp.github.io/resmon/2026/04/24/resmon-update-1/" rel="alternate" type="text/html" title="resmon Update 1 — April 24, 2026" /><published>2026-04-24T16:00:00+00:00</published><updated>2026-04-24T16:00:00+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/2026/04/24/resmon-update-1</id><content type="html" xml:base="https://ryanjosephkamp.github.io/resmon/2026/04/24/resmon-update-1/"><![CDATA[<h1 id="update-1--config-deletion-propagation-fix--query-keyword-transparency">Update 1 — Config-Deletion Propagation Fix &amp; Query Keyword Transparency</h1>

<h2 id="metadata">Metadata</h2>

<ul>
  <li><strong>Update number:</strong> 1</li>
  <li><strong>Update type:</strong> mixed (bugfix + feature)</li>
  <li><strong>Date:</strong> 2026-04-24</li>
  <li><strong>Author:</strong> Ryan Kamp</li>
  <li><strong>Commit hash:</strong> 707c07221f607297a7a919bd3f4d527a968832c8</li>
  <li><strong>Commit timestamp:</strong> 2026-04-25T03:01:35-04:00</li>
  <li><strong>GitHub push timestamp:</strong> 2026-04-25T07:01:45Z</li>
</ul>

<h2 id="summary">Summary</h2>

<p>This update ships the first post-release change set for resmon. It fixes a frontend cache-staleness bug where deleted <code class="language-plaintext highlighter-rouge">manual_dive</code> / <code class="language-plaintext highlighter-rouge">manual_sweep</code> configurations continued to appear in the Deep Dive and Deep Sweep <code class="language-plaintext highlighter-rouge">Load Configuration</code> dropdowns, and it introduces a “Query Keyword Transparency” feature that surfaces, on every page where a user picks repositories, exactly how each repository’s upstream API combines space-separated keywords. The feature also adds a consolidated, grouped, color-coded glossary on the Repositories &amp; API Keys page and per-repository keyword-combination metadata in the catalog and on <code class="language-plaintext highlighter-rouge">.ai/prep/repos.csv</code>. Per-repository research replaces the previously lumped row for SSRN, RePEc, PLOS, DBLP, and IEEE Xplore (SSRN and RePEc are not in the active catalog and were therefore not added; PLOS, DBLP, and IEEE were classified individually).</p>

<h2 id="motivation">Motivation</h2>

<p>Both items in this update originated in the user’s first-after-release feedback session and map back to the change inventory in <code class="language-plaintext highlighter-rouge">.ai/updates/update_4_24_26/update_4_24_26_workflow.md</code>:</p>

<ul>
  <li><strong>Bug — Improper Config Deletion</strong> (workflow item #1, batch 1). After deleting a <code class="language-plaintext highlighter-rouge">manual_dive</code> or <code class="language-plaintext highlighter-rouge">manual_sweep</code> configuration from the Configurations page, the deleted entry was still selectable in the <code class="language-plaintext highlighter-rouge">Load Configuration</code> dropdown on the Deep Dive and Deep Sweep pages and could still auto-populate the form. The Routines page was unaffected. Root cause: the shared <code class="language-plaintext highlighter-rouge">ConfigLoader</code> only refetched on its <code class="language-plaintext highlighter-rouge">configType</code> / <code class="language-plaintext highlighter-rouge">refreshKey</code> props, neither of which changed when a deletion happened on a different page; there was no cross-page invalidation channel.</li>
  <li><strong>Feature — Query Keyword Transparency</strong> (workflow item #2, batch 2). Users had no in-app indication of how each repository’s upstream API combines keywords (some are implicit AND, some are implicit OR, several are relevance-ranked Lucene/Solr backends). The user requested per-repo banners on Deep Dive, Deep Sweep, and Routines, an enriched expander plus a glossary on the Repositories &amp; API Keys page, the same metadata in <code class="language-plaintext highlighter-rouge">.ai/prep/repos.csv</code>, and individual research for the lumped row in <code class="language-plaintext highlighter-rouge">.ai/prep/keyword_booleans_overview.md</code>.</li>
</ul>

<h2 id="changes">Changes</h2>

<h3 id="batch-1--improper-config-deletion-cluster-shared-root-cause">Batch 1 — Improper Config Deletion (cluster, shared root cause)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/lib/configurationsBus.ts</code> <em>(new, 33 lines)</em> — tiny in-renderer pub/sub bus with <code class="language-plaintext highlighter-rouge">notifyConfigurationsChanged()</code> and <code class="language-plaintext highlighter-rouge">useConfigurationsVersion()</code> so any mutation site can invalidate every mounted <code class="language-plaintext highlighter-rouge">ConfigLoader</code> without page-to-page coupling.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Forms/ConfigLoader.tsx</code> — subscribe to the bus via <code class="language-plaintext highlighter-rouge">useConfigurationsVersion()</code> and add the version to the fetch effect’s dependency array so deletions, saves, and imports anywhere in the app force a refetch.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/ConfigurationsPage.tsx</code> — call <code class="language-plaintext highlighter-rouge">notifyConfigurationsChanged()</code> after a successful delete and after a successful import.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepDivePage.tsx</code> — call <code class="language-plaintext highlighter-rouge">notifyConfigurationsChanged()</code> after a successful “Save Configuration” so newly saved rows immediately appear in sibling loaders.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepSweepPage.tsx</code> — same notify-on-save wiring as Deep Dive.</li>
</ul>

<h3 id="batch-2--query-keyword-transparency-single-feature">Batch 2 — Query Keyword Transparency (single feature)</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/implementation_scripts/repo_catalog.py</code> — add <code class="language-plaintext highlighter-rouge">keyword_combination</code> and <code class="language-plaintext highlighter-rouge">keyword_combination_notes</code> fields to <code class="language-plaintext highlighter-rouge">RepoCatalogEntry</code> and populate them for all 16 active repositories, using “Implicit AND”, “Explicit OR”, “Relevance-ranked”, or “Relevance-ranked (… upstream-default, unverified)” labels per the workflow’s uncertainty policy. DBLP, IEEE Xplore, and PLOS are individually classified (replacing the previously lumped row); SSRN and RePEc are not in the active catalog and were therefore not added.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/verification_scripts/test_repo_catalog.py</code> — extend <code class="language-plaintext highlighter-rouge">expected_keys</code> and add <code class="language-plaintext highlighter-rouge">test_keyword_combination_populated_for_every_active_repo</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/api/repositories.ts</code> — extend the <code class="language-plaintext highlighter-rouge">RepoCatalogEntry</code> TypeScript interface with the two new optional fields.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Forms/KeywordCombinationBanner.tsx</code> <em>(new, 42 lines)</em> — compact banner mounted under the repository selector on Deep Dive, Deep Sweep, and Routines that shows the keyword-combination label and notes for each currently selected repository, with a tooltip pointing to the consolidated glossary.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Repositories/KeywordSemanticsGlossary.tsx</code> <em>(new, 182 lines)</em> — consolidated, expandable glossary on the Repositories &amp; API Keys page. Three categories (“Boolean combination”, “Ranking &amp; confidence”, “Underlying search platforms”), each rendered as a color-accented section with a card grid of color-coded badge + short label + definition for every term (“Implicit AND”, “Explicit AND”, “Implicit OR”, “Explicit OR”, “Relevance-ranked”, “upstream-default, unverified”, “Lucene”, “Solr”). Iterated through three visual formats during review; final layout is the grouped card-grid format.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/components/Repositories/RepoDetailsPanel.tsx</code> — render the new “Effective Default Keyword Combination” and “Keyword Combination Notes” rows in the per-repo expander.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepDivePage.tsx</code> — mount <code class="language-plaintext highlighter-rouge">KeywordCombinationBanner</code> for the selected single repository.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/DeepSweepPage.tsx</code> — mount <code class="language-plaintext highlighter-rouge">KeywordCombinationBanner</code> for each selected repository.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RoutinesPage.tsx</code> — mount <code class="language-plaintext highlighter-rouge">KeywordCombinationBanner</code> in the Create/Edit modal for each selected repository.</li>
  <li><code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/src/pages/RepositoriesPage.tsx</code> — mount <code class="language-plaintext highlighter-rouge">KeywordSemanticsGlossary</code> above the catalog table.</li>
  <li><code class="language-plaintext highlighter-rouge">.ai/prep/repos.csv</code> <em>(internal scaffolding, not tracked by git)</em> — extended with <code class="language-plaintext highlighter-rouge">keyword_combination</code> and <code class="language-plaintext highlighter-rouge">keyword_combination_notes</code> columns mirroring the catalog entries; per the user’s directive this file is enriched in place rather than relocated.</li>
</ul>

<h3 id="out-of-band-additions-t-upd-add">Out-of-Band Additions (T-UPD-ADD)</h3>

<p>None.</p>

<h2 id="verification">Verification</h2>

<ul>
  <li><strong>Backend unit tests:</strong> <code class="language-plaintext highlighter-rouge">python -m pytest resmon_scripts/verification_scripts/test_repo_catalog.py -q</code> → <code class="language-plaintext highlighter-rouge">10 passed in 0.02s</code>. Includes the new <code class="language-plaintext highlighter-rouge">test_keyword_combination_populated_for_every_active_repo</code> and the extended <code class="language-plaintext highlighter-rouge">test_catalog_as_dicts_shape</code> <code class="language-plaintext highlighter-rouge">expected_keys</code> set.</li>
  <li><strong>Frontend production build:</strong> <code class="language-plaintext highlighter-rouge">npm run build:renderer</code> (run from <code class="language-plaintext highlighter-rouge">resmon_scripts/frontend/</code>) → <code class="language-plaintext highlighter-rouge">webpack 5.106.2 compiled successfully in 5111 ms</code> after batch 2’s components were finalized. Re-run after each visual iteration of the glossary; final iteration also compiled cleanly.</li>
  <li><strong>Manual UI verification:</strong> the live Electron app (attached to the running daemon on <code class="language-plaintext highlighter-rouge">127.0.0.1:8742</code>) was used to verify (a) deleting a <code class="language-plaintext highlighter-rouge">manual_dive</code> config from the Configurations page makes it disappear from the Deep Dive <code class="language-plaintext highlighter-rouge">Load Configuration</code> dropdown without a reload, and the same for <code class="language-plaintext highlighter-rouge">manual_sweep</code> on the Deep Sweep page; (b) the keyword-combination banner appears under the repository selector on Deep Dive, Deep Sweep, and the Routines create/edit modal, and lists one row per selected repository with the correct label; (c) the consolidated glossary on the Repositories &amp; API Keys page expands, renders three category sections with colored accents, and shows every defined term as a card; (d) each row in the catalog table exposes the two new entries in its expander.</li>
  <li><strong>Skipped tests:</strong> none. No frontend Jest suite was run because the project does not currently maintain one for these surfaces; manual UI verification is the existing standard for renderer-only changes.</li>
</ul>

<h2 id="follow-ups">Follow-ups</h2>

<ul>
  <li>Optional: add a Playwright/Jest smoke test for <code class="language-plaintext highlighter-rouge">ConfigLoader</code> cross-page invalidation so future regressions are caught without manual reproduction.</li>
  <li>Optional: confirm the “upstream-default, unverified” labels for DBLP, IEEE Xplore, and PLOS by directly probing each upstream search box; promote the labels to the unqualified form once verified.</li>
  <li>The cloud catalog mirror (if/when the cloud service serializes the catalog independently) will need the two new fields surfaced in its <code class="language-plaintext highlighter-rouge">/api/v2/repositories/catalog</code> response; not in scope for this update.</li>
</ul>]]></content><author><name>Ryan Kamp</name></author><category term="updates" /><summary type="html"><![CDATA[Update 1 — Config-Deletion Propagation Fix &amp; Query Keyword Transparency]]></summary></entry><entry><title type="html">Welcome to the resmon blog</title><link href="https://ryanjosephkamp.github.io/resmon/2026/04/23/welcome-to-the-resmon-blog/" rel="alternate" type="text/html" title="Welcome to the resmon blog" /><published>2026-04-23T00:00:00+00:00</published><updated>2026-04-23T00:00:00+00:00</updated><id>https://ryanjosephkamp.github.io/resmon/2026/04/23/welcome-to-the-resmon-blog</id><content type="html" xml:base="https://ryanjosephkamp.github.io/resmon/2026/04/23/welcome-to-the-resmon-blog/"><![CDATA[<p>This is the first post on the resmon blog. Going forward, every release update
to the resmon app will be mirrored here as its own post, including the full
contents of that update’s <code class="language-plaintext highlighter-rouge">update_&lt;DATE&gt;.md</code> document, screen-recording GIFs
of new features and bug fixes (where applicable), and any other context that
helps users understand the change.</p>

<p>The blog is also surfaced inside the resmon app itself, on the
<strong>About resmon → Blog</strong> tab. The in-app reader fetches this site’s feed at
<code class="language-plaintext highlighter-rouge">/feed.xml</code> and renders the post list directly from it, so any post published
here appears in the app on the next tab open without an app re-release.</p>

<p>Thanks for reading.</p>]]></content><author><name>Ryan Kamp</name></author><category term="announcements" /><summary type="html"><![CDATA[This is the first post on the resmon blog. Going forward, every release update to the resmon app will be mirrored here as its own post, including the full contents of that update’s update_&lt;DATE&gt;.md document, screen-recording GIFs of new features and bug fixes (where applicable), and any other context that helps users understand the change.]]></summary></entry></feed>