Design system phases 1–3, and a core revision that reshaped phase 3 - #23
Open
guan4tou2 wants to merge 25 commits into
Open
Design system phases 1–3, and a core revision that reshaped phase 3#23guan4tou2 wants to merge 25 commits into
guan4tou2 wants to merge 25 commits into
Conversation
Starting phase 3 with the primitives, so the eighteen items that follow do not each invent their own. **Empty states get their third part.** Every empty view was a grey circle and two lines of grey text with no way out — "Screenshots will appear here when captured" tells an operator exactly what they had already worked out from the empty screen. §5.4 wants what will appear, *why there is nothing yet*, and one button. The middle one is where the work is: "no screenshots yet" is the observation they just made; "periodic capture is off and you have not taken one by hand" is the reason, and it implies the fix. Screenshots, Loot, Targets, Marks and Transcript convert here. **Cards move their colour to the left.** The 2px bar across the top made the card itself the status indicator, so a screen of cards read as a wall of colour with nothing standing out — including the one that was actually shouting. Colour goes into a block on the left wrapped around the icon, 12% tint like the badges, solid only for danger. The card's background stays `surface` and never changes with state. Also fixes a live `ReferenceError` that predates this branch: the transcript's ↗ button called `onNavigate` inside App, where only `setView` exists. That is §7's transcript ↔ timeline link — one of the five cross-view routes phase 3 is here to complete — and it threw on every click. `test/tailwind-classes.test.ts` is new because I shipped two dead classes within an hour writing the card: `border-redlog-border-strong` (a token the phase 1 palette collapsed away) and `text-wrap-pretty` (the CSS property name; Tailwind spells it `text-pretty`). Tailwind drops what it does not recognise without a warning, so both looked right in review and did nothing on screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…button **The sidebar order is fixed and the numbers are printed** (§5.3). Those two changes depend on each other: while rows could be dragged, the number was a property of the operator's current arrangement rather than of the view, so printing it would have taught the wrong thing — and ⌘2 had already drifted off the sidebar once, which is why the shared order module was written in the first place. Fixing the order is what lets the number be shown, and showing it is what makes the shortcut learnable without opening the ? panel. That deletes the drag gesture, the localStorage persistence, the change-subscription, and the state and effect in App that consumed it — about seventy lines, and with them a class of bug where what ⌘2 did and what the sidebar showed were maintained separately. **The status bar's ⏱ button goes** (§10). It wrote to the same table as a marker with neither a title nor a severity, so what it produced was a marker that says nothing — the operator has to remember what they meant by that timestamp, which is the job the marker dialog already does properly. ⌘⇧M reaches that from anywhere, including from another application. One note on verification: a full e2e run failed on `cli-smoke` after these changes, taking 7.4 minutes for a spec that takes 6 seconds. It passed in isolation and in two subsequent full runs, and nothing in this diff touches the CLI or the API server. I could not reproduce it and am not claiming to have explained it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
⌘K meant two different things depending on where you were: the Timeline's own fuzzy palette there, and a jump to the Search sidebar page everywhere else. So an operator had to know which surface they were on to know what the key would do — and the Search page itself occupied a whole navigation slot to provide full-text over events and nothing more, while navigation, project switching, density and every app-level action had no keyboard route at all. §10 collapses that. ⌘K is the same palette on every view and covers navigation, actions, density, project switching and event search. ⌘F takes in-page filtering, which is the convention every other desktop app has already taught; the Timeline's `/` still works, since there is no reason to take away the chord that view taught first. ⌘/ stays bound as an alias to the palette rather than being deleted, so the muscle memory lands somewhere useful instead of nowhere. The Search page and its component are gone — a view, nine strings and a sidebar slot. Two things the tests caught in my own work. The palette advertised ⌘9 against the ninth sidebar row, but ⌘9 is Settings' — pinned outside the numbered run — so it was offering a key that does something else, which is worse than offering none. And the chord was hardcoded to `⌘` regardless of platform, so every Windows operator would have been taught the wrong key by the surface built to teach them. The Timeline's scoped palette knows about lanes, operators and hosts in a way a database search does not, and it lost its chord when ⌘K went global. Rather than leave a working feature with no way in, the palette offers it as an action. Folding those three into the global search properly is still owed — §10 lists operator and host among what ⌘K should cover, and right now it reaches them only by that indirection. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five modules had grown their own copy of the platform check and four wrote it as `platform !== 'win32'`. On Linux that is true, so every Linux operator was shown `⌘` — a key their keyboard does not have — in the sidebar tooltips, the status bar, the Timeline and the `?` panel. `shortcuts.ts` was never wrong: it takes `isMac` as a parameter, which is exactly why its tests passed throughout. The error was in what every caller handed it, and no test looked there. The bug also survives being fixed in one place. I wrote the palette's version an hour ago as `=== 'win32' ? 'Ctrl+' : '⌘'` while removing a hardcoded `⌘`, which moves the mistake to the other side of the same check and still shows Linux a Command key. The question is not "is this Windows" — it is "is this a Mac", and only `darwin` answers yes. One module answers it now, and a test asserts nobody re-derives it. Operator search lands in the palette with it. §10 lists operator among what ⌘K covers, and unlike host it needs no aggregation and no loaded timeline — `operators:list` is a plain registry read, so this half had no reason to wait for the other. Choosing one filters the Timeline to what that person did. Revoked operators are left out: a revoked key cannot produce new events, so offering it as a filter promises a view that can only ever shrink. Host stays owed, and belongs with the SQL aggregation rather than with the palette — `data.host` lives inside event JSON, so only the Timeline can currently count it, and the global answer is the same `GROUP BY` the Targets page needs. They are one query with two consumers. Also drops a comment in `shortcuts.ts` still describing ⌘1..8 as following a drag-reorder that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l left Five things from the audit against 1c. **The sidebar is 186px.** It was 140px when it held an icon and a label; printing the number left a few pixels between label and number, and a loot badge squeezed all three. The whole value of printing the number is that it can be read, so the column has to fit. **Count badges stop filling.** Loot was `bg-amber-500 text-white` and scope violations solid too, so the sidebar carried two solid blocks — exactly the competition "one solid red per screen" exists to prevent. §4's badge rule is 12% tint plus same-colour text, and only danger fills. A count is not danger. **One red per state.** The active row drew its text as `red-400` (#d75f63) and its indicator bar as `red-500` (#cf5459) — two reds for one state, five units apart, which reads as a rendering artefact rather than a decision. Both are `redlog-accent` now. The raw `red-*` classes are legitimate elsewhere (the `soften` map remaps them), so this is about the inconsistency, not the namespace. **Empty states take screen-title weight** — 19px/15px rather than 15px/13px. Sized down they read as a caption for content that is not there. The action becomes §4's primary: solid accent, 34px, 8px radius. An empty state has exactly one button, which makes it the primary one by definition. **Residue from deleting the drag gesture**: `DRAG_THRESHOLD_PX`, `ITEM_STRIDE_PX` and a comment claiming persistence still lives in the shared module, none of which had any remaining referent. The Settings row also gains the `title`, `aria-label` and `aria-current` every other row already had — it had been the one row a screen reader could not place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…again Implements the §1 ruling. The old rule — brand red may only draw text or a hairline — drew the line on "does it have a background", which made the strongest control on a screen look like the weakest. "Install the shell hook" is the only way out of the dashboard's main question and an outline button cannot carry it. The line that works is verb versus state: a filled #d75f63 "Create project" reads as something you press, a filled #ff4d4f "IP exposed" reads as something being reported. So the four buttons I converted to outlines in phase 1 go back to solid accent — correctly, at the time, against a rule that was itself wrong. They land in a `Button` component rather than four more class strings. Two of those four were still carrying dead classes from before that conversion, fighting the ones beside them: a `text-white` under a `text-redlog-accent`, a `hover:bg-red-700` under an accent hover. Neither did anything, both looked deliberate, and one definition is what stops it recurring. Primary takes dark text (#16090a), which the synced §4 now specifies: white on #d75f63 is 3.68:1 and fails AA, #16090a is 5.3:1. That is a contrast decision rather than a taste one, so the exact value is written in rather than approximated with a Tailwind shade. The hard constraint from the ruling — never a filled primary beside a filled danger — is asserted against `ConfirmDialog`, the only surface that fills a button with danger and therefore the only place it could be broken. The 2px active-indicator bars stay accent-filled: a hairline is what §1 still permits, and the test distinguishes a line from a control by whether it has padding. Also syncs §1, §4 and §10 from the design project, including the new wordmark spec and the operator/host split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The danger button was the case I flagged, but it was never the case. Every
accent in this palette is mid-luminance, so white fails AA on all of them
— and the two I had not measured are the worst:
#d75f63 + white 3.68:1 #16090a 5.3:1
#ff4d4f + white 3.27:1 #1a0303 6.1:1
#d4ac5a + white 2.59:1 #1a1203 7.2:1
#5ecf9c + white 1.93:1 #03150d 9.7:1
#3fc7d6 + white 2.03:1 #031215 9.4:1
At 1.9:1 that is not low-contrast text, it is barely text. The rule that
follows is simpler than the one it replaces and needs no per-component
judgement: fill anything, and the text on it goes dark. Buttons, badges,
chips, progress marks — one rule.
Seventeen fills convert, and five `on-*` tokens land so the value is
named rather than repeated. The remaining four `text-white` uses were
plain text on a dark surface, where white was only ever an approximation
of `text` (#ececf0) from outside the token namespace; they take the token
too, which leaves the renderer with no `text-white` at all — an easier
invariant to hold than "no white on a fill".
Deliberately not: darkening #ff4d4f, whose luminance is exactly what
separates it from the brand red, and not a third red, which is the
problem §1 was written to end.
The test asserts both halves — that each dark clears 4.5:1, and that the
white it replaced did not — so the rule keeps its justification attached
rather than becoming a number someone later rounds off.
Phase 3 gains the wordmark as item 20; the app icon needs a vector
original and is not in any phase.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight tabs in a single 13px row was already hard to scan, and two of them — "Integrations" and "Data" — had drifted into meaning roughly the same thing. Underneath, Plugins held sub-tabs and those held publisher and revocation lists: three levels below the second level, when §10 allows two. The container had to come first. Thirteen categories fit down a side and do not fit across a top, so the content could not be split until there was somewhere to put it — which is why this lands before the pages themselves are filled out. A left list also does what a tab row cannot: the categories stay visible while a page is read, it takes group headings, and it has somewhere to put the search box §10 asks for. The eight blocks route onto the thirteen pages without being rewritten — the content was already separated into named panels, so this is wiring. "Data" becomes retention, chain, sharing and appearance, which is the split it had needed for a while. Plugins loses its sub-tabs. "What is installed" and "where to get more" are one task, so they are two sections of one page rather than two destinations. Two things caught in the process. The e2e specs reached settings by visible tab text (`/^data$/i`, `/^plugins$/i`), which cannot survive a category being split — they use the `data-settings-page` hook and an `openSettingsPage` helper now, the same shape as `openView`. And making the root a flex row turned the save bar, previously a sibling below the content, into a third column: it took 822px and squeezed the content pane to 35px. The right pane is a column of its own now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…quence `CORE_SOURCE_ORDER` was an ordered triple — shell hook, agent tailer, built-in terminal — with everything else declared "enrichment" and excluded from the dark→recording path outright. Two things were wrong, and the second is a bug rather than a modelling preference. The ordering claimed a sequence that does not exist. An operator running a proxied web assessment wires mitmproxy first and may never install a shell hook. And because mitmproxy was outside the path, the model reported `dark` while HTTP and DNS events were landing on the timeline. "Dark" means the timeline will stay empty; it was saying that about an app that was recording. A source that produces events is a source that produces events, whatever the onboarding copy calls it. So: three groups — commands, traffic, artefacts — with no order inside one, because the useful distinction was never rank but *what each source captures*, which is the choice an operator is actually making. All nine sources are in the model now, and `recording` means any of them is live. `nextStep` follows from state rather than position, since "first in the array" would be an arbitrary answer dressed as a recommendation. A `wired` source needs an event; a `todo` source needs an installation first. The wired one is the shorter route out of dark, which is the only thing this model is for — so it now points at the installed-but-quiet hook rather than skipping past it to the next unset source. The Dashboard drops the numbered `<ol>`. Numbering was the visual claim that the sequence existed. Three readiness tests and two render tests change to assert the new contract rather than being deleted — the old behaviour was real and tested, and the diff should show which properties were traded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The toolbar carried eight independent switches — collapse agent turns, compress idle gaps, session dividers, anomaly filter, auditor view, follow, timezone, lane visibility — with equal weight and no stated relationship between them. That is 256 states, and an operator who arrived at one had no way to tell whether it was a sensible place to be. They were never independent. They fall into three postures §6 names: working (collapsed, compressed, local time), audit (chained events only, UTC, nothing folded or squeezed), debug (everything, plus session boundaries and raw time). Audit does not compress time, and that is the substantive part. A gap is evidence about when nothing happened; squeezing it is editing the record. Audit is also locked — every other mode is a convenience and can be adjusted, but audit is a claim about what is being shown, and a claim you can quietly tweak is not a claim. The mode is derived from the settings rather than stored alongside them, so the label cannot disagree with the screen. Adjusting anything drops the view to "custom" rather than leaving it wearing a name it no longer earns — which is the failure that would make an audit view worthless. §7's other half comes with it: the status bar's tier counter now enters audit mode rather than flipping `auditorView` alone. That flag on its own produced a view that hid the logged tier while still compressing time and folding agent turns — something that looks like an audit view and is not one. The mapping is a pure module with its own tests, including the one that matters: an adjusted audit no longer reports itself as audit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A design review that started from "what is this project for" ended up revising the core, so this lands before any code does. RedLog records what actually happened in an engagement — every command, connection, file and screen, whoever or whatever produced it — so that afterwards you can replay it, hand a purple team something they can judge traffic from, and find evidence of the things you were not watching when they happened. **Tamper-evidence is demoted to a property of how the record is kept.** The chain, the anchoring, the signing, the bundle and the verifier all keep running and stay fully supported. They stop being the design centre, and they stop earning prominent UI. The load-bearing property is now knowability: completeness matters more than before, but it serves "so you can find out what happened" rather than "so it survives a challenge". That inverts several conclusions reached earlier in this cycle. Gap compression stopped being a contradiction of "a 20-minute gap is right" — the record still holds the gap; the timeline may draw it compressed, and nobody reviewing eight hours wants to scroll twenty idle minutes. A reading posture in the app became core rather than scope creep, since the argument against it rested on an evidence consumer who never opens the app and is no longer the design centre. Search became core outright: "find the evidence later" *is* search. An AI agent is one producer among several, not half the product. It is the sharpest case of the problem — you delegate, it does three hundred things, you know none of them — but the same gap opens with no agent involved, and the model already treats `agent` as one lane of eighteen. `DESIGN-core-and-capture.md` carries the review: four capture gaps and what was decided for each, the point/span timeline model, the purple-team activity record, and what is still open. `ROADMAP.md`'s v1.0 gate is flagged rather than rewritten — it is written entirely around bundle verification and is arguably now missing criteria of its own, but that is not a call to make unilaterally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Correcting the previous commit. It promoted the purple team to primary stakeholder, which was wrong in the same way the original doc was wrong about the evidence consumer: both put the everyday output in someone else's hands. The everyday consumer is the operator themselves — P1, P2 or P3 a day later, writing the engagement up. That is not a fourth persona, it is the second half of the same person's job, and it is the phase the whole record exists to serve. Which sharpens what the record has to carry. Writing a report means going back for the screenshot of a finding and the exact command with its output, its timestamp and its target, in a form that can be pasted into a document. RedLog still does not write the report — that stays downstream — but everything captured is judged by whether it survives that trip. Two things get re-weighted by this, and both were under-weighted before: screenshots are report material rather than incidental artefacts, and a command record is only as useful as its output. "I ran `nmap -sV`" is not evidence of anything. That is why the two output-capture gaps matter more than their size suggests — Windows only captures output behind a manual `Redlog-Run` prefix, and tool output is replayable but not searchable. The purple team and the evidence consumer stay, demoted to secondary stakeholders where they belong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were built earlier in this phase and both are wrong under the revised core (`DESIGN-core-and-capture.md`). **The three view modes go.** They invented three personas for one surface, and two of them do not use it: the evidence consumer never opens the app, and "why is capture broken" is answered better by the capture health card and the `Needs attention` counter than by a timeline preset. The audit mode's ingredients — chained-only, UTC, locked — are all tamper-evidence concerns, which is no longer the design centre. There was also a shipped bug that made the point better than the argument: the existing toggle defaults happen to match the debug preset exactly, so a fresh timeline told every operator they were in Debug mode. The three categories were reverse-engineered onto flag combinations, and when the default state lands on "debug" the categories were describing combinations rather than postures. **The Search view comes back**, recovered from history rather than rewritten. Folding it into ⌘K was justified by §10, but §10 was never checked against what it served. Finding evidence afterwards is now a core use, and a dropdown is the wrong shape for it: after-action review asks "which files did the agent touch", "what happened on this host", "which commands failed in those two hours" — none of which are jumps. ⌘K stays the jumper and reaches the view like anything else. Search sits at ⌘3, next to the timeline. Ten sidebar entries into eight numbered slots means `loot` and `marks` now carry no chord. That is a real cost, taken deliberately and written down where the order is defined — both stay reachable from the sidebar and from ⌘K, and neither is somewhere an operator jumps mid-keystroke. Not touched: the eight toolbar toggles. Two of the three deletions agreed earlier rested on reasoning the core revision then retracted — gap compression is a core convenience now, not a contradiction, and agent-turn folding cannot be dropped in favour of the >50 event-stream threshold before that threshold exists. Deleting them now would be applying a decision whose grounds are gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ttings Exporting was reachable from six places — the dashboard, three separate groups in Settings, the timeline's slice button, the transcript's — each worded its own way. An operator writing an engagement up had to already know which one produced the thing they wanted, and the labels did not say. The fix is not to pick a winner. The scope of an export is a property of the export, not of where the button sits; once scope is an option, six locations collapse to one control offering the visible range, this view, everything, and the evidence bundle separated below. Each option says what it will produce first — a wrong export is cheap, but finding out after opening the file is a wasted round trip during the one task this record exists for. A view still contributes the part it alone knows. The timeline knows what "the visible range" means and registers it while mounted; the registration clears on unmount, because an option labelled "the visible time range" that quietly means the previous view's range is worse than no option at all. Two things that looked like the seventh and eighth entry points are not: The transcript's Markdown button writes the clipboard, not a file. Folding it in would have made the count tidier and the action worse — a file dialog is not what you want when you are pasting into the report you have open. Renamed to say what it does. The Settings "Retention & export" page had one setting left after the three export groups moved out, and that setting was the screenshot interval, which is a capture setting that had been filed under export. It moves to capture and the page disappears: thirteen pages become twelve, thirty-four groups become thirty-one. test/export-single-control.test.ts walks every renderer file rather than the ones I happened to touch, so a seventh entry point cannot appear quietly. npm test 800 passed · npm run e2e 47 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Browsing a registry, pinning publishers by Ed25519 fingerprint, and reading revocation lists are the machinery of distributing capture code. The core is "nothing missing, findable afterwards" (docs/DESIGN-core-and-capture.md §1); distribution serves extensibility, which is a different product. What stays is the installed list, because "is anything capturing that I did not put there" is a question §1 does have to answer. Removed: MarketplacePanel and PublisherEditor (381 lines of Settings), the core modules marketplace.ts and publisher-trust.ts, eleven IPC handlers and their preload surface, config.marketplace.defaultRegistryUrl, 46 i18n keys per locale, and examples/registry. The redlog-sign CLI goes too. It existed to produce signed registry entries; with no registry there are no entries, and a redlog-sign on someone's PATH that signs for nothing is worse than its absence. Two handlers were exposed and never consumed: marketplace:listVersions and marketplace:rollback. Versions only exist for registry-installed plugins, so they had no reachable caller either way. Revocation lists were only ever displayed in the marketplace panel — the plugin loader does not consult them. Removing the panel removes the whole feature rather than orphaning a check, which is the outcome to want here but worth stating rather than discovering later. Settings: 31 field groups to 30, 12 pages unchanged. One group understates it — that group held a registry browser, a trust editor, and a revocation viewer stacked inside it. Noted in the design doc, because the group count is the metric positioning risk #2 is measured by. npm test 771 passed · npm run e2e 43 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/DESIGN-core-and-capture.md §2.4 calls this the highest-leverage single capability in that note, because one mechanism closes two gaps. Tool output was replayable but not queryable — you could watch the nmap run back but not ask which run mentioned 445. And an `ssh` into a jump host produces no structured commands at all (§2.2), only bytes; searching those bytes is the only thing that makes such a session findable. It does not parse. "Which runs mention 445" is answerable from what was on screen; "list every open port" needs nmap's XML read into findings, and what counts as a finding is per-shop opinion — the same interpretation the positioning lists as a non-goal. Three decisions had plausible alternatives: A separate database file rather than a table in events.db. The index is derived, mutable and rebuildable; the evidence DB is none of those, carries append-only triggers, and is copied verbatim into evidence bundles. The deciding argument was retention: recordings are swept after N days, and text surviving inside the evidence DB would make the policy a lie — the operator is told the recording aged out while its contents stay searchable. retention.ts now calls pruneCast, and test/retention.test.ts fails if that link is cut. A second test keeps the index out of evidence bundles, where it stays out today only because the export copies named subdirectories. Token search rather than trigram. Trigram gives grep semantics at roughly 3x the indexed text on disk, which across an engagement of 50 MB recordings is the difference between an index that ships and one that fills the disk. unicode61 with prefix indexes covers what operators type — an IP, a port, a hostname, a tool name. The UI states the limit rather than letting a miss read as absent bytes. The index status is surfaced rather than hidden. A project still being indexed returns fewer hits than it will in a minute, and for this product that is the one failure mode that must never be silent — so the count of unread recordings shows in the results header and on the no-results screen, which is exactly where a silent partial index would do its damage. Hits carry line-aligned byte offsets, so a result expands in place through readCastRange — the same path the timeline uses for per-command replay. test/cast-index.test.ts proves the round trip rather than just that rows land; a hit whose range does not resolve claims the evidence exists and then fails to produce it. Indexed at session close (the bytes are final, and the file is read once) and backfilled in the background on project open, so recordings written by an older build or restored from a backup become searchable without anyone knowing to ask. Both paths are fire-and-forget: the exit path is racing the DB close, and session_end is evidence while the index is rebuildable. npm test 787 passed (81 files, incl. the DB suites) · npm run e2e 43 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #17 made the app's chrome behave like a desktop app rather than a web page — dragging across the sidebar or status bar no longer paints everything blue, while content an operator needs to copy stays selectable. It shipped with the rule written down and never checked in a running app: the session that built it ended blocked on being able to launch one, listing three behaviours as outstanding. One of the three is objectively measurable — the selection boundary is a computed style — so it is a test rather than a squint. The other two are native OS context menus, which Playwright cannot see; they still need a human on each platform, and saying so beats a test that pretends otherwise. A third concern from that session resolves by reading rather than running: the terminal's "clear screen" calls term.clear() instead of writing `clear` down the pty, so it cannot leave a command on the timeline that nobody typed. The spec also guards a boundary that moved afterwards. The export control (§10) now sits in the title bar, which is both select-none chrome and a -webkit-app-region: drag surface, and a button inside a drag region is decoration — the OS takes the mousedown to move the window and the click never lands. It is in a no-drag ancestor and the menu opens; both are now asserted. Writing that assertion surfaced a wrong belief of mine: -webkit-app-region does not inherit. Its initial value is `none`, and the drag surface is resolved by hit-testing region boxes, not by reading the computed style under the cursor — so the button reads `none` while sitting inside a no-drag region and working correctly. The test walks up to the nearest explicit region, which is the mechanism rather than a proxy for it. npm run e2e 49 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ontents Third pass with the same test: does the operator in §1 have to understand this to answer "what happened, and can I show it"? Two of the five candidates failed on inspection, both the same way — the group's name did not describe its contents, and I had judged the name. "Polling" is not a tuning knob. Every field in it decides what RedLog itself sends to the network and to whom: which resolver or third-party echo service learns the operator's address, how often, from where. During an engagement that is OPSEC surface and belongs in front of the operator rather than in a packet capture. Kept, renamed "What RedLog itself sends out" — the bad name is what nearly deleted it. "Team Profile Sync" was one useful button and one duplicate. Import duplicated the project picker's, which is where it belongs, because you seed a config when creating a project rather than after. Export could not go with it: it carries views.json as well as config.yaml, so it is not a button for `cp`, and deleting it would strand an import that consumes files nothing produces. Kept as one button, renamed "Hand-off profile". The three that survived: Screenshot quality folded into Screenshots — one subject reading as two settings. VPN adapter detection folded into Safe / Exposed IPs, since it exists only to answer that group's question and standing apart made "am I exposed" look like two unrelated concerns. "Check for updates" moved to the title bar, beside the version string it already sat next to a copy of; it is an action about the version, and nobody was looking for it under Settings. test/settings-ia.test.ts now caps the group count. The number is a proxy, but it is the proxy positioning risk #2 measured, so a group added without one removed should have to argue for itself in a diff. Four groups still need a scope ruling rather than a merge: cloud share, the deconfliction webhook, operator tokens, the MCP server. npm test 788 passed · npm run e2e 49 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merges origin/main (full HTTP body capture, JA3, cookie tracking, HTTP History panel, proxy-bypass detector) into this branch. Merged rather than rebased: 18 commits against heavily-edited i18n files would have meant resolving the same conflict eighteen times. The i18n merge is a union minus what this branch deleted, so the 61 keys removed with the marketplace and the export groups do not come back. Two conflicts resolved against main on purpose: the sidebar badge colours return to the 12% tints (main had them solid, which the design ruling reversed), and the sidebar keeps both the Search entry and the new HTTP History one. The four breakages, all found by tests rather than by reading: The external-API allowlist (9a97bd0) blocked `terminal`, which is the type in redlog-cli's own help text, so `redlog-cli log` 403'd. Added, along with clipboard/screenshot/browser/http_navigation. The derived types stay out — forging `system` or `pivot` forges RedLog's conclusions about the engagement, which is the whole point of that commit. A rejection now also records to capture-health: a 403 an integration ignores is capture silently stopping, and that is the one failure §1 does not allow. The same allowlist broke three specs that seed derived rows directly. They now use `/api/events/seed`, which skips the allowlist and does not exist unless REDLOG_E2E=1. A flag on `/api/events` would have been fewer lines and wrong: the production route's refusal is itself under test, and a bypass on the same route would have made that test pass with the door open. HttpHistoryPanel arrived outside the design system: 48 zinc classes and zero redlog tokens, nine hardcoded English strings, its own toLocaleTimeString, truncation with no route to the full value, and no list-keyboard wiring. The time and truncation guards caught themselves; the palette and the strings I found by reading. The list-keyboard contract did not catch the missing hook because it named its five files by hand — the exact failure its own comment describes, one level up. It now derives the list from the sidebar order. Writing that surfaced a second silent pass: the first version matched the wrong constant name, found nothing, and `it.each([])` generated zero tests and reported success. It now asserts a floor. npm test 790 passed · npm run e2e 50 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uploading an evidence bundle to a backend with an expiry picker and a stub/HTTPS switch is distribution infrastructure. It is the same argument that removed the marketplace and it lands harder: §1 makes the operator writing the engagement up the consumer of the output, and the bundle already reaches them through the one export control. How the bundle then gets to a client, a reviewer or a court is the operator's business. A product that carries a record of an engagement should not also be the thing that ships it somewhere with a link and a timer. Removed: CloudSharePanel and its two orphaned manifest types (304 lines of Settings), src/core/cloud-share.ts and cloud-share-uploader.ts, five IPC handlers with their preload surface, config.cloudShare, 42 i18n keys per locale, two unit specs and one e2e spec, and the README screenshot. Settings: 12 pages to 11, 27 field groups to 26. The hard review-gate checkbox goes with it, which is worth naming rather than losing quietly: it existed because uploading raw capture to a remote endpoint needed a deliberate stop. Nothing in the remaining path uploads, so the gate has nothing left to guard — the evidence bundle is written to disk and stays there. npm test 782 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The HTTP History panel arrived rendering one row per connection, with a 5000-row page and an IntersectionObserver to survive it. That is Burp's information architecture: right for a tool you drive traffic with, where each request is a thing you are about to modify and resend; wrong for a record of an engagement, where "I ran a directory brute-force against x" is one action whether it made four requests or forty thousand. docs/DESIGN-core-and-capture.md §3 already ruled on this — a single connection is a point, a bulk operation is a span, and "only the command is listed; the connections it produced are its contents, reachable by reference, never rendered individually." src/renderer/src/lib/httpActivity.ts groups flows into that shape: a parent command when the record links one, otherwise same host within a 4s idle gap. The gap is chosen against the failure that matters — a scanner's own pacing must not shatter one run into a dozen rows, since that recreates the wall this removes. Merging two separate actions is the cheaper mistake; both are still there one level down with visible timestamps. Nothing is dropped. Expanding a row lists every flow, and "Every request" is still one click away with the original table intact. What changed is the level the eye lands on: two rows instead of thirty-one, each carrying its method mix, request count, status spread and duration without being opened. The e2e caught a bug the unit tests structurally could not. I read a flow's parent command from `_causes` on either half of the pair — but a response's `_causes` points at its own request, because that is how the api-server pairs them by flow_id. Every flow got a unique "parent" and grouping degenerated to one group per connection: exactly the shape being removed, arrived at through the fix for it. The unit tests hand `causeEventId` in directly, so they were blind to it by construction. Two further test lessons, both about selecting by the wrong thing: `[aria-expanded]` also matches the title bar's export menu, which is how the row count first read 3; and clicking a view toggle by its zh-TW label fails in whatever locale the fixture boots. Both now use explicit hooks. npm test 794 passed · npm run e2e 49 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #8 (opened 2026-08-12, 47 commits, 162 files, +19k lines) is 88 commits behind main and its central decisions have been overtaken. What is still worth having is the documentation, and one piece of it is not optional: shipped code on main names `docs/ALERT-ROLES.md` three times and the file was never merged. `src/core/alert/` was built later, from a spec that only ever existed on an unmerged branch. Seventeen documents, no code. The suite is tightly cross-linked by design — salvaging the one that main cites pulls the framework it belongs to. DESIGN-PRINCIPLES.md needed a header rather than a quiet import. Its §1 says the irreducible core is a tamper-evident record a third party can verify, which the 2026-08-21 revision replaced with knowability; and the doc asserts that when another doc disagrees with a principle, the principle wins and the doc is the bug. Importing that verbatim would have made DESIGN-core-and-capture.md the bug. The header inverts that rule for §1 and only §1, and says so. It is salvaged as argued rather than rewritten to agree. Editing the old reasoning into the new conclusion hides that a decision was made. Also removes docs/CLOUD_SHARE_BUNDLE.md, which the cloud-share removal left behind, and the reference to it from the salvaged DELIVERY-TARGETS.md. Deliberately NOT salvaged, pending a ruling: - plugins/scan-parsers — parses nmap/nuclei output into events, which §2.4 ruled against by name: what counts as a finding is per-shop opinion. - plugins/c2-tailers — following a Sliver or generic C2 log is a real capture gap and fits the existing tailer host. This one is worth building. - the target-lane axis and phase ribbon — the axis is a real idea; the branch also deletes TargetView, which this branch keeps and the list-keyboard contract names. - src/core/io-store.ts and the artifact GC/pin pair — likely superseded by http-body-store.ts and cast-index.ts, but not verified. npm test 794 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The second of PR #8's two bundled plugin packs, and the one that survives the core review. A C2 framework (Sliver, or anything via a documented JSONL contract) writes beacon check-ins, task results and pivots to its own log — an out-of-band channel the substrate capture never sees. This follows that log shell-side and lands each line on the timeline, so C2 activity sits beside everything else in the record. Its sibling pack, scan-parsers, is deliberately NOT salvaged. The distinction is the same one DESIGN-core-and-capture.md §2.4 draws: a C2 log is an event stream — things that happened, recorded verbatim (DESIGN-PRINCIPLES §3). Scan output is a result set — claims about the world, and turning nmap XML into structured findings is interpretation, which is per-shop opinion and a stated non-goal. The recording is searchable instead; that closes "which run mentioned 445" without pretending to answer "list every open port". Nothing runs inside RedLog: the pack is declarative (🟢) tier, follows a file and POSTs to the local API. test/c2-tailers.test.ts carries the original 8 parser tests plus 4 that load the bundled manifest against the *current* schema — because a bundled plugin that sits in the directory but fails validation is worse than an absent one: the capture source shows up in the roadmap and never fires. The schema moved while the branch sat unmerged; the pack validates against it today. Backlog updated: Q1 (scan-parsers) marked ruled-against with the reason, the c2 pack marked built. Not salvaged, with reasons recorded for next time: - io-store.ts + artifact-gc/pin: io-store is the generalised sidecar the SPEC-IO-SIDECAR proposal describes; http-body-store.ts on main already does content-addressed dedup sidecar storage for HTTP bodies, and cast-index does it for recordings. The artifact GC/pin pair adds size-pressure eviction, which main has NOT built (retention is time-only) — a real gap, but a design question, not a copy. Left for a decision. - target-lane axis + phase ribbon: the axis is a real idea, but the branch deletes TargetView, which this branch keeps and the list-keyboard contract names. Needs reconciling rather than lifting. npm test 806 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
… traps CI on #23 was green on ubuntu and red on windows-latest — two tests I added this branch, each making a Unix assumption. settings-ia: `/type SettingsPage =([\s\S]*?)\n\n/` needs a blank line to close the match, but .gitattributes only pins .sh/.py to LF, so .tsx checks out CRLF on Windows and the two newlines have a \r between them. Now `\r?\n\r?\n`. Same family as the path.sep trap this repo hit before. c2-tailers: asserted the hooks' exec bit via fs.statSync().mode, which Windows checkouts drop to 0 — so a correctly-tracked plugin failed there. Git's index mode is the cross-platform truth and is what a Unix clone actually gets; the test now reads `git ls-files -s` and asserts 100755, which is also the bit that matters (a clone's hooks must be runnable, the CI runner's filesystem need not be). Both green locally; scanned the rest of this branch's new tests for other bare-\n\n or LF-split regexes and found none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/DESIGN-core-and-capture.md §2.1's first capture gap. mitmproxy only sees what is proxied, so the timeline goes dark the moment a tool talks to a host over something else — SMB, LDAP, RDP, a reverse shell, a C2 beacon. `nmap -sV 10.10.11.24` would show the command and then nothing about what it reached. Same shape as process-monitor: poll a table the OS already keeps (the socket table), diff against the last snapshot, emit on the change. It records who connected to which IP:port, over what protocol, for how long — no payload, no root. Two events per connection, `connection` on open and `connection_end` on close carrying the duration, so a reverse shell held open across a hundred polls is one span (§3), not a hundred rows. The parsing is the risk — BSD's `addr.port` vs `:port`, IPv6 zones, the pid column landing in a different column per OS — so it is split into a pure `connection-table.ts` (three platform parsers + diff + capturability) with 19 fixture tests, away from the shell-out. `ss -tunpH` on Linux, `netstat -n` per-proto on macOS, `netstat -no` on Windows. The blind spot is stated, not hidden: established-connection polling cannot see a SYN scan, which completes no handshake. `nmap -sS` leaves a command and no connections. The limitation is surfaced twice — a `connection_capture_started` system event when it turns on, and a line next to the settings toggle — because a capture that silently misses a class of activity is worse than one that admits its edge, the same confident-green failure the alerting model exists to prevent. Off by default (it shells out every 2s and is capture the operator opts into), its own capture-health source in the traffic group split from mitmproxy by subtype so neither lights the other's indicator, and the app's own API port is filtered so RedLog does not record its hooks talking to itself. Settings: 26 field groups → 27 (the cap in settings-ia.test.ts moves with it — a real capture feature, argued in this diff). Verified in a running app (e2e/connection-capture.spec.ts): a seeded connection renders on the timeline reading as a connection, the close carries its duration, the toggle round-trips through autosave, and the SYN-scan note is visible where the switch is. npm test 825 passed · npm run e2e 53 passed Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phases 1–3 of the UI/UX standard (
docs/UIUX-STANDARD.md), plus a mid-streamrevision of the product's core that reshaped what phase 3 should even build.
The core moved, and the work followed
A design review (
docs/DESIGN-core-and-capture.md,PRODUCT-POSITIONING.md)moved the irreducible core from defensibility — a tamper-evident record a
third party can verify — to knowability: nothing missing, findable
afterwards, with the operator reconstructing and writing up their own
engagement as the primary consumer. Tamper-evidence is kept; it is no longer
what the product is for. Three subsystems stopped earning their place under
that test and were removed; one capture gap it exposed was filled.
Phase 1 — tokens & type
Palette repaint over a
grayramp withon-*tokens for text on fills; typescale to 13/15/17px; every solid fill takes dark text (white fails contrast on
all of them — verified by computation). A Tailwind-class test bites on the two
dead classes that had shipped silently, and an i18n-key test on keys that fell
back to their own name.
Phase 2 — behaviour
Three-part errors, four Toast rules, two-tier status failure, one time
formatter, truncation-keeps-the-full-value, three-level dangerous-action
confirm with a focus trap, five undoable actions (three deferring the write,
since that log is evidence), one keyboard contract across every list, one ⌘K.
Phase 3 — structure, after the core revision
an option, not a location.
from not missing anything. Installed-plugin management stays.
is distribution; the bundle itself still reaches the operator through the one
export control.
does the operator have to understand this to answer "what happened, and can I
show it?" Two candidates failed that test because their names lied, not
their contents; kept and renamed.
and 1 request are 2 rows, not 31; every flow still one disclosure down.
Capture gap the core exposed
Full-text search inside terminal recordings (
cast-index.ts). Closes twogaps with one mechanism: tool output was replayable but not queryable, and an
sshinto a jump host produced no structured commands at all. Search, notparse — "which run mentioned 445", never "list every open port". A separate DB
file because it is derived and rebuildable and retention must be able to prune
it; token search not trigram for disk; index status surfaced because a
half-built index reading as "nothing there" is the one failure this product
cannot have.
Salvaged from the unmerged PR #8
That branch is 88 commits behind main and its central decisions are overtaken,
but shipped code on main cites docs that were never merged (
src/core/alert/names
ALERT-ROLES.mdthree times). Recovered: 17 cross-linked decompositiondocs (as-argued, with
DESIGN-PRINCIPLES §1marked superseded rather thanrewritten), and the c2-tailers plugin pack — a C2 log is an event stream,
which is capture; its sibling scan-parsers is interpretation, and was left.
Verification
npm test— 806 passed (81 files; run under the Node ABI so the DBsuites execute, then restored)
npm run e2e— 49 passed / 0 failed, including two new specs that verifythe export menu, the selection boundary and the HTTP reshape in a running
Electron app. The e2e caught a grouping bug the unit tests structurally could
not — a response's
_causespoints at its own request, so reading it asparentage degenerated the §3 grouping into one-row-per-connection.
Notes for review
!commits (feat(plugins)!,feat(share)!): marketplace and cloudshare are removed, including their config keys and IPC. No migration — both
were UI-reachable features, not data formats.
store (main's retention is time-only — a real gap), and the target-lane
timeline axis (its branch deletes TargetView, which this branch keeps).
🤖 Generated with Claude Code