Shell & Honesty

The frame around every surface: the sidebar you navigate with, the 48px header that never changes height, the bar that replaces both on a phone. Plus the mechanism Flux built to tell you how finished the page you are standing on actually is. That mechanism is the interesting one. Most products decide what to claim in marketing; Flux decided it in a TypeScript file, and then rendered the claim above the feature.

Telling the truth about the page

1 component

One component, one job: read src/lib/route-truth.ts for the current pathname and, if that route is not ready to be believed, say so above it. The status decides the colour, the icon and the one-word label. The sentence next to it is the route's own honesty string, verbatim. This is a good idea implemented cheaply, and both halves of that are worth showing.

BetaBanner
A strip under the header that admits what a page cannot yet do. Six variants keyed by a route's declared status; the sentence is written per route, not per status, so a user reading it learns what specifically is missing rather than that "this is beta".
src/components/shared/beta-banner.tsx
shallow · amber, AlertTriangle · 9 routes
Prototype — Goals are seeded sample data. Discovery AI not implemented.
partial · blue, FlaskConical · 5 routes
Beta — Conversation list renders. AI processing not yet functional.
reference · slate, Info · declared, never rendered
Reference — Help content is explanatory. Features described may not be live.
broken · red, AlertTriangle · zero routes declare it
Needs Fix — This route has not been evaluated.
hidden · red, AlertTriangle · zero routes declare it
Hidden — This route has not been evaluated.
live · empty label, renders null
nothing is rendered here

Two guards, not one. The component returns null if the route sets showBadge: false, and returns null again if the matched variant has an empty label. The live variant is the second case: it is declared with an empty string precisely so it disappears, which is why the sixth box above is empty rather than blank-looking. An unmapped path falls back to status: 'shallow' with the text "This route has not been evaluated." That fallback text is what the broken and hidden boxes above are showing, because no route supplies a sentence for those statuses: broken, hidden and the fix-before-launch disposition are declared in the type and used by exactly zero routes today. reference is a third case worth naming: four routes declare it, but all four set showBadge: false, so the slate variant never reaches a screen either. Of six variants, two render.

The honesty text has drifted, and it currently undersells the product. Route-truth calls Goals, Systems, Frictions, Choices, Insights, Environments and the Audit Log seeded, mock or static sample data. All seven read and write real rows: Goals and Frictions through src/lib/domains/goals.ts and frictions.ts, Systems and Choices through useDomainCollection, Environments across two tables, Insights via day_rollups, and the Audit Log directly from sync_log and agent_actions. The app's own changelog agrees: v0.5.0 records "no more demo data" for that group and v0.6.0 records "Audit Log is real: actual sync and agent activity, no more sample rows." The banner was written before the wiring landed and was never updated, so a user on /goals is told their real, saved goals are samples. Reproduced here as shipped. A mechanism that tells the truth is only as good as the day someone last checked it, and that is worth saying plainly rather than quietly correcting.

The frame

5 components

Everything that is on screen no matter which surface you are looking at. Between them these five decide what you can reach, how much room the content gets, and how many hours you have left today. On a phone the sidebar and header step aside for a bar of five shortcuts you chose yourself.

AppSidebar Placeholder
The whole map of Flux in one column: 11 core routes, 8 more folded into a Preview accordion, 5 documentation routes in the footer, and the settings for the sidebar itself. What it shows changes with your onboarding state, your admin role and your chosen display mode.
src/components/layout/sidebar.tsx
Expanded · onboarding done, not an admin, Calendar active
The four states that change what is on screen
!onboardingCompleted
Now
isAdmin
Icon collapsed · 3.25rem

The user block in the footer is a mockup. It renders the string "User", the string "user@flux.com" and a placehold.co avatar, and it never calls useUser(), so it shows the same person to everyone including a signed-in Supabase account whose real email the header two rows away is perfectly capable of signing out. The rest of the card is real: 11 core items, 8 preview items at opacity-70, 5 App Mgmt items, an Admin link gated on useIsAdmin(), and a Complete Setup item that appears only while onboardingCompleted is false.

The 11 core items render flat and always; Preview is a closed Radix accordion holding 8 more. The footer stacks the user block, a second accordion of 5 documentation routes, then Help and Settings, then the sidebar's own display settings. Active state is pathname.startsWith(item.href), with a special case so that / only matches itself: without it, Now would be permanently highlighted. The App Mgmt items use exact matching instead. Two deliberate deviations from stock shadcn: collapsed width is 3.25rem (52px) rather than 3rem, and menu icons are size-5 (20px) rather than size-4, inside a 32px row with 8px of padding, which is why a collapsed item is tighter than it looks. Collapsible mode comes from sidebarDisplayMode: only "hide on close" maps to offcanvas, every other mode maps to icon.

