Public Demo Surfaces
Overview
Section titled “Overview”Logrite has two public, sign-in-free surfaces, both served by the Frontend container and both living inside the React router rather than beside it.
/app-recordings is a recorded library — narrated screen captures with a synchronised cue
track. The guided walkthrough is a live tour that runs on the real application routes, inside
the real DashboardLayout, with a fixture transport feeding demo data to the genuine product
screens.
| Setting | Value |
|---|---|
| Served by | Frontend (Deployd-io/Logrite-UI), port 3000 |
| Recorded library | /app-recordings — static, no auth, no backend |
| Guided walkthrough | Six walkthroughs on the real product routes. Entry: ?tour=<id> on any route, /tour (picker), or /experience/<id> for a signed-out visitor |
| Auth | None on the public path. The walkthrough also runs for signed-in users, on their own account, showing the same demo data |
| Backend contact | POST /api/v1/demo-requests (lead form) and /api/v1/demo-events (telemetry) only — everything else is refused by the transport |
| Branch | feat/platform-tour in Logrite-UI |
Recorded walkthroughs — /app-recordings
Section titled “Recorded walkthroughs — /app-recordings”The recordings are static files under public/app-recordings/, produced by a recording rig the
component’s header comment names as new-arch_or_extend/demo-portal — which is not in this repo
or on the dev VM, so treat the checked-in assets as the source of truth:
public/app-recordings/├── manifest.json # 8 videos: id, name, tagline, problem, solution, route, file, cues, duration├── personas.json # 4 role personas, each an ordered list of section ids└── media/ ├── <id>.mp4 # 102–177 s each, ~16 min in total └── <id>.cues.json # {section, name, duration, cues:[{t, step, total, title, desc, vo}]}| Section id | Title | Mirrors route | Length |
|---|---|---|---|
ai-warden | AI Warden | /ai-warden | 118 s |
generator-compliance | Generator / Compliance Flow | /generator | 114 s |
analytics | Analytics | /analytics | 177 s |
cortex-ai | Cortex AI | /logrite-cortex | 108 s |
collector-agents | Collector Agents | /agents | 116 s |
agent-flow | Automated Generator Flow | /agents-flow/onboarding | 102 s |
m26-14 | M-26-14 Maturity | /m26-14 | 107 s |
dashboard | Dashboard | /dashboard | 106 s |
The four personas in personas.json (dev, sre, sec, lead) do not change the content — each
is an ordering of the same eight sections, so an SRE opens on Dashboard and a security lead
opens on M-26-14. The open section lives in the URL as ?s=<id>, so a walkthrough can be linked to
directly and Back leaves the player rather than the page.
serve.json redirects /demo, /demo/ and /demo/index.html here. Those redirects are load-bearing
for anything already sharing a /demo link and are deliberately untouched by the /experience work.
Why it is a React route and not a static page
Section titled “Why it is a React route and not a static page”Read the header comment in src/components/AppRecordings/AppRecordings.jsx before “simplifying”
this — it is the kind of constraint that costs an afternoon to rediscover.
The container serves the built app with serve dist -s. serve-handler applies the -s catch-all
rewrite (** → /index.html) before it ever stats a directory, and re-applies the remaining rules
recursively, so no extensionless URL can be made to resolve to a standalone
app-recordings/index.html. /app-recordings would always land on the SPA, hit the protected-route
wrapper, and bounce to /login. Owning the clean URL inside the router is what makes it both clean
and sign-in free — the component then fetches the static JSON sitting next to it.
The guided walkthrough
Section titled “The guided walkthrough”Entry points
Section titled “Entry points”Four, and the branch selects the behaviour rather than the slug:
| Entry | Who it is for | What happens |
|---|---|---|
/experience | A stranger with no idea which walkthrough they want | The public landing page: six walkthroughs grouped by problem. Calls no API and reads no auth state |
/experience/<tour-id> | A stranger who has chosen, or anyone with a shared link | Arms sandbox mode, then redirects to the walkthrough’s first real path. The visitor spends the whole tour on genuine product URLs |
?tour=<tour-id> on any route | A shared link, and the picker | Consumed once, stripped from the URL, and navigates to step 1’s route |
/tour | Anyone, any time | A picker listing all six walkthroughs. Permanent entry in the sidebar’s bottom nav, and the first card on the Support page |
| First login | A new account | Offered once, on /dashboard only, gated on the showPlatformTour user setting |
The landing page, and why it is grouped by problem
Section titled “The landing page, and why it is grouped by problem”/experience and /tour render one component, src/tour/ui/WalkthroughIndex.jsx, in two
variants — public and in-app. They were two pages for about a day and the copy had already begun to
diverge.
The cards lead with a symptom, not a pillar name. The walkthroughs are named Governing your AI
traffic, Finding the log you need — facts about our product, not about anyone’s week — and a
stranger has to translate each one into a problem they recognise before they can choose. The
grouping and problem copy live in src/tour/content/useCases.js, apart from the tours themselves so
content and layout stay separable.
Five of the six problem statements are verbatim from src/experience/content/slugs.js as it
stood before commit a9cf594d deleted it (git show a9cf594d^:src/experience/content/slugs.js).
Its authoring rules came back with the copy and are restated in the file:
no question mark in a headline · no radio buttons · no “Step 1 of 3”, no progress bar · no Continue button — the cards are the action · every card leads with a symptom the visitor has experienced · no persona is named on any card
That last rule is why there is no role filter, even though public/app-recordings/personas.json
would supply one: “For CISOs” on a card is the single thing that makes a chooser feel like a form.
The six walkthroughs
Section titled “The six walkthroughs”Each is one file under src/tour/content/tours/, registered in index.js. Every step names a
route, an anchor selector that must already exist in src/, and optionally an act that
presses a real control.
| id | Name | Steps | Runs on |
|---|---|---|---|
ai-governance | Governing your AI traffic | 7 | /ai-warden |
search-everything | Finding the log you need | 6 | /analytics/discover, /analytics/semantic-search |
collect-everything | Getting logs off your hosts | 7 | /agents |
ask-your-logs | Asking your logs a question | 7 | /logrite-cortex |
instrument-your-code | Writing the log nobody wrote | 7 | /generator |
prove-compliance | Proving it to an auditor | 4 | /m26-14 |
Gentle rails, not a cage
Section titled “Gentle rails, not a cage”Off-script clicks are permitted. Wandering to a screen the walkthrough does not cover shows a light “this screen isn’t part of the walkthrough” card with a way back — it never blocks.
That is a design decision with a matching implementation detail: the spotlight scrim carries
pointerEvents: 'none'. It was 'auto' at one point, which swallowed every click outside the
cutout and turned the whole “gentle rails” design into a cage while every automated assertion stayed
green.
The synthetic-data disclosure
Section titled “The synthetic-data disclosure”The walkthrough shows the same demo data to everyone, including a signed-in customer looking at what is otherwise their own dashboard. Without a permanent, non-dismissible disclosure that customer is shown fabricated AI Warden traffic and a fabricated M-26-14 score with nothing on screen saying so.
TourRail carries a SYNTHETIC DATA badge that is first in the reading order, has no close button,
and renders on every branch including off-script — the moment a visitor wanders is precisely
when they are looking at fabricated numbers with no step explaining them.
The rail is position: sticky, not fixed: a fixed bar is the classic WCAG 2.2 §2.4.11 failure
because it can cover the element someone just tabbed to.
Default-deny transport
Section titled “Default-deny transport”src/tour/runtime/ patches fetch, XMLHttpRequest, WebSocket and EventSource. Axios uses the
XHR adapter, so every HTTP client in the app bottoms out in one of these.
An unmatched request is refused, never forwarded. That is what makes it safe to arm around a signed-in session: no authenticated call can leave the browser whatever the caller does.
Two deliberate holes, and only two:
| Path | Why |
|---|---|
/api/v1/auth/** | A lockout fix. Default-deny used to swallow sign-in and token refresh, so a ?tour= link followed while signed out redirected to /login with a form that could never succeed |
/api/v1/demo-events, /api/v1/demo-requests | Telemetry and the lead form — the only calls that are supposed to leave |
Routes are origin-pinned. /health is one segment with no product prefix and both the AI
service and semantic search want it. Registering an origin-less pattern alongside the pinned one
made the bare route match every origin, so Cortex’s /health answered semantic search’s request
with the wrong body and /analytics/semantic-search sat in its service-unavailable state for the
entire walkthrough — while every assertion passed, because the anchor resolved fine and the
spotlight landed neatly on an error card.
Demo data for everyone
Section titled “Demo data for everyone”One data path, signed in or out. seedAll() seeds all six pillars at once rather than per route,
because gentle rails means the visitor can click any sidebar entry at any moment and a pillar they
wander into must not be empty merely because the script had not reached it.
For a signed-out visitor the shell is wrapped in a second Redux store built from rootReducer
with preloadedState: { user: { isLoggedIn: true, … } }. The nested store is not a convenience:
the real user slice has its own userPersistConfig writing to localStorage, so setting that
field on the real store would persist it and the visitor would appear signed in across every tab
and reload with a session that does not exist. The synthetic user’s token is deliberately empty —
nothing is minted and nothing is signed.
The founder card — video region and narration region
Section titled “The founder card — video region and narration region”One floating card, 322×360, holding two regions: a 16:9 video region and a narration region beneath it. Absent below 768px, collapses to a pill at 768–1023, pauses on step change without rewinding, picks a corner away from the coach-mark, and yields when the coach-mark reports a collision.
Three decisions worth keeping:
- Autoplay is muted, always. Every modern browser blocks autoplay with sound; muted autoplay
plus a one-click unmute is the only thing that works without a user gesture.
prefers-reduced-motionturns the automatic half off entirely and leaves the manual controls. - The poster is drawn twice. Inline SVG inside the component, and as
public/tour/video/poster.svgfor the<video poster>attribute — because a poster attribute takes a URL and a detached asset cannot read the app’s theme. The theme here is a redux-persisted user choice written as inline custom properties, notprefers-color-scheme, so an<img>would show a light poster to a dark-theme user on a light OS. - The card is a fixed height on purpose. A cue changes every few seconds; a content-sized
narration region would resize the card, re-fire
onRect, re-place the coach-mark and reopen the render loop this feature has already been frozen by once.
Lead capture
Section titled “Lead capture”POST /api/v1/demo-requests, from the permanent rail link and again at the end of a walkthrough.
Reaching the last step used to unmount the whole overlay, leaving the visitor on a screen full of
demo data with no disclosure and nothing to do next.
The form posts cross-origin, so Chromium sends a real OPTIONS preflight first. The passthrough is
matched on pathname and is method-agnostic, so admitting the path admits the preflight.
The demo data, and the four defects that lived in it
Section titled “The demo data, and the four defects that lived in it”The walkthrough’s fixtures are not only what a step touches. Gentle rails means a visitor can click anything, and four defects lived in that gap while every step-level assertion stayed green. All four are fixed; they are recorded because each is a shape that will recur.
| Defect | What a visitor saw | Cause |
|---|---|---|
| A field filter that did not filter | status_code:504 returned 53 rows, 6 of them 504s | the field:value branch ended in a bare substring test that discarded the field; _source is nested, so dotted paths always fell through to it |
| A fifth of every result list as raw JSON | walls of JSON beside clean cards on semantic search | 446 of 2318 documents are spans and carried no message; both renderers fall back to JSON.stringify(_source) |
| A Trace Review of nothing, or of everything | SPANS 1000, DURATION 246972776.00ms, most rows Unknown Operation | every document minted its own trace id (446 one-span traces), and runSearch had no term handling — so a null query meant match-all |
| A file breakdown with counts over an empty list | “4 files” above nothing, and no Copy button | skippedFiles.files was [] while counts was populated; the modal gates Copy on list.length > 0 |
npm run check:tour-fixtures now asserts these invariants directly — every document has a
message, a field query scopes to its field, no trace exceeds a minute, every document’s trace
holds at least one span, a term query is not treated as match-all, and every skipped-files bucket
lists exactly as many files as it counts. It also reads the tour copy and checks the hit count
it quotes is the one the screen shows: compressing spans onto their trace clock moved two documents
out of the default 24-hour window, and the copy silently became wrong by two. Cheap, deterministic
and browser-free, so it runs alongside the fast checks rather than in the browser job.
Verification — a real browser is mandatory
Section titled “Verification — a real browser is mandatory”npm run check:tour-anchors # in CI — every anchor exists, every tour is registerednpm run check:tour-fixtures # in CI — invariants of the demo data itselfnpm run check:tour-browser # in CI since 2026-08-16, as its own parallel jobcheck:tour-browser drives all six walkthroughs end to end, the public landing page, the
signed-out sandbox, the three in-product triggers, the CTA, the first-login auto-start in both
directions, the two modals a visitor reaches off-script, and a responsive matrix at
390 / 768 / 1024 / 1440. 121 checks locally; on the live dev URL two groups skip rather than pass,
because /tour and /support correctly bounce a synthetic session to /login.
Two of those checks exist because the defects they guard were invisible to everything else:
every spotlight rings its own anchorreads each step’s anchor selector out of the tour files and compares the ring’s rect to the element’s. 14 of 38 steps once failed it, the worst by 1241px — a ring drawn entirely off the screen — while every other assertion passed. “An anchor resolved” and “the ring is drawn around it” are different claims and only the first was ever being made.the step card does not move after it appearssamples the card’s rect from before the click and fails over 8px. The card used to travel up to 563px after the step’s text was on screen.
Build with the full env or the app requests undefined/api/v1/..., which matches no fixture route
and you will debug the wrong thing:
VITE_API_BASE_URL VITE_WARDEN_RELAY_URL VITE_AI_SERVICE_API VITE_ANALYTICS_PROXY_APIVITE_DASHBOARD_PROXY_API VITE_SEMANTIC_SEARCH_API_URL VITE_OSD_VERSIONStill open
Section titled “Still open”- Real founder recordings. Ten placeholder slates; the narration is real and already playing. Drop the mp4s in and flip one constant.
- The lead endpoint on prod. Live on dev, 404 on
main.
Closed 2026-08-17 — every walkthrough has its own script
Section titled “Closed 2026-08-17 — every walkthrough has its own script”search-everything was the last tour with no honest video match: it borrowed general, a platform
overview, at confidence: 'fallback'. It now has search-everything.vtt, written for its six
steps rather than rehomed onto them — the 356-hit histogram, the 6 sets of logs and their origin
badges, sendgrid returning 4 HTTP 429s, 34 fields nobody configured, a line opening whole, and the
“slow database queries” chip returning deadlock detected, which carries none of those three words.
It keeps the tour’s own ordering of the argument: keyword search working first, the “you have to
know the text” limit as the pivot, semantic search as the answer to that sentence — the same
capability-then-limit shape tours/analytics.js adopted after leading with the limitation was found
to cost every visitor who left at step 4.
No row in TOUR_VIDEOS is 'fallback' any more. The value stays in the type so that the next
walkthrough added without a script has to declare it rather than quietly borrow someone else’s.
general moved to UNUSED_VIDEOS, where it joins audit-evidence, pii-in-logs and
slow-payments; nothing outside videos.js names it, because videoForTour is keyed by tour id and
the chooser has no video region.
npm run check:tour-videos now guards the set: every registered slug has a readable .vtt and
.mp4, every mapped slug exists in VIDEOS, seconds is within 1s of the real last-cue end, and
every cue is ≤2 lines of ≤42 characters. The .mp4 assertion is the load-bearing one — a slug with
no paired file is inert today and a 404 <video> the moment VIDEOS_ARE_PLACEHOLDERS goes false,
which is precisely the one-line change the ship gate advertises as safe.