Skip to content

Public Demo Surfaces

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.

SettingValue
Served byFrontend (Deployd-io/Logrite-UI), port 3000
Recorded library/app-recordings — static, no auth, no backend
Guided walkthroughSix walkthroughs on the real product routes. Entry: ?tour=<id> on any route, /tour (picker), or /experience/<id> for a signed-out visitor
AuthNone on the public path. The walkthrough also runs for signed-in users, on their own account, showing the same demo data
Backend contactPOST /api/v1/demo-requests (lead form) and /api/v1/demo-events (telemetry) only — everything else is refused by the transport
Branchfeat/platform-tour in Logrite-UI

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 idTitleMirrors routeLength
ai-wardenAI Warden/ai-warden118 s
generator-complianceGenerator / Compliance Flow/generator114 s
analyticsAnalytics/analytics177 s
cortex-aiCortex AI/logrite-cortex108 s
collector-agentsCollector Agents/agents116 s
agent-flowAutomated Generator Flow/agents-flow/onboarding102 s
m26-14M-26-14 Maturity/m26-14107 s
dashboardDashboard/dashboard106 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.

Four, and the branch selects the behaviour rather than the slug:

EntryWho it is forWhat happens
/experienceA stranger with no idea which walkthrough they wantThe 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 linkArms 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 routeA shared link, and the pickerConsumed once, stripped from the URL, and navigates to step 1’s route
/tourAnyone, any timeA picker listing all six walkthroughs. Permanent entry in the sidebar’s bottom nav, and the first card on the Support page
First loginA new accountOffered 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.

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.

idNameStepsRuns on
ai-governanceGoverning your AI traffic7/ai-warden
search-everythingFinding the log you need6/analytics/discover, /analytics/semantic-search
collect-everythingGetting logs off your hosts7/agents
ask-your-logsAsking your logs a question7/logrite-cortex
instrument-your-codeWriting the log nobody wrote7/generator
prove-complianceProving it to an auditor4/m26-14

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 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.

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:

PathWhy
/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-requestsTelemetry 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.

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-motion turns 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.svg for 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, not prefers-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.

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.

DefectWhat a visitor sawCause
A field filter that did not filterstatus_code:504 returned 53 rows, 6 of them 504sthe 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 JSONwalls of JSON beside clean cards on semantic search446 of 2318 documents are spans and carried no message; both renderers fall back to JSON.stringify(_source)
A Trace Review of nothing, or of everythingSPANS 1000, DURATION 246972776.00ms, most rows Unknown Operationevery 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 buttonskippedFiles.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”
Terminal window
npm run check:tour-anchors # in CI — every anchor exists, every tour is registered
npm run check:tour-fixtures # in CI — invariants of the demo data itself
npm run check:tour-browser # in CI since 2026-08-16, as its own parallel job

check: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 anchor reads 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 appears samples 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_API
VITE_DASHBOARD_PROXY_API VITE_SEMANTIC_SEARCH_API_URL VITE_OSD_VERSION
  • 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.