AppHeader
48px of chrome on every surface: a sidebar toggle, the page title, and on the right a countdown of the minutes left in the day. Three of its four right-hand slots only appear under a specific condition.
src/components/layout/header.tsx
With a description · countdown showing
Calendar
Drag to move, drag an edge to resize.
0599.06
No description · on "/", so no countdown
Now
source "local-dev" · the amber badge, desktop only
Planner
Local mode only
0599.06
source "supabase" · sign-out appears
Journal
0599.06

The countdown is minutes, not hours. DailyCountdown counts down to 23:59:59.999 and prints total minutes padded to four digits, then a literal ".", then seconds padded to two: at two in the afternoon it reads 0599.06 and the four-digit pad exists because the number peaks at 1439 just after midnight. It renders on every route except /, on the theory that the Now page is already about time. Three conditionals to remember: the description row only exists when description is passed; the "Local mode only" badge needs user.source === "local-dev" and a viewport at md or wider, since it ships hidden md:inline-flex; and the sign-out button needs user.source === "supabase". On a phone in local mode, the badge that tells you nothing is being saved is the thing that hides.

MobileDrawer
The sidebar's replacement on a phone: five shortcuts you chose, a divider, and a chevron that unfolds everything else into a four-column grid. It is the only navigation a mobile user has.
src/components/layout/mobile-drawer.tsx
Collapsed · the five defaults, Calendar active
!isMobile · returns null
On a desktop viewport this component renders nothing at all.

Three routes are unreachable from a phone. The drawer keeps its own allMenuItems array of 17 entries, and that array has fallen behind the sidebar's: Tabatha, People and Context Inbox are in coreItems but not here, so they appear neither in the five shortcuts nor in the expanded grid. There is no "more" link past the grid, so on a phone those three pages have no route to them. MobileDrawerSettings duplicates the same stale array, which is why you cannot pin them either. The rest is straightforward: !isMobile returns null, the expanded grid is allMenuItems.filter(item => !settings.mobileDrawer.includes(item.href)), and active is an exact pathname === item.href match here rather than the sidebar's startsWith.

ThemeToggle
The sun-and-moon button in the header. Three choices, not two: light, dark, and following whatever the operating system says.
src/components/shared/theme-toggle.tsx
Light · sun lit
Dark · moon lit
The menu · three items
Light
Dark
System

No JavaScript runs to swap the icon. Both Sun and Moon are always in the DOM, stacked, each carrying transition-all plus a dark: pair of rotate and scale utilities: light rotates the moon 90 degrees to scale-0, dark does the same to the sun. The cross-fade is a class on <html> and nothing else. Sizing is the odd note: h-[1.2rem] w-[1.2rem] is 19.2px, an arbitrary value where the rest of Flux uses size-4 or size-5. Both variants above are reproduced with the same CSS technique the product uses.

First run and what changed

3 components

Two dialogs that open by themselves, at opposite ends of a user's life with the app: one the first time they arrive, one every time the app has moved on without them. Both are gated on state rather than on a button, and both return null rather than render an empty shell.

OnboardingWizard Placeholder
A four-step setup dialog reached from the sidebar, or from the Profile card once you have finished it. The frame is complete and the contents are not: every step renders the same stub.
src/components/onboarding/onboarding-wizard.tsx
Step 0 · Sparkles · Back is replaced by an empty div
Welcome to Flux
Let's get you set up. This quick onboarding process will help you configure Flux to your workflow.
Step 1 Content
Step 1 · Link · Back appears
Connect Your Tools
Flux works best when connected to your calendar and task managers. Let's link them up.
Step 2 Content
Step 2 · SlidersHorizontal
Define Your Priorities
Tell us about your core life events and priorities so Flux can help you protect that time.
Step 3 Content
Step 3 · ClipboardList · Next becomes Finish
You're All Set!
You've completed the basic setup. You can always change these settings later.
Step 4 Content

The chrome is real and the steps do nothing. All four bodies render the same literal stub, reproduced exactly above: <p>Step {currentStep + 1} Content</p> in a bg-muted/50 box. Nothing is connected, no tool is linked, no priority is defined, and the only step that has an effect is the last one: Finish writes onboardingCompleted: true and raises a toast reading "Onboarding Complete!". That flag is doing real work elsewhere. It removes the Complete Setup item from the sidebar and adds the Re-run Onboarding block to the Profile card, so a user can finish onboarding, change nothing about their setup, and have the app agree they are set up. The footer forks twice: at step 0 the Back button is replaced by an empty <div /> to hold the layout, and at the last step Next becomes Finish.

