feat(pulse): alternate mobile interface at /m, sharing the desktop's components - #1697
feat(pulse): alternate mobile interface at /m, sharing the desktop's components#1697elhoim wants to merge 3 commits into
Conversation
Pulse's dashboard is built for a 1600-1920px canvas. On a phone the header
collapses to a hamburger but the content keeps desktop density, and the only
navigation is one long drop-down. This adds a phone interface at /m that shows
the same information with the same structure — because it renders the same
components.
How it avoids becoming a second copy of Pulse:
- Every mobile route is a generated file that re-exports the desktop page
(`export { default } from "@/app/health/page"`). There is no markup on the
mobile side that could drift from the desktop, because there is no markup.
`scripts/gen-mobile-routes.ts` discovers desktop routes from the filesystem,
so an install with more pages simply generates more routes.
- Density is a React context, not a media query. The shared chrome primitives
(PageShell/Panel/PanelHeader/StatTile/TabBar/Pill/EmptyState) read it and
tighten themselves. Desktop routes render with no provider above them, so
they take the default branch and are unchanged.
- Navigation derives from the existing nav-manifest. Adding a page there puts
it in the mobile menu with no mobile-side edit.
TELOS is the one exception, and it is bounded. The desktop stacks twelve
sections into a single scroll, which is the wrong structure on a phone rather
than a wrong amount of detail — so /m/telos is a section index and each section
gets its own route. Both surfaces render from one shared section manifest +
registry, so neither can hold a section the other lacks. Routes that earn a
hand-written mobile treatment must be named in HAND_WRITTEN in the generator.
Rollback is first-class:
- Any visitor: tap the monitor icon to pin the desktop UI for that browser.
The desktop header carries the reverse toggle.
- Whole layer: `bun LIFEOS/PULSE/Tools/MobileUi.ts disable` — no phone is ever
redirected, and /m sends visitors to the desktop route. `status` prints the
exact paths to delete for total removal.
- Desktop is untouched: zero route pages under src/app/<page>/page.tsx are
modified. The four touched files each take a guarded early-return, a single
added element, or a mechanical extraction.
Sizes: the hand-written mobile layer is ~1000 lines presenting ~11,300 lines of
desktop pages, and each mobile page costs ~10 kB more JS than its desktop twin
(the shell) rather than bundling every page.
Verified in real Chrome at a 390x844 viewport: no horizontal page scroll and no
element wider than the viewport outside a scroll container across the tier-1
routes; thumb bar fixed at the viewport bottom; all twelve TELOS sections
rendering; the burger sheet listing every page; the redirect and both toggle
directions surviving reload. Desktop re-checked at 1600px — computed padding,
gap, and type sizes equal the untouched source literals.
Includes tests/mobile-registry.test.ts (17 assertions) covering route coverage,
re-export purity, manifest-derived navigation, section/renderer parity, layer
size, and a regression guard against the dynamic [section] route that built
fine and died on hydration.
…ngside
Reported as "when viewing knowledge in the mobile view, I cannot see an entry".
Reproduced, and it turned out to be three separate faults, one of which is not
mobile-specific at all.
1. RESEARCH NOTES WERE UNREACHABLE ON BOTH INTERFACES (modules/wiki.ts)
`handleKnowledgeNote` validated the domain against its own hardcoded map of
people/companies/ideas/blogs/books. `research` was missing from it. So all 669
Research notes indexed correctly, appeared in the tree, and were counted in the
stats — while opening any one returned `404 Invalid knowledge domain
"research"`. Desktop `/knowledge?category=research&slug=…` showed a permanent
"Loading..." exactly like mobile did. Research dominates Recent Changes, which
is why it looked like "entries don't open".
The domain table existed in three hardcoded copies in this one file
(indexKnowledgeArchive, the tree builder, handleKnowledgeNote) and only the
third had drifted. Fixed by making one KNOWLEDGE_DOMAIN_TABLE the source and
deriving all three, so a domain cannot be half-added again. The lookup now also
accepts the directory name and any casing, since links carry the category
("research") while bookmarks tend to carry the directory ("Research").
2. PLANE SWITCHING DROPPED THE QUERY STRING (lib/mobile/config.ts)
`toMobilePath`/`toDesktopPath` operated on the path only. Knowledge, Docs and
TELOS all identify what you are looking at with `?slug=` / `?id=`, so a
redirect from `/knowledge?category=research&slug=x` rebuilt it as
`/m/knowledge` and returned you to the index. Both helpers now carry query and
hash through, and MobileRedirect passes `search` + `hash` rather than just
`pathname`.
3. IN-PAGE LINKS LEFT THE MOBILE PLANE (components/mobile/MobileShell.tsx)
Because the mobile interface renders the desktop's own components, their links
are desktop-absolute. Tapping an entry navigated to `/knowledge?…`, which then
bounced back through the viewport redirect — a full page load out of and back
into /m. The shell now rewrites same-origin clicks to their `/m` equivalent,
but only when that route exists, so anything without a mobile route still
reaches a working desktop page instead of a 404. The route list is emitted by
gen-mobile-routes.ts (`routes.generated.ts`) so it cannot disagree with the
tree that was just written.
ALSO: the browser tab read "Pulse | Home" on every /m route. On a full page
load Next commits its streamed metadata after the shell's title effect and
resets it; a client-side navigation wins that race, which is why an earlier
check passed. AppHeader already carries a short-lived MutationObserver for this
exact reason — the shell now does too.
Tests: +2 covering query/hash preservation through both helpers and the
generated route list including the HAND_WRITTEN routes (19 total, all passing).
|
Pushed a follow-up commit ( 1. All 669 Research notes were unreachable, on desktop too. The domain table existed in three hardcoded copies in that one file and only the third had drifted. Now one 2. Plane switching dropped the query string. 3. In-page links left the mobile plane. Reusing desktop components means their links are desktop-absolute. The shell now rewrites same-origin clicks to their Also fixed: the tab read "Pulse | Home" on every Tests 17 → 19, build |
|
Follow-up on
Query survives the plane switch on every one, none stuck on One honest limit on the |
WikiMeta is a fixed 224px right rail. That is a fair trade on a wide screen and a bad one on a phone: measured at a 500px viewport it took 210px, leaving 282px for the note itself. Collapsed, the note body gets 458px. It now collapses to a 34px strip carrying an expand button and a vertical "METADATA" label, so the panel stays discoverable rather than simply vanishing. The choice persists per browser under `pulse.wikiMetaRail`. Built into WikiMeta rather than into the Knowledge page because /knowledge, /docs and /system all render this same component — one change, three surfaces. The default follows DensityContext rather than a media query: expanded at desktop density, exactly as before for anyone already using it, and collapsed inside the mobile shell where the space actually matters. A stored preference always wins over the default. Storage failures fall back to the density default rather than throwing. Purely additive: 82 insertions, 0 deletions.
|
Added
Built into The default follows Verified in real Chrome:
Purely additive (82 insertions, 0 deletions). Build I've also corrected the PR description: it said four touched shared files, and with this it is five. The description now also flags that |
Pulse's dashboard is built for a 1600–1920px canvas. On a phone the header collapses to a hamburger but the content keeps desktop density, and the only navigation is one long drop-down. This adds a phone interface at
/mthat shows the same information with the same structure — because it renders the same components.Noting up front that the public repo is generated from a private source tree and community PRs get ported rather than merged — this is written to be easy to port: the mobile layer is almost entirely additive, and the four desktop files it touches take one guarded change each.
The idea
/m/healthisn't a mirror of/health. It is/health:There is no markup on the mobile side that could drift from the desktop, because there is no markup.
scripts/gen-mobile-routes.tsdiscovers desktop routes from the filesystem, so an install with more pages generates more routes automatically.What's actually mobile is the layer around it:
PageShell/Panel/PanelHeader/StatTile/TabBar/Pill/EmptyState) read it and tighten themselves. Desktop routes render with no provider above them, so they take the default branch and are unchanged. A media-query approach would have meant editing all 33 route pages; this touches the primitives once.nav-manifest. Add a page there and it appears in the mobile menu with no mobile-side edit.TELOS is the one exception, and it's bounded
The desktop stacks twelve sections into a single scroll. That's the wrong structure on a phone rather than a wrong amount of detail — so
/m/telosis a section index and each section gets its own route. Both surfaces render from one shared section manifest + registry, so neither can hold a section the other lacks. A route earns a hand-written mobile treatment only by being named inHAND_WRITTENin the generator.Rollback
bun LIFEOS/PULSE/Tools/MobileUi.ts disable— no phone is redirected, and/msends visitors to the desktop route.MobileUi.ts statusprints the exact paths to delete and files to revert.Desktop is untouched: zero route pages under
src/app/<page>/page.tsxare modified. The five touched shared files areapp/layout.tsx(redirect + CSS import + viewport),AppHeader.tsx(early-return on/m, one affordance),ui/chrome.tsx(density branches),telos/_v7/app.tsx(the twelve-section stack extracted to the shared registry, same order, same props), andwiki/WikiMeta.tsx(the metadata rail became collapsible — purely additive, and desktop keeps its expanded default). One further file,modules/wiki.ts, is a backend fix that is not mobile-specific and may be worth porting separately; see the commit and comments below.Cost
/m/healthagainst 605 kB for/health— per-route files fixed it, and a test now pins the shape.Testing evidence
Built clean from a fresh checkout of this repo's tree plus this commit:
✓ Generating static pages (77/77), exit 0.tests/mobile-registry.test.ts— 17 assertions, all passing — covering route coverage, re-export purity (every generated route file must matchexport { default } from "@/app/…";and nothing else), manifest-derived navigation, section/renderer parity, layer size, and a regression guard described below.Verified in real Chrome at a 390×844 viewport:
position: fixedwith its bottom edge exactly at the viewport bottom.Three bugs were caught by that browser pass and are fixed here — worth flagging because two of them passed every non-browser check:
app/m/telos/[section]route withgenerateStaticParamscompiled, exported twelve HTML files and served 200s, then threw a client-side exception on hydration on every one. Error boundaries around the page body and the whole route both failed to catch it; the neighbouring static route was fine. Static generated routes are the fix, and a test guards against the dynamic variant returning.AppHeader's title observer kept running on/m— the early return stops rendering, but hooks still run.Not run: a fresh-system install verification. This was developed and verified against a working install and a clean checkout build, not a from-scratch
install.shrun.Note for reviewers
This branch is built against this repo's page set.
nav-manifestlists/projectsand/security, but neithersrc/app/projects/page.tsxnorsrc/app/security/page.tsxis present here — those are already dead links on desktop in this repo. The mobile tree correctly declines to fabricate pages that don't exist, and the coverage test is gated on the desktop page existing, so on an install that has them the generator produces their mobile routes automatically. Possibly a public-release packaging gap worth a separate look; out of scope here.