scribe-cms 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,5 +53,13 @@ export declare function listTranslationsForEnSlug(db: Database.Database, content
53
53
  export declare function listTranslationsForLocale(db: Database.Database, contentType: string, locale: string): TranslationRow[];
54
54
  export declare function bulkLoadTranslations(db: Database.Database): TranslationRow[];
55
55
  export declare function countTranslations(db: Database.Database): number;
56
+ export declare function countTranslationsByLocale(db: Database.Database): Array<{
57
+ locale: string;
58
+ count: number;
59
+ }>;
60
+ export declare function latestTranslationAtByLocale(db: Database.Database): Array<{
61
+ locale: string;
62
+ latest: string;
63
+ }>;
56
64
  export declare function countStaleTranslations(db: Database.Database, contentType: string, enSlug: string, currentEnHash: string): number;
57
65
  //# sourceMappingURL=translations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/storage/translations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,cAAc;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,eAAe,GAAG,MAAM,CAsB3F;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,UAAU,EAAE,MAAM,GACjB,aAAa,GAAG,SAAS,CAI3B;AAED,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,qBAAqB,EAAE,CAWzB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAyBtF;AAED,wBAAgB,cAAc,CAC5B,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,cAAc,GAAG,SAAS,CAM5B;AAED,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,GAClB,cAAc,EAAE,CAIlB;AAED,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,cAAc,EAAE,CAMlB;AAED,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,cAAc,EAAE,CAIlB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,cAAc,EAAE,CAE5E;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAG/D;AAED,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,MAAM,CAQR"}
1
+ {"version":3,"file":"translations.d.ts","sourceRoot":"","sources":["../../../src/storage/translations.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,8 @@ import type { ResolvedTranslateConfig } from "../resolve-translate-config.js";
2
2
  import type { SlugStrategy } from "../../core/types.js";
3
3
  export declare const LOCALE_NAMES: Record<string, string>;
4
4
  export declare function defaultLocalizationPrompt(localeName: string, locale: string): string;
5
+ /** Type/project `translate.prompt` extras are appended before the locale directive. */
6
+ export declare function buildLocalizationPrompt(promptOverride: string | undefined, localeName: string, locale: string): string;
5
7
  export declare function buildPageTranslationPrompt(input: {
6
8
  resolved: ResolvedTranslateConfig;
7
9
  targetLocale: string;
@@ -1 +1 @@
1
- {"version":3,"file":"translation-prompt.d.ts","sourceRoot":"","sources":["../../../../src/translate/prompts/translation-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiB/C,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOpF;AA8CD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,QAAQ,EAAE,uBAAuB,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,MAAM,CA8CT"}
1
+ {"version":3,"file":"translation-prompt.d.ts","sourceRoot":"","sources":["../../../../src/translate/prompts/translation-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAC9E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAiB/C,CAAC;AAEF,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAOpF;AAED,uFAAuF;AACvF,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,MAAM,CAIR;AA8CD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IAChD,QAAQ,EAAE,uBAAuB,CAAC;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,MAAM,CAgDT"}
@@ -3,7 +3,7 @@ export interface TranslationWorkItem {
3
3
  contentType: string;
4
4
  enSlug: string;
5
5
  locale: string;
6
- reason: "missing" | "stale";
6
+ reason: "missing" | "stale" | "forced";
7
7
  currentEnHash: string;
8
8
  storedEnHash?: string;
9
9
  /**
@@ -15,11 +15,14 @@ export interface TranslationWorkItem {
15
15
  }
16
16
  export type TranslationWorklistStrategy = "all" | "missing-only";
17
17
  export interface WorklistOptions {
18
+ /** Single id or comma-separated ids (e.g. `vertical,platform`). */
18
19
  contentType?: string;
19
20
  locales?: string[];
20
21
  enSlug?: string;
21
22
  /** Which pages to include: all stale/missing (default) or missing only. */
22
23
  strategy?: TranslationWorklistStrategy;
24
+ /** Include fresh translations so prepareTranslation re-runs despite matching hashes. */
25
+ force?: boolean;
23
26
  }
24
27
  /** List locale pages that are missing or stale vs current EN content. */
25
28
  export declare function buildWorklist(config: ScribeConfig, options?: WorklistOptions): TranslationWorkItem[];
@@ -1 +1 @@
1
- {"version":3,"file":"worklist.d.ts","sourceRoot":"","sources":["../../../src/translate/worklist.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAOrD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,2BAA2B,GAAG,KAAK,GAAG,cAAc,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,2BAA2B,CAAC;CACxC;AAWD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,EAAE,CAkDxG;AAED,+DAA+D;AAC/D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,MAAM,CAAC,EAAE,MAAM,EACf,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,MAAM,EAAE,CAMV"}
1
+ {"version":3,"file":"worklist.d.ts","sourceRoot":"","sources":["../../../src/translate/worklist.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAOrD,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,2BAA2B,GAAG,KAAK,GAAG,cAAc,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,2BAA2B,CAAC;IACvC,wFAAwF;IACxF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAoBD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,EAAE,CAoDxG;AAED,+DAA+D;AAC/D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,YAAY,EACpB,MAAM,CAAC,EAAE,MAAM,EACf,eAAe,CAAC,EAAE,MAAM,EAAE,GACzB,MAAM,EAAE,CAMV"}
@@ -515,6 +515,15 @@ function getTranslation(db, contentType, enSlug, locale) {
515
515
  function listTranslationsForLocale(db, contentType, locale) {
516
516
  return db.prepare(`SELECT * FROM translations WHERE content_type = ? AND locale = ? ORDER BY en_slug`).all(contentType, locale);
517
517
  }
518
+ function countTranslations(db) {
519
+ const row = db.prepare(`SELECT COUNT(*) as count FROM translations`).get();
520
+ return row.count;
521
+ }
522
+ function latestTranslationAtByLocale(db) {
523
+ return db.prepare(
524
+ `SELECT locale, MAX(translated_at) as latest FROM translations GROUP BY locale`
525
+ ).all();
526
+ }
518
527
  unified.unified().use(remarkParse__default.default).use(remarkMdx__default.default);
519
528
 
520
529
  // src/loader/normalize-en.ts
@@ -630,6 +639,11 @@ function getTranslatablePayload(doc, type) {
630
639
  body: doc.content
631
640
  };
632
641
  }
642
+ function parseContentTypeFilter(contentType) {
643
+ if (!contentType) return void 0;
644
+ const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
645
+ return ids.length > 0 ? new Set(ids) : void 0;
646
+ }
633
647
  function listEnSlugs(rootDir, contentDir) {
634
648
  const dir = path2__default.default.join(rootDir, contentDir);
635
649
  if (!fs2__default.default.existsSync(dir)) return [];
@@ -639,8 +653,9 @@ function buildWorklist(config, options = {}) {
639
653
  const db = openStore(config, "readonly");
640
654
  const items = [];
641
655
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
656
+ const contentTypes = parseContentTypeFilter(options.contentType);
642
657
  for (const type of config.types) {
643
- if (options.contentType && type.id !== options.contentType) continue;
658
+ if (contentTypes && !contentTypes.has(type.id)) continue;
644
659
  const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs(config.rootDir, type.contentDir);
645
660
  for (const enSlug of enSlugs) {
646
661
  const enDoc = readEnDocument(config, type, enSlug);
@@ -660,12 +675,13 @@ function buildWorklist(config, options = {}) {
660
675
  });
661
676
  continue;
662
677
  }
663
- if (existing.en_hash !== currentEnHash) {
678
+ const stale = existing.en_hash !== currentEnHash;
679
+ if (stale || options.force) {
664
680
  items.push({
665
681
  contentType: type.id,
666
682
  enSlug,
667
683
  locale,
668
- reason: "stale",
684
+ reason: stale ? "stale" : "forced",
669
685
  currentEnHash,
670
686
  storedEnHash: existing.en_hash
671
687
  });
@@ -933,12 +949,21 @@ function renderLayout(title, body, project, options = {}) {
933
949
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
934
950
  .tag-warn { color: var(--stale); }
935
951
  .tag-err { color: var(--missing); }
952
+
953
+ /* dashboard */
954
+ .cards { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
955
+ .card { flex: 1 1 120px; background: var(--bg); padding: 12px; min-width: 120px; }
956
+ .card-value { font-size: 22px; font-weight: 300; line-height: 1.2; }
957
+ .card-label { font-size: var(--fs-sm); color: var(--dim); margin-top: 2px; }
958
+ .bar { display: inline-block; width: 120px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
959
+ .bar-fill { display: block; height: 100%; }
936
960
  </style>
937
961
  </head>
938
962
  <body>
939
963
  <div class="app">
940
964
  <nav class="actbar">
941
965
  <a href="/" title="Overview">\u2302</a>
966
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
942
967
  <a href="/staleness" title="Staleness">\u26A0</a>
943
968
  </nav>
944
969
  <aside class="sidebar">
@@ -995,6 +1020,119 @@ async function startStudio(project, options = {}) {
995
1020
  </table>`;
996
1021
  return c.html(renderLayout("Overview", html, project));
997
1022
  });
1023
+ app.get("/dashboard", (c) => {
1024
+ const db = openStore(config, "readonly");
1025
+ const types = project.listTypes();
1026
+ const targetLocales = config.locales.filter((l) => l !== config.defaultLocale);
1027
+ const docCountByType = /* @__PURE__ */ new Map();
1028
+ let totalEnDocs = 0;
1029
+ for (const type of types) {
1030
+ const n = type.list().length;
1031
+ docCountByType.set(type.id, n);
1032
+ totalEnDocs += n;
1033
+ }
1034
+ const worklist = buildWorklist(config);
1035
+ const missingByLocale = /* @__PURE__ */ new Map();
1036
+ const staleByLocale = /* @__PURE__ */ new Map();
1037
+ const missingByType = /* @__PURE__ */ new Map();
1038
+ const staleByType = /* @__PURE__ */ new Map();
1039
+ let missingTotal = 0;
1040
+ let staleTotal = 0;
1041
+ for (const item of worklist) {
1042
+ if (item.reason === "missing") {
1043
+ missingByLocale.set(item.locale, (missingByLocale.get(item.locale) ?? 0) + 1);
1044
+ missingByType.set(item.contentType, (missingByType.get(item.contentType) ?? 0) + 1);
1045
+ missingTotal++;
1046
+ } else if (item.reason === "stale") {
1047
+ staleByLocale.set(item.locale, (staleByLocale.get(item.locale) ?? 0) + 1);
1048
+ staleByType.set(item.contentType, (staleByType.get(item.contentType) ?? 0) + 1);
1049
+ staleTotal++;
1050
+ }
1051
+ }
1052
+ const storedTranslations = countTranslations(db);
1053
+ const latestByLocale = /* @__PURE__ */ new Map();
1054
+ for (const row of latestTranslationAtByLocale(db)) {
1055
+ if (row.latest) latestByLocale.set(row.locale, row.latest);
1056
+ }
1057
+ db.close();
1058
+ const expectedTotal = totalEnDocs * targetLocales.length;
1059
+ const coverageTotal = expectedTotal > 0 ? (expectedTotal - missingTotal) / expectedTotal : 0;
1060
+ const coveragePct = expectedTotal > 0 ? Math.round(coverageTotal * 100) : 0;
1061
+ const pct = (fraction) => Math.round(fraction * 100);
1062
+ const num = (n, warnClass) => n > 0 ? `<span class="${warnClass}">${n}</span>` : `<span class="dim">0</span>`;
1063
+ const coverageColor = expectedTotal > 0 && coveragePct === 100 ? ` style="color:var(--ok)"` : "";
1064
+ const coverageValue = expectedTotal > 0 ? `${coveragePct}%` : `<span class="dim">\u2014</span>`;
1065
+ const staleColor = staleTotal > 0 ? ` style="color:var(--stale)"` : "";
1066
+ const missingColor = missingTotal > 0 ? ` style="color:var(--missing)"` : "";
1067
+ const cards = `<div class="cards">
1068
+ <div class="card"><div class="card-value">${totalEnDocs}</div><div class="card-label">Documents (EN)</div></div>
1069
+ <div class="card"><div class="card-value">${targetLocales.length}</div><div class="card-label">Target locales</div></div>
1070
+ <div class="card"><div class="card-value">${storedTranslations}</div><div class="card-label">Stored translations</div></div>
1071
+ <div class="card"><div class="card-value"${coverageColor}>${coverageValue}</div><div class="card-label">Coverage</div></div>
1072
+ <div class="card"><div class="card-value"${staleColor}>${staleTotal}</div><div class="card-label">Stale</div></div>
1073
+ <div class="card"><div class="card-value"${missingColor}>${missingTotal}</div><div class="card-label">Missing</div></div>
1074
+ </div>`;
1075
+ let localeRows;
1076
+ if (totalEnDocs === 0 || targetLocales.length === 0) {
1077
+ localeRows = `<tr><td colspan="7" class="dim">No target locales or documents</td></tr>`;
1078
+ } else {
1079
+ localeRows = targetLocales.map((locale) => {
1080
+ const expected = totalEnDocs;
1081
+ const missing = missingByLocale.get(locale) ?? 0;
1082
+ const stale = staleByLocale.get(locale) ?? 0;
1083
+ const translated = expected - missing;
1084
+ const upToDate = translated - stale;
1085
+ const coverage = expected > 0 ? translated / expected : 0;
1086
+ const covPct = pct(coverage);
1087
+ const fillColor = covPct === 100 ? "var(--ok)" : "var(--stale)";
1088
+ const fallbacks = config.localeFallbacks[locale]?.length ? escapeHtml(config.localeFallbacks[locale].join(" \u2192 ")) : `<span class="dim">\u2014</span>`;
1089
+ const latest = latestByLocale.get(locale);
1090
+ const last = latest ? escapeHtml(latest.slice(0, 10)) : `<span class="dim">\u2014</span>`;
1091
+ return `<tr>
1092
+ <td>${escapeHtml(locale)}</td>
1093
+ <td><span class="bar"><span class="bar-fill" style="width:${covPct}%;background:${fillColor}"></span></span>${covPct}%</td>
1094
+ <td>${upToDate}</td>
1095
+ <td>${num(stale, "tag-warn")}</td>
1096
+ <td>${num(missing, "tag-err")}</td>
1097
+ <td>${fallbacks}</td>
1098
+ <td>${last}</td>
1099
+ </tr>`;
1100
+ }).join("");
1101
+ }
1102
+ let typeRows;
1103
+ if (types.length === 0 || targetLocales.length === 0) {
1104
+ typeRows = `<tr><td colspan="5" class="dim">No types or target locales</td></tr>`;
1105
+ } else {
1106
+ typeRows = types.map((type) => {
1107
+ const stale = staleByType.get(type.id) ?? 0;
1108
+ const missing = missingByType.get(type.id) ?? 0;
1109
+ return `<tr>
1110
+ <td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
1111
+ <td class="dim">${escapeHtml(type.id)}</td>
1112
+ <td>${docCountByType.get(type.id) ?? 0}</td>
1113
+ <td>${num(stale, "tag-warn")}</td>
1114
+ <td>${num(missing, "tag-err")}</td>
1115
+ </tr>`;
1116
+ }).join("");
1117
+ }
1118
+ const html = `<div class="toolbar">Dashboard</div>
1119
+ ${cards}
1120
+ <div class="section">
1121
+ <div class="section-head">Locales</div>
1122
+ <table class="data">
1123
+ <thead><tr><th>Locale</th><th>Coverage</th><th>Up to date</th><th>Stale</th><th>Missing</th><th>Fallbacks</th><th>Last translated</th></tr></thead>
1124
+ <tbody>${localeRows}</tbody>
1125
+ </table>
1126
+ </div>
1127
+ <div class="section">
1128
+ <div class="section-head">Types</div>
1129
+ <table class="data">
1130
+ <thead><tr><th>Type</th><th>ID</th><th>EN docs</th><th>Stale</th><th>Missing</th></tr></thead>
1131
+ <tbody>${typeRows}</tbody>
1132
+ </table>
1133
+ </div>`;
1134
+ return c.html(renderLayout("Dashboard", html, project));
1135
+ });
998
1136
  app.get("/type/:id", (c) => {
999
1137
  const typeId = c.req.param("id");
1000
1138
  const type = project.getType(typeId);