Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions LifeOS/install/LIFEOS/PULSE/Observability/scripts/gen-mobile-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env bun
/**
* Generates the mobile route tree: one `src/app/m/<path>/page.tsx` per desktop
* route page, each a pure re-export of the desktop module.
*
* Why generated files instead of one catch-all route: a catch-all has to
* reference every page from a single module, and Next then puts all of them in
* that route's client manifest — measured at 2.1 MB of chunks for `/m/health`
* against 605 kB for `/health`, and `next/dynamic` did not change it. Real
* route files give real per-route code splitting.
*
* Why re-exports instead of mobile page components: a re-export cannot drift.
* There is no markup here to fall out of date with the desktop page, because
* there is no markup here at all.
*
* bun scripts/gen-mobile-routes.ts write the tree
* bun scripts/gen-mobile-routes.ts --check fail if the tree is stale (CI)
*/

import { mkdirSync, readdirSync, rmSync, statSync, existsSync, readFileSync, writeFileSync } from "node:fs";
import { join, relative } from "node:path";
import { TELOS_SECTION_META } from "../src/app/telos/_v7/section-manifest";

const SRC = join(import.meta.dir, "..", "src");
const APP = join(SRC, "app");
const M = join(APP, "m");

/**
* Routes whose mobile treatment is deliberately NOT a re-export of the desktop
* page, and which the generator must therefore neither write nor prune.
*
* Keep this set tiny. A route earns a place here only when the desktop page is
* structurally wrong for a phone — as `/telos` is: it stacks twelve sections
* into one scroll, so mobile lands on a section index and loads one section
* per route from the shared registry instead. Everything else stays a
* re-export, because a re-export cannot drift.
*
* "" is the home route, which renders the same TELOS view as /telos.
*/
const HAND_WRITTEN = new Set(["", "telos"]);

/** Every desktop route page, discovered from the filesystem — no hand list. */
function discoverRoutes(dir: string, prefix = ""): string[] {
const routes: string[] = [];
for (const entry of readdirSync(dir)) {
// Skip the mobile tree itself, private folders (_v7), and route groups.
if (entry === "m" && prefix === "") continue;
if (entry.startsWith("_") || entry.startsWith("(") || entry.startsWith("[")) continue;
const p = join(dir, entry);
if (statSync(p).isDirectory()) {
routes.push(...discoverRoutes(p, prefix ? `${prefix}/${entry}` : entry));
} else if (entry === "page.tsx") {
routes.push(prefix);
}
}
return routes;
}

/** Desktop layouts that must wrap their mobile counterpart too. */
function layoutFor(route: string): string | null {
const candidate = join(APP, route, "layout.tsx");
return route && existsSync(candidate) ? route : null;
}

const HEADER = (target: string) =>
`// Generated by scripts/gen-mobile-routes.ts — do not edit.\n` +
`// The mobile interface renders the desktop component itself; this file\n` +
`// exists only so Next gives the route its own JS chunk.\n` +
`export { default } from "@/app/${target}";\n`;

const ROOT_HEADER =
`// Generated by scripts/gen-mobile-routes.ts — do not edit.\n` +
`// The mobile interface renders the desktop component itself; this file\n` +
`// exists only so Next gives the route its own JS chunk.\n` +
`export { default } from "@/app/page";\n`;

const routes = discoverRoutes(APP).sort();
const files = new Map<string, string>();

/**
* One STATIC route per TELOS section.
*
* This started as a single dynamic route, `m/telos/[section]`, with
* generateStaticParams. It built and served 200s for all twelve paths — and
* every one of them died in the browser with Next's generic "a client-side
* exception has occurred", after the data arrived. Error boundaries around the
* section body AND around the whole route both failed to catch it, which is
* what identified it as a hydration failure of the dynamic route rather than a
* fault in any section: `[section]` was the only dynamic route in the mobile
* tree, and the neighbouring STATIC route /m/telos/item was fine throughout.
*
* Static routes remove the failure mode entirely, and match how the rest of
* this tree already works.
*/
const SECTION_HEADER = (slug: string) =>
`// Generated by scripts/gen-mobile-routes.ts — do not edit.\n` +
`// One static route per TELOS section. NOT a dynamic [section] route:\n` +
`// that variant hydrated into a client-side exception on every section.\n` +
`"use client";\n\n` +
`import TelosSectionView from "@/components/mobile/TelosSectionView";\n\n` +
`export default function Page() {\n` +
` return <TelosSectionView slug="${slug}" />;\n` +
`}\n`;

for (const s of TELOS_SECTION_META) {
files.set(join(M, "telos", s.slug, "page.tsx"), SECTION_HEADER(s.slug));
}

if (!HAND_WRITTEN.has("")) files.set(join(M, "page.tsx"), ROOT_HEADER);
for (const route of routes) {
if (route === "" || HAND_WRITTEN.has(route)) continue;
files.set(join(M, route, "page.tsx"), HEADER(`${route}/page`));
const withLayout = layoutFor(route);
if (withLayout) files.set(join(M, withLayout, "layout.tsx"), HEADER(`${withLayout}/layout`));
}

