# Session Log — 2026-05-30 (work spanning 2026-05-29 evening → 2026-05-30)

## User
- **User:** Mike Swanson (mike)
- **Machine:** GURU-5070
- **Role:** admin

## Session Summary

The session opened as a GuruRMM feature request ("Mobile device support") and ran through the `/feature-request` flow. After clarifying scope (MDM for phones/tablets **plus** a GuruRMM mobile agent app — treated as one coherent feature), produced `SPEC-017-mobile-device-support.md`. The central technical finding documented: the iOS/Android capability asymmetry — an Android Device Admin app delivers real remote lock/wipe with no server certificate, but a sandboxed iOS App Store app cannot lock/wipe without an MDM enrollment profile (which needs the free Apple MDM Push Certificate). Mike then confirmed ACG now holds **both** Apple certificates (Developer Program + signing, and the MDM Push Certificate), so the spec was updated to mark both iOS phases Apple-cert-unblocked, with the annual MDM-push-cert renewal trap flagged.

The bulk of the session was a full GuruConnect (GC) modernization effort. Mike asked whether a `gc-audit` equivalent to `/rmm-audit` existed; it did not, so a `gc-audit` skill was authored, adapted to GC's actual architecture (protobuf wire format, runtime sqlx, Gitea Actions CI, static-HTML+component-library dashboard) rather than copying RMM assumptions. The skill was then run as a dry run: seven parallel/ sequential audit passes on Opus surfaced **three CRITICAL relay-plane auth failures** (any-JWT-joins-any-session, viewer-WS blacklist bypass, JWT-accepted-as-agent-key) plus the dashboard's wire-incompatible "protobuf" decoder, a stubbed deploy step leaving production 57 commits stale, and several HIGH/MEDIUM items. The audit report was committed and the skill was refined (use `.claude/standards/` as the compliance baseline; reconcile all `docs/specs/SPEC-*.md` + `specs/*/plan.md` `[DONE]` markers; tag already-planned findings `[TRACKED]` during a rebuild).

Mike then directed a ground-up re-spec. Produced `SPEC-002-v2-modernization-architecture.md` from four locked decisions: greenfield-but-salvage-proven-Rust-cores; native-first with full key fidelity (Win+R / Ctrl+Alt+Del / clipboard) and WebRTC only as a fallback; standalone-first with a versioned `/api/integration/v1/` RMM contract; hardened single-tenant now with a tenancy-ready schema. File transfer (clipboard cut/paste + drag-and-drop, bidirectional) was elevated to a headline differentiator after Mike named it as a favorite ScreenConnect feature. `/shape-spec` then produced `specs/v2-secure-session-core/` (Phase 1).

The Phase-1 keystone was implemented end to end across four tasks, each via a Coding Agent (Opus) → mandatory Code Review (Opus) → Gitea Agent commit loop: Task 1 (v2 schema + per-agent `cak_` keys + tenancy-ready columns), Task 2 (auth rebuild deleting the JWT-as-agent-key branch, session-scoped viewer tokens, per-agent key issuance, folding in a pre-existing machine-metadata bug fix), Task 3 (secure relay WS — viewer-token verification with blacklist + session-claim match, agent identity binding, frame caps, input throttle), and Task 4 (in-memory rate limiting + single-use widened support codes). A review-driven authorization-strength fix split viewer tokens into VIEW_ONLY vs CONTROL gated on permission, fully closing CRITICAL #1. Because the dev machine has no Rust toolchain, all code was verified on the build host (172.16.3.30) and confirmed compiling + passing tests (32/32), and the Gitea Actions CI was confirmed green. Every audit CRITICAL and HIGH in the auth/session core is now remediated in code.

