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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../studio/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAoC,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAiW5F,mFAAmF;AACnF,wBAAsB,WAAW,CAC/B,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,OAAO,CAAC,IAAI,CAAC,CAoOf"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../studio/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAoC,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA4W5F,mFAAmF;AACnF,wBAAsB,WAAW,CAC/B,OAAO,EAAE,aAAa,EACtB,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,OAAO,CAAC,IAAI,CAAC,CA4Wf"}
@@ -504,6 +504,15 @@ function getTranslation(db, contentType, enSlug, locale) {
504
504
  function listTranslationsForLocale(db, contentType, locale) {
505
505
  return db.prepare(`SELECT * FROM translations WHERE content_type = ? AND locale = ? ORDER BY en_slug`).all(contentType, locale);
506
506
  }
507
+ function countTranslations(db) {
508
+ const row = db.prepare(`SELECT COUNT(*) as count FROM translations`).get();
509
+ return row.count;
510
+ }
511
+ function latestTranslationAtByLocale(db) {
512
+ return db.prepare(
513
+ `SELECT locale, MAX(translated_at) as latest FROM translations GROUP BY locale`
514
+ ).all();
515
+ }
507
516
  unified().use(remarkParse).use(remarkMdx);
508
517
 
509
518
  // src/loader/normalize-en.ts
@@ -619,6 +628,11 @@ function getTranslatablePayload(doc, type) {
619
628
  body: doc.content
620
629
  };
621
630
  }
