Le wrapper apiFetch jetait `API error: 422` sans le body. Désormais lit
detail (string ou liste Pydantic) et le concatène au message :
`422: Neo4j config requires a non-empty 'index_name'`.
Permet aux pages StoreCreatePage / StoreEditPage de surfacer la cause
réelle du refus serveur dans l'errorMessage du form.
- domain : StoreKind.NEO4J ajouté à l'enum
- service : validation config par kind étendue (Neo4j requires non-empty index_name)
- frontend : Neo4jConfigForm (index_name + database optionnelle), dispatcher StoreConfigForm, dropdown kind, i18n FR/EN
- tests service : 2 cas Neo4j (create OK + missing index_name 422)
Auth (URI/user/password) reste pilotée par les variables d'environnement
globales (cf. infra/neo4j.py) — la config par store ne porte que les
paramètres routables (index, database).
- StoresListPage : bouton "Nouveau store" header + ligne action Edit/Delete (delete bloqué sur 'default'), navigation par slug, colonne "Défaut", confirm window.confirm
- StoreDetailPage : actions Edit / Delete dans le header, redirection vers la liste après suppression
- api.test.ts : couvre la nouvelle shape StoreInfo (slug + isDefault + embedder)
Quand l'utilisateur naviguait d'un doc à un autre via /docs/:id, Vue
réutilisait l'instance du composant — onMounted ne se redéclenchait pas,
laissant l'analyse précédente (et donc les bbox du StructureViewer)
visibles tant que le nouveau fetch n'était pas terminé.
- DocWorkspacePage: watch props.id pour recharger le doc + :key sur les
onglets (forçe un remount propre, reset l'état interne du
StructureViewer comme selectedPage/hiddenTypes).
- DocInspectTab / DocAskTab: watch props.docId avec immediate:true,
reset l'état au début du load + guard de race condition (ignore les
réponses pour un docId obsolète).
Le commit précédent l'avait untracké. Or .github/workflows/ci.yml et
release-gate.yml référencent explicitement le lockfile pour le cache npm
et la step `npm ci` exige sa présence. Resultat : CI cassé.
On garde docs/git-workflow/autonomy-mode.md ignoré (artefact local).
- #225: rename all push/index/pousser/indexé labels to ingest/ingérer/ingéré
across status tooltips, docs/chunks push modals, and legacy ingestion panel
- #224: add DocStoreLink type + storeLinks on Document; StaleStoresStrip
component shows per-store state badges with one-click re-ingest buttons
Backend
- HealthResponse exposes inspectModeEnabled / chunksModeEnabled /
askModeEnabled (additive; defaults true). main.py /api/health
populates them from settings.
- infra/settings.py: three new env-var-driven booleans (defaults true)
parsed in from_env() like the existing reasoning_enabled flag.
- tests/test_api_endpoints.py: extra assertion that /api/health
surfaces the three new fields with their defaults.
Frontend — flag store
- features/feature-flags/store.ts: FeatureFlag union extended with
inspectMode / chunksMode / askMode. New entries in featureRegistry
are gated on context fields populated from health. Missing fields
fall back to true so a frontend pointed at an older backend keeps
every mode visible.
- store gains a modeFlags() helper returning Record<DocMode, boolean>
so the routing guard does not need to know the FeatureFlag union.
Frontend — routing
- shared/routing/resolveMode.ts: pure resolver. If the requested mode
is enabled, return it; else first enabled in priority ask > chunks
> inspect; else null.
- app/router/index.ts: beforeEach guard scoped to ROUTES.DOC_WORKSPACE.
Disabled mode → rewrite ?mode= to the first enabled one. All three
off → redirect to /docs?reason=no-mode-enabled.
Frontend — flash
- pages/DocsLibraryPage.vue: shows a banner when ?reason=no-mode-enabled
is set. #211 will move this into the proper library page banner.
- i18n flags.allModesDisabled added in fr + en.
Tests
- shared/routing/resolveMode.test.ts (6 cases): every (requested,
enabled) combination including all-disabled, priority order,
missing requested.
- features/feature-flags/store.test.ts: three new cases covering the
new fields in /api/health, fall-back-to-true on missing fields, and
modeFlags() shape.
Refs #210
The sidebar was the project's actual nav (the design doc called it
'top nav' but the existing app is sidebar-driven). Five entries
reflecting the doc-centric IA replace the seven analysis-centric ones.
Sidebar
- shared/ui/AppSidebar.vue: data-driven over a NavItem[] table
instead of seven hard-coded RouterLinks. Inline SVG icon
components keep the visual weight close to the previous version
with no new dependency.
- Five entries: Home / Docs (★ primary, bolder label) / Stores /
Runs / Settings.
- Active state via matchesActive(path, prefixes): exact match for
Home, prefix match (with segment / query boundary) for the others.
Pure helper in shared/ui/navActive.ts, unit tested.
Removed from the sidebar (legacy pages still reachable by URL):
Studio, Documents, Search, Reasoning, History.
i18n
- nav.docs / nav.stores / nav.runs added in fr + en, grouped under
the 0.6.0 nav block.
- Legacy nav labels (nav.studio / nav.documents / nav.history /
nav.reasoning / nav.search) kept — the legacy pages still render
headings using them. Duplicate nav.search entries removed (the key
now lives once per locale, in the nav block).
Tests
- shared/ui/navActive.test.ts (5 cases): exact-match for Home,
prefix boundary semantics, multi-prefix matching, empty list edge.
Refs #209
Adds the breadcrumb anchoring the user across modes on the doc
workspace. Empty / hidden on routes that don't opt in.
Components
- shared/breadcrumb/AppBreadcrumb.vue: data-driven, accessible
(<nav aria-label>, <ol>, aria-current=page on the leaf).
Renders nothing when crumbs.length === 0.
- shared/breadcrumb/types.ts: Crumb = LinkCrumb | LeafCrumb
discriminated union.
- shared/breadcrumb/store.ts: Pinia store + useCrumbs(source)
composable that auto-clears on unmount. Accepts a static array
OR a reactive ref/computed so pages with async doc fetches can
rebuild crumbs as data lands.
- shared/breadcrumb/text.ts: truncate(text, max) helper.
Wiring
- App.vue main outlet now sits below <AppBreadcrumb>; the shell
reads from useBreadcrumbStore so pages don't need teleports.
- DocWorkspacePage provides Studio > <id-truncated> > <mode-label>;
once the doc is fetched (#216 / E4), the id placeholder will
swap for the truncated filename.
i18n
- breadcrumb.aria, breadcrumb.studio, breadcrumb.mode.{ask,inspect,
chunks} added in fr + en.
Tests
- shared/breadcrumb/text.test.ts: 5 cases on truncate.
- shared/breadcrumb/store.test.ts: store actions + useCrumbs reactive/
static seeding (the lifecycle-clear path is covered end-to-end by
the integration tests landing in #211 / #216 — the project doesn't
use @vue/test-utils so a unit test for onBeforeUnmount is overkill).
Refs #208
Adds the routing scaffold for the doc-centric pivot. Each new route
renders a placeholder page until E3/E4/E5 implement them; legacy
routes (/studio, /documents, /history, /search, /reasoning) keep
working in parallel.
New routes (Vue Router, history mode):
/docs library (placeholder, #211)
/docs/new import (placeholder, #214)
/docs/:id?mode= workspace (placeholder, #216)
/index stores list (placeholder, 0.7.0)
/index/:store store detail (placeholder)
/index/:store/query RAG playground (placeholder)
/runs run history (placeholder)
/runs/:id run detail (placeholder)
Mode parsing
- shared/routing/modes.ts: DocMode union ('ask'|'inspect'|'chunks'),
parseMode() returns the default ('ask') for missing or unknown
values. #210 will layer feature-flag-aware redirection on top.
Route names
- shared/routing/names.ts: ROUTES typed const so callers do
router.push({ name: ROUTES.DOC_WORKSPACE, ... }) instead of
stringly-typed names.
Pages
- ComingSoonShell shared component: card + back-home link, themed
with existing CSS tokens.
- 8 thin placeholder pages (one per new route) that compose the shell
and forward route params.
- i18n keys under comingSoon.* added in fr + en.
Tests
- shared/routing/modes.test.ts (10 cases): isDocMode + parseMode +
ALL_MODES invariants.
- app/router/router.test.ts (5 cases): every doc-centric route
resolves to a component, legacy routes still work, doc workspace
receives id and parsed mode as props, unknown mode falls back to
ask, unknown path redirects to home.
Routes table extracted to routes.ts so tests build a router with
createMemoryHistory() (no window required) instead of needing the
production createWebHistory() router.
Refs #207
Adds a first-class lifecycle state to every document, distinct from
AnalysisJob.status. The lifecycle describes the document as a whole and
is the foundation for the doc-centric pivot in 0.6.0.
Domain
- DocumentLifecycleState enum (Uploaded/Parsed/Chunked/Ingested/Stale/Failed)
- Document.lifecycle_state and lifecycle_state_at fields
- Document.transition_to() validates against a transition table
(domain/lifecycle.py) and returns a DocumentLifecycleChanged event
- InvalidLifecycleTransitionError on disallowed transitions
Persistence
- ALTER TABLE documents to add the two columns (default 'Uploaded')
- New index idx_documents_lifecycle_state for filter perf
- _COLUMN_MIGRATIONS refactored to support multiple tables
- _POST_MIGRATION_DDL list for indexes on freshly-added columns
- SqliteDocumentRepository.update_lifecycle()
Services
- AnalysisService drives transitions on parse / chunk / re-chunk / fail
via _transition_document(); idempotent and resilient (logs WARN and
continues if a stale state is somehow encountered)
API
- DocumentResponse exposes lifecycleState + lifecycleStateAt
(additive — existing 'status' field kept for backwards compat)
Frontend
- Document type extended with lifecycleState and lifecycleStateAt
- DocumentLifecycleState union literal mirroring the backend enum
Tests
- 24 new tests in test_lifecycle.py covering transitions, idempotency,
invariant preservation, and event emission
- test_repos.py: round-trip + every-enum-value check + update_lifecycle
- test_chunking.py: rechunk path now mocks document_repo correctly
Refs #202
nginx.conf was hard-capped at 5M, causing 413 on uploads larger than 5MB
despite the backend accepting up to MAX_FILE_SIZE_MB (default 50).
Both nginx configs become envsubst templates. NGINX_MAX_BODY_SIZE defaults
to 200M so the backend application limit is always the effective arbiter.
gettext-base added to the single-image apt deps to provide envsubst.
The watch-based plumbing from iteration click to PDF scroll relied on a
"flip via null" pattern (assign null then the value) to coerce Vue into
re-firing the watcher. Vue 3 collapses synchronous mutations of the same
ref and only delivers the final value, so the trick was a no-op: a second
click on the same iteration left the document view stuck on the previous
page. The bug only showed when the trace had a single iteration — with
several, the user naturally clicks different ones and the value really
changes.
Replace the watch chain with imperative dispatch. ReasoningPanel now just
emits iterationFocus; ReasoningWorkspace handles it by calling the graph
focus and the new StructureViewer.scrollToFocused method directly. Both
side effects fire on every click regardless of state.
Propagate Docling `self_ref` through PageElement so bboxes and graph nodes
share a stable identity. Add a Document/Graph mode switch to the reasoning
workspace; selecting a node highlights its bbox (numbered badge, focus ring,
optional dim of non-visited) and clicking a bbox re-centers the graph.
Backend — live runner
- New `POST /api/documents/:id/rag` endpoint. Loads `document_json` from
SQLite, reconstructs the DoclingDocument, wraps the model id in
`ModelIdentifier(ollama_name=...)`, and calls `agent._rag_loop`
off-thread (blocking sync call). Returns a `RAGResult` in the shape
the existing v1 import path already consumes, so the frontend overlay
is fully reused.
- `_rag_loop` is private upstream; we call it because `run()` wraps the
answer in a synthetic DoclingDocument and drops the iteration trace.
- Settings: `RAG_ENABLED`, `OLLAMA_HOST`, `RAG_MODEL_ID`. Router mounts
unconditionally; handler 503s when the flag is off or deps aren't
installed. `rag_available` surfaced in `/api/health`.
- Maps known docling-agent bugs to readable HTTP errors: 502 with
"the model couldn't produce a parseable answer" when `_rag_loop`
raises `IndexError` from `find_json_dicts([])[0]` after 3 + 3
rejection-sampling retries (model-dependent).
- Tests: 11 cases (flag off, query empty, no analysis, happy path,
model_id wrap, Ollama env, IndexError → 502, other errors → 500,
deps missing → 503).
Backend — bug fix
- Default `BATCH_PAGE_SIZE` flipped from `10` to `0` to match the
dataclass default. The old default silently dropped `document_json`
(see `domain/services.merge_results`) for any doc > 10 pages, which
broke the reasoning tunnel. Set `BATCH_PAGE_SIZE>0` explicitly on
memory-constrained deploys if batching is wanted.
Frontend — runner UX
- `features/reasoning/api.ts:runReasoning()` — POST wrapper.
- `RunReasoningDialog.vue` — query textarea + optional model_id
override. Blocks close while running, 20-40s loading state,
synthesises a sidecar-shaped envelope so the panel surfaces query +
model the same way an imported trace would.
- `ReasoningWorkspace.vue` — primary "Run reasoning" button; "Import
trace" relegated to ghost secondary.
- Store: `runDialogOpen`, `running`, `setRunning`.
Frontend — answer polish
- Answer rendered through `marked` + DOMPurify (models emit markdown
lists; `pre-wrap` rendered them as plain "1. …" strings).
- Dedicated answer block with orange border, "ANSWER" label, "Copy"
button (clipboard + "Copied ✓" feedback).
- IterationCard: drop the duplicate `response` block (the main answer
is authoritative); style reasons equal to `"fallback"` (docling-agent
`select_from_failure` placeholder) as italic muted "— no structured
rationale".
Frontend — node details contents
- Clicking a SectionHeader (or any node with compound children) lists
its contained elements in `NodeDetailsPanel` under a new "Contents"
block. Children come from the same `parentMap` used for Cytoscape
compound parenting (explicit PARENT_OF + synthetic section scope),
inverted once and cached as a computed.
- Click a child row → pan the viewport to it + swap the selection.
Housekeeping
- `cytoscape-navigator` removed from `package-lock.json` (follow-up
from the minimap removal in the previous commit).
Adds the `docling-agent` reasoning-trace viewer as a Studio tunnel, per
`docs/design/reasoning-trace.md`. Users pick an analyzed document, import
a RAGResult JSON, and the iterations are overlaid on the document graph.
Graph source is decoupled from Neo4j: a new pure builder
(`infra/docling_graph.build_graph_payload`) reads `document_json` from
SQLite and emits the same Cytoscape-shaped payload that `fetch_graph`
returns from Neo4j. Neo4j stays exclusive to the Maintain ingestion
pipeline. Shared DoclingDocument helpers live in `infra/docling_tree.py`
so TreeWriter and the builder can't drift on label taxonomy or tree walks.
Also removes the Cytoscape minimap (cytoscape-navigator) from GraphView:
second render instance hurt perf on large documents for no UX win.
Backend
- new `GET /api/documents/:id/reasoning-graph` (SQLite-only)
- new `infra/docling_tree.py`, `infra/docling_graph.py`
- `analysis_repo.find_latest_completed_by_document`
- tests: `test_docling_graph.py` (builder), `test_graph_api.py` (endpoint)
Frontend
- `features/reasoning/` — store, overlay, types, panel, import dialog,
workspace, doc picker
- new `ReasoningPage` + `/reasoning` and `/reasoning/:docId` routes
- `GraphView` gains a `fetcher` prop so reasoning can inject the
SQLite-backed fetcher while Maintain keeps using the Neo4j one
- drops minimap (nav container, dep, CSS)
- legend filters + section parenting extracted for reuse
- i18n base strings (FR + EN)
Move the graph view from a Verify-tab (where it sat post-analysis, off
the main flow) to a dedicated Maintain step after Ingest, so the graph
result is visible at the natural end of the Configure → Verify →
Prepare → Ingest → Maintain pipeline.
- StudioPage: new 'maintain' mode toggle + panel rendering GraphView
- ResultTabs: remove obsolete graph tab
- i18n: add studio.maintain (fr + en)
- GraphView: fix init order — flip loading off and await nextTick before
renderGraph so the canvas <div> is mounted when cytoscape reads its
container ref (previous code bailed silently on null ref)
ChunkWriter mirrors chunks into Neo4j after OpenSearch indexing, creating
HAS_CHUNK edges and DERIVED_FROM back-references to the source Elements
(via doc_items propagated from the local chunker).
Graph API: GET /api/documents/{id}/graph returns a cytoscape-shaped
payload with nodes + edges for Document / Element / Page / Chunk.
Hard cap at 200 pages returns HTTP 413 per design §8.4.
Frontend: new Graph tab in Studio results, rendered with Cytoscape.js +
dagre layout (lazy-loaded, ~175 KB gz). Legend, node styling per element
label, directional edges styled per edge type.
README gains a Neo4j section with the schema, three demo Cypher
queries, and env vars. Backend tests skip cleanly when the neo4j python
package is not installed locally.
Refs #186
Hybrid approach: reuse LocalChunker to chunk the DoclingDocument JSON
returned by Serve, so chunking works identically in both local and
remote modes without calling Serve's chunk endpoint.
Backend:
- _build_chunker() always returns LocalChunker (remove engine guard)
- Use docling-core[chunking] extra for required dependencies
- Skip client-side batching in remote mode (Serve manages its own
resources, and batching discards document_json needed for chunking)
- Fix Serve form fields: remove generate_page_images (not a Serve
field), use repeated form keys for to_formats and page_range
- Log Serve error response body on 4xx/5xx for diagnosis
- Fix FastAPI 204 DELETE routes missing response_model=None
Frontend:
- Update chunking feature flag to enable Prepare UI in remote mode
Closes#51
ChunkPanel emitted 'rechunked' which triggered an async re-fetch via
analysisStore.select() — but Vue does not await async emit handlers,
so chunk-cards never appeared before the E2E 30s timeout.
Now rechunk/edit/delete write returned chunks directly into the
analysis store via updateChunks(), removing the async round-trip.
- Conditionally mount ingestion router only when OpenSearch + embedding are configured
- Add `ingestionAvailable` field to /api/health response
- Add `ingestion` feature flag to frontend (hides Search nav, Ingest button,
OpenSearch badge, indexed badges/filters when disabled)
- Skip ingestion polling when flag is off
- Make OpenSearch + embedding optional in docker-compose via profiles
- Add docker-compose.ingestion.yml override for full-stack ingestion
- Set BATCH_PAGE_SIZE=5 default in Docker local image
- Lead Quick Start with one-liner docker run command
- Document ingestion as opt-in with dedicated section
- Add BATCH_PAGE_SIZE, MAX_FILE_SIZE_MB, MAX_PAGE_COUNT, RATE_LIMIT_RPM to config tables
- Update test counts (380 backend, 159 frontend)
- Date CHANGELOG 0.4.0, bump frontend version to 0.4.0
- Sync CONTRIBUTING.md with E2E Karate test sections
Closes#180
Add dedicated data-e2e selectors (configure-btn, verify-btn, prepare-btn)
to StudioPage toggle buttons and update E2E tests to use waitFor + click
on these selectors instead of locateAll index tricks. Fixes timeout in
rechunk.feature caused by race with async feature flag loading.
Closes#176
Add Ingest as a dedicated mode tab in the Studio pipeline
(Configure → Verify → Prepare → Ingest). Create IngestPanel
component in features/ingestion/ui/ with summary, stepper,
and action button. Remove orphan Ingest button and inline
stepper from StudioPage. Remove auto-ingest on analysis complete.
Closes#160
Move chunk search from DocumentsPage into a new Search bounded context
(features/search/) with its own store, API layer, page and route.
Clean search state out of the ingestion module.
Closes#159
Green/red dot in the sidebar footer showing OpenSearch connectivity.
Tooltip: 'OpenSearch connected' / 'OpenSearch unreachable'.
Polls every 30s via ingestionStore.startPolling(). GET /api/ingestion/status
now returns opensearchConnected boolean from a live ping.
Backend: GET /api/ingestion/search?q=…&doc_id=… endpoint with
SearchResponse schema. Frontend: search bar in Documents page, results
with filename, page, chunk index, relevance score. 3 new API tests.
Visual stepper in Studio topbar: Embedding → Indexing → Done.
Each step shows pending/active/done with animated dot. Store tracks
currentStep through the pipeline. Auto-resets after 2s.
- Replace French mode strings (configurer/verifier/preparer) with English
equivalents (configure/verify/prepare) in StudioPage.vue and tests
- Extract _build_conversion_options, _run_conversion, _finalize_analysis
from _run_analysis_inner to respect Single Responsibility Principle
- Rename _get_default_converter to _ensure_default_converter to reflect
its lazy-init side effect
Closes#136, closes#137, closes#138
Frontend decoupling:
- Create shared/appConfig.ts as reactive bridge (locale, maxFileSizeMb,
maxPageCount) eliminating shared→features and feature→feature imports
- Give history feature its own Pinia store and API layer (was re-export
of analysis store)
- Give chunking feature its own Pinia store and API layer (was importing
from analysis)
- ChunkPanel receives analysis data via props instead of cross-feature
store import
- document/store reads maxFileSizeMb from shared config instead of
importing feature-flags store
- shared/i18n reads locale from shared config instead of importing
settings store
Backend:
- Add HealthResponse Pydantic schema for /api/health endpoint
Closes#140, closes#141, closes#142, closes#143
Read version from the backend /api/health endpoint instead of
hardcoding from package.json at build time. Add About section
in Settings with link to the DZone design article.