living-documentation 7.6.0 → 7.7.0

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.
Files changed (36) hide show
  1. package/dist/src/frontend/accuracy-gauge.js +47 -0
  2. package/dist/src/frontend/documents.js +5 -0
  3. package/dist/src/frontend/i18n/en.json +14 -0
  4. package/dist/src/frontend/i18n/fr.json +14 -0
  5. package/dist/src/frontend/index.html +148 -0
  6. package/dist/src/frontend/metadata.js +292 -0
  7. package/dist/src/frontend/sidebar.js +8 -8
  8. package/dist/src/lib/hash.d.ts +2 -0
  9. package/dist/src/lib/hash.d.ts.map +1 -0
  10. package/dist/src/lib/hash.js +18 -0
  11. package/dist/src/lib/hash.js.map +1 -0
  12. package/dist/src/lib/metadata.d.ts +30 -0
  13. package/dist/src/lib/metadata.d.ts.map +1 -0
  14. package/dist/src/lib/metadata.js +109 -0
  15. package/dist/src/lib/metadata.js.map +1 -0
  16. package/dist/src/mcp/server.d.ts.map +1 -1
  17. package/dist/src/mcp/server.js +93 -0
  18. package/dist/src/mcp/server.js.map +1 -1
  19. package/dist/src/mcp/tools/metadata.d.ts +34 -0
  20. package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
  21. package/dist/src/mcp/tools/metadata.js +76 -0
  22. package/dist/src/mcp/tools/metadata.js.map +1 -0
  23. package/dist/src/routes/browse-source.d.ts +3 -0
  24. package/dist/src/routes/browse-source.d.ts.map +1 -0
  25. package/dist/src/routes/browse-source.js +79 -0
  26. package/dist/src/routes/browse-source.js.map +1 -0
  27. package/dist/src/routes/metadata.d.ts +3 -0
  28. package/dist/src/routes/metadata.d.ts.map +1 -0
  29. package/dist/src/routes/metadata.js +107 -0
  30. package/dist/src/routes/metadata.js.map +1 -0
  31. package/dist/src/server.d.ts.map +1 -1
  32. package/dist/src/server.js +4 -0
  33. package/dist/src/server.js.map +1 -1
  34. package/dist/starting-doc/.annotations.json +1 -13
  35. package/dist/starting-doc/.metadata.json +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,47 @@
