diff --git a/docs/LLMISER_CONTROLLER_PAGINATION.md b/docs/LLMISER_CONTROLLER_PAGINATION.md new file mode 100644 index 0000000..8c03d87 --- /dev/null +++ b/docs/LLMISER_CONTROLLER_PAGINATION.md @@ -0,0 +1,49 @@ +# Controller detail pagination + +Updated 29 October 2026. Candidate in [PR #19](https://github.com/DevBehindYou/LLMiser/pull/19). Initial pagination source `2f2afb0` passed all seven GitHub CI jobs, including 278 frontend tests and build, but failed the Vercel preview in Next.js's Google font loader. The owner supplied the build log; a bundled-font fix is being validated. Do not merge or claim production acceptance until the updated runtime preview passes. Previously released runtime remains PR #17, `9131a2ac6163f22b630b008b40accb482ed3e0b3`. + +Opening a large job previously returned all content nodes, work items, events and registered files. The controller now starts with 50 records per section, with independent **Load more** and retry controls. Counts distinguish loaded rows from the full section total. The tree and status badges explicitly describe loaded records; Refresh restarts every section. Six interface languages are supported; blogs and their visuals are unchanged. + +## Read and export contracts + +- Authenticated `GET /api/admin/jobs/:id` returns the job summary, four bounded arrays and `pages` metadata (`total`, `next`, and whole-job `active_total` for files). +- `?section=files|nodes|items|events&limit=50&cursor=...` reads one section. Default 50, maximum 100. Files additionally accept `stored=1` and `kind=INPUT|OUTPUT|EXPORT|PREVIEW`. +- Every query is scoped to the validated job UUID. Stable ascending order uses sequence/UUID for nodes and timestamp/UUID for the other sections. Opaque cursors bind job, section and file filters. PostgreSQL timestamp microseconds are preserved. +- Updated clients send `x-llmiser-detail-paging: 1`. An authenticated old cached client receives 409 with reload guidance, preventing its old ZIP code from exporting only page one. Authentication precedes this check. Sessions, compatibility cookies/CSRF headers, no-store/noindex and existing action/audit rules remain intact. +- Exact totals use PostgREST `Prefer: count=exact` and `Content-Range`. Later cursor requests also issue a HEAD count without the cursor. Initial detail reads use six REST calls: job summary, four section reads and active-file count. Counts and pages are separate reads, not a transactionally consistent snapshot; running jobs can change. Refresh reconciles them. +- ZIP actions resolve every matching ACTIVE file page before signing or downloading. Single-job ZIPs include all stored file kinds; selected-job ZIPs include OUTPUT files. Existing 200-file/200 MiB limits remain. Missing pages, duplicate IDs, changing counts, errors and cancellation reject collection rather than silently exporting an incomplete list. A change after collection can still make a signed download fail; this is not a Storage snapshot. + +No migration, index, RLS, retention, worker, pool, cron or service setting is changed. The existing `get_job_tree` RPC remains available for compatibility; this controller stops calling it. + +## Implementation map + +| Location | Responsibility | +| --- | --- | +| `frontend/lib/server/adminService.js` | Field allowlists, validation, stable cursors, bounded detail sections | +| `frontend/lib/server/supabaseRest.js` | GET/HEAD exact-count response handling; existing calls retain their old shape | +| `frontend/app/api/admin/jobs/[id]/route.js` | Authenticated paging negotiation and section routing | +| `frontend/lib/models/adminDetail.js` | Page deduplication and complete export metadata collection | +| `frontend/viewmodels/useAdminViewModel.js` | Independent section state, aborts and generation guards on refresh/job switch | +| `frontend/components/admin/JobDetail.js` | Loaded/total controls, Refresh and partial-tree notice | +| `frontend/lib/models/contentTree.js` | Set membership for PDF partitioning; PROCESSING status summaries | + +## Local evidence and limits + +Focused service, transport, route, export, React hook, localized rendering and tree checks passed: **12 tests**. Migrated PostgreSQL fixtures contain 123 records per section for two jobs, tied keys and microsecond timestamps. Traversal has exact IDs with no gaps or duplicates; filters and cursors cannot be reused across jobs/sections. A simulated server row cap still produces continuation. React tests exercise job switches, late responses despite abort, repeated clicks, concurrent sections, page failure/retry and refresh invalidation. Controls render counts, disabled state, live announcements and retry alerts in all six languages. `react-test-renderer` 18.3.1 is a development-only dependency matching installed React. + +Generated local measurement: 21 jobs and 22,000 rows per section, including 2,000 rows per section in the target job. The previous response shape was **3,701,322 bytes**; the initial bounded response was **65,901 bytes**, about **98.2% smaller**. This compares serialized response sizes, not heap peaks or page latency. All four collections traversed completely in 20 pages of 100 rows. Typed PostgreSQL EXPLAIN queries used existing job indexes for initial/deep reads and scoped counts. Some queries still sort/filter a job's matching rows; exact counts scan matching entries. This is evidence against adding an index in this slice, not a production latency or constant-cost guarantee. Raw generated plans and logs are private under `project-docs/controller-pagination/`. + +Reproduce generated plans/sizes from `frontend` with `node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/measure-controller-pagination.mjs`. This creates an in-memory migrated PGlite database and prints JSON; it does not contact production. Run `node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test test/adminDetail*.test.mjs test/contentTree.test.mjs` for focused regression. + +Production baseline: one generated 72-text-file ZIP completed through the public Storage flow. Authenticated detail returned 73 nodes, four registered ACTIVE files, one work item and two events. The job has normal 48-hour retention; no cleanup or retention mutation was performed. After release, compare every section's IDs against this baseline, retrieve every stored file and reopen a ZIP containing them. No user documents are needed for acceptance. + +Full local regression: 276 of 278 passed in the sandbox; the two deployment-scope tests could not create temporary Git configurations. Both passed when rerun outside the sandbox. Lint, six dictionaries (637 messages each), SQL consistency, blog consistency and diff whitespace checks passed. The initial lint/build attempt also encountered local filesystem restrictions; elevated lint and production build passed. All seven Linux CI jobs passed; its full frontend run passed all 278 tests and build. React/SSR acceptance does not establish native browser layout, mobile overflow or representative-device performance. + +## Remaining release steps + +1. Validate the bundled-font fix in the real frontend preview (`albedo-projects/llmiser`, root `frontend`). The owner's log for the initial failed preview identifies Next.js 14.2.35's Google loader extension regex returning null at `loader.js:112`. Local/CI builds alone did not reproduce it. This is separate from pagination logic. +2. The app now bundles the exact 16 WOFF2 assets from the successful original build, all 25 face declarations, Unicode ranges, weights, fallback metrics and the same three Latin preloads (93,996 bytes). `frontend/app/fonts.css` and `frontend/public/fonts/` replace the Google loader; official pinned SIL licenses and hash manifest accompany the files. No font/framework upgrade, runtime dependency or blog edit is included. Validate a clean build, font-integrity tests, CI and actual Vercel preview, then merge the pinned passing revision under the owner's auto-merge authorization. A documentation-only skipped build is not acceptance. +3. Verify the production frontend, then run the generated authenticated comparison described above. Private `verify-live.mjs after` reuses the recorded generated job, traverses every section, collects ACTIVE files through one-row pages, signs/downloads all files and reopens an assembled ZIP; it also tests 401/409/400 and private headers. It reads configured keys into memory and never prints keys, cookies or signed links. +4. Run the public HTTP/redirect and uncached pool-readiness checks; record actual results and update current handovers to released only after acceptance. No new conversion job is needed if the baseline is still retained; its `auto_delete_at` is 31 October 2026 17:43:53 UTC. + +PostgREST documents [pagination/count headers and HEAD](https://docs.postgrest.org/en/stable/references/api/pagination_count.html). PostgreSQL explains why [unique ordering is necessary and large offsets remain costly](https://www.postgresql.org/docs/16/queries-limit.html). These contracts guide the cursor design; production measurements must remain separate from local generated evidence. diff --git a/docs/LLMISER_OPTIMIZATION_PLAN.md b/docs/LLMISER_OPTIMIZATION_PLAN.md index 7fae379..07fa48e 100644 --- a/docs/LLMISER_OPTIMIZATION_PLAN.md +++ b/docs/LLMISER_OPTIMIZATION_PLAN.md @@ -22,7 +22,11 @@ The copied source already contains atomic job startup/finalization, cleanup reco Search migration and public redirects are live; see [search migration](LLMISER_SEARCH_MIGRATION.md). Analytics ingestion is verified; controller login/configuration is verified but authenticated operations are not; see [access and analytics](LLMISER_ANALYTICS_CONTROLLER.md). Keep-warm Vault and cron changes were applied and verified in the earlier cron task. Dependency-aware service retirement remains separate operational work. -## Current slice: retained result recovery +## Current slice: controller detail pagination + +Candidate: PR #19 on `codex/controller-detail-pagination`; see [implementation and evidence](LLMISER_CONTROLLER_PAGINATION.md). Files, nodes, work items and events load 50 rows each, with stable unique cursors, exact live totals, independent retry and refresh/job-switch guards. ZIP exports fetch every matching stored file page. Existing indexes suffice; no SQL change. Initial source `2f2afb0` passed all seven CI jobs/278 tests/build, but Vercel's Google font URL parser failed. The owner supplied the error; identical bundled fonts remove that build dependency. Updated checks/preview and deployed comparison remain before release. + +## Historical slice: retained result recovery Branch: `codex/result-download-recovery`. Implementation and local acceptance are complete; CI/review and a separate release decision remain. This is not the production revision. @@ -57,7 +61,7 @@ Acceptance: expired link, later-part failure, complete Unicode assembly, restart The remaining queue is not an effort percentage. Track implementation, local regression/browser evidence, CI and deployed behavior independently. Do not apply production SQL or retire old services merely to complete a checklist. -## Current verification +## Historical recovery verification - Full frontend regression suite: **232 passed, 0 failed** on 22 October 2026. Log: private `project-docs/optimization-evidence/frontend-tests.log`. - Synthetic browser flow: initial final-download 403, browser reload, metadata-only recent-session recovery, complete Markdown download. Fixture counters remained **1 upload, 1 conversion start, 1 retrieval**. No production conversion services were called. diff --git a/docs/START_HERE.md b/docs/START_HERE.md index 7cf6324..addd8b5 100644 --- a/docs/START_HERE.md +++ b/docs/START_HERE.md @@ -4,6 +4,8 @@ Updated 29 October 2026. **LLMiser is the sole working repository and the source ## Current state +Current candidate: [controller detail pagination](LLMISER_CONTROLLER_PAGINATION.md), PR #19 on `codex/controller-detail-pagination`. Initial source `2f2afb0` passed all seven CI jobs (278 frontend tests/build), but Vercel's Google-font URL parser failed. The owner supplied its log. An exact-byte bundled-font fix removes the Google build dependency while retaining existing typography/subsets/preloads; updated validation is in progress. Hold merge until the updated preview passes, then compare the authenticated production baseline. No database migration is required. Production remains the revision below. + The app is live at https://llmiser.devbehindyou.com. PRs #1–#17 are merged; the latest verified runtime revision is `9131a2ac6163f22b630b008b40accb482ed3e0b3` from PR #17. All seven CI jobs passed before and after merge, including 270 frontend tests and Linux OCR; all six production Vercel statuses succeeded. Release checks confirmed that exact revision and readiness on every N/O/Z worker. Thirty protocol checks, eight workbook/ZIP equivalence checks, four post-release Storage conversion/recovery jobs and 20 public HTTP/redirect checks passed; all pools were 2/2 ready. See [shared deadlines](LLMISER_SHARED_DEADLINES.md), [Excel/assembly](LLMISER_EXCEL_ASSEMBLY_MEMORY.md), [CSV capacity](LLMISER_PARSER_CAPACITY.md) and [retirement evidence](LLMISER_LEGACY_RETIREMENT.md). The full app supports English (US), German, Japanese, Spanish, Russian and Simplified Chinese. The browser remembers the interface language; documents are not translated and OCR languages are unchanged. Blogs remain English. Eight articles, eight banners and forty figures are complete. The Chrome extension source is merged, but native acceptance and store publication remain pending. diff --git a/docs/ai-handover/HANDOVER.md b/docs/ai-handover/HANDOVER.md index 0de1aee..661997a 100644 --- a/docs/ai-handover/HANDOVER.md +++ b/docs/ai-handover/HANDOVER.md @@ -4,6 +4,8 @@ Updated 29 October 2026. Start with [START_HERE](../START_HERE.md). This replace ## What the owner wants now +Active implementation: [controller detail pagination](../LLMISER_CONTROLLER_PAGINATION.md), PR #19 on `codex/controller-detail-pagination`. Initial source `2f2afb0` passed all seven CI jobs (278 frontend tests/build); Vercel's Google font URL extension parser failed separately. The owner supplied the log. Exact existing WOFF2 assets, subset declarations/fallback metrics and preloads are now bundled with licenses and hashes; updated checks/preview remain before merge. No connected Vercel tool/CLI session is assumed. Keep complete ZIP traversal, timestamp precision and job/section/filter cursor binding. Counts are separate live reads; Refresh resets all sections. No new SQL/index is needed. Validate the corrected preview, merge under existing authorization, then compare the generated authenticated baseline and public HTTP/wake results. Runtime remains PR #17 until release evidence is recorded. + The owner resumed the optimization queue after the Excel/assembly release. Shared conversion deadlines are now released in PR #17. Continue from the remaining queue rather than repeating either slice. LLMiser must also remain independently maintainable after MDify and MDify-Pro are deleted. Keep immediate permanent redirects on the old public hosts and the minimal LLMiser-only fallback; repository/service deletion is not being executed here. ## Work already shipped diff --git a/docs/ai-handover/PROGRESS.md b/docs/ai-handover/PROGRESS.md index 2db3e3a..f62a83a 100644 --- a/docs/ai-handover/PROGRESS.md +++ b/docs/ai-handover/PROGRESS.md @@ -51,6 +51,8 @@ See [release record](../LLMISER_RELEASE_2026-10-26.md), [localization](../LLMISE ## Remaining work +In progress: [controller detail pagination](../LLMISER_CONTROLLER_PAGINATION.md), PR #19. Initial source `2f2afb0` passed all seven CI jobs/278 frontend tests/build, but Vercel's Google font URL parser failed. The owner supplied the diagnostic. The fix bundles identical existing fonts, subset/weight ranges, fallback metrics and preloads with licenses/hashes; updated checks and preview are pending. Pagination has bounded collections, exact live totals, independent retry and complete ZIP collection. Existing-index plans need no migration. Generated authenticated baseline: 73 nodes, four ACTIVE files, one work item and two events, normal 48-hour retention. Do not call this released until updated CI/preview and production comparison pass. Six dictionaries have 637 candidate messages; production still has 631. + Released: [shared conversion deadlines](../LLMISER_SHARED_DEADLINES.md), PR #17 at 9131a2a. All seven CI jobs passed before/after merge, all six Vercel statuses succeeded and all six workers reported the exact revision/readiness. Thirty production auth/expired/invalid/valid/legacy protocol requests passed; eight future-header workbook/ZIP comparisons and two Storage baselines retained identical output/counts. Two additional Storage jobs exercised image-ZIP OCR/merge and scanned-PDF OCR/merge with exact recovery. Twenty public HTTP checks passed and all pools were 2/2 ready. Six generated Storage jobs in total across pre/post/multi-stage probes follow existing retention; cleanup was not manually run. Controller detail pagination is next; no whole-job lifetime or exactly-once write guarantee is claimed. Released: [Excel conversion and final-result assembly memory](../LLMISER_EXCEL_ASSEMBLY_MEMORY.md), explicitly selected by the owner, PR #15 at 3db4114. Local suites: 144 normal/101 archive tests; installed-version checks passed. All seven Linux CI jobs and six Vercel production statuses passed, with exact revision/readiness verified on all six workers including Render. Eight post-release generated multipart probes and two Storage-backed XLSX/ZIP conversions passed, including all four output objects and exact saved-result recovery. Before/after Markdown, counts and index hashes match; manifest job IDs correctly differ. All 20 HTTP checks passed and all pools were 2/2 ready. Four real jobs across before/after tests follow normal retention; cleanup execution itself was not tested. Local Windows OCR readiness-suite failure is preserved in the report; Linux OCR CI passed. diff --git a/frontend/app/api/admin/jobs/[id]/route.js b/frontend/app/api/admin/jobs/[id]/route.js index c16be19..c4acaa3 100644 --- a/frontend/app/api/admin/jobs/[id]/route.js +++ b/frontend/app/api/admin/jobs/[id]/route.js @@ -1,6 +1,16 @@ -import { jobDetail } from '../../../../../lib/server/adminService'; +import { jobDetail, jobDetailPage } from '../../../../../lib/server/adminService'; import { adminJson, withAdmin } from '../../../../../lib/server/adminRoute'; export const dynamic = 'force-dynamic'; -export const GET = withAdmin(async ({ params, db }) => adminJson(await jobDetail(db, params.id)), { name: 'job' }); +export const GET = withAdmin(async ({ request, params, db }) => { + // Old cached clients must reload rather than silently export only page one. + if (request.headers.get('x-llmiser-detail-paging') !== '1') { + return adminJson({ detail: 'Reload the controller to use paginated job details.' }, { status: 409 }); + } + const query = new URL(request.url).searchParams; + const section = query.get('section'); + return adminJson(section + ? await jobDetailPage(db, params.id, section, { limit: query.get('limit') ?? undefined, cursor: query.get('cursor'), stored: query.get('stored'), kind: query.get('kind') }) + : await jobDetail(db, params.id)); +}, { name: 'job' }); diff --git a/frontend/app/fonts.css b/frontend/app/fonts.css new file mode 100644 index 0000000..8b43483 --- /dev/null +++ b/frontend/app/fonts.css @@ -0,0 +1,31 @@ +/* Pinned self-hosted fonts; see public/fonts/v1/README.md and licenses. */ +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-1.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-2.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-3.woff2) format("woff2");unicode-range:u+1f??} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-4.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-5.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-6.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c4,u+2113,u+2c60-2c7f,u+a720-a7ff} +@font-face{font-family:"LLMiser Inter";font-style:normal;font-weight:100 900;font-display:swap;src:url(/fonts/v1/inter-latin.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} +@font-face{font-family:"LLMiser Inter Fallback";src:local("Arial");ascent-override:90.49%;descent-override:22.56%;line-gap-override:0.00%;size-adjust:107.06%} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-1.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-2.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-3.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-4.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-5.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c4,u+2113,u+2c60-2c7f,u+a720-a7ff} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/jetbrains-mono-latin.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-1.woff2) format("woff2");unicode-range:u+0460-052f,u+1c80-1c8a,u+20b4,u+2de0-2dff,u+a640-a69f,u+fe2e-fe2f} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-2.woff2) format("woff2");unicode-range:u+0301,u+0400-045f,u+0490-0491,u+04b0-04b1,u+2116} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-3.woff2) format("woff2");unicode-range:u+0370-0377,u+037a-037f,u+0384-038a,u+038c,u+038e-03a1,u+03a3-03ff} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-4.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-5.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c4,u+2113,u+2c60-2c7f,u+a720-a7ff} +@font-face{font-family:"LLMiser JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/v1/jetbrains-mono-latin.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} +@font-face{font-family:"LLMiser JetBrains Mono Fallback";src:local("Arial");ascent-override:75.79%;descent-override:22.29%;line-gap-override:0.00%;size-adjust:134.59%} +@font-face{font-family:"LLMiser Patrick Hand";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/patrick-hand-1.woff2) format("woff2");unicode-range:u+0102-0103,u+0110-0111,u+0128-0129,u+0168-0169,u+01a0-01a1,u+01af-01b0,u+0300-0301,u+0303-0304,u+0308-0309,u+0323,u+0329,u+1ea0-1ef9,u+20ab} +@font-face{font-family:"LLMiser Patrick Hand";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/patrick-hand-2.woff2) format("woff2");unicode-range:u+0100-02ba,u+02bd-02c5,u+02c7-02cc,u+02ce-02d7,u+02dd-02ff,u+0304,u+0308,u+0329,u+1d00-1dbf,u+1e00-1e9f,u+1ef2-1eff,u+2020,u+20a0-20ab,u+20ad-20c4,u+2113,u+2c60-2c7f,u+a720-a7ff} +@font-face{font-family:"LLMiser Patrick Hand";font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/v1/patrick-hand-latin.woff2) format("woff2");unicode-range:u+00??,u+0131,u+0152-0153,u+02bb-02bc,u+02c6,u+02da,u+02dc,u+0304,u+0308,u+0329,u+2000-206f,u+20ac,u+2122,u+2191,u+2193,u+2212,u+2215,u+feff,u+fffd} +@font-face{font-family:"LLMiser Patrick Hand Fallback";src:local("Arial");ascent-override:127.97%;descent-override:38.32%;line-gap-override:0.00%;size-adjust:81.43%} +:root { + --font-inter: "LLMiser Inter", "LLMiser Inter Fallback"; + --font-jetbrains-mono: "LLMiser JetBrains Mono", "LLMiser JetBrains Mono Fallback"; + --font-patrick-hand: "LLMiser Patrick Hand", "LLMiser Patrick Hand Fallback"; +} diff --git a/frontend/app/layout.js b/frontend/app/layout.js index 52b07b0..18d1e5f 100644 --- a/frontend/app/layout.js +++ b/frontend/app/layout.js @@ -1,4 +1,4 @@ -import { Inter, JetBrains_Mono, Patrick_Hand } from 'next/font/google'; +import './fonts.css'; import './globals.css'; import ConsentBanner from '../components/ConsentBanner'; import WakeOnOpen from '../components/WakeOnOpen'; @@ -7,23 +7,6 @@ import JsonLd from '../components/JsonLd'; import LanguageProvider from '../components/i18n/LanguageProvider'; import { DESCRIPTION, HOME_TITLE, KEYWORDS, SITE_NAME, SITE_URL, TAGLINE, siteJsonLd } from '../lib/siteContent'; -// next/font downloads these at build time and serves them from our own -// origin, so visitors' browsers never contact Google's font servers (GDPR). -const inter = Inter({ subsets: ['latin'], variable: '--font-inter', display: 'swap' }); -const jetbrainsMono = JetBrains_Mono({ - subsets: ['latin'], - weight: ['400', '500'], - variable: '--font-jetbrains-mono', - display: 'swap', -}); -const patrickHand = Patrick_Hand({ - subsets: ['latin'], - weight: '400', - variable: '--font-patrick-hand', - display: 'swap', -}); -const fontVariables = `${inter.variable} ${jetbrainsMono.variable} ${patrickHand.variable}`; - const BASE_URL = SITE_URL; export const metadata = { @@ -112,8 +95,12 @@ export const viewport = { themeColor: [ export default function RootLayout({ children }) { return ( - + + {/* Exact existing Latin subsets; other scripts load through unicode-range. */} + + + {/* Apply saved theme before paint (default dark), then point the favicon at the matching icon once the head links exist. */}