diff --git a/frontend/fonts/BricolageGrotesque-latin-ext.woff2 b/frontend/fonts/BricolageGrotesque-latin-ext.woff2 new file mode 100644 index 0000000..6e8caa8 Binary files /dev/null and b/frontend/fonts/BricolageGrotesque-latin-ext.woff2 differ diff --git a/frontend/fonts/BricolageGrotesque-latin.woff2 b/frontend/fonts/BricolageGrotesque-latin.woff2 new file mode 100644 index 0000000..fcc4eb1 Binary files /dev/null and b/frontend/fonts/BricolageGrotesque-latin.woff2 differ diff --git a/frontend/fonts/HankenGrotesk-latin-ext.woff2 b/frontend/fonts/HankenGrotesk-latin-ext.woff2 new file mode 100644 index 0000000..1dc0e89 Binary files /dev/null and b/frontend/fonts/HankenGrotesk-latin-ext.woff2 differ diff --git a/frontend/fonts/HankenGrotesk-latin.woff2 b/frontend/fonts/HankenGrotesk-latin.woff2 new file mode 100644 index 0000000..518e2c4 Binary files /dev/null and b/frontend/fonts/HankenGrotesk-latin.woff2 differ diff --git a/frontend/fonts/JetBrainsMono-latin-ext.woff2 b/frontend/fonts/JetBrainsMono-latin-ext.woff2 new file mode 100644 index 0000000..82f9668 Binary files /dev/null and b/frontend/fonts/JetBrainsMono-latin-ext.woff2 differ diff --git a/frontend/fonts/JetBrainsMono-latin.woff2 b/frontend/fonts/JetBrainsMono-latin.woff2 new file mode 100644 index 0000000..4d09cda Binary files /dev/null and b/frontend/fonts/JetBrainsMono-latin.woff2 differ diff --git a/frontend/fonts/fonts.css b/frontend/fonts/fonts.css new file mode 100644 index 0000000..b56a2c5 --- /dev/null +++ b/frontend/fonts/fonts.css @@ -0,0 +1,51 @@ +/* Generated by scripts/fetch-fonts.js — do not edit by hand. */ +@font-face { + font-family: 'Bricolage Grotesque'; + font-style: normal; + font-weight: 600 800; + font-stretch: 100%; + font-display: swap; + src: url(BricolageGrotesque-latin-ext.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-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Bricolage Grotesque'; + font-style: normal; + font-weight: 600 800; + font-stretch: 100%; + font-display: swap; + src: url(BricolageGrotesque-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, 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: 'Hanken Grotesk'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(HankenGrotesk-latin-ext.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-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Hanken Grotesk'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(HankenGrotesk-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, 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: 'JetBrains Mono'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(JetBrainsMono-latin-ext.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-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url(JetBrainsMono-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, 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; +} diff --git a/frontend/index.html b/frontend/index.html index 90eef71..aba8703 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,16 +13,13 @@ YT Player - - - + + + diff --git a/frontend/sw.js b/frontend/sw.js index 684f754..bf34b05 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -68,6 +68,13 @@ const SHELL = [ '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', + '/fonts/fonts.css', + '/fonts/BricolageGrotesque-latin-ext.woff2', + '/fonts/BricolageGrotesque-latin.woff2', + '/fonts/HankenGrotesk-latin-ext.woff2', + '/fonts/HankenGrotesk-latin.woff2', + '/fonts/JetBrainsMono-latin-ext.woff2', + '/fonts/JetBrainsMono-latin.woff2', ]; // ---- Install: pre-cache the app shell ---- diff --git a/package.json b/package.json index 5992785..07e0c6a 100755 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "update-ytdlp": "node scripts/setup-ytdlp.js --force", "make-icon": "node scripts/make-icon.js", "make-pwa-icons": "node scripts/make-pwa-icons.js", + "fetch-fonts": "node scripts/fetch-fonts.js", "tauri": "tauri", "tauri:dev": "tauri dev", "tauri:build": "tauri build", diff --git a/plans/INDEX.md b/plans/INDEX.md index 43ed99c..d260751 100644 --- a/plans/INDEX.md +++ b/plans/INDEX.md @@ -10,7 +10,7 @@ green, app boots with no JS errors, P2P on by default, offline boot works). |-----|----|-------|--------|--------|-------| | 001 | 001-perf-timing-marks-105acc | Add startup, search and play timing marks plus yt-dlp duration logs | done | Add startup, search and play timing marks plus yt-dlp duration logs | baseline numbers for the rest | | 002 | 002-compress-and-etag-shell-bd459c | Serve the app shell gzip/brotli-compressed with ETags | done | Serve the app shell gzip/brotli-compressed with ETags | app.js 426 KB → 94 KB (br) | -| 003 | 003-self-host-fonts-89466b | Self-host the three web fonts and drop the render-blocking Google Fonts CSS | in-progress | | needs network once; CSP font-src fix | +| 003 | 003-self-host-fonts-89466b | Self-host the three web fonts and drop the render-blocking Google Fonts CSS | done | Self-host the three web fonts and drop the render-blocking Google Fonts CSS | needs network once; CSP font-src fix | | 004 | 004-coalesce-stream-resolves-a92d40 | Coalesce concurrent resolveStreams calls for the same video | queued | | | | 005 | 005-warm-streams-on-intent-47b3d3 | Warm the stream cache for likely next plays | queued | | cold play 7.5 s → cached 1.2 s | | 006 | 006-innertube-search-48066b | Answer searches from YouTube InnerTube directly with yt-dlp fallback | queued | | search 4–5 s → ~0.8 s | diff --git a/plans/active/003-self-host-fonts-89466b.md b/plans/done/003-self-host-fonts-89466b.md similarity index 91% rename from plans/active/003-self-host-fonts-89466b.md rename to plans/done/003-self-host-fonts-89466b.md index 1346fe7..486fba2 100644 --- a/plans/active/003-self-host-fonts-89466b.md +++ b/plans/done/003-self-host-fonts-89466b.md @@ -129,3 +129,9 @@ Output ONLY the following, no other prose: 3. `Findings:` — max 10 lines. Do not commit. Do not push. Do not touch files outside the Steps. + +## Execution log + +- Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0. +- Orchestrator re-ran Verification: 6 woff2 + fonts.css present, Google Fonts link count 0, CSP `font-src 'self'` 1, each woff2 precached in sw.js once, 52 frontend tests pass, `/fonts/fonts.css` 200 text/css and woff2 200 font/woff2 from a real server. +- Executor Findings (verbatim): Plan executed successfully. All 6 font files generated (3 families x 2 subsets). CSP updated to allow 'self' fonts. SW.js updated to precache all fonts. Google Fonts links completely removed. Server responds correctly to font requests. Unit tests pass. diff --git a/scripts/fetch-fonts.js b/scripts/fetch-fonts.js new file mode 100644 index 0000000..9c76b5e --- /dev/null +++ b/scripts/fetch-fonts.js @@ -0,0 +1,31 @@ +// Downloads the app's Google fonts once (latin + latin-ext subsets) into +// frontend/fonts/ and writes frontend/fonts/fonts.css pointing at them. +// Re-run only when the font list changes. +const fs = require('node:fs'); +const path = require('node:path'); +const CSS_URL = 'https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600..800&family=Hanken+Grotesk:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap'; +const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36'; +const OUT = path.join(__dirname, '..', 'frontend', 'fonts'); +(async () => { + fs.mkdirSync(OUT, { recursive: true }); + const css = await (await fetch(CSS_URL, { headers: { 'User-Agent': UA } })).text(); + // parts[k] ends with the "/* */" comment of the block in parts[k+1]. + const parts = css.split('@font-face'); + let out = '/* Generated by scripts/fetch-fonts.js — do not edit by hand. */\n'; + let n = 0; + for (let k = 1; k < parts.length; k++) { + const subset = (parts[k - 1].match(/\/\*\s*([\w-]+)\s*\*\/\s*$/) || [])[1]; + const b = parts[k].slice(0, parts[k].indexOf('}') + 1); // just this block + if (subset !== 'latin' && subset !== 'latin-ext') continue; + const url = (b.match(/url\((https:[^)]+\.woff2)\)/) || [])[1]; + const fam = (b.match(/font-family:\s*'([^']+)'/) || [])[1]; + if (!url || !fam) continue; + const file = `${fam.replace(/\s+/g, '')}-${subset}.woff2`; + const buf = Buffer.from(await (await fetch(url)).arrayBuffer()); + fs.writeFileSync(path.join(OUT, file), buf); + out += '@font-face' + b.replace(url, file).trimEnd() + '\n'; + n++; + } + fs.writeFileSync(path.join(OUT, 'fonts.css'), out); + console.log(`wrote ${n} font files + fonts.css`); +})().catch((e) => { console.error(e); process.exit(1); });