1
+ // ── Accuracy gauge ──────────────────────────────────────────────────────────
2
+ // Shown in the sticky document header (right-aligned, own row) when the
3
+ // current document has at least one metadata entry.
4
+
5
+ function accuracyColor(ratio) {
6
+ const pct = Math.max(0, Math.min(1, ratio)) * 100;
7
+ if (pct > 80) return "#16a34a"; // green-600
8
+ if (pct >= 60) return "#eab308"; // yellow-500
9
+ if (pct >= 40) return "#f97316"; // orange-500
10
+ return "#dc2626"; // red-600
11
+ }
12
+
13
+ function renderAccuracyGauge(report) {
14
+ const wrap = document.getElementById("accuracy-gauge");
15
+ if (!wrap) return;
16
+ if (!report || report.total === 0) {
17
+ wrap.classList.add("hidden");
18
+ return;
19
+ }
20
+ wrap.classList.remove("hidden");
21
+
22
+ const pct = Math.round(report.accuracy * 100);
23
+ const color = accuracyColor(report.accuracy);
24
+
25
+ const label = document.getElementById("accuracy-gauge-label");
26
+ const bar = document.getElementById("accuracy-gauge-bar");
27
+ const value = document.getElementById("accuracy-gauge-value");
28
+
29
+ if (label) label.textContent = window.t("accuracy.label");
30
+ if (bar) {
31
+ bar.style.width = pct + "%";
32
+ bar.style.backgroundColor = color;
33
+ }
34
+ if (value) {
35
+ value.textContent = pct + "%";
36
+ value.style.color = color;
37
+ }
38
+
39
+ wrap.title = window
40
+ .t("accuracy.tooltip")
41
+ .replace("{unchanged}", String(report.unchanged))
42
+ .replace("{modified}", String(report.modified))
43
+ .replace("{missing}", String(report.missing))
44
+ .replace("{total}", String(report.total));
45
+ }
46
+
47
+ window.renderAccuracyGauge = renderAccuracyGauge;
@@ -164,6 +164,11 @@ async function openDocument(id, skipHistory = false, fromLink = false, anchor =
164
164
  // Load annotations for this document
165
165
  loadAnnotations(id);
166
166
 
167
+ // Load source-file metadata report (drives the accuracy gauge)
168
+ if (typeof loadMetadataReport === "function") {
169
+ loadMetadataReport(id);
170
+ }
171
+
167
172
  // Add IDs to headings for anchor navigation
168
173
  contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((h) => {
169
174
  if (!h.id) {
@@ -69,6 +69,20 @@
69
69
  "doc.failed_to_load": "Failed to load document: ",
70
70
  "doc.copied": "✓ Copied!",
71
71
 
72
+ "metadata.button": "Metadata",
73
+ "metadata.button_title": "Manage source-file metadata",
74
+ "metadata.title": "Source metadata",
75
+ "metadata.subtitle": "Attach source files to track whether this document is still accurate.",
76
+ "metadata.add": "Add source file",
77
+ "metadata.refresh": "Refresh hashes",
78
+ "metadata.remove": "Remove",
79
+ "metadata.empty": "No metadata yet — add a source file to start tracking accuracy.",
80
+ "metadata.status.unchanged": "Unchanged",
81
+ "metadata.status.modified": "Modified",
82
+ "metadata.status.missing": "Missing",
83
+ "accuracy.label": "Accuracy",
84
+ "accuracy.tooltip": "Unchanged: {unchanged} · Modified: {modified} · Missing: {missing} · Total: {total}",
85
+
72
86
  "modal.new_folder.title": "New folder",
73
87
  "modal.new_folder.name_label": "Folder name",
74
88
  "modal.new_folder.name_placeholder": "my-folder",
@@ -69,6 +69,20 @@
69
69
  "doc.failed_to_load": "Impossible de charger le document : ",
70
70
  "doc.copied": "✓ Copié !",
71
71
 
72
+ "metadata.button": "Métadonnées",
73
+ "metadata.button_title": "Gérer les métadonnées des fichiers source",
74
+ "metadata.title": "Métadonnées des sources",
75
+ "metadata.subtitle": "Rattachez des fichiers source pour suivre si ce document reste à jour.",
76
+ "metadata.add": "Ajouter un fichier source",
77
+ "metadata.refresh": "Rafraîchir les hashs",
78
+ "metadata.remove": "Retirer",
79
+ "metadata.empty": "Aucune métadonnée — ajoutez un fichier source pour commencer à suivre la justesse.",
80
+ "metadata.status.unchanged": "Inchangé",
81
+ "metadata.status.modified": "Modifié",
82
+ "metadata.status.missing": "Manquant",
83
+ "accuracy.label": "Justesse",
84
+ "accuracy.tooltip": "Inchangés : {unchanged} · Modifiés : {modified} · Manquants : {missing} · Total : {total}",
85
+
72
86
  "modal.new_folder.title": "Nouveau dossier",
73
87
  "modal.new_folder.name_label": "Nom du dossier",
74
88
  "modal.new_folder.name_placeholder": "mon-dossier",
@@ -47,6 +47,8 @@
47
47
  <script defer src="/misc.js"></script>
48
48
  <script defer src="/snippets.js"></script>
49
49
  <script defer src="/annotations.js"></script>
50
+ <script defer src="/metadata.js"></script>
51
+ <script defer src="/accuracy-gauge.js"></script>
50
52
  <script defer src="/export.js"></script>
51
53
  <script defer src="/diagram-link-modal.js"></script>
52
54
  <script defer src="/new-folder-modal.js"></script>
@@ -573,6 +575,15 @@
573
575
  >
574
576
  <i class="fa-solid fa-link"></i> <span data-i18n="doc.copy_link_btn">Copy link</span>
575
577
  </button>
578
+ <button
579
+ onclick="openMetadataModal()"
580
+ id="metadata-btn"
581
+ data-i18n-title="metadata.button_title"
582
+ title="Manage source-file metadata"
583
+ class="no-print text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
584
+ >
585
+ <i class="fa-solid fa-code-compare"></i> <span data-i18n="metadata.button">Metadata</span>
586
+ </button>
576
587
  <button
577
588
  onclick="enterEditMode()"
578
589
  data-i18n-title="doc.edit"
@@ -620,6 +631,33 @@
620
631
  </div>
621
632
  </div>
622
633
  </div>
634
+ <!-- Accuracy gauge (shown when the current doc has metadata entries) -->
635
+ <div
636
+ id="accuracy-gauge"
637
+ onclick="openMetadataModal()"
638
+ class="hidden no-print mt-3 flex items-center gap-3 ml-auto w-full sm:w-80 cursor-pointer select-none"
639
+ >
640
+ <span
641
+ id="accuracy-gauge-label"
642
+ data-i18n="accuracy.label"
643
+ class="text-xs text-gray-500 dark:text-gray-400 shrink-0"
644
+ >Accuracy</span>
645
+ <div
646
+ class="flex-1 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden"
647
+ >
648
+ <div
649
+ id="accuracy-gauge-bar"
650
+ class="h-full transition-all"
651
+ style="width: 0%; background-color: #9ca3af"
652
+ ></div>
653
+ </div>
654
+ <span
655
+ id="accuracy-gauge-value"
656
+ class="text-xs font-semibold shrink-0 tabular-nums"
657
+ style="color: #9ca3af"
658
+ >0%</span>
659
+ </div>
660
+
623
661
  <!-- Navigation history back-links -->
624
662
  <div
625
663
  id="doc-back"
@@ -660,6 +698,116 @@
660
698
  </div>
661
699
  <!-- end root -->
662
700
 
701
+ <!-- ── Metadata modal ── -->
702
+ <div
703
+ id="metadata-modal"
704
+ class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50"
705
+ >
706
+ <div
707
+ class="bg-white dark:bg-gray-900 rounded-xl shadow-xl w-full max-w-2xl mx-4 p-6 space-y-4"
708
+ >
709
+ <div class="flex items-start justify-between gap-4">
710
+ <div>
711
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-50">
712
+ <i class="fa-solid fa-code-compare mr-1"></i>
713
+ <span data-i18n="metadata.title">Source metadata</span>
714
+ </h3>
715
+ <p
716
+ data-i18n="metadata.subtitle"
717
+ class="text-xs text-gray-500 dark:text-gray-400 mt-1"
718
+ >
719
+ Attach source files to track whether this document is still accurate.
720
+ </p>
721
+ </div>
722
+ <button
723
+ onclick="closeMetadataModal()"
724
+ data-i18n-title="common.close"
725
+ title="Close"
726
+ class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
727
+ >
728
+ <i class="fa-solid fa-xmark text-lg"></i>
729
+ </button>
730
+ </div>
731
+
732
+ <!-- Summary -->
733
+ <div
734
+ id="metadata-summary"
735
+ class="text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-800 rounded-lg px-3 py-2"
736
+ ></div>
737
+
738
+ <!-- List -->
739
+ <div class="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
740
+ <div
741
+ id="metadata-list"
742
+ class="max-h-64 overflow-y-auto"
743
+ ></div>
744
+ <div
745
+ id="metadata-empty"
746
+ data-i18n="metadata.empty"
747
+ class="hidden px-3 py-4 text-center text-xs text-gray-400"
748
+ >
749
+ No metadata yet — add a source file to start tracking accuracy.
750
+ </div>
751
+ </div>
752
+
753
+ <!-- Actions -->
754
+ <div class="flex items-center gap-2 flex-wrap">
755
+ <button
756
+ onclick="metadataToggleBrowser()"
757
+ class="text-sm px-3 py-1.5 rounded-lg border border-blue-200 dark:border-blue-700 text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/30 transition-colors"
758
+ >
759
+ <i class="fa-solid fa-plus"></i> <span data-i18n="metadata.add">Add source file</span>
760
+ </button>
761
+ <button
762
+ onclick="metadataRefresh()"
763
+ id="metadata-refresh-btn"
764
+ class="text-sm px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
765
+ >
766
+ <i class="fa-solid fa-arrows-rotate"></i> <span data-i18n="metadata.refresh">Refresh hashes</span>
767
+ </button>
768
+ </div>
769
+
770
+ <!-- Source browser -->
771
+ <div
772
+ id="metadata-browser"
773
+ class="hidden border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden text-sm"
774
+ >
775
+ <div
776
+ class="flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"
777
+ >
778
+ <button
779
+ id="metadata-browse-up"
780
+ onclick="metadataBrowseUp()"
781
+ data-i18n="common.up"
782
+ class="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-30 disabled:pointer-events-none shrink-0"
783
+ >
784
+ ↑ Up
785
+ </button>
786
+ <span
787
+ id="metadata-browse-path"
788
+ class="text-xs text-gray-500 dark:text-gray-400 font-mono truncate flex-1"
789
+ ></span>
790
+ </div>
791
+ <div
792
+ id="metadata-browse-list"
793
+ class="max-h-56 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-800"
794
+ ></div>
795
+ </div>
796
+
797
+ <p id="metadata-error" class="hidden text-xs text-red-500"></p>
798
+
799
+ <div class="flex justify-end">
800
+ <button
801
+ onclick="closeMetadataModal()"
802
+ data-i18n="common.close"
803
+ class="text-sm px-4 py-2 rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
804
+ >
805
+ Close
806
+ </button>
807
+ </div>
808
+ </div>
809
+ </div>
810
+
663
811
  <!-- ── New Folder modal ── -->
664
812
  <div
665
813
  id="new-folder-modal"
@@ -0,0 +1,292 @@
1
+ // ── Metadata (source-file dependencies) ─────────────────────────────────────
2
+ // Exposes: openMetadataModal(), closeMetadataModal(),
3
+ // metadataRefresh(), metadataAddPath(), metadataRemovePath(),
4
+ // loadMetadataReport(docId) → used by accuracy-gauge.js
5
+
6
+ let metadataReport = null;
7
+ let metadataBrowseCurrent = ""; // relative to sourceRoot
8
+ let metadataBrowseCache = null;
9
+
10
+ function metadataCurrentDocId() {
11
+ return typeof currentDocId !== "undefined" ? currentDocId : null;
12
+ }
13
+
14
+ async function loadMetadataReport(docId) {
15
+ if (!docId) {
16
+ metadataReport = null;
17
+ return null;
18
+ }
19
+ try {
20
+ const r = await fetch(
21
+ "/api/metadata/" + encodeURIComponent(docId),
22
+ );
23
+ if (!r.ok) throw new Error(r.statusText);
24
+ metadataReport = await r.json();
25
+ } catch {
26
+ metadataReport = null;
27
+ }
28
+ if (typeof renderAccuracyGauge === "function") {
29
+ renderAccuracyGauge(metadataReport);
30
+ }
31
+ return metadataReport;
32
+ }
33
+
34
+ function statusBadge(status) {
35
+ if (status === "unchanged") {
36
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
37
+ <i class="fa-solid fa-check"></i>
38
+ <span data-i18n="metadata.status.unchanged">Unchanged</span>
39
+ </span>`;
40
+ }
41
+ if (status === "modified") {
42
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300">
43
+ <i class="fa-solid fa-triangle-exclamation"></i>
44
+ <span data-i18n="metadata.status.modified">Modified</span>
45
+ </span>`;
46
+ }
47
+ return `<span class="inline-flex items-center gap-1 text-xs font-semibold px-2 py-0.5 rounded-full bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300">
48
+ <i class="fa-solid fa-circle-xmark"></i>
49
+ <span data-i18n="metadata.status.missing">Missing</span>
50
+ </span>`;
51
+ }
52
+
53
+ function renderMetadataList() {
54
+ const listEl = document.getElementById("metadata-list");
55
+ const emptyEl = document.getElementById("metadata-empty");
56
+ if (!listEl || !emptyEl) return;
57
+
58
+ const items = (metadataReport && metadataReport.items) || [];
59
+ if (items.length === 0) {
60
+ listEl.innerHTML = "";
61
+ emptyEl.classList.remove("hidden");
62
+ return;
63
+ }
64
+ emptyEl.classList.add("hidden");
65
+
66
+ listEl.innerHTML = items
67
+ .map((it) => {
68
+ const safePath = esc(it.path);
69
+ return `<div class="flex items-center gap-2 px-3 py-2 border-b border-gray-100 dark:border-gray-800">
70
+ <div class="flex-1 min-w-0">
71
+ <div class="text-sm font-mono truncate text-gray-800 dark:text-gray-200" title="${safePath}">${safePath}</div>
72
+ </div>
73
+ ${statusBadge(it.status)}
74
+ <button
75
+ onclick="metadataRemovePath('${safePath.replace(/'/g, "\\'")}')"
76
+ data-i18n-title="metadata.remove"
77
+ title="Remove"
78
+ class="text-xs px-2 py-1 rounded-lg border border-red-200 dark:border-red-700 text-red-500 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/40 transition-colors"
79
+ >
80
+ <i class="fa-solid fa-trash"></i>
81
+ </button>
82
+ </div>`;
83
+ })
84
+ .join("");
85
+
86
+ if (typeof window.applyI18n === "function") window.applyI18n();
87
+ }
88
+
89
+ function renderMetadataSummary() {
90
+ const el = document.getElementById("metadata-summary");
91
+ if (!el) return;
92
+ if (!metadataReport || metadataReport.total === 0) {
93
+ el.textContent = "";
94
+ return;
95
+ }
96
+ const pct = Math.round(metadataReport.accuracy * 100);
97
+ const { total, unchanged, modified, missing } = metadataReport;
98
+ el.innerHTML = `<span class="font-semibold">${pct}%</span> · ${unchanged}/${total} ${window.t("metadata.status.unchanged")} · ${modified} ${window.t("metadata.status.modified")} · ${missing} ${window.t("metadata.status.missing")}`;
99
+ }
100
+
101
+ async function openMetadataModal() {
102
+ const docId = metadataCurrentDocId();
103
+ if (!docId) return;
104
+ await loadMetadataReport(docId);
105
+ renderMetadataList();
106
+ renderMetadataSummary();
107
+ document.getElementById("metadata-modal").classList.remove("hidden");
108
+ document.getElementById("metadata-error").classList.add("hidden");
109
+ // Reset browser
110
+ metadataBrowseCurrent = "";
111
+ metadataBrowseCache = null;
112
+ document.getElementById("metadata-browser").classList.add("hidden");
113
+ }
114
+
115
+ function closeMetadataModal() {
116
+ document.getElementById("metadata-modal").classList.add("hidden");
117
+ }
118
+
119
+ async function metadataRefresh() {
120
+ const docId = metadataCurrentDocId();
121
+ if (!docId) return;
122
+ const btn = document.getElementById("metadata-refresh-btn");
123
+ if (btn) btn.disabled = true;
124
+ try {
125
+ const r = await fetch(
126
+ "/api/metadata/" + encodeURIComponent(docId) + "/refresh",
127
+ { method: "POST" },
128
+ );
129
+ if (!r.ok) throw new Error(r.statusText);
130
+ metadataReport = await r.json();
131
+ renderMetadataList();
132
+ renderMetadataSummary();
133
+ if (typeof renderAccuracyGauge === "function") {
134
+ renderAccuracyGauge(metadataReport);
135
+ }
136
+ } catch (err) {
137
+ showMetadataError(err.message);
138
+ } finally {
139
+ if (btn) btn.disabled = false;
140
+ }
141
+ }
142
+
143
+ async function metadataRemovePath(path) {
144
+ const docId = metadataCurrentDocId();
145
+ if (!docId) return;
146
+ try {
147
+ const r = await fetch(
148
+ "/api/metadata/" + encodeURIComponent(docId),
149
+ {
150
+ method: "DELETE",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ path }),
153
+ },
154
+ );
155
+ if (!r.ok) throw new Error(r.statusText);
156
+ metadataReport = await r.json();
157
+ renderMetadataList();
158
+ renderMetadataSummary();
159
+ if (typeof renderAccuracyGauge === "function") {
160
+ renderAccuracyGauge(metadataReport);
161
+ }
162
+ refreshBrowserIfOpen();
163
+ } catch (err) {
164
+ showMetadataError(err.message);
165
+ }
166
+ }
167
+
168
+ async function metadataAddPath(relPath) {
169
+ const docId = metadataCurrentDocId();
170
+ if (!docId) return;
171
+ try {
172
+ const r = await fetch(
173
+ "/api/metadata/" + encodeURIComponent(docId),
174
+ {
175
+ method: "POST",
176
+ headers: { "Content-Type": "application/json" },
177
+ body: JSON.stringify({ path: relPath }),
178
+ },
179
+ );
180
+ if (!r.ok) {
181
+ const body = await r.json().catch(() => ({}));
182
+ throw new Error(body.error || r.statusText);
183
+ }
184
+ metadataReport = await r.json();
185
+ renderMetadataList();
186
+ renderMetadataSummary();
187
+ if (typeof renderAccuracyGauge === "function") {
188
+ renderAccuracyGauge(metadataReport);
189
+ }
190
+ refreshBrowserIfOpen();
191
+ } catch (err) {
192
+ showMetadataError(err.message);
193
+ }
194
+ }
195
+
196
+ function refreshBrowserIfOpen() {
197
+ const b = document.getElementById("metadata-browser");
198
+ if (b && !b.classList.contains("hidden")) {
199
+ metadataBrowseLoad(metadataBrowseCurrent);
200
+ }
201
+ }
202
+
203
+ function showMetadataError(msg) {
204
+ const el = document.getElementById("metadata-error");
205
+ if (!el) return;
206
+ el.textContent = window.t("common.error_prefix") + msg;
207
+ el.classList.remove("hidden");
208
+ setTimeout(() => el.classList.add("hidden"), 5000);
209
+ }
210
+
211
+ // ── Source browser ─────────────────────────────────────────────────────────
212
+
213
+ function metadataToggleBrowser() {
214
+ const b = document.getElementById("metadata-browser");
215
+ if (b.classList.contains("hidden")) {
216
+ b.classList.remove("hidden");
217
+ metadataBrowseLoad("");
218
+ } else {
219
+ b.classList.add("hidden");
220
+ }
221
+ }
222
+
223
+ async function metadataBrowseLoad(relPath) {
224
+ const listEl = document.getElementById("metadata-browse-list");
225
+ const pathEl = document.getElementById("metadata-browse-path");
226
+ const upBtn = document.getElementById("metadata-browse-up");
227
+ if (!listEl) return;
228
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-gray-400">${esc(window.t("common.loading"))}</div>`;
229
+ try {
230
+ const r = await fetch(
231
+ "/api/browse-source?path=" + encodeURIComponent(relPath || ""),
232
+ );
233
+ if (!r.ok) {
234
+ const body = await r.json().catch(() => ({}));
235
+ throw new Error(body.error || r.statusText);
236
+ }
237
+ const data = await r.json();
238
+ metadataBrowseCache = data;
239
+ metadataBrowseCurrent = data.current || "";
240
+ pathEl.textContent = data.current ? "/" + data.current : "/ (sourceRoot)";
241
+ upBtn.disabled = data.parent === null;
242
+ upBtn.classList.toggle("opacity-30", data.parent === null);
243
+ upBtn.classList.toggle("pointer-events-none", data.parent === null);
244
+
245
+ const dirRows = data.dirs.map(
246
+ (d) => `<button
247
+ onclick="metadataBrowseLoad('${d.path.replace(/'/g, "\\'")}')"
248
+ class="w-full text-left px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-2"
249
+ >
250
+ <i class="fa-solid fa-folder text-yellow-500"></i>
251
+ <span class="truncate">${esc(d.name)}</span>
252
+ </button>`,
253
+ );
254
+ const attached = new Set(
255
+ ((metadataReport && metadataReport.items) || []).map((it) => it.path),
256
+ );
257
+ const fileRows = data.files
258
+ .filter((f) => !attached.has(f.path))
259
+ .map(
260
+ (f) => `<button
261
+ onclick="metadataAddPath('${f.path.replace(/'/g, "\\'")}')"
262
+ class="w-full text-left px-3 py-1.5 text-sm hover:bg-blue-50 dark:hover:bg-blue-900/30 flex items-center gap-2"
263
+ >
264
+ <i class="fa-solid fa-file text-gray-400"></i>
265
+ <span class="truncate">${esc(f.name)}</span>
266
+ <i class="fa-solid fa-plus ml-auto text-blue-500 text-xs"></i>
267
+ </button>`,
268
+ );
269
+ const rows = [...dirRows, ...fileRows];
270
+ listEl.innerHTML = rows.length
271
+ ? rows.join("")
272
+ : `<div class="px-3 py-2 text-xs text-gray-400" data-i18n="common.empty_dir">${esc(window.t("common.empty_dir"))}</div>`;
273
+ } catch (err) {
274
+ listEl.innerHTML = `<div class="px-3 py-2 text-xs text-red-500">${esc(err.message)}</div>`;
275
+ }
276
+ }
277
+
278
+ function metadataBrowseUp() {
279
+ if (!metadataBrowseCache || metadataBrowseCache.parent === null) return;
280
+ metadataBrowseLoad(metadataBrowseCache.parent);
281
+ }
282
+
283
+ // Expose
284
+ window.openMetadataModal = openMetadataModal;
285
+ window.closeMetadataModal = closeMetadataModal;
286
+ window.metadataRefresh = metadataRefresh;
287
+ window.metadataRemovePath = metadataRemovePath;
288
+ window.metadataAddPath = metadataAddPath;
289
+ window.metadataToggleBrowser = metadataToggleBrowser;
290
+ window.metadataBrowseLoad = metadataBrowseLoad;
291
+ window.metadataBrowseUp = metadataBrowseUp;
292
+ window.loadMetadataReport = loadMetadataReport;
@@ -250,10 +250,10 @@ function toggleHideCategories() {
250
250
  function applyHideCategoriesButtonState() {
251
251
  const btn = document.getElementById("toggle-categories-btn");
252
252
  if (!btn) return;
253
- btn.classList.toggle("text-blue-500", hideCategories);
254
- btn.classList.toggle("dark:text-blue-400", hideCategories);
255
- btn.classList.toggle("text-gray-400", !hideCategories);
256
- btn.classList.toggle("dark:text-gray-500", !hideCategories);
253
+ btn.classList.toggle("text-blue-500", !hideCategories);
254
+ btn.classList.toggle("dark:text-blue-400", !hideCategories);
255
+ btn.classList.toggle("text-gray-400", hideCategories);
256
+ btn.classList.toggle("dark:text-gray-500", hideCategories);
257
257
  }
258
258
 
259
259
  function toggleHideAttachments() {
@@ -270,8 +270,8 @@ function toggleHideAttachments() {
270
270
  function applyHideAttachmentsButtonState() {
271
271
  const btn = document.getElementById("toggle-attachments-btn");
272
272
  if (!btn) return;
273
- btn.classList.toggle("text-blue-500", hideAttachments);
274
- btn.classList.toggle("dark:text-blue-400", hideAttachments);
275
- btn.classList.toggle("text-gray-400", !hideAttachments);
276
- btn.classList.toggle("dark:text-gray-500", !hideAttachments);
273
+ btn.classList.toggle("text-blue-500", !hideAttachments);
274
+ btn.classList.toggle("dark:text-blue-400", !hideAttachments);
275
+ btn.classList.toggle("text-gray-400", hideAttachments);
276
+ btn.classList.toggle("dark:text-gray-500", hideAttachments);
277
277
  }
@@ -0,0 +1,2 @@
1
+ export declare function sha256File(absPath: string): string | null;
2
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/lib/hash.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.sha256File = sha256File;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ function sha256File(absPath) {
10
+ try {
11
+ const buf = fs_1.default.readFileSync(absPath);
12
+ return crypto_1.default.createHash("sha256").update(buf).digest("hex");
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash.js","sourceRoot":"","sources":["../../../src/lib/hash.ts"],"names":[],"mappings":";;;;;AAGA,gCAOC;AAVD,4CAAoB;AACpB,oDAA4B;AAE5B,SAAgB,UAAU,CAAC,OAAe;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACrC,OAAO,gBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,30 @@
1
+ export interface MetadataEntry {
2
+ path: string;
3
+ hash: string;
4
+ }
5
+ export type MetadataStore = Record<string, MetadataEntry[]>;
6
+ export type MetadataStatus = "unchanged" | "modified" | "missing";
7
+ export interface MetadataItem {
8
+ path: string;
9
+ storedHash: string;
10
+ currentHash: string | null;
11
+ status: MetadataStatus;
12
+ }
13
+ export interface AccuracyReport {
14
+ items: MetadataItem[];
15
+ total: number;
16
+ unchanged: number;
17
+ modified: number;
18
+ missing: number;
19
+ accuracy: number;
20
+ }
21
+ export declare function metadataPath(docsPath: string): string;
22
+ export declare function readMetadataStore(docsPath: string): MetadataStore;
23
+ export declare function writeMetadataStore(docsPath: string, store: MetadataStore): void;
24
+ export declare function resolveSourceRoot(docsPath: string): string;
25
+ export declare function assertUnderSourceRoot(relOrAbs: string, sourceRoot: string): string;
26
+ export declare function classifyEntry(entry: MetadataEntry, sourceRoot: string): MetadataItem;
27
+ export declare function buildReport(entries: MetadataEntry[], sourceRoot: string): AccuracyReport;
28
+ export declare function getDocEntries(docsPath: string, docId: string): MetadataEntry[];
29
+ export declare function setDocEntries(docsPath: string, docId: string, entries: MetadataEntry[]): void;
30
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../../src/lib/metadata.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAQjE;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,aAAa,GACnB,IAAI,CAMN;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,MAAM,CAUR;AAED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,aAAa,EACpB,UAAU,EAAE,MAAM,GACjB,YAAY,CAmBd;AAGD,wBAAgB,WAAW,CACzB,OAAO,EAAE,aAAa,EAAE,EACxB,UAAU,EAAE,MAAM,GACjB,cAAc,CAmBhB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,aAAa,EAAE,CAGjB;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EAAE,GACvB,IAAI,CAKN"}