phase: 106_document_dates
Everything is verified green. Final report: **Phase 106 — Document dates (final verification pass; all 10 tasks already complete)** - Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed. - `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓) - `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up) - 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation** - `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings** **Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions. - **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring). - **Next pending phase:** none — `todo/` holds only this phase.
This commit is contained in:
+285
-9
@@ -66,6 +66,42 @@
|
||||
* everyone exactly as phase 36 — the public viewer stays
|
||||
* byte-for-byte identical (no button, no wiring, no admin-only
|
||||
* network call) until the admin gate resolves true.
|
||||
*
|
||||
* Phase 106 (task 08, D8): the .doc-meta badge row gains the Created
|
||||
* badge BEFORE the Indexed one (the owner's verbatim position — the
|
||||
* date at the top of a clicked document): `Created <locale date>` via
|
||||
* fmtDate(doc.created_at), the full ISO timestamp on the badge's
|
||||
* title (the titleEl ellipsis-precision idiom). ONE shared core —
|
||||
* the modal and /document.html both render it (no per-surface copy);
|
||||
* the date arrives in the same content payload (task 05 added
|
||||
* created_at to GET /api/documents/content). The date EDITOR (the
|
||||
* admin inline edit + PATCH /api/documents/date) is the task-09
|
||||
* section below.
|
||||
*
|
||||
* Phase 106 (task 09, D7): the badge row gains the ADMIN-ONLY date
|
||||
* editor (the phase-57 wireSummaryEdit idiom — the same
|
||||
* docAdminReady() gate on the module-cached whoami promise, so no
|
||||
* second request per page): an "Edit date" text button after the
|
||||
* Created badge (task 08's insertion point) swaps in-place for an
|
||||
* inline editor — a native type=date input + Save / Cancel + the
|
||||
* muted "Revert to sync" clear affordance (the phase-57 "clear =
|
||||
* explicit" contrast) + a role=status live line and a role=alert
|
||||
* error line. Save PATCHes /api/documents/date with { source, path,
|
||||
* date } and the badge re-renders from the RESPONSE's created_at
|
||||
* (never the input's optimistic value); "Revert to sync" sends
|
||||
* date: null (the D7 CLEAR — the manual flag drops, the stored date
|
||||
* stands until the next sync). An EMPTY date input can't clear (Save
|
||||
* disables itself — the explicit Revert is the only clear path, no
|
||||
* accidental wipes). §7.4 never-stale: the controls disable
|
||||
* immediately on submit (one PATCH at a time); a failure (non-2xx
|
||||
* or network) lands the server detail (or the canned retry copy) in
|
||||
* the role=alert line, reverts the input to the stored date, and
|
||||
* re-enables — the UI never claims a state the server didn't save.
|
||||
* A non-admin / token holder / failed whoami keeps the byte-for-byte
|
||||
* task-08 badge row (no button, no wiring, no admin-only network
|
||||
* call — the phase-57 split). ONE shared core — the modal and
|
||||
* /document.html both get it (document-modal.js imports
|
||||
* renderDocument from this module — no per-surface copy).
|
||||
*/
|
||||
|
||||
import { bindSharedHeaderControls, fetchIsAdmin, initSharedHeader } from "./header.js";
|
||||
@@ -94,23 +130,29 @@ function contentUrl(s, p) {
|
||||
return "/api/documents/content?source=" + encodeURIComponent(s) + "&path=" + encodeURIComponent(p);
|
||||
}
|
||||
|
||||
function metaBadge(cls, text) {
|
||||
function metaBadge(cls, text, title) {
|
||||
const el = document.createElement("span");
|
||||
el.className = cls;
|
||||
el.textContent = text;
|
||||
// Phase 106 (task 08): optional hover precision — the meta row may
|
||||
// clip (nowrap), so the exact value stays reachable on the badge's
|
||||
// title (the titleEl ellipsis-precision idiom). Omitted → the
|
||||
// pre-phase two-argument shape (the other badges are unchanged).
|
||||
if (title !== undefined) el.setAttribute("title", title);
|
||||
return el;
|
||||
}
|
||||
|
||||
/* ---------- shared renderer (phase 26, task 02) ----------
|
||||
* Populates the three elements every render surface provides: a title,
|
||||
* a .doc-meta badge row (source · format · mono path · indexed ·
|
||||
* chunks), and a content container — an optional .doc-summary section
|
||||
* first (phase 36: only when doc.summary is non-empty — markdown docs
|
||||
* and fail-soft rows carry none, so they render exactly as before),
|
||||
* then .doc-md for md/markdown (the shared escape-first renderer),
|
||||
* <pre class="doc-raw"> otherwise. The XSS contract: innerHTML only
|
||||
* through renderMarkdown; every document-derived string (summary text
|
||||
* included) is a text node. */
|
||||
* a .doc-meta badge row (source · format · mono path · created ·
|
||||
* indexed · chunks — phase 106 (task 08, D8) added created BEFORE
|
||||
* indexed), and a content container — an optional .doc-summary
|
||||
* section first (phase 36: only when doc.summary is non-empty —
|
||||
* markdown docs and fail-soft rows carry none, so they render exactly
|
||||
* as before), then .doc-md for md/markdown (the shared escape-first
|
||||
* renderer), <pre class="doc-raw"> otherwise. The XSS contract:
|
||||
* innerHTML only through renderMarkdown; every document-derived
|
||||
* string (summary text included) is a text node. */
|
||||
export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
|
||||
titleEl.textContent = doc.title;
|
||||
// Phase 34 task 04: the titlebar title ellipsizes — the full title
|
||||
@@ -124,10 +166,28 @@ export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
|
||||
metaBadge("doc-source-badge", doc.source),
|
||||
metaBadge("format-badge", doc.format),
|
||||
pathCode,
|
||||
// Phase 106 (task 08, D8): the Created badge BEFORE the Indexed
|
||||
// one (the owner's verbatim position — the date at the top of a
|
||||
// clicked document). textContent = the locale date (fmtDate — the
|
||||
// Indexed idiom), and the badge's title = the FULL ISO timestamp
|
||||
// (the titleEl ellipsis-precision idiom — the row clips, the exact
|
||||
// value stays reachable).
|
||||
metaBadge("doc-created", `Created ${fmtDate(doc.created_at)}`, doc.created_at),
|
||||
metaBadge("doc-indexed", `Indexed ${fmtDate(doc.indexed_at)}`),
|
||||
metaBadge("doc-chunks", `${doc.chunks} chunk${doc.chunks === 1 ? "" : "s"}`),
|
||||
);
|
||||
|
||||
// Phase 106 (task 09, D7): the ADMIN-ONLY date editor — the
|
||||
// module-cached whoami promise (docAdminReady — the SAME single
|
||||
// request per page the summary edit shares) gates the wiring, so a
|
||||
// non-admin / token holder / failed whoami keeps EXACTLY the task-08
|
||||
// badge row: no button, no wiring, no admin-only network call (the
|
||||
// phase-57 split). The button lands after the Created badge (task
|
||||
// 08's insertion point); the editor itself lives in wireDateEdit.
|
||||
void docAdminReady().then((admin) => {
|
||||
if (admin) wireDateEdit(metaEl, doc);
|
||||
});
|
||||
|
||||
contentEl.replaceChildren();
|
||||
// Phase 36: the summary panel — labeled section ABOVE the original
|
||||
// content, on BOTH surfaces (page + modal) through this one core.
|
||||
@@ -308,6 +368,222 @@ function wireSummaryEdit(section, doc) {
|
||||
cancelBtn.addEventListener("click", () => closeEditor(""));
|
||||
}
|
||||
|
||||
/* ---------- date editing (phase 106, task 09 — D7, admin-only) ----------
|
||||
* The .doc-meta badge row (task 08's Created badge) is the ONE place
|
||||
* the stored creation date is edited (page + modal through the
|
||||
* shared renderDocument core — no per-surface copy). Only an admin
|
||||
* (docAdminReady) ever gets the affordance; a non-admin / token
|
||||
* holder / failed whoami keeps exactly the task-08 badge row (no
|
||||
* button, no wiring, no admin-only network call — the phase-57
|
||||
* split). Save PATCHes /api/documents/date with { source, path,
|
||||
* date } — a non-empty date sets it (the server normalizes it, D3,
|
||||
* and stores the manual flag, D1); the explicit "Revert to sync"
|
||||
* affordance sends date: null (the D7 CLEAR — the manual flag drops
|
||||
* and the stored date stands until the next sync refreshes it).
|
||||
* The badge re-renders from the RESPONSE's created_at — the UI shows
|
||||
* exactly what the server stored, never the input's optimistic
|
||||
* value. §7.4 never-stale: the controls disable IMMEDIATELY on
|
||||
* Save/Revert (no double-submit); a failure (non-2xx or network)
|
||||
* lands the server detail (or the canned retry copy) in the
|
||||
* role=alert line, reverts the input to the stored date, and
|
||||
* re-enables — the UI never claims a state the server didn't save.
|
||||
* An EMPTY date input can't clear: Save disables itself on an empty
|
||||
* input (the explicit Revert is the only clear path — no accidental
|
||||
* wipes). */
|
||||
|
||||
/* The server detail of a non-2xx response (the git-sources.js
|
||||
* apiDetail shape — the phase-89 error-line idiom: a string detail
|
||||
* or the 422 array's first msg), with the neutral fallback for a
|
||||
* non-JSON body. */
|
||||
async function apiDetail(res, fallback) {
|
||||
try {
|
||||
const data = await res.json();
|
||||
if (Array.isArray(data.detail) && data.detail[0] && data.detail[0].msg) {
|
||||
return String(data.detail[0].msg);
|
||||
}
|
||||
if (typeof data.detail === "string" && data.detail) return data.detail;
|
||||
} catch {
|
||||
/* non-JSON error body */
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/* The date editor on one rendered .doc-meta badge row: the "Edit
|
||||
* date" text button after the Created badge (admin-only — the public
|
||||
* row keeps task 08's shape). Edit swaps the button for the inline
|
||||
* editor in place — a native <input type="date"> (value, never
|
||||
* innerHTML — XSS contract) prefilled with the stored date's UTC
|
||||
* date part, Save / Cancel, the muted "Revert to sync" clear, and
|
||||
* the role=status / role=alert live lines. One PATCH at a time (the
|
||||
* controls disable before the fetch and re-enable in the finally —
|
||||
* every outcome, never stale, PLAN §7.4). */
|
||||
function wireDateEdit(metaEl, doc) {
|
||||
const createdBadge = metaEl.querySelector(".doc-created");
|
||||
if (!createdBadge) return;
|
||||
|
||||
/* Edit affordance (admin-only — the public badge row keeps exactly
|
||||
* task 08's shape: no button, no wiring). setAttribute, never
|
||||
* innerHTML (the document-derived pair is user-storable). */
|
||||
const editBtn = document.createElement("button");
|
||||
editBtn.type = "button";
|
||||
editBtn.className = "doc-date-edit";
|
||||
editBtn.textContent = "Edit date";
|
||||
editBtn.setAttribute(
|
||||
"aria-label",
|
||||
`Edit creation date: ${doc.source}/${doc.path}`,
|
||||
);
|
||||
createdBadge.insertAdjacentElement("afterend", editBtn);
|
||||
|
||||
/* Editor parts (built once; the input is rebuilt on every open so
|
||||
* it always starts from the CURRENT stored date). */
|
||||
let input = null;
|
||||
const box = document.createElement("span");
|
||||
box.className = "doc-date-editor";
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.type = "button";
|
||||
saveBtn.className = "doc-date-save";
|
||||
saveBtn.textContent = "Save";
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.type = "button";
|
||||
cancelBtn.className = "doc-date-cancel";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
const revertBtn = document.createElement("button");
|
||||
revertBtn.type = "button";
|
||||
revertBtn.className = "doc-date-revert";
|
||||
revertBtn.textContent = "Revert to sync";
|
||||
const status = document.createElement("span");
|
||||
status.className = "doc-date-status";
|
||||
status.setAttribute("role", "status");
|
||||
status.setAttribute("aria-live", "polite");
|
||||
const errorLine = document.createElement("span");
|
||||
errorLine.className = "doc-date-error";
|
||||
errorLine.setAttribute("role", "alert");
|
||||
errorLine.setAttribute("aria-live", "assertive");
|
||||
|
||||
/* The stored date as a native date-input value: the UTC date part
|
||||
* of the stored ISO timestamp (D3 stores UTC timestamptz). */
|
||||
function storedValue() {
|
||||
return new Date(doc.created_at).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/* One PATCH at a time (PLAN §7.4): the controls disable
|
||||
* IMMEDIATELY on submit; Save ADDITIONALLY disables on an EMPTY
|
||||
* input (the explicit Revert is the only clear path — no
|
||||
* accidental wipes). */
|
||||
function setControlsLocked(locked) {
|
||||
if (input) input.disabled = locked;
|
||||
saveBtn.disabled = locked || input.value === "";
|
||||
cancelBtn.disabled = locked;
|
||||
revertBtn.disabled = locked;
|
||||
}
|
||||
|
||||
/* Back to the display state: the badge row + the Edit button (the
|
||||
* task-08 shape restored) and focus back on the opener. Idempotent
|
||||
* (a stale success-beat timer may call it after a Cancel — the
|
||||
* re-insert is a same-position move and the box is already out).
|
||||
* The last announcement stays readable a short beat because the
|
||||
* caller defers the call (the phase-57 "confirmation stays
|
||||
* readable" precedent). */
|
||||
function closeEditor() {
|
||||
editBtn.hidden = false;
|
||||
createdBadge.insertAdjacentElement("afterend", editBtn);
|
||||
box.remove();
|
||||
editBtn.focus();
|
||||
}
|
||||
|
||||
async function saveDate(dateValue) {
|
||||
setControlsLocked(true); // one PATCH at a time (never stale)
|
||||
errorLine.textContent = "";
|
||||
try {
|
||||
const r = await fetch("/api/documents/date", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
source: doc.source,
|
||||
path: doc.path,
|
||||
date: dateValue,
|
||||
}),
|
||||
});
|
||||
if (!r.ok) {
|
||||
// The server detail into the role=alert line (the canned
|
||||
// retry copy on a non-JSON body); the input reverts to the
|
||||
// stored date; the controls re-enable in the finally. The
|
||||
// editor stays open — the user's attempt is visible.
|
||||
errorLine.textContent = await apiDetail(
|
||||
r,
|
||||
"Couldn't save the date — try again.",
|
||||
);
|
||||
input.value = storedValue();
|
||||
return;
|
||||
}
|
||||
const res = await r.json();
|
||||
// Response-driven: the badge shows exactly what the server
|
||||
// stored (never the input's optimistic value), and the doc
|
||||
// object syncs so a re-open prefills the CURRENT date.
|
||||
doc.created_at = res.created_at;
|
||||
doc.created_at_manual = res.created_at_manual;
|
||||
createdBadge.textContent = `Created ${fmtDate(res.created_at)}`;
|
||||
createdBadge.setAttribute("title", res.created_at);
|
||||
// The confirmation lands AFTER the badge update (the phase-89
|
||||
// last-announce order), then the row collapses back to the
|
||||
// task-08 shape a short beat later (the announcement stays
|
||||
// readable — the status line leaves with the box).
|
||||
status.textContent =
|
||||
dateValue === null
|
||||
? "Reverted to sync-managed date."
|
||||
: `Date saved for ${doc.source}/${doc.path}.`;
|
||||
setTimeout(closeEditor, 2000);
|
||||
} catch {
|
||||
// Network failure: the canned retry copy (the phase-55
|
||||
// neutral shape), the input reverts to the stored date, the
|
||||
// editor stays open.
|
||||
errorLine.textContent = "Couldn't save the date — try again.";
|
||||
input.value = storedValue();
|
||||
} finally {
|
||||
setControlsLocked(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openEditor() {
|
||||
input = document.createElement("input");
|
||||
input.type = "date";
|
||||
input.className = "doc-date-input";
|
||||
input.setAttribute("aria-label", "Document creation date");
|
||||
input.value = storedValue(); // value, never innerHTML
|
||||
input.addEventListener("input", () => {
|
||||
// An EMPTY input can't clear: Save disables (the explicit
|
||||
// Revert below is the only clear path — no accidental wipes).
|
||||
// The locked state owns the controls while a PATCH is in
|
||||
// flight (the input is disabled then, so this can't race it).
|
||||
if (!input.disabled) saveBtn.disabled = input.value === "";
|
||||
});
|
||||
status.textContent = "";
|
||||
errorLine.textContent = "";
|
||||
box.replaceChildren(input, saveBtn, cancelBtn, revertBtn, status, errorLine);
|
||||
editBtn.hidden = true;
|
||||
createdBadge.insertAdjacentElement("afterend", box);
|
||||
setControlsLocked(false);
|
||||
input.focus();
|
||||
}
|
||||
|
||||
editBtn.addEventListener("click", openEditor);
|
||||
saveBtn.addEventListener("click", () => {
|
||||
// Save is only ever enabled with a NON-empty input (the empty
|
||||
// state disables it) — the clear path is the explicit Revert.
|
||||
void saveDate(input.value);
|
||||
});
|
||||
cancelBtn.addEventListener("click", () => {
|
||||
// Cancel: back to the display state, no PATCH (the stored value
|
||||
// is untouched — the badge was never mutated).
|
||||
status.textContent = "";
|
||||
errorLine.textContent = "";
|
||||
closeEditor();
|
||||
});
|
||||
revertBtn.addEventListener("click", () => {
|
||||
void saveDate(null); // the D7 CLEAR: { source, path, date: null }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- /document.html page (phases 10/13/19) ----------
|
||||
* Phase 26: viewer-page-specific — see the import-safety note in the
|
||||
* header. The guard is #doc-title: it exists only on this page, so the
|
||||
|
||||
@@ -262,6 +262,29 @@
|
||||
* trigger a view switch; for a foreign entry the router has already
|
||||
* switched views, and this listener's render of the now-hidden view
|
||||
* is harmless).
|
||||
*
|
||||
* Phase 106 (task 08, D8) — the date COLUMNS (display only — the
|
||||
* admin date EDITOR is task 09):
|
||||
*
|
||||
* • the file table gains the Created column BEFORE Indexed (the
|
||||
* owner's verbatim position, D8): makeRow builds the date cells
|
||||
* explicitly (the plain-td loop can't carry per-cell titles) —
|
||||
* textContent = the locale date (fmtDate, the Indexed idiom), and
|
||||
* the Created cell's title = the FULL ISO value (hover precision —
|
||||
* the path-cell idiom; the E2E asserts on the locale-stable
|
||||
* title, not on the toLocaleString output). The row object fed
|
||||
* from the tree's file nodes carries created_at (task 05's tree
|
||||
* shape — the flat GET /api/docs shape has the field too).
|
||||
* • the folder/source table gains the Updated column BETWEEN
|
||||
* Documents and Description (the owner's verbatim position): the
|
||||
* subtree's MAX document created_at — the tree API DERIVES it
|
||||
* (D9, never stored); makeSourceRow / makeFolderRow render
|
||||
* fmtDate(updated_at) with the ISO value on the cell's title and
|
||||
* the "–" null idiom (the statLast shape) for a 0-document
|
||||
* source.
|
||||
* • the stat cards are UNTOUCHED — they keep their indexed_at
|
||||
* "last indexed" semantics (the owner asked for the column, not
|
||||
* the cards).
|
||||
*/
|
||||
|
||||
import { fetchIsAdmin } from "./header.js";
|
||||
@@ -1230,6 +1253,16 @@ export async function mount(root) {
|
||||
const countTd = document.createElement("td");
|
||||
countTd.textContent = String(s.documents);
|
||||
tr.appendChild(countTd);
|
||||
// Phase 106 (task 08, D8/D9): the Updated cell — the source's
|
||||
// subtree MAX document created_at (the tree API derives it),
|
||||
// BETWEEN the count and the Description (the owner's verbatim
|
||||
// position). A 0-document source has none → the statLast "–" null
|
||||
// idiom; the ISO value rides the title (hover precision — the
|
||||
// makeRow path-cell idiom).
|
||||
const updatedTd = document.createElement("td");
|
||||
updatedTd.textContent = s.updated_at ? fmtDate(s.updated_at) : "–";
|
||||
if (s.updated_at) updatedTd.title = s.updated_at; // full ISO on hover
|
||||
tr.appendChild(updatedTd);
|
||||
tr.appendChild(makeDescCell(s, s.name, "", s.name)); // Description + ALWAYS-present Edit (task 05)
|
||||
return tr;
|
||||
}
|
||||
@@ -1255,6 +1288,17 @@ export async function mount(root) {
|
||||
const countTd = document.createElement("td");
|
||||
countTd.textContent = String(f.documents);
|
||||
tr.appendChild(countTd);
|
||||
// Phase 106 (task 08, D8/D9): the Updated cell — the folder's
|
||||
// subtree MAX document created_at (the tree API derives it),
|
||||
// BETWEEN the count and the Description (the owner's verbatim
|
||||
// position); the ISO value rides the title (hover precision — the
|
||||
// makeRow path-cell idiom). The "–" branch is defensive: a folder
|
||||
// node always has ≥1 document, so its max is never null (a
|
||||
// 0-document source IS the null case, makeSourceRow).
|
||||
const updatedTd = document.createElement("td");
|
||||
updatedTd.textContent = f.updated_at ? fmtDate(f.updated_at) : "–";
|
||||
if (f.updated_at) updatedTd.title = f.updated_at; // full ISO on hover
|
||||
tr.appendChild(updatedTd);
|
||||
tr.appendChild(
|
||||
makeDescCell(f, current.source, f.path, current.source + "/" + f.path)
|
||||
); // Description + ALWAYS-present Edit (task 05)
|
||||
@@ -1345,6 +1389,7 @@ export async function mount(root) {
|
||||
path: f.path,
|
||||
title: f.title,
|
||||
chunks: f.chunks,
|
||||
created_at: f.created_at, // phase 106 (task 08, D8): the tree's file date
|
||||
indexed_at: f.indexed_at,
|
||||
})
|
||||
);
|
||||
@@ -1398,11 +1443,26 @@ export async function mount(root) {
|
||||
pathTd.appendChild(link);
|
||||
tr.appendChild(pathTd);
|
||||
|
||||
for (const value of [d.title, String(d.chunks), fmtDate(d.indexed_at)]) {
|
||||
// Phase 106 (task 08, D8): the cell order is [title, chunks,
|
||||
// created, indexed] — the Created cell lands BEFORE Indexed (the
|
||||
// owner's verbatim position). The date cells are built EXPLICITLY
|
||||
// (the plain-td loop can't carry per-cell titles): textContent is
|
||||
// the locale date (fmtDate — the Indexed idiom), and the Created
|
||||
// cell carries the FULL ISO value as its title (hover precision —
|
||||
// the path-cell idiom; the E2E asserts on the locale-stable title,
|
||||
// not on the toLocaleString output).
|
||||
for (const value of [d.title, String(d.chunks)]) {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = value;
|
||||
tr.appendChild(td);
|
||||
}
|
||||
const createdTd = document.createElement("td");
|
||||
createdTd.textContent = fmtDate(d.created_at);
|
||||
createdTd.title = d.created_at; // full ISO on hover (locale-stable)
|
||||
tr.appendChild(createdTd);
|
||||
const indexedTd = document.createElement("td");
|
||||
indexedTd.textContent = fmtDate(d.indexed_at);
|
||||
tr.appendChild(indexedTd);
|
||||
return tr;
|
||||
}
|
||||
|
||||
|
||||
+153
-10
@@ -1937,7 +1937,9 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
directory's STORED description — the surface-panel language of the
|
||||
phase-93 page heads), and #folders-table (the ONE folders/sources
|
||||
table — the .docs-table language; .kb-folders-table only re-styles
|
||||
the 2nd/3rd column cells). Phase-08 tokens only — NO new hue
|
||||
the 2nd/4th column cells — phase 106 (task 08, D8) moved
|
||||
Description from 3rd to 4th, the Updated column taking 3rd). Phase-08
|
||||
tokens only — NO new hue
|
||||
(the phase-92 monochrome invariant): brand-ink on surface 9.0:1,
|
||||
ink 13.8:1, ink-soft 5.1:1 (every pair AA). :focus-visible via the
|
||||
global 3px outline rule; row targets keep the .docs-table cell
|
||||
@@ -1981,7 +1983,12 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
.kb-level p { margin: 0; color: var(--ink-soft); } /* 5.1:1 on --surface */
|
||||
.kb-folders-table { min-width: 480px; }
|
||||
.kb-folders-table td:nth-child(2) { font-family: var(--font); font-size: inherit; max-width: none; }
|
||||
.kb-folders-table td:nth-child(3) {
|
||||
/* Phase 106 (task 08, D8): the Updated column sits at 3rd (BETWEEN
|
||||
Documents and Description) — it takes the .docs-table cell defaults
|
||||
(nowrap text, the table ink: var(--ink) on the wrap's --surface —
|
||||
13.8:1, the pair recorded at :root). NO new CSS: the one-line clamp
|
||||
below follows the Description cell, which moved to 4th. */
|
||||
.kb-folders-table td:nth-child(4) {
|
||||
white-space: nowrap; /* phase 99 (D1): one line — the clamp's budget is the column width */
|
||||
min-width: 18rem;
|
||||
max-width: 44rem;
|
||||
@@ -3637,10 +3644,11 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* Meta row: source badge · format badge · mono path · indexed · chunks.
|
||||
Phase 12/34: it may clip, but it must NEVER wrap — the meta stays on
|
||||
one line in the titlebar row (the row is content-sized, so a wrap
|
||||
would grow it). */
|
||||
/* Meta row: source badge · format badge · mono path · created ·
|
||||
indexed · chunks (phase 106 (task 08, D8) added created BEFORE
|
||||
indexed). Phase 12/34: it may clip, but it must NEVER wrap — the
|
||||
meta stays on one line in the titlebar row (the row is content-
|
||||
sized, so a wrap would grow it). */
|
||||
.doc-meta {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
@@ -3682,6 +3690,13 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
min-width: 6rem;
|
||||
}
|
||||
.doc-chunks { font-family: var(--mono); }
|
||||
/* Phase 106 (task 08, D8): the Created badge — the SAME family as the
|
||||
Indexed one (no own look: the meta row's text, var(--ink-soft) on
|
||||
the titlebar's --surface — 5.1:1, the pair recorded at :root; the
|
||||
modal's .doc-modal-meta row records the same pair). The explicit
|
||||
declaration pins the inherited value: a themed --ink-soft moves the
|
||||
badge with the row (byte-identical rendering). */
|
||||
.doc-created { color: var(--ink-soft); /* 5.1:1 on --surface */ }
|
||||
|
||||
.doc-shell {
|
||||
display: flex;
|
||||
@@ -3856,6 +3871,133 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
}
|
||||
.doc-summary-status:empty { margin-top: 0; }
|
||||
|
||||
/* Date edit affordance (phase 106, task 09 — D7, admin-only): the
|
||||
"Edit date" text button after the Created badge (the
|
||||
.doc-summary-edit family — one pill style for both inline
|
||||
editors) and the editor box that swaps in for the button IN the
|
||||
.doc-meta row: the native type=date input, Save / Cancel, the
|
||||
muted underlined "Revert to sync" clear affordance (the phase-57
|
||||
"clear = explicit" contrast), a role=status live line, and a
|
||||
role=alert error line (the .git-source-error err pair). The meta
|
||||
row may clip (nowrap) on the page, so the box wraps ITS children
|
||||
internally (the modal's .doc-modal-meta already wraps). The
|
||||
global 3px :focus-visible outline rule applies — no per-control
|
||||
rule (the phase-105 checkbox idiom). :disabled = opacity +
|
||||
cursor: wait (the .git-source-remove:disabled idiom — one PATCH
|
||||
at a time). Contrast (verified, house style): button / Cancel /
|
||||
revert / status ride the row's ink-soft on --surface (5.1:1, AA);
|
||||
the input is ink on --bg (16.7:1, AA); Save is the solid brand
|
||||
pill (--bg on --brand = 5.2:1, AA); the error line is the err
|
||||
pair (9.1:1 on --err-bg, AA). Anonymous / token holders never see
|
||||
any of it — the wiring is admin-gated (docAdminReady in
|
||||
document.js), so the public badge row stays byte-for-byte the
|
||||
task-08 shape. */
|
||||
.doc-date-edit {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0.15rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.02em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.doc-date-edit:hover { background: var(--brand-soft); color: var(--brand-ink); border-color: var(--brand); }
|
||||
.doc-date-edit[hidden] { display: none; } /* the hidden attr must beat the display above */
|
||||
.doc-date-editor {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.doc-date-input {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
color: var(--ink); /* 16.7:1 on --bg (AA) */
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
.doc-date-save {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0.15rem 0.7rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--brand);
|
||||
color: var(--bg); /* --bg on --brand = 5.2:1 (AA) */
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.doc-date-save:hover { background: var(--brand-hover); } /* the house hover lightening */
|
||||
.doc-date-cancel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0.15rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.doc-date-cancel:hover { background: var(--err-bg); color: var(--err-ink); border-color: var(--err-line); }
|
||||
.doc-date-revert {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0.15rem 0.2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--ink-soft); /* 5.1:1 on --surface (AA) — the muted marker */
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.doc-date-revert:hover { color: var(--ink); } /* 14.5:1 on --surface (AA) */
|
||||
.doc-date-save:disabled,
|
||||
.doc-date-cancel:disabled,
|
||||
.doc-date-revert:disabled,
|
||||
.doc-date-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: wait; /* one PATCH at a time (the .git-source-remove:disabled idiom) */
|
||||
}
|
||||
.doc-date-status {
|
||||
font-size: 0.78rem;
|
||||
color: var(--ink-soft); /* 5.1:1 on --surface (AA) */
|
||||
}
|
||||
.doc-date-error {
|
||||
flex-basis: 100%; /* drops onto its own row under the controls */
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere; /* a long server detail never overflows the box */
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
background: var(--err-bg);
|
||||
color: var(--err-ink); /* 9.1:1 on --err-bg (AA — the .git-source-error pair) */
|
||||
border: 1px solid var(--err-line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.1rem 0.5rem;
|
||||
}
|
||||
.doc-date-status:empty, .doc-date-error:empty { display: none; } /* an empty live line takes no space */
|
||||
|
||||
/* Raw (non-markdown) formats: full-width mono pre, horizontal scroll. */
|
||||
.doc-raw {
|
||||
width: 100%;
|
||||
@@ -4039,10 +4181,11 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
.doc-modal-close svg { width: 18px; height: 18px; display: block; }
|
||||
|
||||
/* Meta row: the SAME badge classes as the viewer's .doc-meta (source
|
||||
badge · format badge · mono path · indexed · chunks — no duplicate
|
||||
badge styling here); unlike the fixed-height header bar it may WRAP,
|
||||
so nothing clips. aria-live="polite" on the element announces the
|
||||
load → meta swap (phase-10 a11y contract, modal variant). */
|
||||
badge · format badge · mono path · created · indexed · chunks — no
|
||||
duplicate badge styling here); unlike the fixed-height header bar it
|
||||
may WRAP, so nothing clips. aria-live="polite" on the element
|
||||
announces the load → meta swap (phase-10 a11y contract, modal
|
||||
variant). */
|
||||
.doc-modal-meta {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
|
||||
@@ -472,6 +472,13 @@
|
||||
<tr>
|
||||
<th scope="col">Folder</th>
|
||||
<th scope="col">Documents</th>
|
||||
<!-- Phase 106 (task 08, D8): the Updated column — the
|
||||
subtree's MAX document created_at (the tree API
|
||||
derives it — D9), BETWEEN Documents and
|
||||
Description (the owner's verbatim position). The
|
||||
"–" when a source has no documents (the statLast
|
||||
null idiom, sources.js). -->
|
||||
<th scope="col">Updated</th>
|
||||
<th scope="col">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -488,6 +495,12 @@
|
||||
<th scope="col">Path</th>
|
||||
<th scope="col">Title</th>
|
||||
<th scope="col">Chunks</th>
|
||||
<!-- Phase 106 (task 08, D8): the Created column — the
|
||||
document's sourced creation date, BEFORE Indexed
|
||||
(the owner's verbatim position). sources.js builds
|
||||
the cell: locale date (fmtDate) + the full ISO
|
||||
value on the cell's title (hover precision). -->
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Indexed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
Reference in New Issue
Block a user