Commit graph

59 commits

Author SHA1 Message Date
Pier-Jean Malandrino
27e3323bff feat(#257): surface gating via STUDIO_MODE + RAG_PIPELINE master flags
Some checks failed
Auto-close issues on release branch merge / Close referenced issues (push) Has been cancelled
Introduces two master feature flags that select which UI surface is
exposed, replacing the previous "delete legacy pages" approach with a
softer isolation:

- STUDIO_MODE_ENABLED  (default false) — legacy OCR-debug surface
- RAG_PIPELINE_ENABLED (default true)  — new doc-centric ingestion + viz

At least one master must be enabled (validated server-side at startup).
Sub-flags (inspect / linked / ask) are effective only when the RAG
pipeline master is on.

CHUNKS_MODE_ENABLED renamed to LINKED_MODE_ENABLED in anticipation of
T3 (Linked view replaces the Chunks tab). The DocMode union value
'chunks' is preserved for now and will be renamed in T3 alongside the
route segment, to keep this PR scoped.

Router-level guard added: requests to a route whose surface is disabled
are redirected to the other surface's landing page (or /home as a
defensive fallback). Logic extracted into a pure resolveSurface helper
with full test coverage.

i18n strings that pointed users to "Studio" rewritten to be surface-
agnostic ("from the library" / "depuis la bibliothèque") since Studio
is hidden by default in 0.6.1.

Backend:
- infra/settings.py: add studio_mode_enabled + rag_pipeline_enabled;
  rename chunks_mode_enabled → linked_mode_enabled; add at-least-one
  master validation in __post_init__
- api/schemas.py: HealthResponse exposes both master flags + renamed
  sub-flag
- main.py: health endpoint wires the new fields
- tests: surface-flag + renamed sub-flag assertions

Frontend:
- features/feature-flags/store: add studioMode + ragPipeline registry
  entries; rename chunksMode → linkedMode; sub-flags now require
  ragPipeline enabled; modeFlags() maps linkedModeEnabled → key 'chunks'
  (transitional)
