phase: 105_hidden_folders_toggle
Build and Push Containers / build-and-push-app (push) Successful in 1m44s
Build and Push Containers / build-and-push-db (push) Successful in 13s

All completion criteria verified. Everything is green.

**Phase 105 final verification pass — all criteria verified**

- Verified the full implementation in the working tree: `git_sources.include_hidden` column + alembic `0019` (dev DB at head, column present), `iter_importable_files`/`import_sources` flag support with `str(root)`-keyed map used by both walk and progress pre-walk, `GitSourcePatchIn` rename with optional fields, sync/CLI pipeline wiring (OR-collision), and the per-row "Hidden" checkbox + tag + error line on the Sources page
- Unit + integration: `uv run pytest` → exit 0 (2148 tests collected, all pass; this sandbox occasionally swallows pytest's final status line — exit codes verified)
- Coverage: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (3879 stmts, 15 miss) — >90% gate ✓
- Dedicated E2E: `uv run pytest tests/e2e/test_hidden_folders_toggle.py -v --no-cov` → **6 passed in 23.20s** (DB up, in isolation)
- Regression E2E in isolation: `test_source_ignore_paths` 6 passed, `test_git_sources_admin` 6 passed, `test_local_directory_sources` 3 passed, `test_sync_button` 3 passed, `test_smoke` 3 passed
- Lint/types: `uv run ruff check .` + `uv run pyright` → clean (0 errors/warnings)

**Completion criteria:** (1) checkbox persists via PATCH 200 → "hidden on" tag + GET round-trips `include_hidden: true`; failure path reverts box + `role="alert"` canned message ✓; (2) flag OFF byte-identical (only `visible.md` indexed), ON indexes `.hidden/note.md` into the KB catalog, `EXCLUDED_DIRS` excluded both states ✓; (3) A2: flag OFF → `detail.pruned==1`, doc gone from catalog ✓; (4) PATCH bool-only/list-only/both/neither no-op, phase-89 fixed 422s unchanged, 404, anonymous 403 (incl. bool-only body) ✓; (5) env-fallback rows render no checkbox, WCAG-clean (aria-label, keyboard focus, visible label, text tag) ✓; (6) full gate green ✓; (7) commit left to the harness per instructions (no `git add`/`commit` run; phase files untouched).

**Deviations:** none — no defects found; no code changes were needed on this pass.

**Next pending phase:** `.agents/phases/todo/98_sync_summary_visibility`.
This commit is contained in:
2026-09-12 21:26:30 -04:00
parent ecc921098a
commit d731169b8b
49 changed files with 3553 additions and 93 deletions
+119
View File
@@ -178,6 +178,31 @@
* tag next to the location <code> (text + a distinct background —
* never color alone, WCAG 1.4.1).
*
* Phase 105 (task 05) — the per-row "Hidden" toggle: every STORED
* row (s.id truthy — env-fallback rows get NO checkbox, A3: nothing
* is stored to flag) gets a native labeled checkbox (visible
* "Hidden" text + the checkbox's own aria-label naming the full
* source value — setAttribute, never innerHTML) in the actions
* cell, LEFT of the "Ignore paths" button (Remove stays last).
* Checked state comes ONLY from the server row (s.include_hidden —
* makeRow renders server state, never a prior local flip). When on,
* the source cell shows a "hidden on" text tag (text + background,
* never color alone — WCAG 1.4.1). Flipping runs the §7.4
* never-stale lifecycle (toggleHidden): the box disables at once
* (no double-flip), PATCH /api/git-sources/{id} with ONLY
* { include_hidden } (task 03's optional field — the row's ignore
* list is untouched); 200 → the error line clears, loadSources()
* (the row re-renders from the server, the "hidden on" tag lands)
* and the confirmation is the LAST announcement (the reload's "N
* sources listed." lands first — the phase-89 order); failure →
* the box REVERTS to the server state and re-enables, and the
* detail lands in the page-level #git-sources-hidden-error line
* (role=alert — the checkbox is a table-cell control, so its error
* is at page level; the ignore dialog carries its own INSIDE the
* modal). A healed load clears the line (loadSources' success path
* calls hideHiddenError — the phase-89 "happy path heals the error
* state" precedent).
*
* Scope boundary (phase locked decisions): adding a git repo does
* NOT clone — the sync service (server-side) does that. Removing a
* source, however, performs the FULL cleanup server-side (phase 69):
@@ -253,6 +278,10 @@ export async function mount(root) {
const ignoreErrorEl = root.querySelector("#ignore-editor-error");
const ignoreCancelBtn = root.querySelector("#ignore-editor-cancel");
const ignoreSaveBtn = root.querySelector("#ignore-editor-save");
/* Phase 105: the per-row "Hidden" toggle's page-level error line
(the checkbox is a table-cell control — no dialog of its own —
so its failure announces here, next to the table). */
const hiddenErrorEl = root.querySelector("#git-sources-hidden-error");
/* Polite live region: the screen-reader confirmation for loads, adds,
and removals (the phase-15 announcer pattern). */
@@ -312,6 +341,10 @@ export async function mount(root) {
return;
}
hideLoadError();
/* Phase 105: a healed list clears the stale hidden-toggle error
line (the phase-89 "happy path heals the error state" — the
same hideLoadError precedent). */
hideHiddenError();
const sources = Array.isArray(data.sources) ? data.sources : [];
renderSources(sources, data.from_env === true);
announce(`${sources.length} source${sources.length === 1 ? "" : "s"} listed.`);
@@ -381,6 +414,16 @@ export async function mount(root) {
count.textContent = `${s.ignore_paths.length} ignored`;
urlTd.append(count);
}
/* Phase 105 (A5): the "hidden on" state tag — the
.git-source-ignore-count idiom (TEXT + background, never
color alone — WCAG 1.4.1), so the flag is readable at a
glance without hovering the checkbox. */
if (s.id && s.include_hidden === true) {
const hiddenTag = document.createElement("span");
hiddenTag.className = "git-source-hidden-count";
hiddenTag.textContent = "hidden on";
urlTd.append(hiddenTag);
}
tr.appendChild(urlTd);
const addedTd = document.createElement("td");
@@ -390,6 +433,29 @@ export async function mount(root) {
const actTd = document.createElement("td");
actTd.className = "git-source-actions-cell";
if (s.id) {
/* Phase 105 (A5): the per-row hidden-folders toggle — a native
labeled checkbox (the WCAG focus/label idiom) LEFT of the
"Ignore paths" button; Remove stays last. Stored rows only
(A3 — env-fallback rows fall through to the "from .env"
tag). The aria-label is the ONLY place `value` appears
(setAttribute — never innerHTML). Checked state comes from
the SERVER row (s.include_hidden), never from a prior local
flip (§7.4 — makeRow only ever renders server state). */
const hiddenLabel = document.createElement("label");
hiddenLabel.className = "git-source-hidden";
hiddenLabel.title =
"When checked, files inside hidden (dot) folders are indexed on the next sync. Caches (.git, node_modules, .venv, …) stay excluded.";
const hiddenBox = document.createElement("input");
hiddenBox.type = "checkbox";
hiddenBox.className = "git-source-hidden-box";
hiddenBox.checked = s.include_hidden === true;
hiddenBox.setAttribute(
"aria-label",
`Index hidden folders for ${kindLabel} source: ${value}`,
);
hiddenLabel.append(hiddenBox, document.createTextNode("Hidden"));
hiddenBox.addEventListener("change", () => toggleHidden(s, hiddenBox));
actTd.appendChild(hiddenLabel);
/* Phase 89: the per-row ignore-paths editor trigger (A3: stored
rows only — env-fallback rows fall through to the "from
.env" tag below). The aria-label is the ONLY place `value`
@@ -747,6 +813,59 @@ export async function mount(root) {
}
}
/* ---------- hidden folders (PATCH /api/git-sources/{id}) — the per-row toggle ----------
* Phase 105: the hidden-folders toggle — PATCH { include_hidden }
* only (the row's list is untouched — the PATCH body's optional
* fields, task 03). §7.4 never-stale: the box disables at once
* (no double-flip while the PATCH is out); on 200 the row
* re-renders from the server (loadSources) and the CONFIRMATION is
* the LAST announcement (the reload's "N sources listed." lands
* first — the phase-89 order); on failure the box REVERTS to the
* server state and the detail lands in #git-sources-hidden-error
* (role=alert). The checkbox is a table-cell control (no dialog of
* its own — the ignore editor carries its error INSIDE the modal),
* so this toggle's error is the PAGE-level line; a healed
* loadSources clears it (hideHiddenError on the success path). */
function toggleHidden(s, box) {
const value = s.kind === "local" ? (s.path ?? s.url) : s.url;
const wanted = box.checked;
box.disabled = true; // a PATCH is out — the box must not flip twice
fetch(`/api/git-sources/${s.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "same-origin",
body: JSON.stringify({ include_hidden: wanted }),
})
.then(async (r) => {
if (r.ok) {
hideHiddenError();
await loadSources();
announce(`Hidden folders ${wanted ? "enabled" : "disabled"} for ${value}.`);
return;
}
const detail = await apiDetail(
r, `Could not update the hidden-folders setting (${r.status}).`,
);
showHiddenError(detail);
box.checked = s.include_hidden === true; // revert to server state
box.disabled = false;
})
.catch(() => {
showHiddenError("Could not reach the server — the setting was not changed.");
box.checked = s.include_hidden === true;
box.disabled = false;
});
}
function showHiddenError(message) {
if (hiddenErrorEl) hiddenErrorEl.textContent = message;
if (hiddenErrorEl) hiddenErrorEl.hidden = false;
}
function hideHiddenError() {
if (hiddenErrorEl) hiddenErrorEl.textContent = "";
if (hiddenErrorEl) hiddenErrorEl.hidden = true;
}
/* The dialog's own buttons (static markup — wired once). */
if (ignoreCancelBtn) ignoreCancelBtn.addEventListener("click", cancelIgnoreEditor);
if (ignoreBackdrop) ignoreBackdrop.addEventListener("click", cancelIgnoreEditor);
+50
View File
@@ -2729,6 +2729,56 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
white-space: nowrap;
}
/* Phase 105 (A5): the per-row "Hidden" checkbox — the actions
cell, LEFT of the "Ignore paths" button (JS builds it,
git-sources.js makeRow). A native labeled checkbox: the
visible "Hidden" text + the checkbox's own aria-label
(full source value). ~44px hit height matches the action
buttons; :focus-visible via the GLOBAL rule (L146) — no
per-control ring needed. */
.git-source-hidden {
display: inline-flex;
align-items: center;
gap: 0.4rem;
height: 44px;
padding: 0 0.5rem;
font-size: 0.8rem;
color: var(--ink);
cursor: pointer;
user-select: none;
white-space: nowrap;
}
/* --brand checkbox fill: the native check renders in --bg on
--brand — VERIFIED (phase 105): built-in dark theme
#0f0a0a on #f43f5e = 5.2:1 (the house brand pairing;
measured 5.35:1 — WCAG AA ≥4.5:1); every theme-tab-saved
palette is gated by the tab's eight-pair contrast check
(theme.js PAIRS — "bg" on "brand" at AA_MIN 4.5), so the
checked-state ratio stays AA in all themes. */
.git-source-hidden input[type="checkbox"] {
width: 1.05rem;
height: 1.05rem;
margin: 0;
accent-color: var(--brand);
cursor: pointer;
}
.git-source-hidden:disabled { opacity: 0.5; cursor: wait; }
/* Phase 105 (A5): the "hidden on" state tag — a copy of the
.git-source-ignore-count idiom (TEXT + background, never
color alone — WCAG 1.4.1: --ink on --bg 16.7:1). */
.git-source-hidden-count {
display: inline-block;
margin-left: 0.55rem;
padding: 0.08rem 0.5rem;
border: 1px solid var(--line);
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
color: var(--ink);
background: var(--bg);
white-space: nowrap;
}
/* Env-fallback rows carry no Remove (nothing is stored to remove) —
the tag says where the row comes from (brand pair, 6.9:1). */
.git-source-env-tag {