scribe-cms 0.0.16 → 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.
@@ -773,6 +773,15 @@ function listTranslationsForLocale(db, contentType, locale) {
773
773
  function bulkLoadTranslations(db) {
774
774
  return db.prepare(`SELECT * FROM translations ORDER BY content_type, en_slug, locale`).all();
775
775
  }
776
+ function countTranslations(db) {
777
+ const row = db.prepare(`SELECT COUNT(*) as count FROM translations`).get();
778
+ return row.count;
779
+ }
780
+ function latestTranslationAtByLocale(db) {
781
+ return db.prepare(
782
+ `SELECT locale, MAX(translated_at) as latest FROM translations GROUP BY locale`
783
+ ).all();
784
+ }
776
785
 
777
786
  // src/translate/validate-mdx-body.ts
778
787
  init_cjs_shims();
@@ -2131,6 +2140,11 @@ function computePageEnHash(translatableFrontmatter, body) {
2131
2140
 
2132
2141
  // src/translate/worklist.ts
2133
2142
  init_sqlite();
2143
+ function parseContentTypeFilter(contentType) {
2144
+ if (!contentType) return void 0;
2145
+ const ids = contentType.split(",").map((id) => id.trim()).filter(Boolean);
2146
+ return ids.length > 0 ? new Set(ids) : void 0;
2147
+ }
2134
2148
  function listEnSlugs4(rootDir, contentDir) {
2135
2149
  const dir = path3__default.default.join(rootDir, contentDir);
2136
2150
  if (!fs2__default.default.existsSync(dir)) return [];
@@ -2140,8 +2154,9 @@ function buildWorklist(config, options = {}) {
2140
2154
  const db = openStore(config, "readonly");
2141
2155
  const items = [];
2142
2156
  const locales = options.locales ?? config.locales.filter((locale) => locale !== config.defaultLocale);
2157
+ const contentTypes = parseContentTypeFilter(options.contentType);
2143
2158
  for (const type of config.types) {
2144
- if (options.contentType && type.id !== options.contentType) continue;
2159
+ if (contentTypes && !contentTypes.has(type.id)) continue;
2145
2160
  const enSlugs = options.enSlug ? [options.enSlug] : listEnSlugs4(config.rootDir, type.contentDir);
2146
2161
  for (const enSlug of enSlugs) {
2147
2162
  const enDoc = readEnDocument(config, type, enSlug);
@@ -2161,12 +2176,13 @@ function buildWorklist(config, options = {}) {
2161
2176
  });
2162
2177
  continue;
2163
2178
  }
2164
- if (existing.en_hash !== currentEnHash) {
2179
+ const stale = existing.en_hash !== currentEnHash;
2180
+ if (stale || options.force) {
2165
2181
  items.push({
2166
2182
  contentType: type.id,
2167
2183
  enSlug,
2168
2184
  locale,
2169
- reason: "stale",
2185
+ reason: stale ? "stale" : "forced",
2170
2186
  currentEnHash,
2171
2187
  storedEnHash: existing.en_hash
2172
2188
  });
@@ -3945,12 +3961,21 @@ function renderLayout(title, body, project, options = {}) {
3945
3961
  .tag { font-size: var(--fs-sm); color: var(--dim); margin-left: 6px; }
3946
3962
  .tag-warn { color: var(--stale); }
3947
3963
  .tag-err { color: var(--missing); }
3964
+
3965
+ /* dashboard */
3966
+ .cards { display: flex; flex-wrap: wrap; gap: 1px; background: var(--border); border-bottom: 1px solid var(--border); }
3967
+ .card { flex: 1 1 120px; background: var(--bg); padding: 12px; min-width: 120px; }
3968
+ .card-value { font-size: 22px; font-weight: 300; line-height: 1.2; }
3969
+ .card-label { font-size: var(--fs-sm); color: var(--dim); margin-top: 2px; }
3970
+ .bar { display: inline-block; width: 120px; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; vertical-align: middle; margin-right: 8px; }
3971
+ .bar-fill { display: block; height: 100%; }
3948
3972
  </style>
3949
3973
  </head>
3950
3974
  <body>
3951
3975
  <div class="app">
3952
3976
  <nav class="actbar">
3953
3977
  <a href="/" title="Overview">\u2302</a>
3978
+ <a href="/dashboard" title="Dashboard">\u25A6</a>
3954
3979
  <a href="/staleness" title="Staleness">\u26A0</a>
3955
3980
  </nav>
3956
3981
  <aside class="sidebar">
@@ -4007,6 +4032,119 @@ async function startStudio(project, options = {}) {
4007
4032
  </table>`;
4008
4033
  return c.html(renderLayout("Overview", html, project));
4009
4034
  });
4035
+ app.get("/dashboard", (c) => {
4036
+ const db = openStore(config, "readonly");
4037
+ const types = project.listTypes();
4038
+ const targetLocales = config.locales.filter((l) => l !== config.defaultLocale);
4039
+ const docCountByType = /* @__PURE__ */ new Map();
4040
+ let totalEnDocs = 0;
4041
+ for (const type of types) {
4042
+ const n = type.list().length;
4043
+ docCountByType.set(type.id, n);
4044
+ totalEnDocs += n;
4045
+ }
4046
+ const worklist = buildWorklist(config);
4047
+ const missingByLocale = /* @__PURE__ */ new Map();
4048
+ const staleByLocale = /* @__PURE__ */ new Map();
4049
+ const missingByType = /* @__PURE__ */ new Map();
4050
+ const staleByType = /* @__PURE__ */ new Map();
4051
+ let missingTotal = 0;
4052
+ let staleTotal = 0;
4053
+ for (const item of worklist) {
4054
+ if (item.reason === "missing") {
4055
+ missingByLocale.set(item.locale, (missingByLocale.get(item.locale) ?? 0) + 1);
4056
+ missingByType.set(item.contentType, (missingByType.get(item.contentType) ?? 0) + 1);
4057
+ missingTotal++;
4058
+ } else if (item.reason === "stale") {
4059
+ staleByLocale.set(item.locale, (staleByLocale.get(item.locale) ?? 0) + 1);
4060
+ staleByType.set(item.contentType, (staleByType.get(item.contentType) ?? 0) + 1);
4061
+ staleTotal++;
4062
+ }
4063
+ }
4064
+ const storedTranslations = countTranslations(db);
4065
+ const latestByLocale = /* @__PURE__ */ new Map();
4066
+ for (const row of latestTranslationAtByLocale(db)) {
4067
+ if (row.latest) latestByLocale.set(row.locale, row.latest);
4068
+ }
4069
+ db.close();
4070
+ const expectedTotal = totalEnDocs * targetLocales.length;
4071
+ const coverageTotal = expectedTotal > 0 ? (expectedTotal - missingTotal) / expectedTotal : 0;
4072
+ const coveragePct = expectedTotal > 0 ? Math.round(coverageTotal * 100) : 0;
4073
+ const pct = (fraction) => Math.round(fraction * 100);
4074
+ const num = (n, warnClass) => n > 0 ? `<span class="${warnClass}">${n}</span>` : `<span class="dim">0</span>`;
4075
+ const coverageColor = expectedTotal > 0 && coveragePct === 100 ? ` style="color:var(--ok)"` : "";
4076
+ const coverageValue = expectedTotal > 0 ? `${coveragePct}%` : `<span class="dim">\u2014</span>`;
4077
+ const staleColor = staleTotal > 0 ? ` style="color:var(--stale)"` : "";
4078
+ const missingColor = missingTotal > 0 ? ` style="color:var(--missing)"` : "";
4079
+ const cards = `<div class="cards">
4080
+ <div class="card"><div class="card-value">${totalEnDocs}</div><div class="card-label">Documents (EN)</div></div>
4081
+ <div class="card"><div class="card-value">${targetLocales.length}</div><div class="card-label">Target locales</div></div>
4082
+ <div class="card"><div class="card-value">${storedTranslations}</div><div class="card-label">Stored translations</div></div>
4083
+ <div class="card"><div class="card-value"${coverageColor}>${coverageValue}</div><div class="card-label">Coverage</div></div>
4084
+ <div class="card"><div class="card-value"${staleColor}>${staleTotal}</div><div class="card-label">Stale</div></div>
4085
+ <div class="card"><div class="card-value"${missingColor}>${missingTotal}</div><div class="card-label">Missing</div></div>
4086
+ </div>`;
4087
+ let localeRows;
4088
+ if (totalEnDocs === 0 || targetLocales.length === 0) {
4089
+ localeRows = `<tr><td colspan="7" class="dim">No target locales or documents</td></tr>`;
4090
+ } else {
4091
+ localeRows = targetLocales.map((locale) => {
4092
+ const expected = totalEnDocs;
4093
+ const missing = missingByLocale.get(locale) ?? 0;
4094
+ const stale = staleByLocale.get(locale) ?? 0;
4095
+ const translated = expected - missing;
4096
+ const upToDate = translated - stale;
4097
+ const coverage = expected > 0 ? translated / expected : 0;
4098
+ const covPct = pct(coverage);
4099
+ const fillColor = covPct === 100 ? "var(--ok)" : "var(--stale)";
4100
+ const fallbacks = config.localeFallbacks[locale]?.length ? escapeHtml(config.localeFallbacks[locale].join(" \u2192 ")) : `<span class="dim">\u2014</span>`;
4101
+ const latest = latestByLocale.get(locale);
4102
+ const last = latest ? escapeHtml(latest.slice(0, 10)) : `<span class="dim">\u2014</span>`;
4103
+ return `<tr>
4104
+ <td>${escapeHtml(locale)}</td>
4105
+ <td><span class="bar"><span class="bar-fill" style="width:${covPct}%;background:${fillColor}"></span></span>${covPct}%</td>
4106
+ <td>${upToDate}</td>
4107
+ <td>${num(stale, "tag-warn")}</td>
4108
+ <td>${num(missing, "tag-err")}</td>
4109
+ <td>${fallbacks}</td>
4110
+ <td>${last}</td>
4111
+ </tr>`;
4112
+ }).join("");
4113
+ }
4114
+ let typeRows;
4115
+ if (types.length === 0 || targetLocales.length === 0) {
4116
+ typeRows = `<tr><td colspan="5" class="dim">No types or target locales</td></tr>`;
4117
+ } else {
4118
+ typeRows = types.map((type) => {
4119
+ const stale = staleByType.get(type.id) ?? 0;
4120
+ const missing = missingByType.get(type.id) ?? 0;
4121
+ return `<tr>
4122
+ <td><a href="/type/${encodePathSegment(type.id)}">${escapeHtml(type.config.label)}</a></td>
4123
+ <td class="dim">${escapeHtml(type.id)}</td>
4124
+ <td>${docCountByType.get(type.id) ?? 0}</td>
4125
+ <td>${num(stale, "tag-warn")}</td>
4126
+ <td>${num(missing, "tag-err")}</td>
4127
+ </tr>`;
4128
+ }).join("");
4129
+ }
4130
+ const html = `<div class="toolbar">Dashboard</div>
4131
+ ${cards}
4132
+ <div class="section">
4133
+ <div class="section-head">Locales</div>
4134
+ <table class="data">
4135
+ <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>
4136
+ <tbody>${localeRows}</tbody>
4137
+ </table>
4138
+ </div>
4139
+ <div class="section">
4140
+ <div class="section-head">Types</div>
4141
+ <table class="data">
4142
+ <thead><tr><th>Type</th><th>ID</th><th>EN docs</th><th>Stale</th><th>Missing</th></tr></thead>
4143
+ <tbody>${typeRows}</tbody>
4144
+ </table>
4145
+ </div>`;
4146
+ return c.html(renderLayout("Dashboard", html, project));
4147
+ });
4010
4148
  app.get("/type/:id", (c) => {
4011
4149
  const typeId = c.req.param("id");
4012
4150
  const type = project.getType(typeId);
@@ -4774,7 +4912,7 @@ Export-static flags:
4774
4912
  --locale <code>... Locales to export (default: all configured)
4775
4913
 
4776
4914
  Translate flags:
4777
- --type <id> Content type (interactive picker in a TTY when omitted)
4915
+ --type <id> Content type, or comma-separated ids (interactive picker in a TTY when omitted)
4778
4916
  --preset <name> Locale preset from config (interactive picker in a TTY)
4779
4917
  --locale <code>... Target locale(s); overrides --preset
4780
4918
  --slug <en-slug> Single English document
@@ -4866,7 +5004,8 @@ mode to use.
4866
5004
  contentType: selection.contentType,
4867
5005
  locales,
4868
5006
  enSlug: options.slug,
4869
- strategy: selection.strategy ?? "all"
5007
+ strategy: selection.strategy ?? "all",
5008
+ force: options.force
4870
5009
  });
4871
5010
  if (worklist.length === 0) {
4872
5011
  if (!options.dryRun) {