- shared/routing/resolveSurface: pure helper + tests
- app/router: beforeEach guard consumes resolveSurface
- shared/i18n: Studio-pointing strings rewritten (en + fr) + test sync
- features/reasoning: stale "from StudioPage" comment generalized
2026-05-11 16:18:34 +02:00
Pier-Jean Malandrino
b4ad874600 fix(#256): drop pushDocumentToStore duplicate, align rechunk return shape
The frontend had two endpoints pointing at the same operation:
  - features/document/api.ts:pushDocumentToStore  → POST /push
  - features/chunks/api.ts:pushChunksToStore      → POST /chunks/push

Backend now exposes a single /chunks/push route (semantically more
accurate — it's chunks that get pushed, not the doc itself), so the
document-side variant is removed. document/store.ts:pushToStore now
delegates to chunks/api.

rechunkDocument returned {jobId} based on the old async-job model.
The new backend rechunk runs synchronously and returns the chunk
list directly. The DocsLibraryPage caller now reports a count
(via new docs.rechunkDone i18n key) instead of a fake job id.

Adds data-e2e=tab-{mode} on the doc workspace tab strip so the
new Karate scenario can target the chunks tab without text matching.

Extends chunks/api.test.ts with HTTP error-propagation guards on
every call — the original 404 bug would have surfaced in CI.
2026-05-11 15:00:57 +02:00
Pier-Jean Malandrino
df1bbef143 feat(#251): expose backend error detail in apiFetch errors
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.
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
151505cdbf feat(#251): add Neo4j as a StoreKind
- 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).
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
7ad25aeb2a feat(#251): API client typé create/update/delete + StoreForm + Create/Edit pages
Frontend store API client :
- Types StoreInfo (avec slug + isDefault + embedder), StoreDetail, StoreCreatePayload, StoreUpdatePayload
- Fonctions createStore (POST), updateStore (PATCH), deleteStore (DELETE), fetchStore (GET /:slug)
- Tests Vitest étendus pour les 4 nouveaux endpoints

Composants formulaire (features/store/ui/) :
- StoreForm : champs communs (name/slug/kind/embedder/isDefault) avec validation slug locale, lock du slug pour 'default'
- StoreConfigForm : dispatcher dynamique par kind (extensible)
- OpenSearchConfigForm : champ indexName, validation requise

Pages CRUD :
- StoreCreatePage (/index/new) : formulaire + redirect liste
- StoreEditPage (/index/:store/edit) : préremplit via fetchStore, redirige vers la fiche

Routes + i18n :
- Routes ROUTES.STORE_CREATE et ROUTES.STORE_EDIT ajoutées (names + routes.ts)
- Clés i18n FR/EN sous stores.* et storeForm.*
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
1e9e57ab2d feat(#243,#244,#245): E8 — Stores pages (list, detail, query)
- features/store: api + types (StoreInfo, StoreDocEntry, QueryResult)
- StoresListPage: table with connection status, click → StoreDetail
- StoreDetailPage: doc table with StatusBadge, remove, bulk remove, link to query
- StoreQueryPage: query form (Ctrl+Enter), topK, scored results with doc links
- i18n: stores.* / storeDetail.* / storeQuery.* keys (FR + EN)

Closes #243
Closes #244
Closes #245
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
2718d86262 feat(#242): DocAskTab — Reasoning intégré au workspace (doc structure + question + trace cliquable) 2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
0062890830 feat(#240,#241): DocInspectTab — Markdown/Elements/Images from Docling analysis 2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
e40b5ba0a5 feat(#225,#224): vocabulary rename index→ingest + stale stores strip
- #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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
88f109ed4c feat(#216): E4/E5 — Doc workspace shell, DocTreeRail, DocWorkspaceHeader, editable chunks editor
Closes #216
Closes #217
Closes #218
Closes #219
Closes #220
Closes #221
Closes #222
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
6527307553 feat(#211): E3 — Document library (/docs), filters, bulk actions, multi-file import, StatusBadge
- StatusBadge component with 6 lifecycle states, compact/full variants, tooltip (#215)
- DocsLibraryPage: table (Name/Status/Stores/Updated), empty state (#211)
- Filter bar: status pills, store chips, debounced search, URL sync (#212)
- Multi-select with sticky bulk bar: Re-chunk, Push to store modal, Delete (#213)
- DocsNewPage: multi-file drop zone with per-file progress, sequential upload (#214)
- Document.stores?: string[] field, formatRelativeTime(iso, locale) helper
- rechunkDocument / pushDocumentToStore API + store actions (rechunk / pushToStore)
- i18n keys status.*, docs.*, docsNew.* in FR + EN

Closes #211, Closes #212, Closes #213, Closes #214, Closes #215
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
7b6d64c434 feat(#210): feature-flag mode gating + deep-link redirect
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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
058d718116 feat(#209): rework sidebar nav (Home / Docs / Stores / Runs / Settings)
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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
ab5de2aac3 feat(#208): doc workspace breadcrumb (Studio > <doc> > <mode>)
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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
47e0d197b3 feat(#207): document-centric routing skeleton
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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
ff0b795b6d feat(#202): introduce Document lifecycle state machine
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
2026-05-05 09:38:39 +02:00
Pier-Jean Malandrino
efc27932dd refactor(audit): remediate 0.5.0 audit findings — clean architecture, security, DRY, SOLID, perf
Closes the 12 MAJ raised by the release/0.5.0 audit pipeline (cf.
docs/audit/reports/release-0.5.0/summary.md → summary-reaudit.md).

Volet 1 — Reasoning architecture (audits 01/02/06/07 strengthening)
  * Domain ports: LLMProvider, ReasoningRunner, ReasoningParseError
  * Domain DTOs: LLMProviderType, ReasoningResult, ReasoningIteration
  * infra/llm/ollama_provider.py — OllamaProvider with health_check
  * infra/docling_agent_reasoning.py — runner adapter, encapsulates the
    private _rag_loop call (tracked at docling-project/docling-agent#26),
    commits OLLAMA_HOST once at boot (eliminates the per-request env race),
    translates upstream IndexError into ReasoningParseError
  * api/reasoning.py — zero coupling to docling-agent / mellea / docling-core,
    consumes app.state.reasoning_runner via the port
  * main.py — DI wires OllamaProvider + DoclingAgentReasoningRunner at boot
    when REASONING_ENABLED=true and deps are importable
  * Rename RAG_* env vars → REASONING_*, endpoint /rag → /reasoning,
    type RAGResult → ReasoningResult, frontend feature flag wiring,
    i18n strings, tests, docs (BREAKING — pre-1.0 surface, no external
    consumers in production)
  * 17 new tests: adapter unit tests with sys.modules stubs, OllamaProvider
    httpx tests, R3 concurrent-host isolation, R6 multi-iteration trace
    serialization, R13 Protocol conformance via isinstance
  * E2E Karate scenario: nav-reasoning hidden when REASONING_ENABLED=false
  * README — Live Reasoning section (env vars, archi, link to issue #26)

Bloc B — Security (audit 08, dev-only context)
  * docker-compose.yml — DEV DEFAULTS header, OpenSearch DISABLE_SECURITY_PLUGIN
    flagged as dev-only with link to OpenSearch security docs
  * main.py — boot warning if NEO4J_URI is set with the default 'changeme'
    password, so prod operators can't silently inherit it

Bloc C — DRY frontend (audit 05)
  * shared/storage/keys.ts — STORAGE_KEYS centralised (theme, locale)
  * features/settings/store.ts — dead apiUrl ref + orphan i18n keys removed
  * api/schemas.py — DOCUMENT_STATUS_UPLOADED constant

Bloc D — Quality (audits 02/06/07/09/10/12)
  * domain/ports.py — DocumentConverter.supports_page_batching property
    (LSP fix, replaces isinstance(ServeConverter) check)
  * domain/ports.py — VectorStore.ping() (encapsulation, replaces
    _vector_store._client.info() reach-around)
  * api/analyses.py + api/ingestion.py — path params {job_id} → {analysis_id}
    aligned with the user-facing terminology (URLs unchanged)
  * api/documents.py — Path.read_bytes() + generate_preview() wrapped in
    asyncio.to_thread, unblocks the FastAPI event loop on /preview
  * infra/docling_tree.py — PEP 604 union for isinstance (Ruff UP038)
  * src/__tests__/integration/ — cross-feature integration test relocated
    out of features/history/ so feature folders stay self-contained
  * Tightened terminal `assert X is not None` checks (isinstance(.., datetime),
    exact value comparisons)

Validation
  * 446 backend pytest, 202 frontend vitest — all green
  * ruff + ruff format + ESLint + Prettier + vue-tsc clean
  * Re-audit verdict: 0 CRIT / 0 MAJ, score ~94/100, GO

Closes #200
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
8694353d8b feat(reasoning): bidirectional PDF ↔ graph focus + DocumentView mode
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.
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
5b7700df83 feat(reasoning): live docling-agent runner + UX polish
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).
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
8103460e9c feat(reasoning): reasoning-trace viewer v1 with SQLite-backed graph
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)
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
03e5dfac17 feat(ui): add Maintain step with graph visualization
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)
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
c2550867b7 feat(neo4j): Day 3 — ChunkWriter, graph API, GraphView, README
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
2026-04-29 14:00:00 +02:00
Pier-Jean Malandrino
845425af62 chore: add Coming soon to batch chunking notice, set BATCH_PAGE_SIZE default to 10 2026-04-13 13:23:27 +02:00
Pier-Jean Malandrino
ba54427445 feat(#180): feature-flag ingestion pipeline and add brainless one-liner Quick Start
- 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
2026-04-13 11:18:56 +02:00
Pier-Jean Malandrino
9961d1e080 feat(#160): promote Ingest to 4th Studio mode after Prepare
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
2026-04-11 10:17:12 +02:00
Pier-Jean Malandrino
bae00e4025 feat(#159): extract search into dedicated sidebar tab
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
2026-04-11 10:14:02 +02:00
Pier-Jean Malandrino
daaea86173 feat(#80): OpenSearch connection status indicator in sidebar
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.
2026-04-10 22:49:40 +02:00
Pier-Jean Malandrino
efabe84d66 feat(#77): multi-step ingestion progress stepper
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.
2026-04-10 22:49:20 +02:00
Pier-Jean Malandrino
5ba953fc58 feat(frontend): My Documents screen with ingestion status, search, filter, sort
Closes #75
2026-04-10 21:19:09 +02:00
Pier-Jean Malandrino
80b0c44e8c feat(chunking): soft-delete chunk with confirmation dialog
Closes #90
2026-04-10 20:28:24 +02:00
Pier-Jean Malandrino
c74a3277b8 feat(chunking): inline chunk text editing
Closes #89
2026-04-10 19:37:29 +02:00
Pier-Jean Malandrino
3a09c18c59 fix(decoupling): eliminate cross-feature imports and add typed health endpoint
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
2026-04-10 13:11:29 +02:00
Pier-Jean Malandrino
163a6f289a feat: add i18n keys for batch notice and about section (FR + EN) 2026-04-09 15:52:03 +02:00
Pier-Jean Malandrino
abf3325923 feat: single source of truth for app version from health check
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.
2026-04-09 15:51:59 +02:00
Pier-Jean Malandrino
fdbab2f49e feat: add Karate UI e2e tests with data-e2e selectors (#124)
- Add e2e/ui/ as peer project to e2e/api/ (own pom.xml, runner, config)
- 5 critical UI journeys: upload, delete, analysis, batch-progress, rechunk
- 4 local-only tests: sidebar, i18n, error-states, pipeline-options
- 1 full happy path workflow covering all modes
- Add data-e2e attributes on all tested Vue components (decoupled from CSS)
- Add CONVENTIONS.md with 7 golden rules for writing Karate UI tests
- Update CI with dedicated e2e-ui job (Chrome headless, --no-sandbox)
- Update docs/contributing.md with UI test instructions

Closes #124
2026-04-08 17:53:52 +02:00
Pier-Jean Malandrino
704ba550d4 feat: make document max size configurable via MAX_FILE_SIZE_MB env var
Replace hardcoded 5 MB upload limit with a configurable setting.
Backend exposes the value via /api/health, frontend reads it
dynamically for validation and UI messages.

Closes #48
2026-04-08 10:27:38 +02:00
Pier-Jean Malandrino
fc866ce229 feat: batch large documents with page_range and progress reporting
Use Docling's native page_range parameter to split large PDFs into
sequential batches, preventing memory exhaustion and timeouts.
Progress is reported via existing polling mechanism.

Closes #56
2026-04-07 17:54:40 +02:00
Pier-Jean Malandrino
09145206d2 fix: limit upload to 5 MB / 20 pages max and increase conversion timeout
Prevents PyTorch/Docling pipeline crashes on HF Spaces CPU by:
- Reducing max file size from 50 MB to 5 MB
- Adding configurable MAX_PAGE_COUNT setting (env var, default unlimited)
- Increasing conversion timeout from 600s to 900s
- Adding frontend upload validation with explicit error messages
- Exposing maxPageCount via /api/health for dynamic UI hints
2026-04-07 10:56:16 +02:00
Pier-Jean Malandrino
af4400b1f7 Add Github badge 2026-04-07 08:57:39 +02:00
Pier-Jean Malandrino
e1ccc7a6e0 Add document size limit (50 MB) to disclaimer banner 2026-04-07 08:35:39 +02:00
Pier-Jean Malandrino
ee57b04f84 Add disclaimer banner for shared deployments (HuggingFace)
Feature-flagged via DEPLOYMENT_MODE env var and /api/health endpoint.
Disabled by default (self-hosted), enabled when deploymentMode=huggingface.
2026-04-04 21:06:38 +02:00
Pier-Jean Malandrino
226ae5994e Format all frontend files with Prettier
Batch reformat to eliminate 23 files with inconsistent formatting
that were accumulating across feature branches.
2026-04-03 15:50:32 +02:00
Pier-Jean Malandrino
6a791cec48 Fix i18n placeholder replacement to handle repeated occurrences
Use replaceAll instead of replace to substitute all instances of
a placeholder in translation strings, not just the first one.
2026-04-03 13:05:44 +02:00
Pier-Jean Malandrino
1ff8c5108f Add release process, dynamic versioning, and changelog catch-up
Replace hardcoded version strings with build-time injection:
- Frontend: Vite __APP_VERSION__ from env or package.json
- Backend: APP_VERSION env var exposed via /health endpoint
- Docker: build arg propagated through both stages
- CI: release workflow extracts version from git tag

Document branching strategy and release process in CONTRIBUTING.md.
Catch up CHANGELOG with v0.2.0 and Unreleased sections.
Sync package.json version to 0.3.0.
2026-04-02 16:08:37 +02:00
Pier-Jean Malandrino
4af6b5b231 Add chunk-to-bbox hover highlighting in Prepare mode
Extract bounding boxes from chunk doc_items provenance in the chunker,
propagate through domain/service/API layers, and render highlighted
bboxes on canvas when hovering a chunk card. Reset highlights on
mode and page changes to prevent stale visual state.
2026-04-02 14:47:31 +02:00
Pier-Jean Malandrino
102a11765a Add reusable pagination composable and PaginationBar component
Generic usePagination composable with page navigation, size control,
and auto-reset on data change. PaginationBar renders prev/next with
page size selector. Both support i18n.
2026-04-02 13:41:50 +02:00
Pier-Jean Malandrino
ebdf9ae1be Add frontend chunking feature with Prepare mode
Chunk/ChunkingOptions types, rechunkAnalysis API, store actions
(currentChunks, rechunk), ChunkPanel UI, Prepare tab in StudioPage
gated by chunking feature flag, i18n keys (FR/EN), and tests.
2026-04-02 12:33:07 +02:00
pjmalandrino
8871c9162c Migrate frontend in typescript 2026-03-21 15:58:43 +01:00
pjmalandrino
2db251182f Refacto and add cp paste btn 2026-03-21 15:03:10 +01:00
pjmalandrino
7bb4f8e298 Split the results of OCR 2026-03-20 21:45:26 +01:00