PRD-234 — Cross-OS journal source
Status: ✅ done (2026-05-08) ·
completion_pct: 100·latest_log_id: 15· 5 phases shipped across 2 repos · daemon tagsv0.35.0/v0.36.0/v0.37.0. The full PRD lives atn1-cloud/docs/prd/PRD-234-2026-05-08-rename-windows-tab-journal-cross-os/PRD-234.md.
Problem
Section titled “Problem”The box-detail page in n1-cloud/packages/web/src/features/boxes/pages/BoxPage.tsx previously exposed ?tab=windows rendering a WindowsEventsTab component that read the PRD-136 windows_event Cloudflare Analytics Engine dataset — populated exclusively by the n1-cloud-daemon’s n1-cloud-daemon/internal/wintelemetry/ ETW listener.
Two surface-level consequences justified the rename:
- Operator-visible asymmetry. The “Windows” tab label was OS-specific even though most of the surrounding chrome (composite-health badge, capability install, oracle drift) is OS-agnostic. Linux operators didn’t see the tab; Windows operators did.
- Pipeline-internal naming drift. The PRD-136 daemon → AE pipeline (
windows_eventdataset,WINDOWS_EVENT_AEbinding,subscribeWindowsEvents()ETW listener, api-worker route/api/realtime/windows-events) was structurally identical to what a Linuxjournalctlfollower / macOSlog streamfollower / FreeBSDtail /var/log/messagesfollower would need — but the names anchored the write-path to the Windows-only origin.
Solution
Section titled “Solution”Hard-cut rename — no ?tab=windows redirect — landed across five PRs (Phase 0 + A/B/C/D), each running the greploop pre-merge gate per PRD-0/DO-23 .
Architecture
Section titled “Architecture”Wire shape (cross-repo contract)
Section titled “Wire shape (cross-repo contract)”The TypeScript zod schema lives at n1-cloud/packages/types/src/journal-event.types.ts . The Go struct in the daemon at n1-cloud-daemon/internal/wintelemetry/types.go mirrors the JSON tags byte-for-byte; its name WindowsEventFrame is retained on purpose to avoid churn in dependents — only the TS module was renamed in PRD-234. New cross-OS callers use JournalEventFrame in TypeScript and wintelemetry.WindowsEventFrame{EventClass: ClassJournal, …} in Go.
Per-OS dispatch (build-tag, not runtime probe)
Section titled “Per-OS dispatch (build-tag, not runtime probe)”The daemon’s internal/journal/ package uses Go build tags so each binary ships with exactly one source path linked in:
-
n1-cloud-daemon/internal/journal/start_linux.go—//go:build linux. Single restart loop choosing journalctl (when systemd is running + binary on PATH) ortail -Fof the syslog path probed at startup. -
n1-cloud-daemon/internal/journal/start_darwin.go—//go:build darwin. Single source:log stream --style ndjson --info. -
n1-cloud-daemon/internal/journal/start_freebsd.go—//go:build freebsd. MultiSource: two independent restart-loop goroutines (one fortail -F /var/log/messages, one fordmesg -w) feeding the same channel. -
n1-cloud-daemon/internal/journal/start_stub.go—//go:build !linux && !darwin && !freebsd. No-op fallback. - Windows side preserved:
n1-cloud-daemon/internal/wintelemetry/forwarder.go:126POSTs to the new cross-OS endpoint/api/daemon/journal.
Restart-loop contract
Section titled “Restart-loop contract”Every per-OS source runner uses exec.CommandContext(ctx, …) (which installs an internal kill-on-cancel goroutine) and defer cmd.Wait() to reap zombies. No manual go func() { <-ctx.Done(); cmd.Process.Kill() }() — that was Greptile’s P1 finding on Phase B’s PR #2 (the manual goroutine leaked one entry per restart cycle when the child exited naturally before ctx cancelled). The fix landed at commit n1-cloud-daemon/internal/journal/journalctl_linux.go:26 with the rationale comment future agents will hit when they re-add the redundant goroutine.
Control plane
Section titled “Control plane”- AE binding renamed:
n1-cloud/packages/api-worker/wrangler.jsonc— bindingJOURNAL_EVENT/ datasetjournal_event(replacesWINDOWS_EVENT_AE/windows_event). - Ingest route:
n1-cloud/packages/api-worker/src/admin/journal.lib.ts+ the/api/daemon/journalmount inn1-cloud/packages/api-worker/src/index.ts. - Admin read:
GET /api/admin/boxes/:id/journal-events(replaces/api/admin/boxes/:id/windows-events). - MCP tool renamed:
n1-cloud/packages/n1-mcp/src/tools/admin/journal-events.tool.tsexposesn1_admin_journal_events(wasn1_admin_windows_events).
Frontend
Section titled “Frontend”- Box-page tab slug
journal(loader returns 404 for?tab=windows):n1-cloud/packages/web/src/features/boxes/pages/BoxPage.tsx. - Tab component:
n1-cloud/packages/web/src/features/boxes/components/JournalTab.tsx(wasWindowsEventsTab.tsx). - Service:
n1-cloud/packages/web/src/lib/services/journal-events.service.ts.
Phased ship
Section titled “Phased ship”| Phase | PR | Merge | Daemon tag |
|---|---|---|---|
| 0 (FreeBSD type-system widening + raise) | n1-cloud#128 | f61ff19d | — |
| A (web tab rename) | n1-cloud#134 | f861fa87 | — |
| B (control plane + Linux journal) | n1-cloud#138 + n1-cloud-daemon#2 | 0d26575a + 1267511 | v0.35.0 |
| C (macOS Apple Unified Logging) | n1-cloud-daemon#3 | 5d64250 | v0.36.0 |
| D (FreeBSD MultiSource: tail + dmesg) | n1-cloud-daemon#5 | b032b31 | v0.37.0 |
Every PR cleared greploop with Confidence Score: 5/5 + zero unresolved review threads before squash-merge. No --admin. Notable findings fixed inside the loop:
- Phase B (n1-cloud-daemon): kill-goroutine leak in
runJournalctl+runSyslogTail(P1) + UTF-8-safe truncation on the raw-line cap. - Phase B (n1-cloud):
buildJournalEventsSqlwas outside thetryblock injournal.lib.ts— a malformed ULIDboxIdraisedRangeErrorand leaked as HTTP 500 instead of returning a structured warning. Moved inside thetry(P1). - Phase C: Apple’s NDJSON timestamp layout uses variable-precision fractional seconds (
.999999, not.000000) — initial parse failure rate was ~30% on test data. Fixed atn1-cloud-daemon/internal/journal/decode_darwin.go. Also addeddefer cmd.Wait()to avoid zombielog streamprocesses on shutdown (P1). - Phase D:
dmesg -won FreeBSD replays the entire kernel ring buffer on every restart (no-n 0equivalent). Mitigated via a 500 ms quiet-period drain atn1-cloud-daemon/internal/journal/dmesg_freebsd.gomatching the from-now semantics of Linuxjournalctl -f -n 0+ macOSlog stream.
Notes on decisions made without asking
Section titled “Notes on decisions made without asking”Verbatim from
n1-cloud/docs/prd/PRD-234-2026-05-08-rename-windows-tab-journal-cross-os/PRD-234.md.
- README description blocks for PRD-234 and PRD-235 were re-authored from the conversation summary + worktree README content after the original raise files were lost (transcript-loss recovery; see L-5 in the implementation log if filed). The substantive content (path-fork answers, locked DOs/DONTs, phased plans) is anchored verbatim from the summary.
- AE dataset rename
windows_event→journal_eventchosen over additivejournal_event+ retain-windows_event. Bootstrap-mode +No deprecations tolerated; PRD-136 has no consumer outside the renamedJournalTabsurface in this PRD. - Linux non-systemd fallback to
tail -F /var/log/messageschosen over rejecting non-systemd distros. Alpine + Gentoo OpenRC remain bookable Linux variants; failing-loud here would be friction without justification. - No metric / event-volume estimate in this PRD’s Costs lens — AE pricing is per-row + per-byte, and the current
windows_eventdataset is empty in production (verified viawrangler analytics-engine sql --query "select count(*) from windows_event"); will be confirmed during Phase B rollout.
Operator-deferred follow-ups
Section titled “Operator-deferred follow-ups”The completion gate at n1-cloud/docs/prd/PRD-234-2026-05-08-rename-windows-tab-journal-cross-os/PRD-234.completion-gate.md tracks five rows still ⏳ (audit-trail follow-ups, not blockers per AGENTS.md “When a PRD is Done”):
- R2 / R3 / R4 — per-OS production smoke (Linux + macOS + FreeBSD) once live boxes are accessible. The acceptance query is
wrangler analytics-engine sql --query "select count(*) from journal_event where blob3='<os>' and timestamp > now()-INTERVAL '5' MINUTE"returning ≥1 within 60s of n1d start. - E2 — Worker-side AE-shape regression test on
JOURNAL_EVENT. Wire shape today is locked by the zod schema injournal-event.types.tswhich the api-worker route validates inbound, but a dedicated regression test would catch silent schema drift. - F2 — cross-OS unfiltered-log volume (Greptile flagged unfiltered
log streamingestion on macOS as a future cost concern;journalctl -fhas the same property on Linux). Raise a separate PRD if AE row volume becomes operationally expensive.
Follow-up PRD
Section titled “Follow-up PRD”PRD-235 ships native FreeBSD pkg(8) install paths for the 69 platform-seeded capability_catalog rows. Until PRD-235 lands, FreeBSD boxes can be booked / provisioned / SSH’d / rendered through every UI surface, but capability deploys return the dispatcher’s fail-loud HTTP 422 — that’s the right posture per PRD-234/DO-8.