Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit afed744706a6625e45e589bba4ad01560ce99e3d
parent bff87f0a506762ece7912df4aabfc5e2fb7b86e6
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 20:58:32 -0400

common: the published accent stays a hex — /site.json, hub-sites.json, the homepage summary (brand S0)

Third-party hubs read other sites' /site.json and draw `accent` as a colour,
so the three publishers route site.json's accent through accentHex: an id
is published as its on-dark value, a custom hex as stored, no accent as no
key (unchanged). Tests: siteDescriptor (new), compose-hub's pool file, and
buildHomepageSummary. wordmarkLead is not published.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/bin/compose-hub.test.ts | 28++++++++++++++++++++++++++++
Mcommon/bin/compose-hub.ts | 4+++-
Mcommon/lib/homepageSummary.test.ts | 13+++++++++++++
Mcommon/lib/homepageSummary.ts | 4+++-
Acommon/lib/siteDescriptor.test.ts | 40++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/siteDescriptor.ts | 4+++-
6 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/common/bin/compose-hub.test.ts b/common/bin/compose-hub.test.ts @@ -53,3 +53,31 @@ test("with no index, compose-hub writes the pool and no hub-summary.json, removi rmSync(root, { recursive: true, force: true }); } }); + +test("hub-sites.json publishes every accent as a hex: an id becomes its on-dark value", async () => { + const root = mkdtempSync(path.join(tmpdir(), "compose-hub-")); + const log = console.log; + try { + const paths = fixturePaths(root); + const write = (id: string, site: Record<string, unknown>) => { + mkdirSync(path.join(paths.sitesDir, id), { recursive: true }); + writeFileSync(path.join(paths.sitesDir, id, "site.json"), JSON.stringify(site)); + }; + write("named", { siteTitle: "Named", siteUrl: "https://named.example", accent: "violet" }); + write("custom", { siteTitle: "Custom", siteUrl: "https://custom.example", accent: "#CC3366" }); + write("plain", { siteTitle: "Plain", siteUrl: "https://plain.example" }); + console.log = () => {}; + await main({ paths }); + console.log = log; + const pool = JSON.parse( + readFileSync(path.join(paths.exportPublicDir, "hub-sites.json"), "utf8"), + ) as Array<{ siteId: string; accent?: string }>; + const accentOf = (id: string) => pool.find((s) => s.siteId === id); + assert.equal(accentOf("named")?.accent, "#b49cf2"); + assert.equal(accentOf("custom")?.accent, "#cc3366"); + assert.ok(accentOf("plain") && !("accent" in accentOf("plain")!)); + } finally { + console.log = log; + rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/common/bin/compose-hub.ts b/common/bin/compose-hub.ts @@ -18,6 +18,7 @@ import path from "node:path"; import { existsSync } from "node:fs"; import { cp, rm, writeFile, access } from "node:fs/promises"; import { getPaths, type Paths } from "../lib/paths"; +import { accentHex } from "../lib/accent"; import { listSites, resolveHubUrl } from "../lib/site"; import { getHomepageConfig } from "../lib/homepage"; import { SITE_DESCRIPTOR_VERSION } from "../lib/siteDescriptor"; @@ -91,7 +92,8 @@ export async function main(opts: { paths?: Paths } = {}): Promise<void> { siteId: site.siteId, siteTitle: site.siteTitle, siteUrl: site.siteUrl, - ...(site.accent ? { accent: site.accent } : {}), + // The hub draws it as a colour: an accent id is published as its hex. + ...(accentHex(site.accent) ? { accent: accentHex(site.accent) } : {}), ...(resolveHubUrl(site) ? { hubUrl: resolveHubUrl(site) } : {}), pwa: site.pwa === true, contract: SITE_DESCRIPTOR_VERSION, diff --git a/common/lib/homepageSummary.test.ts b/common/lib/homepageSummary.test.ts @@ -184,3 +184,16 @@ test("gone counts only a deleted record we hold (it has a downloadedDate)", () = assert.equal(s.sites.find((x) => x.siteId === "beta")!.gone, 0); assert.equal(s.official!.gone, 1); }); + +test("a site's accent is published as a hex: an id becomes its on-dark value", () => { + const sites = [ + { ...site("alpha", ["a1", "a2"], "https://alpha.example"), accent: "brass" }, + { ...site("beta", ["b1"], "https://beta.example"), accent: "#CC3366" }, + ] as Site[]; + const s = buildHomepageSummary(STATS, CHANNEL_SITES, sites, NOW); + assert.equal(s.sites.find((x) => x.siteId === "alpha")!.accent, "#e3b15c"); + assert.equal(s.sites.find((x) => x.siteId === "beta")!.accent, "#cc3366"); + // No accent: no key, as before. + const plain = buildHomepageSummary(STATS, CHANNEL_SITES, SITES, NOW); + assert.ok(plain.sites.every((x) => !("accent" in x))); +}); diff --git a/common/lib/homepageSummary.ts b/common/lib/homepageSummary.ts @@ -1,6 +1,7 @@ import type { Platform } from "./platform"; import type { VideoStat } from "./stats"; import type { Site } from "./site"; +import { accentHex } from "./accent"; import { VIDEO_STATES, type VideoState } from "./availability"; // Pre-computed, lightweight cross-site summary for the hub (homepage) landing. @@ -428,7 +429,8 @@ export function buildHomepageSummary( recordings: siteAcc.get(s.siteId)?.recordings ?? 0, hoursArchived: Math.round((siteAcc.get(s.siteId)?.seconds ?? 0) / 3600), gone: siteAcc.get(s.siteId)?.gone ?? 0, - ...(s.accent ? { accent: s.accent } : {}), + // A published colour: an accent id becomes its hex (lib/accent.ts). + ...(accentHex(s.accent) ? { accent: accentHex(s.accent) } : {}), })) // Keep a public site only if it has any activity in either metric. .filter((s) => s.transcribed.total > 0 || s.downloaded.total > 0) diff --git a/common/lib/siteDescriptor.test.ts b/common/lib/siteDescriptor.test.ts @@ -0,0 +1,40 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildSiteDescriptor } from "./siteDescriptor"; +import { parseSite } from "./siteSchema"; +import type { Manifest } from "./manifest"; +import { ACCENTS } from "./brand"; + +// The public /site.json is read by OTHER hubs, which draw `accent` as a colour. +// A site.json may now store an accent id; the descriptor must still publish a +// hex (lib/accent.ts accentHex). + +const MANIFEST = { + version: 3, + totalCount: 0, + pageSize: 100, + pageCount: 0, + generatedAt: "2026-09-25T00:00:00.000Z", + channels: [], +} as Manifest; + +function descriptorFor(raw: Record<string, unknown>) { + return buildSiteDescriptor(parseSite("s", raw), MANIFEST, [], { pwa: false }); +} + +test("an accent id is published as its on-dark hex", () => { + assert.equal(descriptorFor({ accent: "brass" }).accent, ACCENTS.brass.onDark); + assert.equal(descriptorFor({ accent: "sakura" }).accent, "#ee8fb5"); +}); + +test("a custom hex is published as stored; no accent publishes no key", () => { + assert.equal(descriptorFor({ accent: "#CC3366" }).accent, "#cc3366"); + assert.equal("accent" in descriptorFor({}), false); + assert.equal("accent" in descriptorFor({ accent: "gold" }), false); +}); + +test("the wordmark lead is not part of the public descriptor", () => { + const d = descriptorFor({ headerTitle: "Jeralyzer", wordmarkLead: "Jer" }); + assert.equal("wordmarkLead" in d, false); + assert.equal(d.headerTitle, "Jeralyzer"); +}); diff --git a/common/lib/siteDescriptor.ts b/common/lib/siteDescriptor.ts @@ -1,3 +1,4 @@ +import { accentHex } from "./accent"; import type { ChannelGroup } from "./channelGroups"; import { CONTRACT } from "./corpus"; import type { Manifest } from "./manifest"; @@ -84,7 +85,8 @@ export function buildSiteDescriptor( siteDescription: site.siteDescription, headerTitle: site.headerTitle, homeTagline: site.homeTagline, - ...(site.accent ? { accent: site.accent } : {}), + // Always a hex on the wire: an accent id means nothing to another hub. + ...(accentHex(site.accent) ? { accent: accentHex(site.accent) } : {}), ...(site.siteUrl ? { siteUrl: site.siteUrl } : {}), ...(opts.hubUrl ? { hubUrl: opts.hubUrl } : {}), pwa: opts.pwa,