WhatsNewDialog
Pops once per device when the app has shipped versions this browser has not acknowledged. It leads with plain language and puts the implementation detail behind a button, because a release note has two audiences and one list cannot serve both.
src/components/shared/whats-new-dialog.tsx
One unseen version · the real v0.6.0 entry, plain layer
What's new in Flux
Changes since you were last here.
0.6.0 Flux starts reading your notes back to you 2026-07-04

Write things down the way you already do, and Flux picks out the people, places and tools you mentioned, so your calendar starts explaining itself.

  • Your journals, conversations and voice notes are now read for the people, places and tools in them. Anything Flux is confident about is filed for you. Anything it is unsure about waits in an inbox for a yes or no, and correcting it once teaches it the name for good.
  • Flux warns you before an awkward meeting. If you have an unresolved issue involving someone and a block with them is coming up, it tells you beforehand rather than afterwards.
  • Your insights are built from your real days now, not sample numbers: what you planned against what happened, how much of your time was signal, and a score for the day.
  • Google Calendar works both ways: see your real calendars, pull events in, and push your blocks back out.
  • Add Flux to your phone from the browser and it opens like a normal app.
Technical breakdown open
  • >Ingestion spine: journal / conversation / mental-snapshot / audio saves write ingest_items (migration 015) with claim/finish RPCs.
  • >POST /api/context/process: OpenAI structured extraction -> alias resolution -> ADR-5 confidence thresholds (>=0.85 auto-file, 0.5-0.85 to review). profile_facts carry supersession.
  • >day_rollups + /api/rollups compute planned-vs-actual, signal trend and Good Day score. signal_corrections captures a learning corpus (migration 016).
  • >Google Calendar: real calendar list, import with backlink dedupe, push with timezone. Delta / incremental sync is NOT built; syncing is a manual pull.
unseen.length === 0 · returns null
Nothing new since this device last looked, so nothing mounts.

The dialog reads flux.lastSeenVersion from localStorage and compares it against APP_VERSION in src/lib/changelog.ts, which is the app's real version number (package.json still says 0.1.0 and is stale). Every version in the gap is listed, so a user returning after three releases reads all three. Every localStorage access is wrapped in try/catch with the comment "never block the app for the dialog", and if entriesSince returns nothing the component marks the version seen and renders null without ever opening. Note that on a first run it deliberately shows only the newest entry rather than all twelve.

TechnicalBreakdown
The second layer of a release note. A release has two audiences with incompatible needs, so the plain notes are the default and the implementation detail sits behind this. Opt-in, never in the way, and never hidden from anyone who wants it.
src/components/shared/technical-breakdown.tsx
Closed · the default
Open · chevron rotated
  • >START_HOUR = 7 / TOTAL_HOURS = 15 (7am-10pm) established here and unchanged since.
  • >Firestore auditLog collection — the ancestor of today's sync_log + agent_actions.

Renders null when an entry has no technical array, so an entry can carry one layer or two. The panel is a dashed-border block in monospace, which is the app's real font-code (plain monospace, not a nicer one). What goes in it is a judgement call worth stating: this ships to every user who clicks, so it holds implementation detail rather than secrets. Table names, routes and thresholds are fine; hostnames, credentials and vendor topology are not. The v0.2.0 note used to print the production hostname to every reader and no longer does.

Present but invisible

2 components

Two files under src/components/ that render no markup at all. They are listed rather than dropped for the same reason the dead code elsewhere on this site is listed: a showcase that quietly omits what it cannot draw is a showcase whose count you cannot check.

ThemeProvider Renders no UI
Nine lines wrapping next-themes so the rest of the app can import a theme provider from its own codebase rather than from a package. It is what makes the ThemeToggle above mean anything.
src/components/shared/theme-provider.tsx
Not showcaseable
Renders its children and nothing else.

The entire body is return <NextThemesProvider {...props}>{children}</NextThemesProvider>. It adds no behaviour, no defaults and no types of its own; it exists so that swapping the theming library later is one file rather than every import. Carded so the component count stays reconcilable.

SwRegister Renders no UI
Registers the service worker that makes Flux installable on a phone. It is the whole of the PWA story from the app's side, and it is deliberately impossible to notice.
src/components/shared/sw-register.tsx
Not showcaseable
return null.

An effect registers /sw.js once the document has finished loading, feature-detected on 'serviceWorker' in navigator and swallowing any failure with the comment "offline shell is a progressive enhancement". If the browser refuses, you get no offline shell and no error, which is the right call: the alternative is a toast about a feature the user never asked for. This is what the v0.6.0 note "Install Flux on your phone: Add to Home Screen now works (PWA)" is made of.