The session closed with a `/sync` (pulled four of Howard's auto-sync commits) and a radio-show task: set the "promised vs got / best invention" episode to today's date (Saturday 2026-05-30), preserved Howard's Segments 1-2, and expanded the reserved Segment 3 into a topical May-2026 tech-news segment (AI glasses, AI-and-jobs, subscription squeeze, orbital data centers, AI security reality check, gadget hits) using live web research, since the assistant's training only runs to ~Jan 2026.

## Key Decisions

- **SPEC-017 scope:** treat "mobile device support" as MDM + a GuruRMM mobile agent app together; document the iOS/Android lock-wipe asymmetry rather than over-promising iOS parity.
- **gc-audit adapted, not copied:** GC uses runtime sqlx (not RMM's macros — and CLAUDE.md's "compile-time checked queries" line is stale), protobuf wire format, Gitea Actions CI, and a static-HTML+component-library dashboard. The skill's passes were rewritten accordingly; Pass B's initial "macros are the GC norm" rule was later corrected to flag new `query!` macros as a `[LOW]` deviation.
- **GC v2 direction (4 locked decisions):** greenfield-salvage-cores; native-first full key fidelity (WebRTC fallback only); standalone-first + versioned RMM contract; hardened single-tenant with a tenancy-ready (nullable `tenant_id`) schema so Phase 4 flips on isolation with no migration rewrite.
- **File transfer elevated:** clipboard cut/paste + drag-and-drop (both directions) made a core differentiator with a delayed-render clipboard design, not a deferred panel.
- **v2 sqlx + repo:** confirmed runtime `sqlx::query()` for v2 (GC already uses it); clean architectural reset in-place in the existing `guru-connect` repo (not a new repo).
- **Auth-strength (CRITICAL #1):** viewer-token minting gated on permission, and — after review found `view` is held by every default role — split into VIEW_ONLY (gated on `view`, relay refuses input) vs CONTROL (gated on `control`/admin) tokens. This is what actually closed CRITICAL #1.
- **Codec/transport/cutover:** H.264 default (HEVC opt-in); Phase-2 web viewer on protobuf-over-WSS first (WebRTC later); widened higher-entropy support codes; clean wholesale v1→v2 cutover (no client data to migrate).
- **Verification path:** with no local Rust toolchain, all Rust was verified by building + testing on the build host (172.16.3.30) and by confirming Gitea Actions CI, rather than trusting self-review.
- **Radio Segment 3:** built as a "present-day" bookend tying each item back to Segments 1-2; pulled live (web search) because training is stale for a same-day show.

## Problems Encountered

- **Gitea push failed mid-session** (internal :3000 refused, public 502) — a transient blip; later confirmed reachable and the pending commit had already been swept upstream by auto-sync. No loss.
- **Explore agent reported two GC docs at the repo root** (`FEATURE_ROADMAP.md`, `ARCHITECTURE_DECISIONS.md`) that actually live under `docs/`; caught and corrected the gc-audit skill's paths before finalizing.
- **CI red on Tasks 2/3/authz** — but only at the `cargo fmt --all --check` gate, which short-circuits before clippy/build/test, so the code had never actually compiled in CI. Verified on the build host that it compiled + passed; applied the fmt patch + two clippy one-liners (`8a01935`) → CI green.
- **Task 4 clippy red** — `empty_line_after_doc_comments` (rate_limit.rs) and two dead-code event constants (events.rs); fixed (`2118942`, build-host-verified) → CI green.
- **Audit authz finding:** Task 2/3's first authz gate used `has_permission("view")`, which is held by every default role, so it didn't actually narrow access; reviewer caught it, leading to the VIEW_ONLY/CONTROL split.
- **Coord todo POSTs failed twice on an em-dash** ("error parsing the body"); resolved by using ASCII-only text. (Same lesson recurred and was applied.)
- **No Rust toolchain on GURU-5070** — every Coding Agent could author but not compile; mitigated by build-host verification (172.16.3.30) for each task.

## Configuration Changes

**`azcomputerguru/guru-connect` (separate repo):**
- New: `docs/specs/SPEC-002-v2-modernization-architecture.md`, `reports/2026-05-29-gc-audit.md`, `specs/v2-secure-session-core/{plan,shape,references,standards}.md`.
- New (server): `migrations/004_v2_secure_session_core.sql`, `005_machine_metadata.sql`, `006_widen_support_code.sql`; `src/db/{agent_keys.rs,tenancy.rs}`; `src/auth/agent_keys.rs`; `src/api/machine_keys.rs`.
- Rebuilt/modified (server): `src/middleware/rate_limit.rs` (+mod.rs), `src/relay/mod.rs`, `src/api/sessions.rs`, `src/auth/{jwt.rs,mod.rs}`, `src/db/{machines,sessions,support_codes,events,users,mod}.rs`, `src/support_codes.rs`, `src/main.rs`, `Cargo.toml` (removed `tower_governor`).
- Episode/radio: n/a (different repo).

**`azcomputerguru/gururmm` (submodule):**
- New: `docs/specs/SPEC-017-mobile-device-support.md`; `docs/FEATURE_ROADMAP.md` updated (MDM checklist + Asset Location Tracking cross-link to SPEC-017).

**`azcomputerguru/claudetools` (this repo):**
- New: `.claude/skills/gc-audit/SKILL.md` (then refined twice).
- New memory: `.claude/memory/project_apple_mdm_certs.md`, `.claude/memory/project_guruconnect_v2_direction.md`; `MEMORY.md` index updated.
- Radio: created `projects/radio-show/episodes/2026-05-30-promised-vs-got-and-inventions/show-prep.md` (expanded, 25KB); `git rm` of `projects/radio-show/episodes/tbd-promised-vs-got-and-inventions/`.
- This session log.

## Credentials & Secrets

- No new secrets created.
- Gitea API token used for CI status checks: SOPS vault `services/gitea.sops.yaml`, field `credentials.api.api-token`.
- ACG holds both Apple certs as of 2026-05-29 (Developer Program + signing; MDM Push Certificate). **Still to capture:** the exact owning Apple ID and expiry for the MDM Push Certificate (renews annually on the same Apple ID or all enrolled iOS devices break) — see `.claude/memory/project_apple_mdm_certs.md`.

## Infrastructure & Servers

- **Coordination API:** `http://172.16.3.30:8001/api/coord` (locks, todos) — no auth.
- **Gitea (internal):** `http://172.16.3.20:3000` (azcomputerguru org). Public: `git.azcomputerguru.com` (NPM/Cloudflare; prefer internal).
- **GC build/deploy host:** `172.16.3.30` (Linux, Rust toolchain present; GC server runs on `:3002` behind NPM at `connect.azcomputerguru.com`; GC clone at `/home/guru/guru-connect`). Production GC binary was stale (git `1bfd476`, ~2026-01-18) vs submodule HEAD — deploy step is a stub.
- **Gitea Actions runners (online):** `guruconnect-builder` (ubuntu-latest), `pluto-guruconnect` (windows-msvc, on Pluto 172.16.3.36).
- GC DB: PostgreSQL on the GC host; v2 migrations 004-006 added (not yet applied to production).

## Commands & Outputs

- `cargo fmt --all` / `cargo clippy --all-targets --all-features -- -D warnings` / `cargo build --release --target x86_64-unknown-linux-gnu` / `cargo test --release` — run on `172.16.3.30` to verify GC v2 (no local toolchain). Note: must set `CARGO_BUILD_TARGET=x86_64-unknown-linux-gnu` on Linux because the repo `.cargo/config.toml` defaults to `x86_64-pc-windows-msvc`.
- GC v2 keystone test result on build host: `32 passed; 0 failed`.
- CI: build-and-test run on `2118942` — build-server, build-agent, security-audit all success.
- Coord todo POST: requires ASCII-only body (`text`, `created_by_user`, `created_by_machine` required); em-dashes cause "error parsing the body".
- `git rm -r projects/radio-show/episodes/tbd-promised-vs-got-and-inventions/` — old radio folder removed after writing the dated one.

## Pending / Incomplete Tasks

- **GC v2 Phase 1 remainder:** Task 5 (attended-mode consent — proto `ConsentRequest`/`ConsentResponse`), Task 6 (native viewer full key fidelity — WH_KEYBOARD_LL hook, scan-code injection, SAS for Ctrl+Alt+Del, clipboard sync), Task 7 (HW H.264 + raw/Zstd fallback). Then Phase 2 (file transfer + dashboard + web viewer), Phase 3 (`/api/integration/v1/` RMM contract), Phase 4 (multi-tenancy switch-on). Source of truth: `specs/v2-secure-session-core/plan.md` + `docs/specs/SPEC-002-*.md`.
- **Open coord todos (guruconnect):** `9a462965` (revoke viewer tokens on logout), `3c1f372a` (trusted-proxy client-IP keying — NPM-on-loopback collapses clients to 127.0.0.1), `542137df` (multi-instance fail-closed DB single-use gate). Plus two `TODO(audit-events)` comments in `db/events.rs`.
- **GC v2 deploy:** wire the real `deploy.yml` SSH step (currently a stub) and chain `cargo audit` into release/deploy; v1→v2 cutover after the product-capability tasks.
- **SPEC-017 mobile:** capture the Apple MDM Push Certificate's owning Apple ID + expiry; provision Google Play/FCM.
- **Radio:** Mike's "best invention" pick (Segment 2); refresh Segment 3 items if the show slips past 2026-05-30.

## Reference Information

- **Specs:** `guru-connect/docs/specs/SPEC-002-v2-modernization-architecture.md`, `guru-connect/specs/v2-secure-session-core/`, `guru-connect/specs/native-remote-control/`; `gururmm/docs/specs/SPEC-017-mobile-device-support.md`.
- **Audit report:** `guru-connect/reports/2026-05-29-gc-audit.md`.
- **gc-audit skill:** `.claude/skills/gc-audit/SKILL.md`.
- **Memory:** `.claude/memory/project_apple_mdm_certs.md`, `.claude/memory/project_guruconnect_v2_direction.md`.
- **Commit SHAs — guru-connect:** `486debf` (audit report), `5c60a10` (SPEC-002), `81e4b99` (shape spec), `fef8111` (T1), `41691bf` (T2), `0f25878` (T3), `a453e79` (authz split), `8a01935` (fmt/clippy), `bfcdbb5` (T4), `2118942` (clippy fix).
- **Commit SHAs — gururmm:** `417856e` (SPEC-017).
- **Commit SHAs — claudetools:** `e8ac759`, `df6a2dd`, `e5ccb6a`, `c670471`, `c70cd70` (gc-audit skill).
- **Coord todos (guruconnect):** done — `faf39fe0`, `c8916c89`; open — `9a462965`, `3c1f372a`, `542137df`.
- **Radio episode:** `projects/radio-show/episodes/2026-05-30-promised-vs-got-and-inventions/show-prep.md`.

---

## Update: 11:49 PT — GuruConnect v2 Phase 1 COMPLETED (Tasks 5-7, trusted-proxy fix) + local Rust toolchain

### Session Summary (this update)
Continued from the morning save and completed all of GuruConnect v2 Phase 1 (the secure-session-core). After the morning's Tasks 1-4 + authz split, this block delivered, in order: the trusted-proxy client-IP fix, Task 5 (attended consent), a local Rust toolchain on GURU-5070, the agent-crate clippy cleanup, Task 6 (full key fidelity — the headline), and Task 7 (HW H.264 + negotiated raw fallback). Each followed the Coding Agent (Opus) → Code Review (Opus) → commit loop. Phase 1 is now complete; every CRITICAL and HIGH from the 2026-05-29 audit is remediated in code.

Trusted-proxy fix (todo 3c1f372a): GC runs behind NPM on loopback, so axum `ConnectInfo` was always 127.0.0.1 — the Task-4 rate limiter/lockout bucketed every external client into one key (one abuser could lock out everyone). Added shared `utils::ip_extract::client_ip` honoring X-Real-IP / rightmost-untrusted X-Forwarded-For ONLY when the TCP peer is a configured trusted proxy (`CONNECT_TRUSTED_PROXIES` env, default loopback, fail-closed); wired into the limiter, relay, and audit logging. Task 5 (consent): proto `ConsentRequest`/`ConsentResponse`; the server gates an attended session at `join_session` (invisible to the tech until granted; `StartStream` only fires from `join_session`, so an unconsented session never streams), 60s timeout → teardown, Windows MessageBox dialog (fail-closed).

Mid-session, installed a full local Rust toolchain on GURU-5070 (rustup/cargo 1.96, MSVC C++ Build Tools, protoc 35.0 via winget; `PROTOC` env set), ending the per-task build-host round-trips. This Windows machine builds BOTH the server and the Windows agent locally — better coverage than the Linux build host, which can't compile the agent. The local clippy immediately exposed that CI never clippy-checks the agent crate (build-server clippy is Linux-only; build-agent only runs `cargo build`); 77 pre-existing agent clippy errors had accumulated. Cleaned them up (commit d0de888, behavior-preserving, code-reviewed) and filed a todo to add agent-clippy to CI.

Task 6 (full key fidelity — headline): `WH_KEYBOARD_LL` hook on the viewer diverts system combos (Win/Win+R, Alt+Tab, Ctrl+Esc) to the remote as full `KeyEvent`s and suppresses local handling, GATED on viewer focus + a toggle so it never bricks the technician's own keyboard; scan-code `SendInput` with correct extended-key flags; Ctrl+Alt+Del completes through the SAS helper (SYSTEM `SendSAS`, `SoftwareSASGeneration` policy); modifier hygiene re-syncs key-ups on focus loss. Review caught a BLOCKER — the hook wasn't focus-scoped — fixed. Task 7 (codec, last): encoder trait + factory; capability negotiation (`AgentStatus.supports_h264` + server `select_video_codec` + `StartStream.video_codec`); MF H.264 encoder + viewer decoder (FIRST-CUT, compile-verified-only, default-OFF via `DEFAULT_PREFER_H264=false`); raw+Zstd byte-for-byte unchanged as the guaranteed default. Task 6/7 were authored AND verified locally (fmt/clippy/test/build) — the toolchain payoff.

### Key Decisions (this update)
- Installed the local Rust+MSVC+protoc toolchain on GURU-5070 to end build-host round-trips; Coding Agents now self-verify locally and hand back CI-green code. Recorded in memory `reference_guru5070_rust_toolchain`.
- Cleaned the 77 pre-existing agent clippy errors BEFORE Task 6 (which edits agent code) so the local clippy loop runs on a clean base; filed a CI todo (CI never lints the agent).
- Trusted-proxy IP: honor forwarding headers ONLY from a configured trusted-proxy allowlist (default loopback), fail-closed; never trust a header from an untrusted (spoofable) peer.
- Task 6 hook focus-gated (`VIEWER_FOCUSED` AtomicBool, set from `WindowEvent::Focused`) so it diverts system keys only when the viewer window is focused. SAS named-pipe DACL tightened from NULL/Everyone to Authenticated Users.
- Task 7 ships H.264 dormant (`DEFAULT_PREFER_H264=false`): raw+Zstd is what runs; H.264 is compile-verified-only until live hardware validation (Task 8). `unsafe impl Send for H264Encoder` verified sound (session future is `block_on`-driven, never `spawn`ed).

### Problems Encountered (this update)
- Local clippy (cargo 1.96, newer than CI's 1.94) exposed 77 pre-existing agent clippy errors CI never caught → cleaned up + filed the CI-gap todo. (Local rustfmt 1.9 vs CI 1.8: empirically verified NO skew — `cargo fmt --check` clean on the CI-green HEAD.)
- Task 6 Code Review BLOCKER: the `WH_KEYBOARD_LL` hook diverted system combos regardless of viewer focus, so an unfocused viewer would swallow the technician's own Win/Alt+Tab/Ctrl+Esc → fixed with the focus gate.
- Coord lock-release jq one-liner failed (locks endpoint shape); left to auto-expire (harmless). Em-dash in a todo POST failed again ("error parsing the body") → ASCII-only retry.
- Bash-tool CWD persisted into the `guru-connect` submodule from `git apply` calls, so a relative `.claude/scripts/...` path resolved wrong; use absolute paths.

### Configuration Changes (this update)
**guru-connect:** new `server/src/utils/ip_extract.rs` (trusted-proxy client-IP); new `agent/src/consent/mod.rs` (Task 5); new `agent/src/encoder/{h264,capability,color}.rs` + `agent/src/viewer/decoder.rs` (Task 7). Modified across server (relay, session, middleware/rate_limit+mod, main, auth, db/{machines,sessions,events,users}, api) and agent (viewer/{input,render,mod}, input/{keyboard,mod}, session, bin/sas_service, ~22 files in the clippy sweep), proto/guruconnect.proto, agent/Cargo.toml, Cargo.lock (pruned), and the plan.md task markers.
**claudetools:** new memory `.claude/memory/reference_guru5070_rust_toolchain.md` + MEMORY.md index line.
**GURU-5070 machine:** rustup (cargo 1.96 at `~/.cargo/bin`), VS2022 Build Tools (VCTools workload), protoc 35.0 (winget). `PROTOC` set as a User env var.

### Commands & Outputs (this update)
- Local GC verify (PowerShell, from the guru-connect dir): `$env:PROTOC="C:\Users\guru\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe"; cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo test --workspace; cargo build --workspace` — all green (Task 7: 89 tests). Default target is `x86_64-pc-windows-msvc`; builds server + agent.
- `winget install Rustlang.Rustup` / `Microsoft.VisualStudio.2022.BuildTools` (`--override "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended"`) / `Google.Protobuf`.

### Pending / Incomplete Tasks (this update)
- **GC v2 Phase 1 is COMPLETE (Tasks 1-7).** Next: **Task 8 — live hardware validation** (esp. the H.264 first-cut go-live gates), then **Phase 2** (file transfer + dashboard + web viewer — v2 has NO operator dashboard yet, so it is NOT a full v1 replacement until Phase 2), **Phase 3** (`/api/integration/v1/` RMM contract), **Phase 4** (multi-tenancy switch-on).
- **Open coord todos (guruconnect):** `9a462965` (viewer-token revocation on logout), `542137df` (multi-instance fail-closed DB single-use gate), `addd7eea` (add agent-clippy to the build-agent CI job), + the H.264-go-live gating todo (live-validate, real force-IDR via CODECAPI, document the no-spawn invariant, graceful decode-worker spawn). **Closed this update:** `3c1f372a` (trusted-proxy).
- **GC v2 deploy:** `deploy.yml` SSH step still a stub; v1→v2 cutover deferred until at least Phase 2 (needs the dashboard).

### Reference Information (this update)
- **New commit SHAs — guru-connect:** `5d5cd26`+`8cb0b5b` (trusted-proxy IP), `9082e11`+`fbf9e26` (Task 5 consent), `d0de888` (agent clippy cleanup), `bb73ba6` (Task 6 key fidelity), `f9bdecb` (Task 7 codec).
- **Local toolchain memory:** `.claude/memory/reference_guru5070_rust_toolchain.md`.
- **Plan (source of truth):** `guru-connect/specs/v2-secure-session-core/plan.md` (Tasks 1-7 all marked done/implemented).

---

## Update: 12:59 PT — GuruConnect v2 Dashboard (Phase 2 pass 1)

### Session Summary (this update)
Built pass 1 of the GuruConnect v2 operator dashboard, picking up the Phase 2 "make it usable" track after Phase 1 (secure session core) was completed earlier in the day. The framework was chosen as a React + Vite + TypeScript SPA. A Coding Agent produced the scaffold, an operations-terminal design system, Bearer-token auth, and the Machines view (~30 new files), all local gates green (tsc/lint/build). A CSS bundling bug was caught and fixed during that pass (ui.css had been imported only by a dead barrel; moved the import to the live entry).

Per the agreed standard sequence (build -> impeccable pass -> frontend-design validation + Code Review -> commit), an impeccable critique-and-polish pass ran next via an Opus Coding Agent using the impeccable skill's reference rubric. It converted the modal-heavy machine-detail flow into a non-blocking side drawer (keeping modals only for irreversible delete and the copy-once key reveal), rewrote the palette to OKLCH-tinted neutrals with AA contrast verified numerically, added real dialog a11y (focus trap + inert-on-root + portal-to-body + a dialogStack topmost-only-Esc), replaced the spinner load with table skeletons, made row actions keyboard/touch reachable, fixed an inverted type hierarchy, and standardized destructive copy. All gates stayed green.

A dedicated Opus Code Review of the uncommitted dashboard returned APPROVE with no blockers: admin gating is server-enforced (not hidden UI), the cak_ key is never logged/persisted/cached, error envelopes normalize across the two known shapes, and the a11y is correct rather than cosmetic. The reviewer's one HIGH (a malformed CSS comment opener at table.css:89) was a false alarm against an intermediate state; the committed-to-disk file is correct (grep confirmed no stray escape sequence). The toast-inert-behind-modal behavior was explicitly ruled acceptable (toasts auto-dismiss; aria-live still announces while inert).

The Gitea Agent committed the work inside the guru-connect submodule and pushed it, then created the parent claudetools submodule-pointer commit locally. Coordination state was closed out: the Phase 2 dashboard lock was released and the guruconnect/dashboard component marked built.

### Key Decisions (this update)
- Framework: React + Vite + TypeScript SPA (operations-console posture: dense, status-forward, trustworthy; not marketing).
- Machine detail uses a side drawer, not a modal; modals reserved for irreversible delete and copy-once key reveal (impeccable modal-as-last-resort law).
- Palette migrated from flat hex to OKLCH-tinted neutrals; AA contrast verified by computing sRGB luminance, not by eye.
- Deleting the v1 dashboard remote-session/protobuf stubs (RemoteViewer, SessionControls, useRemoteSession, protobuf.ts, types/protocol.ts) is intentional under the greenfield-salvage-cores + clean-v1-cutover decisions; the web viewer's wire logic gets rebuilt in Phase 2 proper, not salvaged.
- Toast-inert-behind-modal accepted as-is (standard modal semantics; auto-dismiss; aria-live unaffected) rather than blocking on a portal change.

### Problems Encountered (this update)
- CSS bundling bug: ui.css imported only by a dead barrel, so styles did not bundle. Fixed by importing from the live render entry (main.tsx) and removing the barrel. Caught during pass 1, verified by build emitting dist/assets/index-*.css.
- Code Review HIGH (table.css:89 malformed comment) was a false positive against an intermediate working-tree state; grep for the escape sequence returned no matches, current file is correct. No edit needed.

### Configuration Changes (this update)
- New: GuruConnect v2 dashboard under projects/msp-tools/guru-connect/dashboard/src/ (scaffold, ui primitives, layout, auth feature, machines feature, lib, styles/tokens.css) — ~30 new files plus impeccable-pass additions (Drawer.tsx, TableSkeleton.tsx, dialogStack.ts, MachineDetailDrawer.tsx).
- Removed: dashboard v1 stubs RemoteViewer.tsx, SessionControls.tsx, useRemoteSession.ts, protobuf.ts, types/protocol.ts (6 obsolete files).
- Parent repo: submodule pointer projects/msp-tools/guru-connect advanced.

### Pending / Incomplete Tasks (this update)
- Dashboard later passes: Sessions view (join via viewer-token -> static viewer), Support Codes view, Users admin view, production-serving wiring (dist -> server/static + Axum catch-all).
- Phase 2 proper: file transfer (clipboard cut/paste + drag-drop) and the web viewer.
- Phase 3: /api/integration/v1 RMM contract. Phase 4: multi-tenancy switch-on. GC v2 Task 8: live hardware validation.
- Open coord todos (guruconnect): 9a462965 (viewer-token revocation on logout), 542137df (multi-instance fail-closed DB single-use gate), addd7eea (add agent clippy to build-agent CI), the H.264 go-live gating todo.

### Reference Information (this update)
- **New commit SHAs — guru-connect:** `43a9432` (feat(dashboard): v2 operator console pass 1, pushed f9bdecb..43a9432 via http://172.16.3.20:3000).
- **Parent pointer commit (claudetools, local, unpushed):** `5fce962`.
- **Dashboard root:** projects/msp-tools/guru-connect/dashboard/ — React+Vite+TS, dev Vite proxy /api,/ws -> localhost:3002.
- **Coord:** guruconnect/dashboard component = built; Phase 2 dashboard lock e783feb4 released.

---

## Update: 14:25 PT — GuruConnect v2 dashboard completed + SPA serving wired

### Session Summary (this update)
Completed the GuruConnect v2 operator dashboard, taking it from one view (Machines) to feature-complete. Built three more views and wired production serving, each following the standard sequence: explore the real v2 server API -> Coding Agent build on the established primitives -> Code Review (Opus, security-weighted) -> Gitea commit -> coord lock release + component update + server-gap todos. All work is committed and pushed to the guru-connect submodule; the claudetools parent carries local submodule-pointer commits.

Order of work: (1) Sessions view — active-session table with consent-state badges, a viewer-token Join that mints a session-scoped token and reveals it with the relay URL (the agent correctly refused to target the v1 viewer.html, which sends the raw login JWT the v2 plane rejects), and disconnect. (2) Production-serving wiring — Axum now serves the React SPA at / with a deep-link fallback, with /api/*rest and /ws/*rest returning JSON 404 so unrouted API/WS paths never leak index.html; the dead v1 portal HTML and handlers were removed (user confirmed nothing is live on the server). (3) Support Codes view — generate (copy-once reveal modal, ref-guarded to one code per open), list, cancel. (4) Users admin view — admin-gated three ways, real roles/permissions, Web Crypto password generation with copy-once reveal, and self-lockout guards.

Code Review found and gated real bugs across the passes: the Support Codes cancel endpoint returns 200 + plain-text "Code cancelled" which the shared client tried to JSON.parse (made cancel report success as failure) — fixed by making the client tolerant of non-JSON success bodies; and the generate-on-open effect created two real single-use codes per open under StrictMode — fixed with a ref guard. The Users view had a partial-update edge (failed permissions-set left the table stale) fixed by moving invalidation to onSettled with an actionable error message.

The dashboard view set is now complete (Machines, Sessions, Support Codes, Users admin) plus SPA serving. Everything is built and CI-clean but NOT deployed. The decision was made to gitignore the built SPA (server/static/app/) and build it at deploy time, and to keep sourcemaps. The next action is the v2 deploy/cutover (separate from this save).

### Key Decisions (this update)
- SPA artifact gitignored (server/static/app/), built at deploy time via npm ci && npm run build — avoids minified-bundle churn during active dashboard dev. Vite base "/" (absolute asset paths, required for BrowserRouter deep-link reloads), outDir ../server/static/app, emptyOutDir true (only clears app/, never the static root).
- v1 portal removed now rather than deferred to cutover, because the user confirmed nothing is live on the server to preserve and no active connections to drain. Deleted login/dashboard/users/index/viewer .html and the dead serve_* handlers.
- Sessions Join reveals a session-scoped viewer token instead of opening the static viewer.html (which is incompatible with the v2 viewer plane). In-dashboard web viewer deferred to Phase 2 proper.
- Support Codes shows no expiry countdown because the API's serialized SupportCode drops expires_at (only the DbSupportCode row has it). The agent refused to fake a countdown.
- Users self-demotion is guarded client-side only (server blocks self-disable/self-delete but not self-role-demotion). Filed as a server todo.
- Sourcemaps kept on the internet-facing relay (user choice).

### Problems Encountered (this update)
- Support Codes cancel surfaced success as failure: server returns 200 with plain-text body, shared client JSON.parse threw SyntaxError. Fixed in api/client.ts (try/catch around success-body parse; error-envelope path untouched). Re-reviewed, no regression.
- StrictMode double-mint: generate-on-open created two durable single-use codes per open. Fixed with a mintedFor ref that re-arms on close.
- Users partial-update: a failed permissions-set after a successful user-update left the table showing the stale row with a misleading error. Fixed: invalidate on onSettled, and a "User saved, but permissions update failed - retry permissions" message (required broadening EditUserModal onError to surface plain Error.message).
- Code Review flagged a malformed CSS comment (table.css:89) on an intermediate dashboard pass-1 state; the committed file was correct (grep confirmed) — false alarm, no edit.

### Configuration Changes (this update)
- guru-connect submodule commits: 67f3722 (SPA serving + v1 removal), 664f33d (Support Codes view), 96b4fd7 (Users admin view); earlier this day 43a9432 (dashboard pass 1) and 6ecb937 (Sessions view).
- New dashboard features: src/features/{sessions,codes,users}/, src/auth/AdminRoute.tsx, src/api/{sessions,codes,users}.ts, plus additions to src/components/ui/status.ts, src/components/layout/{icons.tsx,Sidebar.tsx}, src/App.tsx, src/api/{types.ts,index.ts,client.ts,stubs.ts}.
- Server: server/src/main.rs (SPA serving + /api,/ws JSON-404 guards + downloads nest + v1 handler removal), server/src/middleware/security_headers.rs (path-aware cache-control), dashboard/vite.config.ts (base/outDir/emptyOutDir), dashboard/README.md, guru-connect .gitignore (/server/static/app/).
- Deleted: server/static/{login,dashboard,users,index,viewer}.html.

### Pending / Incomplete Tasks (this update)
- NEXT: deploy the v2 stack (cutover). Steps: build Linux server binary (x86_64-unknown-linux-gnu, on the Linux build host — GURU-5070 builds the Windows agent/server, not the Linux release), run migrations 004_v2_secure_session_core / 005_machine_metadata / 006_widen_support_code, set env (CONNECT_TRUSTED_PROXIES, JWT_SECRET, DATABASE_URL), npm ci && npm run build the SPA into server/static/app, restart via start-server.sh, smoke test. Nothing live to preserve = zero cutover risk.
- claudetools parent is ahead of origin by the submodule-pointer commits (this /save pushes them).
- Server todos filed (guruconnect): support-code expires_at serialization; users authz gaps (self-demotion guard, last-admin protection, password-reset-reveal, username edit, /clients endpoint). Earlier open: viewer-token revocation on logout (9a462965), multi-instance DB single-use gate (542137df), agent clippy in CI (addd7eea), H.264 go-live gating.
- Dashboard later: in-dashboard web viewer (Phase 2 proper) + file transfer.

### Reference Information (this update)
- **guru-connect submodule HEAD:** 96b4fd7 (pushed to internal Gitea http://172.16.3.20:3000/azcomputerguru/guru-connect).
- **Dashboard:** projects/msp-tools/guru-connect/dashboard/ (React+Vite+TS). Build emits to ../server/static/app/ (gitignored). Dev Vite proxy /api,/ws -> localhost:3002.
- **Server SPA serving:** Axum fallback ServeDir(server/static/app).fallback(index.html); /api/*rest + /ws/*rest -> JSON 404; /downloads nested; /health,/metrics intact. axum 0.7.9, tower-http 0.6.11.
- **Coord:** guruconnect/dashboard component = built (view set complete); all session locks released.

---

## Update: 21:28 PT — Wispr Flow dictation fixed for Claude Code CLI (GURU-5070)

### Session Summary

Mike reported the Wispr Flow dictation hotkey worked in every application except the Claude Code CLI. Diagnosis proceeded in two stages. First, confirmed the CLI terminal was running elevated ("Run as administrator") via a WindowsIdentity/WindowsPrincipal check, while Wispr Flow ran at medium integrity. Windows UIPI blocks a lower-integrity process from installing a global keyboard hook that fires over a higher-integrity foreground window — so Wispr's hotkey went deaf whenever the elevated CLI had focus. Resolved by promoting Wispr Flow to High integrity: created a "Wispr Flow (Elevated)" scheduled task (AtLogOn, RunLevel Highest, targeting the stable Squirrel stub so it survives app updates), disabled the existing non-elevated Startup-folder shortcut to prevent a double launch, killed all Wispr processes, and relaunched via the task. Verified the new process token integrity level read HIGH.

Second stage: hotkey then fired (dictation started) but transcribed text did not land in the CLI. Identified the terminal window class as `ConsoleWindowClass` (legacy conhost), with Windows Terminal installed but not the default. Legacy conhost mishandles the synthetic clipboard paste (Ctrl+V) Wispr uses to insert text. Because Wispr now runs at High integrity, it can inject into Claude regardless of Claude's elevation (UIPI only blocks lower→higher), so the remaining fix was purely about getting a paste-capable terminal. Set Windows Terminal as the default terminal application (HKCU\Console\%%Startup delegation GUIDs), then rewrote the Desktop `Claude.lnk` to launch via `wt.exe` explicitly (self-contained, not reliant on the global default), preserving working dir, args, and icon. Backed up the original shortcut to `Claude.lnk.bak`.

Activation requires Mike to close the current (legacy-conhost) session and relaunch from the updated shortcut; the running session cannot retroactively move into Windows Terminal. Not yet user-confirmed working end-to-end.

### Key Decisions

- Promoted Wispr Flow to elevated via a **scheduled task with RunLevel Highest** rather than a Run-key/Startup shortcut: Task Scheduler launches elevated at logon without a UAC prompt and is the reliable way to make admin auto-start persist.
- Targeted the scheduled task at the **stable Squirrel stub** `...\WisprFlow\Wispr Flow.exe` (not the version-pinned `app-1.5.559\` path) so it survives Wispr auto-updates.
- Left the Claude shortcut **non-elevated** (its original Run-as-admin flag was already False). Elevated Wispr can inject into a non-elevated target, so forcing Claude to admin was unnecessary and avoids running `--dangerously-skip-permissions` as administrator.
- Wrapped the shortcut target in **`wt.exe` explicitly** in addition to setting WT as the default terminal — belt-and-suspenders so the fix doesn't silently break if the global default-terminal GUIDs get reset by a Windows update.

### Problems Encountered

- Elevated CLI starved Wispr's global hotkey hook (UIPI) → fixed by elevating Wispr via scheduled task.
- Legacy conhost dropped Wispr's clipboard-paste text insertion → fixed by routing Claude through Windows Terminal.
- The current elevated session was an anomaly: the shortcut's Run-as-admin flag read False, so normal double-click launches are non-elevated; this session had been manually elevated. Did not force elevation back on.

### Configuration Changes

- **Created:** Scheduled task `Wispr Flow (Elevated)` (AtLogOn, Interactive, RunLevel Highest, action = `C:\Users\guru\AppData\Local\WisprFlow\Wispr Flow.exe`).
- **Renamed (disabled):** `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\Wispr Flow.lnk` → `Wispr Flow.lnk.disabled`.
- **Registry (HKCU):** `HKCU:\Console\%%Startup` — `DelegationConsole = {2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}`, `DelegationTerminal = {E12CFF52-A866-4C77-9A90-F570A7AA2C6B}` (Windows Terminal as default terminal app).
- **Modified:** `C:\Users\guru\Desktop\Claude.lnk` — Target now `...\WindowsApps\wt.exe`; Args `-d "D:\claudetools" "C:\Users\guru\.local\bin\claude.exe" --chrome --dangerously-skip-permissions`; WorkingDir `D:\claudetools`; Icon `claude.exe,0`.
- **Backup:** `C:\Users\guru\Desktop\Claude.lnk.bak` (original shortcut).

### Infrastructure & Servers

- Host: **GURU-5070** (Mike's workstation), Windows 11 Pro 10.0.26200.
- Wispr Flow install: `C:\Users\guru\AppData\Local\WisprFlow\` (v1.5.559); stable stub `Wispr Flow.exe`, updater `Update.exe`, helper at `resources\Release\Wispr Flow Helper.exe`.
- Windows Terminal 1.24.11321.0 (Store/AppX); `wt.exe` alias `C:\Users\guru\AppData\Local\Microsoft\WindowsApps\wt.exe`.
- Claude Code CLI binary: `C:\Users\guru\.local\bin\claude.exe`.

### Commands & Outputs

- Elevation check: `WindowsPrincipal.IsInRole(Administrator)` → True (legacy session).
- Console window class: `ConsoleWindowClass`, title `Administrator: ...` (legacy conhost confirmed).
- Token integrity probe (advapi32 GetTokenInformation, TokenIntegrityLevel=25): Wispr Flow PID → `HIGH (elevated)` after relaunch.

### Pending / Incomplete Tasks

- **User confirmation pending:** Mike must close the current session and relaunch from the Desktop shortcut (opens in Windows Terminal), then test the Wispr hotkey to confirm text now inserts.
- If text still fails in WT: check Wispr's text-insertion method (prefer keystroke/type over clipboard paste if offered) and verify WT's Ctrl+V paste binding is default.

### Reference Information

- Revert shortcut: delete `Claude.lnk`, rename `Claude.lnk.bak` → `Claude.lnk`.
- Revert default terminal: Settings → Privacy & security → For developers → Terminal → "Let Windows decide".
- Revert Wispr elevation: `Unregister-ScheduledTask "Wispr Flow (Elevated)"`; rename `Wispr Flow.lnk.disabled` → `Wispr Flow.lnk`.
- Windows Terminal delegation GUIDs: Console `{2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}`, Terminal `{E12CFF52-A866-4C77-9A90-F570A7AA2C6B}`.

---

## Update: Reverted Wispr Flow elevation; fixed Claude shortcut elevation error (GURU-5070)

Mike asked to undo the Wispr Flow dictation-fix changes and have the Claude shortcut run `wt` without elevation.

### Actions

- **Unregistered scheduled task** `Wispr Flow (Elevated)` — Wispr no longer auto-starts at High integrity.
- **Restored non-elevated Wispr autostart:** a normal `Wispr Flow.lnk` (non-elevated, targets `...\WisprFlow\Wispr Flow.exe`) was already present in the Startup folder, so removed the now-redundant `Wispr Flow.lnk.disabled` duplicate.
- **Reverted default-terminal delegation** in `HKCU:\Console\%%Startup`: `DelegationConsole` and `DelegationTerminal` set back to `{00000000-0000-0000-0000-000000000000}` ("Let Windows decide").
- **Claude shortcut left on `wt`, non-elevated:** `Claude.lnk` already targeted `wt.exe -d "D:\claudetools" "...\claude.exe" --chrome --dangerously-skip-permissions` with RunAsAdmin = False — no change needed. Claude still opens in Windows Terminal because the shortcut invokes `wt.exe` explicitly, independent of the reverted global default.

### Root cause of the `0x800702E4` ("requires elevation") error

After the revert, launching the shortcut threw `ERROR_ELEVATION_REQUIRED` on `claude.exe --chrome --dangerously-skip-permissions`. Cause: per-user **Run as administrator** AppCompat flags set during the earlier elevated session in `HKCU:\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers`:

- `C:\Users\guru\.local\bin\claude.exe` = `~ RUNASADMIN GDIDPISCALING DPIUNAWARE`
- `C:\Users\guru\AppData\Local\Microsoft\WindowsApps\wt.exe` = `~ RUNASADMIN`

These force elevation on every launch, so a non-elevated shortcut failed.

**Fix:**
- `claude.exe` → stripped `RUNASADMIN`, kept DPI flags → `~ GDIDPISCALING DPIUNAWARE`
- `wt.exe` → removed the `RUNASADMIN` value entirely

Both now launch non-elevated. Pending Mike's confirmation that relaunching from `Claude.lnk` opens WT without the elevation error.

### Reference (re-revert / re-apply)

- Re-add RUNASADMIN (if ever needed): set the Layers value back to include `RUNASADMIN` for the exe path.
- Re-elevate Wispr: re-create the `Wispr Flow (Elevated)` scheduled task (AtLogOn, RunLevel Highest, action = `...\WisprFlow\Wispr Flow.exe`) and disable the Startup `Wispr Flow.lnk`.
- The AppCompat Layers key is HKCU — editable without elevation.

---

## Update: Claude taskbar/profile icon via dedicated Windows Terminal profile (GURU-5070)

Mike asked whether the taskbar could show the Claude icon instead of the generic Windows Terminal icon when launching via `wt.exe`.

### Constraint

A shortcut's icon only brands the `.lnk` itself. Once `wt.exe` is the running process, the taskbar button belongs to Windows Terminal — it uses WT's window icon and groups under WT's AppUserModelID (AUMID). A `.lnk` icon cannot override that. The controllable lever is WT's **per-profile `icon`**; on WT 1.24 the window/taskbar icon follows the active profile's icon. Taskbar *grouping* is still keyed to WT's AUMID and cannot be changed without leaving `wt.exe` for a terminal that owns its own identity (e.g. WezTerm/Alacritty).

### Actions

- **Extracted the Claude icon** from `C:\Users\guru\.local\bin\claude.exe` (index 0) to a 256x256 PNG at `C:\Users\guru\.local\bin\claude-icon.png` via `user32!PrivateExtractIcons`.
- **Added a dedicated "Claude" profile** to WT `settings.json` (backed up to `settings.json.bak`):
  - guid `{9cbe74ea-2403-4e9e-89aa-23780ee3c19a}`
  - `commandline`: `"C:\Users\guru\.local\bin\claude.exe" --chrome --dangerously-skip-permissions`
  - `startingDirectory`: `D:\claudetools`
  - `icon`: `C:\Users\guru\.local\bin\claude-icon.png`
- **Repointed `Claude.lnk`** to `wt.exe` with args `-p Claude` (profile now carries the command, dir, and icon). Still non-elevated; `IconLocation` left at `claude.exe,0`.
- Validated `settings.json` parses as JSON after the edit.

### Result / Pending

- Tab icon, new-tab dropdown, and WT jump-list → Claude icon (reliable).
- Taskbar button → expected to show the Claude icon on WT 1.24, but **pending Mike's visual confirmation** (WT taskbar-icon behavior varies by version). If it stays the generic WT logo, fallback options are a WezTerm/Alacritty terminal with a Claude icon, or a custom-AUMID experiment.

### Reference (revert)

- Restore WT settings: copy `settings.json.bak` over `settings.json` (path: `%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\`).
- Revert shortcut to inline launch: set `Claude.lnk` Args back to `-d "D:\claudetools" "C:\Users\guru\.local\bin\claude.exe" --chrome --dangerously-skip-permissions`.
- Icon source PNG: `C:\Users\guru\.local\bin\claude-icon.png` (safe to delete if profile removed).

---

## Update: 15:25 PT — GuruConnect v2 production deploy + hotfixes + client-IP investigation

### Session Summary (this update)
Deployed GuruConnect v2 to production (172.16.3.30, public connect.azcomputerguru.com) and resolved three post-deploy issues. The deploy was manual: the server (hostname gururmm) builds its own Linux binary (rust 1.94.1 + node 20 present via login shell). Recon established the model (systemd service guruconnect, EnvironmentFile server/.env, sqlx migrations auto-run on startup and are embedded in the binary at compile time, SPA served from server/static/app). The server's local git main had diverged from origin (v2 greenfield rewrote history), so reset --hard origin/main to 96b4fd7. Backed up the DB first, built the SPA + binary, set CONNECT_TRUSTED_PROXIES, confirmed the installed unit has no WatchdogSec (so v2 without sd_notify won't restart-loop; do NOT run setup-systemd.sh which would re-add it), restarted, migrations 004/005/006 applied, smoke-tested local + public.

Hotfix 1 (tags decode bug): a startup WARN "Failed to reconcile managed sessions: column tags unexpected null" was a real bug — connect_machines.tags (text[] nullable, no default) had 6 NULL rows, decoded by the derived FromRow as non-Option Vec<String>. Hot-patched the data (NULL->'{}'), then fixed properly: manual FromRow making all nullable-non-Option columns NULL-tolerant + migration 007 (backfill, DEFAULT '{}', NOT NULL). Code Review APPROVE, committed abc55ab, redeployed; migration 007 confirmed applied (tags nullable=NO default='{}').

Login problem: neither admin nor howard could log into the portal. Diagnosed — both accounts were enabled with valid Argon2 hashes and the login API worked end-to-end (verified 401 on wrong password via localhost AND public NPM), so it was wrong passwords, not a code/flow bug. With Mike's authorization, reset both passwords using python argon2-cffi (verify is param-agnostic, so server-compatible), verified login returns 200 local + public.

Client-IP investigation: the relay logged repeated agent rejections "from 172.16.3.20" (agent_id 795cbc06, invalid API key, every ~5s). Ruled out the gururmm-agent container (stop-test: rejects continued). Mike identified DESKTOP-I66IM5Q as the Pavon (Raiders site) external client machine (GeoVision box, public 98.172.64.243). Root cause: the Pavon agent connects via the public URL through Jupiter's NPM (172.16.3.20); since 172.16.3.20 was not in CONNECT_TRUSTED_PROXIES (had wrongly trusted .30), the relay logged the proxy hop instead of reading X-Forwarded-For. Fixed: set CONNECT_TRUSTED_PROXIES=127.0.0.1,::1,172.16.3.20, redeployed; reject log now shows the real client IP 98.172.64.243. Remaining: the Pavon agent's old shared key is invalid post-cutover and needs re-enrollment (client-side todo filed).

### Key Decisions (this update)
- Build on the server itself (rust+node present) rather than cross-build; reset --hard origin/main since the server's divergent local commit (1bfd476) was a stale local-only commit, SHA saved.
- Did NOT run setup-systemd.sh: the installed unit lacks WatchdogSec, which is correct for v2 (no sd_notify); the repo unit would re-add it and cause a 30s restart loop.
- Build the binary while v1 stayed running (atomic rename is safe), restart as the cutover, to avoid downtime on a build failure.
- Reset passwords via python argon2-cffi rather than a server CLI (none exists); argon2 verify is param-agnostic so any argon2id PHC hash is accepted.
- CONNECT_TRUSTED_PROXIES must trust Jupiter NPM (172.16.3.20), not the relay host (.30) — the public ingress proxy is on Jupiter.
- Tags migration 007 sets NOT NULL after backfill: safe because no writer inserts NULL (upsert omits tags -> default; metadata update binds a non-null array).

### Problems Encountered (this update)
- git pull --ff-only refused (divergent history from the greenfield respec) -> git reset --hard origin/main (clean tree, rollback SHA saved).
- npm ci/build failed initially because the tree was still v1 (pull had aborted); resolved after the reset to v2.
- NULL-tags reconcile WARN -> data hot-patch + code fix (007) -> redeploy.
- Login appeared broken; proved the API works (401 on bad pw, both paths) -> it was wrong passwords -> reset.
- Reject "from 172.16.3.20" misattribution: not gururmm-agent (stop-test), not a Jupiter VM (Mike rejected the suspend), not NPM HTTP/stream config found by grep; root cause was the trusted-proxy misconfig truncating the real client IP at the Jupiter NPM hop. Fixed by trusting 172.16.3.20.
- Bash CWD drifted into the submodule (SSH/git work) breaking the relative whoami-block.sh path; re-ran from /d/claudetools.

### Configuration Changes (this update)
- guru-connect submodule: abc55ab (fix: NULL-tags decode + migration 007). Files: server/src/db/machines.rs (manual FromRow), server/migrations/007_fix_machine_tags_null.sql.
- Parent claudetools pointer commits (local, unpushed): 56ce575 (Users view, earlier), 40a2eb4 (tags fix). Plus this session log + memory edits.
- Server (172.16.3.30) /home/guru/guru-connect: reset to abc55ab; server/.env CONNECT_TRUSTED_PROXIES=127.0.0.1,::1,172.16.3.20; rebuilt binary; SPA at server/static/app; service restarted (now v0.2.1).
- DB: migration 007 applied; connect_machines.tags now NOT NULL DEFAULT '{}'; admin+howard password_hash reset.
- Memory: .claude/memory/project_guruconnect_deploy.md created + corrected (trusted-proxy = Jupiter 172.16.3.20).

### Credentials & Secrets (this update)
- GuruConnect portal (connect.azcomputerguru.com), reset 2026-05-30 (change on first login):
  - admin : WNwKn-qp4eW-jkXAs
  - howard : iWACa-Ks5PP-nrP6x

### Infrastructure & Servers (this update)
- GuruConnect server: VM "GuruRMM" = host gururmm = 172.16.3.30:3002 (systemd guruconnect, WorkingDirectory /home/guru/guru-connect/server, EnvironmentFile server/.env, binary target/x86_64-unknown-linux-gnu/release/guruconnect-server). Postgres 14 db guruconnect on localhost.
- Public ingress: connect.azcomputerguru.com -> Jupiter NPM (172.16.3.20, jc21/nginx-proxy-manager) -> 172.16.3.30:3002. Trust 172.16.3.20 for client-IP extraction.
- Jupiter (172.16.3.20, Unraid): runs NPM + VMs (GuruRMM, GuruConnect, Claude-Builder=Pluto, Unifi, OwnCloud) + the gururmm-agent container (host networking; NOT the reject source).
- DB backup: /home/guru/backups/guruconnect/pre-v2-cutover-20260530-213507.sql.gz; v1 binary ~/guruconnect-server.v1.bak; rollback commit /tmp/gc-rollback-commit.txt (1bfd476).
- Pavon/Raiders client machine DESKTOP-I66IM5Q: public 98.172.64.243, private 192.168.0.10, GeoVision box, GuruConnect agent_id 795cbc06 (stale key).

### Pending / Incomplete Tasks (this update)
- Re-enroll/re-key the Pavon DESKTOP-I66IM5Q GuruConnect agent (old shared key invalid post-cutover) - todo filed.
- Mike to log into connect.azcomputerguru.com and validate all four dashboard views against live data.
- claudetools parent: unpushed pointer commits (56ce575, 40a2eb4) + memory edits + this log -> this /save pushes them.
- Open server todos: viewer-token revocation on logout, multi-instance DB single-use gate, agent clippy in CI, support-code expires_at, users authz gaps (self-demotion/last-admin guards), H.264 go-live gating.

### Reference Information (this update)
- guru-connect submodule HEAD: abc55ab. Server component: deployed v0.2.1.
- Deploy memory: .claude/memory/project_guruconnect_deploy.md.
- Verified reject log post-fix: "Agent connection rejected: 795cbc06-... from 98.172.64.243 - invalid API key".

---

## Update: 00:56 PT — GuruConnect/GuruRMM feature specs, RMM CI docs-guard, GC v2 sprint planning

### Session Summary

Started with a clean `/sync` (both repos already in sync). Then handled an infra request: a Pavon machine was hammering the GuruConnect relay with auth failures. Used `/rmm` to identify the offending endpoint — only `DESKTOP-I66IM5Q` (Pavon/Raiders, external IP 98.172.64.243) carried the GuruConnect client; the Curves box did not. Removed it cleanly (killed the running `guruconnect-pavon-raidersreef` process, deleted the GeoVision HKCU Run-key entry, the desktop launcher, and the `C:\Program Files\GuruConnect` copy). Mike pushed back twice that I should match the offending IP to the agent rather than reconning every candidate; the RMM agent record carries no IP fields at all, which became GuruRMM todo `7459428e` (capture local + external agent IPs). Saved a feedback memory.

Answered a Claude Code question (Windows Snipping-Tool clipboard images no longer paste with Alt+V — a confirmed DIB-vs-CF_HDROP regression; copied image files still work). Drafted a `/feedback` writeup and a GitHub issue; Mike submitted the feedback.

Filed a large batch of feature requests as researched specs. GuruConnect (via `/gc-feature-request`): SPEC-003 machine inventory, SPEC-004 stable machine identity + session lifecycle reaping + operator removal, SPEC-005 machines list view (dual Host/Guest indicators + rich rows), SPEC-006 universal machine search, SPEC-007 managed-agent installer builder, SPEC-008 valuable error messages, SPEC-009 feature-rich documented API. GuruRMM (via `/feature-request`): SPEC-018 valuable error messages, SPEC-019 feature-rich documented API, SPEC-020 migrate CI/CD from webhook+shell to Gitea Actions. Pushed everything to Gitea.

Implemented the SPEC-020 Phase-0 interim fix live: added a docs-only build guard to the GuruRMM build webhook handler (`/opt/gururmm/webhook-handler.py` on 172.16.3.30) so pushes touching only `docs/`, `*.md`, `.claude/`, `session-logs/` skip the build. Patched on-host (a local `/tmp` path-mapping bug made the edit round-trip unreliable), backed up the original, unit-tested 12 cases + syntax-checked before deploy, restarted the service, and verified live (a real docs push and a test POST both returned "build skipped", no build locks). Recorded a project memory.

Closed with GC sprint planning. Mike chose "v2 reset first." While scoping Sprint 0 (the 3 relay-auth CRITICAL hotfix), discovered from the git log and the running server that v2 Phase 1 (secure-session-core Tasks 1-7) is ALREADY implemented and DEPLOYED, and the 3 CRITICALs are already closed in production. The roadmap banner written minutes earlier (claiming the bypasses were live) was wrong; corrected it and re-baselined. Created a 5-task tracked list for the actual remainder (verification + code review, not building).

### Key Decisions

- Accepted Mike's correction to identify the offending Pavon endpoint by matching the known external IP rather than reconning all candidates; root-caused that GuruRMM stores no agent IPs and filed todo 7459428e.
- For SPEC-004, made stable machine-derived identity (deterministic `machine_uid`, MachineGuid-based, bound to the per-agent key) the PRIMARY fix per Mike — reaping/removal became defense-in-depth. Flagged that a client-asserted hash is spoofable and must be auth-bound.
- RMM CI: chose the minimal host-script path guard (Phase 0) over migrating to Gitea Actions immediately; the full migration is SPEC-020. Guard is fail-safe toward building (skips only when every changed file is provably non-buildable).
- GC direction: v2 reset first (Mike). Then corrected course on discovering Phase 1 is already done — the planned Sprint 0 CRITICAL hotfix was a no-op. Re-scoped to verification.
- Did NOT patch the stale repo copy `scripts/webhook-handler.py` (109 lines vs deployed 206) — would have triggered a wasteful build and implied maintenance it lacks. Host is source of truth until SPEC-020.

### Problems Encountered

- Local `/tmp` path mismatch: the editor tools and the Bash shell resolved `/tmp/webhook-handler.py` to different physical files, so `pscp` uploaded the un-edited copy. Resolved by patching the file on-host via a Python script piped over SSH stdin.
- `py_compile` failed writing to root-owned `/tmp/__pycache__` — used `python3 -B` / `ast.parse` instead.
- importlib refused a `.py.new` extension; tested via `exec(open(...).read(), ns)` into a namespace.
- Roadmap banner factual error (CRITICALs "live") — self-introduced from the stale 2026-05-29 audit narrative; caught by reading the actual relay code + git log, then corrected.

### Configuration Changes

- guru-connect repo: added `docs/specs/SPEC-003..009*.md`; edited `docs/FEATURE_ROADMAP.md` (entries, v2-first banner, then v2 re-baseline correction).
- guru-rmm repo: added `docs/specs/SPEC-018/019/020*.md`; edited `docs/FEATURE_ROADMAP.md`.
- claudetools (parent): submodule pointer bumps for both; new memories `.claude/memory/feedback_rmm_identify_by_ip.md`, `.claude/memory/project_rmm_webhook_docs_guard.md`; updated `.claude/memory/MEMORY.md`.
- BUILD HOST 172.16.3.30 (NOT in git): `/opt/gururmm/webhook-handler.py` patched with the docs-only guard; backup `/opt/gururmm/webhook-handler.py.bak-20260530-guard`.
- Endpoint `DESKTOP-I66IM5Q`: removed GuruConnect client (Run-key, desktop exe, Program Files dir).

### Credentials & Secrets

- Build/host SSH used: `guru@172.16.3.30:22` — already vaulted at `infrastructure/gururmm-server.sops.yaml` (sudo password same as SSH). No new secrets created.
- RMM API admin creds: `infrastructure/gururmm-server.sops.yaml` `credentials.gururmm-api.*`.
- Gitea webhook secret `gururmm-build-secret`: `projects/gururmm/ci-cd.sops.yaml`.

### Infrastructure & Servers

- 172.16.3.30 (Ubuntu 22.04) — hosts BOTH GuruConnect (`guruconnect.service`, listening :3002, deployed checkout `abc55ab`) and GuruRMM (server :3001, build host). GuruRMM build webhook: `gururmm-webhook.service` → `/opt/gururmm/webhook-handler.py` (binds 127.0.0.1:9000, nginx proxies `/webhook/build`); per-platform builds via `build-shared.sh` + `build-{linux,windows,mac}.sh`; Pluto (172.16.3.36) does the Windows/MSI build over SSH.
- Gitea internal: `http://172.16.3.20:3000` (preferred on-network).
- GC v2 secure-session-core: Tasks 1-7 committed; CRITICALs closed in deployed prod (verified `abc55ab` descends from CRITICAL#1 fix `a453e79` + Task 7 `f9bdecb`).

### Commands & Outputs

- RMM GuruConnect removal verified: `guruconnect procs running: none`, Run-key gone, files deleted.
- Webhook guard live test: docs-only POST → `Docs-only change -- build skipped`; non-main ref → `Ignored push`; no build locks; `last-built-commit` unchanged (`ef0830f`).
- GC prod check: `guruconnect.service active running`, `ss -tlnp` shows `:3002 guruconnect-ser pid 1287186`.

### Pending / Incomplete Tasks

Tracked list (TaskCreate #1-5) — the real GC Phase-1-exit remainder:
1. Code-review secure-session-core Tasks 3-5 (pending review; written without a compiler, since built+deployed). Highest priority.
2. Security re-audit — `/gc-audit --pass=security` + 4 manual CRITICAL checks.
3. Functional verification — consent flow, key fidelity (Win+R/clipboard/Ctrl+Alt+Del/no stuck modifiers), rate limiting, fresh-DB migrations. Needs a real Windows desktop.
4. Live HW-H.264 validation — GPU needed on the AGENT (encode: QuickSync/NVENC/AMF) and the VIEWER (decode); server needs NO GPU. Then flip `DEFAULT_PREFER_H264`. Non-blocking (raw is default).
5. Retire deprecated shared `AGENT_API_KEY` fallback — GATED on confirming zero agents depend on it.

Other open threads:
- SPEC-020 (RMM CI → Gitea Actions) staged as a spec; Phase-0 guard is live. Ratify as RMM ADR-009 when started.
- GuruRMM todo `7459428e` — capture agent local + external IPs.
- GC SPEC-003..009 fold into v2 Phase 2/3 (annotated on the roadmap).

### Reference Information

- GC commits: SPEC-003 `abf499c` → SPEC-009 `7ab8738`; roadmap v2 banner `03f62d4`; v2 re-baseline `786d3e4`.
- RMM commits: SPEC-018 `be2b6f0`, SPEC-019 `ef0830f`, SPEC-020 `950fa08`.
- GC secure-session-core: plan at `specs/v2-secure-session-core/plan.md`; Tasks: 1 `fef8111`, 2 `41691bf`, 3 `0f25878`, CRITICAL#1 split `a453e79`, 4 `bfcdbb5`, 5 `9082e11`, 6 `bb73ba6`, 7 `f9bdecb`.
- Pavon/Raiders endpoint: `DESKTOP-I66IM5Q`, WAN 98.172.64.243; Curves: `DESKTOP-VRBQ6LM`, WAN 174.78.94.186 / LAN 192.168.1.128 / MAC 04:42:1A:0C:8C:A6.
- Claude Code clipboard regression: Alt+V is the correct Windows binding; DIB bitmap (Snipping Tool) fails, CF_HDROP file paste works; CLI v2.1.158.