631
+ function parseContentTypeFilter(contentType) {
632
+ if (!contentType) return void 0;
633
+ const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
634
+ return ids.length > 0 ? new Set(ids) : void 0;
635
+ }
622
636
  function listEnSlugs(rootDir, contentDir) {
623
637
  const dir = path2.join(rootDir, contentDir);
624
638
  if (!fs2.existsSync(dir)) return [];
@@ -628,8 +642,9 @@ function buildWorklist(config, options = {}) {
628
642
  const db = openStore(config, "readonly");
629
643
  const items = [];
630
644
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
645
+ const contentTypes = parseContentTypeFilter(options.contentType);
631
646
  for (const type of config.types) {
632
- if (options.contentType && type.id !== options.contentType) continue;
647
+ if (contentTypes && !contentTypes.has(type.id)) continue;
633
648
  const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs(config.rootDir, type.contentDir);
634
649
  for (const enSlug of enSlugs) {
635
650
  const enDoc = readEnDocument(config, type, enSlug);
@@ -649,12 +664,13 @@ function buildWorklist(config, options = {}) {
649
664
  });
650
665
  continue;
651
666
  }
652
- if (existing.en_hash !== currentEnHash) {
667
+ const stale = existing.en_hash !== currentEnHash;
668
+ if (stale || options.force) {
653
669
  items.push({
654
670
  contentType: type.id,
655
671
  enSlug,
656
672
  locale,
657
- reason: "stale",
673
+ reason: stale ? "stale" : "forced",
658
674
  currentEnHash,
659
675
  storedEnHash: existing.en_hash
660
676
  });
@@ -922,12 +938,21 @@ function renderLayout(title, body, project, options = {}) {
922
938
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
923
939
  .tag-warn { color: var(--stale); }
924
940
  .tag-err { color: var(--missing); }
941
+
942
+ /* dashboard */
943
+ .cards { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
944
+ .card { flex: 1 1 120px; background: var(--bg); padding: 12px; min-width: 120px; }
945
+ .card-value { font-size: 22px; font-weight: 300; line-height: 1.2; }
946
+ .card-label { font-size: var(--fs-sm); color: var(--dim); margin-top: 2px; }
947
+ .bar { display: inline-block; width: 120px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
948
+ .bar-fill { display: block; height: 100%; }
925
949
  </style>
926
950
  </head>
927
951
  <body>
928
952
  <div class="app">
929
953
  <nav class="actbar">
930
954
  <a href="/" title="Overview">\u2302</a>
955
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
931
956
  <a href="/staleness" title="Staleness">\u26A0</a>
932
957
  </nav>
933
958
  <aside class="sidebar">
@@ -984,6 +1009,119 @@ async function startStudio(project, options = {}) {
984
1009
  </table>`;
985
1010
  return c.html(renderLayout("Overview", html, project));
986
1011
  });
1012
+ app.get("/dashboard", (c) => {
1013
+ const db = openStore(config, "readonly");
1014
+ const types = project.listTypes();
1015
+ const targetLocales = config.locales.filter((l) => l !== config.defaultLocale);
1016
+ const docCountByType = /* @__PURE__ */ new Map();
1017
+ let totalEnDocs = 0;
1018
+ for (const type of types) {
1019
+ const n = type.list().length;
1020
+ docCountByType.set(type.id, n);
1021
+ totalEnDocs += n;
1022
+ }
1023
+ const worklist = buildWorklist(config);
1024
+ const missingByLocale = /* @__PURE__ */ new Map();
1025
+ const staleByLocale = /* @__PURE__ */ new Map();
1026
+ const missingByType = /* @__PURE__ */ new Map();
1027
+ const staleByType = /* @__PURE__ */ new Map();
1028
+ let missingTotal = 0;
1029
+ let staleTotal = 0;
1030
+ for (const item of worklist) {
1031
+ if (item.reason === "missing") {
1032
+ missingByLocale.set(item.locale, (missingByLocale.get(item.locale) ?? 0) + 1);
1033
+ missingByType.set(item.contentType, (missingByType.get(item.contentType) ?? 0) + 1);
1034
+ missingTotal++;
1035
+ } else if (item.reason === "stale") {
1036
+ staleByLocale.set(item.locale, (staleByLocale.get(item.locale) ?? 0) + 1);
1037
+ staleByType.set(item.contentType, (staleByType.get(item.contentType) ?? 0) + 1);
1038
+ staleTotal++;
1039
+ }
1040
+ }
1041
+ const storedTranslations = countTranslations(db);
1042
+ const latestByLocale = /* @__PURE__ */ new Map();
1043
+ for (const row of latestTranslationAtByLocale(db)) {
1044
+ if (row.latest) latestByLocale.set(row.locale, row.latest);
1045
+ }
1046
+ db.close();
1047
+ const expectedTotal = totalEnDocs * targetLocales.length;
1048
+ const coverageTotal = expectedTotal > 0 ? (expectedTotal - missingTotal) / expectedTotal : 0;
1049
+ const coveragePct = expectedTotal > 0 ? Math.round(coverageTotal * 100) : 0;
1050
+ const pct = (fraction) => Math.round(fraction * 100);
1051
+ const num = (n, warnClass) => n > 0 ? `<span class="${warnClass}">${n}</span>` : `<span class="dim">0</span>`;
1052
+ const coverageColor = expectedTotal > 0 && coveragePct === 100 ? ` style="color:var(--ok)"` : "";
1053
+ const coverageValue = expectedTotal > 0 ? `${coveragePct}%` : `<span class="dim">\u2014</span>`;
1054
+ const staleColor = staleTotal > 0 ? ` style="color:var(--stale)"` : "";
1055
+ const missingColor = missingTotal > 0 ? ` style="color:var(--missing)"` : "";
1056
+ const cards = `<div class="cards">
1057
+ <div class="card"><div class="card-value">${totalEnDocs}</div><div class="card-label">Documents (EN)</div></div>
1058
+ <div class="card"><div class="card-value">${targetLocales.length}</div><div class="card-label">Target locales</div></div>
1059
+ <div class="card"><div class="card-value">${storedTranslations}</div><div class="card-label">Stored translations</div></div>
1060
+ <div class="card"><div class="card-value"${coverageColor}>${coverageValue}</div><div class="card-label">Coverage</div></div>
1061
+ <div class="card"><div class="card-value"${staleColor}>${staleTotal}</div><div class="card-label">Stale</div></div>
1062
+ <div class="card"><div class="card-value"${missingColor}>${missingTotal}</div><div class="card-label">Missing</div></div>
1063
+ </div>`;
1064
+ let localeRows;
1065
+ if (totalEnDocs === 0 || targetLocales.length === 0) {
1066
+ localeRows = `<tr><td colspan="7" class="dim">No target locales or documents</td></tr>`;
1067
+ } else {
1068
+ localeRows = targetLocales.map((locale) => {
1069
+ const expected = totalEnDocs;
1070
+ const missing = missingByLocale.get(locale) ?? 0;
1071
+ const stale = staleByLocale.get(locale) ?? 0;
1072
+ const translated = expected - missing;
1073
+ const upToDate = translated - stale;
1074
+ const coverage = expected > 0 ? translated / expected : 0;
1075
+ const covPct = pct(coverage);
1076
+ const fillColor = covPct === 100 ? "var(--ok)" : "var(--stale)";
1077
+ const fallbacks = config.localeFallbacks[locale]?.length ? escapeHtml(config.localeFallbacks[locale].join(" \u2192 ")) : `<span class="dim">\u2014</span>`;
1078
+ const latest = latestByLocale.get(locale);
1079
+ const last = latest ? escapeHtml(latest.slice(0, 10)) : `<span class="dim">\u2014</span>`;
1080
+ return `<tr>
1081
+ <td>${escapeHtml(locale)}</td>
1082
+ <td><span class="bar"><span class="bar-fill" style="width:${covPct}%;background:${fillColor}"></span></span>${covPct}%</td>
1083
+ <td>${upToDate}</td>
1084
+ <td>${num(stale, "tag-warn")}</td>
1085
+ <td>${num(missing, "tag-err")}</td>
1086
+ <td>${fallbacks}</td>
1087
+ <td>${last}</td>
1088
+ </tr>`;
1089
+ }).join("");
1090
+ }
1091
+ let typeRows;
1092
+ if (types.length === 0 || targetLocales.length === 0) {
1093
+ typeRows = `<tr><td colspan="5" class="dim">No types or target locales</td></tr>`;
1094
+ } else {
1095
+ typeRows = types.map((type) => {
1096
+ const stale = staleByType.get(type.id) ?? 0;
1097
+ const missing = missingByType.get(type.id) ?? 0;
1098
+ return `<tr>
1099
+ <td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
1100
+ <td class="dim">${escapeHtml(type.id)}</td>
1101
+ <td>${docCountByType.get(type.id) ?? 0}</td>
1102
+ <td>${num(stale, "tag-warn")}</td>
1103
+ <td>${num(missing, "tag-err")}</td>
1104
+ </tr>`;
1105
+ }).join("");
1106
+ }
1107
+ const html = `<div class="toolbar">Dashboard</div>
1108
+ ${cards}
1109
+ <div class="section">
1110
+ <div class="section-head">Locales</div>
1111
+ <table class="data">
1112
+ <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>
1113
+ <tbody>${localeRows}</tbody>
1114
+ </table>
1115
+ </div>
1116
+ <div class="section">
1117
+ <div class="section-head">Types</div>
1118
+ <table class="data">
1119
+ <thead><tr><th>Type</th><th>ID</th><th>EN docs</th><th>Stale</th><th>Missing</th></tr></thead>
1120
+ <tbody>${typeRows}</tbody>
1121
+ </table>
1122
+ </div>`;
1123
+ return c.html(renderLayout("Dashboard", html, project));
1124
+ });
987
1125
  app.get("/type/:id", (c) => {
988
1126
  const typeId = c.req.param("id");
989
1127
  const type = project.getType(typeId);