/**
* The route list, emitted for runtime use.
*
* The mobile shell rewrites in-page desktop links (`/knowledge?slug=…`) to
* their `/m` equivalent, and to do that safely it has to know which routes
* actually exist — rewriting a path with no mobile route would 404 someone who
* would otherwise have reached a working desktop page. Emitting the list from
* the generator means it can never disagree with the tree it just wrote.
*/
const routeKeys = [
...new Set([
...[...files.keys()]
.filter((p) => p.endsWith("page.tsx"))
.map((p) => relative(M, p).replace(/[/\\]page\.tsx$/, "").replace(/\\/g, "/"))
.map((k) => (k === "page.tsx" ? "" : k)),
// HAND_WRITTEN routes are real mobile routes that this script does not
// write, so they are absent from `files` and would otherwise be omitted —
// leaving `/` and `/telos` un-rewritten by the shell's link interceptor.
...HAND_WRITTEN,
]),
].sort();
files.set(
join(SRC, "lib", "mobile", "routes.generated.ts"),
`// Generated by scripts/gen-mobile-routes.ts — do not edit.\n` +
`// Every path that has a mobile route, keyed like the desktop path with no\n` +
`// leading slash ("" is the home route).\n` +
`export const MOBILE_ROUTES: readonly string[] = ${JSON.stringify(routeKeys, null, 2)};\n`
);

const check = process.argv.includes("--check");
let stale = 0;

for (const [path, body] of files) {
const current = existsSync(path) ? readFileSync(path, "utf-8") : null;
if (current === body) continue;
stale++;
if (check) {
console.error(`stale: ${relative(SRC, path)}`);
} else {
mkdirSync(join(path, ".."), { recursive: true });
writeFileSync(path, body);
}
}

// Prune routes that no longer exist upstream.
function existingGenerated(dir: string): string[] {
if (!existsSync(dir)) return [];
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
if (statSync(p).isDirectory()) out.push(...existingGenerated(p));
else if (/^(page|layout)\.tsx$/.test(entry) && readFileSync(p, "utf-8").startsWith("// Generated by")) out.push(p);
}
return out;
}

for (const path of existingGenerated(M)) {
if (files.has(path)) continue;
stale++;
if (check) console.error(`orphan: ${relative(SRC, path)}`);
else rmSync(path);
}

if (check && stale > 0) {
console.error(`\n✗ mobile route tree is ${stale} file(s) out of date — run: bun scripts/gen-mobile-routes.ts`);
process.exit(1);
}

console.log(
check
? `✓ mobile route tree current (${files.size} files, ${routes.length} desktop routes)`
: `✓ wrote ${files.size} mobile route files for ${routes.length} desktop routes (${stale} changed)`
);
14 changes: 13 additions & 1 deletion LifeOS/install/LIFEOS/PULSE/Observability/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import AppHeader from "@/components/AppHeader";
import CommandPalette from "@/components/palette/CommandPalette";
import TemplateOnboarding from "@/components/TemplateOnboarding";
import MobileRedirect from "@/components/mobile/MobileRedirect";
import { Providers } from "./providers";
import "./globals.css";
import "./telos/_v7/styles.css";
import "@/components/mobile/mobile.css";

export const metadata: Metadata = {
title: "Pulse | Home",
Expand All @@ -16,6 +18,15 @@ export const metadata: Metadata = {
},
};

// viewportFit:"cover" is what makes env(safe-area-inset-bottom) non-zero, so
// the mobile thumb bar clears the home indicator on a notched phone.
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
viewportFit: "cover",
themeColor: "#060B1A",
};

export default function RootLayout({
children,
}: Readonly<{
Expand All @@ -25,6 +36,7 @@ export default function RootLayout({
<html lang="en" className="dark">
<body className={`${GeistSans.variable} ${GeistMono.variable} font-sans`}>
<Providers>
<MobileRedirect />
<AppHeader />
<CommandPalette />
<TemplateOnboarding />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/agents/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/air/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/amber/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/arbol/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/assets/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/assistant/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/books/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/bunker/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/business/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/conduit/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/content/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/docs/layout";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/docs/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/finances/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/growth/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/health/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/hooks/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/hypotheses/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/knowledge/graph/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/knowledge/layout";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/knowledge/page";
10 changes: 10 additions & 0 deletions LifeOS/install/LIFEOS/PULSE/Observability/src/app/m/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import MobileShell from "@/components/mobile/MobileShell";

/**
* The mobile plane. Nests inside the root layout, which is why AppHeader
* carries a single early-return for `/m` — the desktop header and this shell
* are two chromes for one app, never both at once.
*/
export default function MobileLayout({ children }: { children: React.ReactNode }) {
return <MobileShell>{children}</MobileShell>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/life/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/local/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/memory/graph/page";
6 changes: 6 additions & 0 deletions LifeOS/install/LIFEOS/PULSE/Observability/src/app/m/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Hand-written — NOT generated. Listed in HAND_WRITTEN in
// scripts/gen-mobile-routes.ts so the generator leaves it alone.
//
// Desktop `/` is the TELOS columns view — all twelve sections in one scroll.
// The mobile home is the same content reached one section at a time.
export { default } from "@/components/mobile/TelosIndex";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/performance/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/skills/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/system/graph/page";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/system/layout";
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by scripts/gen-mobile-routes.ts — do not edit.
// The mobile interface renders the desktop component itself; this file
// exists only so Next gives the route its own JS chunk.
export { default } from "@/app/system/page";
Loading