feat(admin): local directory sources — kind/path on git_sources, combined sync + import, page form + badges
An existing, non-git directory is now a first-class source alongside
the git repos: one table (git_sources + kind discriminator — A13
reversible migration), one admin page, one Sync button (phase locked
decisions; the phase-35 table is extended, not duplicated). The DB is
the local-source registry — no env var for local paths;
BOR_GIT_SOURCES stays a git-only empty-table fallback.
Migration 0007 (reversible, up/down integration-tested):
git_sources.kind TEXT NOT NULL DEFAULT 'git' + ck_git_sources_kind
(kind IN ('git','local')); git_sources.path TEXT NULL +
uq_git_sources_path (mirrors 0006's uq_git_sources_url). Existing rows
read kind='git', path=NULL.
API (phase-35 contract extended, git byte-identical): POST kind=local
requires path — trimmed, ~-expanded, absolute + an existing server
directory, else 422 naming the path (fail loud at add-time); duplicate
path 409 (named); wrong field combos 422. GET rows carry kind + path
(git and env rows: path null); anonymous still 403 on every route (A10).
Sync + import_docs resolve DB git + local rows together: git →
clone_or_pull (unchanged); local → re-verified .is_dir() AT SYNC TIME
(it may have moved/deleted since add-time) — a missing dir raises
"local source missing: <path>" (sanitized) before anything imports;
one import_sources(..., prune=True) over the single combined list
(pruning covers the union). Both-empty fails loudly ("no sources
configured (git or local)"); --source still wins; the env fallback
stays git-only.
Page: second "Add a local directory" form (the same §7.4 never-stale
button + inline-error lifecycle as the git form; 422/409 details name
the path), Git/Local badges on rows (text + color, never color alone —
WCAG), updated hint (git + local together, union prune); the
anonymous sign-in gate is unchanged.
Tests: 0007 up/down; the API local-kind matrix (403/201/422/409) with
the git-kind suite green unchanged; the sync pipeline local/git/
mixed/missing against a host temp dir (the KB actually updated);
import_docs DB resolution + --source precedence. Story E2E (isolated,
deterministic across runs): add (Local badge) → missing path inline
422 naming it / duplicate 409 → the real Sync button imports the
fixture file (GET /api/docs + sentinel in its content) → file deleted
+ sync prunes it (union prune) → row removed; anonymous gate + 403s
(phase-35 regression). test_git_sources_admin.py (phase 35) green
UNCHANGED — no selector collision with the new form;
test_sync_button.py green.
Docs: README — the two managed kinds (git = clone/pull mirror; local =
direct in-place walk), add-time validation, union pruning, "the DB is
the local-source registry (no env var for local paths)";
.env.example — the env fallback is git-only.
This commit is contained in:
+58
-22
@@ -1331,11 +1331,15 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Add form — the tuning form's surface as a single row: visible label
|
||||
+ mono URL input (the credentials case is real, so the input is
|
||||
mono) + the brand "Add source" button; wraps to a column at narrow
|
||||
widths (the <=640px block below). */
|
||||
#git-source-form {
|
||||
/* Add forms — the tuning form's surface as a single row: visible
|
||||
label + mono location input (git URLs may embed credentials, local
|
||||
paths may contain anything, so both inputs are mono) + the brand
|
||||
button; wraps to a column at narrow widths (the <=640px block
|
||||
below). Phase 38: the "Local directory" form (#local-source-form)
|
||||
reuses the git form's rules VERBATIM — one form language for both
|
||||
kinds. */
|
||||
#git-source-form,
|
||||
#local-source-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
@@ -1346,9 +1350,12 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.9rem 1rem 1rem;
|
||||
}
|
||||
#git-source-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); }
|
||||
#git-source-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; }
|
||||
#git-source-url {
|
||||
#git-source-form:focus-within,
|
||||
#local-source-form:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft), var(--shadow); }
|
||||
#git-source-form > label,
|
||||
#local-source-form > label { color: var(--ink); font-weight: 600; white-space: nowrap; }
|
||||
#git-source-url,
|
||||
#local-source-path {
|
||||
flex: 1;
|
||||
min-width: 14rem;
|
||||
min-height: 44px;
|
||||
@@ -1360,9 +1367,12 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
#git-source-url::placeholder { color: var(--ink-soft); }
|
||||
#git-source-url:focus-visible { outline-offset: 0; border-color: var(--brand); }
|
||||
#git-source-add {
|
||||
#git-source-url::placeholder,
|
||||
#local-source-path::placeholder { color: var(--ink-soft); }
|
||||
#git-source-url:focus-visible,
|
||||
#local-source-path:focus-visible { outline-offset: 0; border-color: var(--brand); }
|
||||
#git-source-add,
|
||||
#local-source-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -1376,8 +1386,10 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
#git-source-add:hover:not(:disabled) { background: #7d88f5; }
|
||||
#git-source-add:disabled { opacity: 0.6; cursor: wait; }
|
||||
#git-source-add:hover:not(:disabled),
|
||||
#local-source-add:hover:not(:disabled) { background: #7d88f5; }
|
||||
#git-source-add:disabled,
|
||||
#local-source-add:disabled { opacity: 0.6; cursor: wait; }
|
||||
|
||||
/* The add form's inline error (role=alert): the err pair (9.1:1);
|
||||
flex-basis 100% drops it onto its own row under the input. */
|
||||
@@ -1491,10 +1503,29 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
/* URL cell: mono at the Sources-table size; the <code> is plain
|
||||
(no chip background — the cell IS the mono readout). */
|
||||
/* Location cell: mono at the Sources-table size; the <code> is plain
|
||||
(no chip background — the cell IS the mono readout). Phase 38:
|
||||
leads with the kind badge (Git/Local) — the kinds are told apart by
|
||||
TEXT as much as color (WCAG 1.4.1), and both badge pairs are AA:
|
||||
brand-ink on brand-soft 6.9:1 (Git), ok-ink on ok-bg 10.6:1
|
||||
(Local). */
|
||||
.git-sources-table td.git-source-url-cell { font-family: var(--mono); font-size: 0.82rem; }
|
||||
.git-sources-table td.git-source-url-cell code { font-family: inherit; }
|
||||
.git-source-kind {
|
||||
display: inline-block;
|
||||
margin-right: 0.55rem;
|
||||
padding: 0.08rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-family: var(--font);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
vertical-align: 0.06em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.git-source-kind.is-git { color: var(--brand-ink); background: var(--brand-soft); }
|
||||
.git-source-kind.is-local { color: var(--ok-ink); background: var(--ok-bg); }
|
||||
.git-sources-table tbody tr:hover { background: var(--bg); }
|
||||
.git-sources-table tbody tr:last-child td { border-bottom: 0; }
|
||||
|
||||
@@ -2102,13 +2133,18 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
.doc-modal-meta { padding-inline: 0.9rem; }
|
||||
.doc-modal-content { padding: 0.75rem 0.9rem 1.25rem; }
|
||||
.composer { padding: 0.5rem; }
|
||||
/* Phase 35: the git sources add form stacks like the other cards —
|
||||
label, full-width mono input, full-width button; the table
|
||||
wrapper's horizontal scroll already covers long URLs. */
|
||||
#git-source-form { flex-direction: column; align-items: stretch; }
|
||||
#git-source-form > label { white-space: normal; }
|
||||
#git-source-url { min-width: 0; }
|
||||
#git-source-add { width: 100%; }
|
||||
/* Phase 35 (phase 38: + the local directory form): the add forms
|
||||
stack like the other cards — label, full-width mono input,
|
||||
full-width button; the table wrapper's horizontal scroll already
|
||||
covers long URLs/paths. */
|
||||
#git-source-form,
|
||||
#local-source-form { flex-direction: column; align-items: stretch; }
|
||||
#git-source-form > label,
|
||||
#local-source-form > label { white-space: normal; }
|
||||
#git-source-url,
|
||||
#local-source-path { min-width: 0; }
|
||||
#git-source-add,
|
||||
#local-source-add { width: 100%; }
|
||||
.footer-inner { flex-direction: column; gap: 0.2rem; text-align: center; }
|
||||
main { padding-bottom: env(safe-area-inset-bottom, 0); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user