M29: polish recruiter engineering story
@@ -2647,3 +2647,28 @@ evidence yet."
|
||||
error or traceback appears in post-deployment logs.
|
||||
- Exact next action: none for the selected scope. Optional secret rotation and new n8n
|
||||
execution evidence remain intentionally deferred by the user.
|
||||
|
||||
## M29 — recruiter-facing product polish and engineering evidence (2026-08-10)
|
||||
|
||||
- Added a passwordless **90-second Highlights** entry that presents three directly
|
||||
verifiable engineering stories: transactional vehicle returns, human-controlled data
|
||||
correction and citation-bound knowledge. Rebuilt the Engineering page around domain
|
||||
ownership, commit-before-orchestration, AI evidence, architecture and honest live/demo
|
||||
integration states.
|
||||
- Return completion now carries its persisted correlation ID into a dedicated audit
|
||||
processing trace, linking the return, vehicle state, transactional outbox and delivery
|
||||
evidence. Data-quality detail pages lead with the human rule name instead of a raw
|
||||
identifier; Knowledge Hub source titles, staged progress and provider diagnostics are
|
||||
clearer without overstating RAGcore index evidence.
|
||||
- Improved recruiter entry metadata and social preview, route skeletons, 360 px touch
|
||||
targets, reduced-motion behaviour, trilingual content and deterministic guide fallbacks.
|
||||
Added automated recruiter and mobile-overflow coverage plus reproducible evidence
|
||||
screenshot tooling. Refocused README, architecture and portfolio documentation on
|
||||
inspectable product and reliability claims.
|
||||
- Validation: isolated backend **239 passed**; Ruff clean; mypy clean across 58 files;
|
||||
frontend lint/build passed; full Playwright **150 passed in 5.2 minutes**; full and
|
||||
production npm audits report zero vulnerabilities; screenshot capture **1 passed** and
|
||||
desktop/mobile output was visually inspected.
|
||||
- Exact next action: commit and push M29, create and verify a live database backup,
|
||||
deploy the committed archive to Unraid, then run live health, migration, browser and
|
||||
acceptance checks before recording the final production evidence.
|
||||
|
||||
@@ -1,136 +1,94 @@
|
||||
# Fleet Ops
|
||||
|
||||
**Connected operations for vehicle rental and service teams.**
|
||||
**A recruiter-ready operations platform for vehicle rental and service teams.**
|
||||
|
||||
Fleet Ops is a working proof of concept for a fictitious mobility company. It combines vehicle and booking operations, a controlled vehicle-return workflow, data-quality review, RAGcore-backed internal knowledge, n8n orchestration and read-only tools published through ITWorx MCP Hub.
|
||||
[Open the live demo](http://192.168.10.150:1236) · no password required · choose **Highlights in 90 seconds** for the shortest tour.
|
||||
|
||||
**Naming:** "Fleet Ops" is the product's visible name everywhere in the UI, the demo
|
||||
knowledge base, and this documentation. "MobilityOps" remains the technical
|
||||
identifier only — the repository name, local directory, package/module names, Docker
|
||||
Compose project, deployment directory, and database names. The UI is fully trilingual
|
||||
(nl-BE default, en-GB, fr-BE); see `docs/fleet-ops-correction/` for the localization
|
||||
architecture, the vehicle-status decision table, and the correction evidence, and
|
||||
`docs/fleet-ops-final-localization/` for the follow-up correction round (remaining
|
||||
NL/FR translation gaps, centralized API-error localization, the time-dependent
|
||||
Europe/Brussels dashboard greeting).
|
||||
Fleet Ops turns fragmented vehicle, booking and procedure data into one controlled operational workspace. It is a complete synthetic-data product demo: the company and records are fictional, while the workflows, persistence, validation, authorization, audit trail and integration boundaries are implemented.
|
||||
|
||||
The web application uses the premium responsive **Control Rail** interface: a compact
|
||||
operations-first workspace with persisted readiness metrics, evidence-led exceptions,
|
||||
review-before-commit return handling and mobile navigation designed down to 390 px. See
|
||||
`docs/design/design-directions.md` and `docs/design/implementation-validation.md` for the
|
||||
design decision and visual evidence.
|
||||

|
||||
|
||||
All people, companies, vehicles, bookings and documents are synthetic. The workflows, validation, integrations, audit logging and access boundaries are intended to be real.
|
||||
## The 90-second tour
|
||||
|
||||
## Demo
|
||||
1. Open **Highlights** from the login screen.
|
||||
2. Follow a vehicle return from review to atomic commit, quality issue, outbox and correlated audit trace.
|
||||
3. Compare and merge a duplicate customer with explicit human confirmation.
|
||||
4. Ask the Knowledge Hub a damage question and inspect its cited procedure evidence.
|
||||
5. Open **Engineering** for the architecture, reliability guarantees, test evidence and honest scope boundary.
|
||||
|
||||
The demo presents itself as **Northstar Mobility**, a fictitious Belgian camper/van
|
||||
rental company — the login screen, a permanent "Synthetische demo" indicator, an in-app
|
||||
guided tour (Demo Guide), a curated `/scenarios` overview, and an "Over deze demo" page
|
||||
all make the fictional context, synthetic-data status, and real-vs-simulated boundaries
|
||||
explicit without any verbal explanation. See `docs/demo-release/` for the full demo
|
||||
concept, the five named scenarios, the seed/date-anchoring strategy, the guided-tour
|
||||
design, and the operational runbook (5-minute and 10-minute demo flows, reset, redeploy,
|
||||
rollback).
|
||||
## What makes it more than a mock-up
|
||||
|
||||
## Scope
|
||||
- **Transactional operations:** a return writes the inspection, vehicle/booking state, audit events and outbox record atomically. n8n downtime never rolls back the local business transaction.
|
||||
- **Explainable data quality:** five persisted rule types, SLA deadlines, assignment, bulk queue controls and bounded resolution flows—not decorative warning cards.
|
||||
- **Grounded knowledge:** the live deployment uses RAGcore; insufficient or unavailable evidence produces no invented answer. Citations and provider provenance remain inspectable.
|
||||
- **Safe AI exposure:** four tenant-bound, service-authenticated, read-only Fleet Ops tools are published through ITWorx MCP Hub and audited with correlation IDs.
|
||||
- **Operational reliability:** bounded retries, delivery leases, health/readiness, Prometheus metrics, Grafana, scheduled verified backups and graceful external-dependency degradation.
|
||||
- **Real product ergonomics:** nl-BE, en-GB and fr-BE; responsive from 360 px; keyboard-accessible navigation; role-aware global search; route-level lazy loading; server-enforced permissions.
|
||||
|
||||
The PoC implements:
|
||||
## Architecture
|
||||
|
||||
- operations dashboard with a truthful aggregate n8n/MCP integration-status card;
|
||||
- vehicle and booking workspaces with planning windows, location filters, service/next-
|
||||
booking context, operational ordering and pagination;
|
||||
- server-backed session lifecycle (refresh-safe, central 401 handling);
|
||||
- a role matrix enforced server-side and mirrored in the UI (see
|
||||
`docs/12-security-and-audit.md`);
|
||||
- vehicle return capture → authoritative server-evaluated review → commit → result;
|
||||
- five deterministic data-quality checks with SLA deadlines, assignment, overdue/bulk
|
||||
queue controls, bounded resolution flows and a manual scan action;
|
||||
- human review and customer merge;
|
||||
- audit trail with human-readable before/after evidence and safe entity links;
|
||||
- role-aware global search across vehicles, bookings and (Operations Manager) issues;
|
||||
- safe, confirmed demo reset;
|
||||
- RAGcore-backed knowledge assistant with citations;
|
||||
- four canonical n8n workflows: return processing, scheduled data-quality scanning,
|
||||
RAGcore procedure sync and centralized workflow-error handling, with explicit
|
||||
heartbeat evidence and crash-recoverable outbox delivery leases;
|
||||
- four read-only MCP tools through ITWorx MCP Hub;
|
||||
- deterministic demo reset and five-minute showcase.
|
||||
- audited operational user creation, role/name editing, password reset and activation.
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI["React + TypeScript\nresponsive operations UI"] -->|session cookie| API["FastAPI\nbusiness rules + RBAC"]
|
||||
API --> DB[(PostgreSQL)]
|
||||
API -->|grounded retrieval| RAG[RAGcore]
|
||||
DB --> OUT["Transactional outbox"]
|
||||
OUT -->|bounded retry| N8N["Existing central n8n"]
|
||||
N8N -->|authenticated callback| API
|
||||
HUB["ITWorx MCP Hub"] -->|4 read-only tools| API
|
||||
```
|
||||
|
||||
It is not an ERP, CRM, accounting package, public booking site, payment system or autonomous agent.
|
||||
Fleet Ops owns operational truth. RAGcore owns retrieval, n8n performs post-commit orchestration, and MCP Hub owns tool transport/publication. Neither RAGcore nor MCP Hub accesses the Fleet Ops database directly. See [the as-built architecture](artifacts/evidence/architecture.md).
|
||||
|
||||
## Integration status
|
||||
## Demonstrable scope
|
||||
|
||||
- **n8n**: fully implemented and verified against a real n8n instance, including
|
||||
degraded mode (n8n stopped mid-flow → return still commits, event stays `pending`
|
||||
with backoff, self-heals once n8n returns), the failed-delivery manual-retry path,
|
||||
stale-delivery-lease recovery after a simulated crash, and a second (scheduled
|
||||
quality-scan) workflow live-verified end to end against a real n8n instance.
|
||||
`GET /api/v1/integrations/status` reports a truthful aggregate state from outbox
|
||||
delivery counts, not just the most recent event.
|
||||
- **RAGcore**: the live deployment uses `KNOWLEDGE_PROVIDER=ragcore`. Readiness and real
|
||||
grounded retrieval are verified end to end; when RAGcore's generated-answer endpoint
|
||||
is unavailable, Fleet Ops falls back only to cited extractive search results and never
|
||||
invents an answer. The deterministic demo provider remains available for clean-checkout
|
||||
acceptance and local development.
|
||||
- **ITWorx MCP Hub**: the four read-only provider endpoints are implemented, tested, and
|
||||
directly `curl`-verified with correct auth enforcement and audit logging.
|
||||
`MCP_HUB_REGISTRATION_ENABLED` is actually wired into `Settings` and reported honestly
|
||||
by the integration-status endpoint (evidence-based: real tool-call audit history, not
|
||||
just the flag). The Fleet Ops connector is confirmed live in the ITWorx MCP Hub's own
|
||||
production deployment (Tower), with a real contract fix already applied there
|
||||
(`vehicle.get`'s wire parameter normalized to `vehicleRef`).
|
||||
- dashboard, vehicle fleet, booking lifecycle and controlled returns;
|
||||
- data-quality queue, assignment, review, merge and resolution;
|
||||
- correlated human-readable audit history;
|
||||
- cited Knowledge Hub with honest provider state;
|
||||
- n8n delivery monitoring and manual retry;
|
||||
- user administration, privacy export/anonymisation and retention guards;
|
||||
- deterministic reset with 2 users, 180 customers, 50 vehicles, 254 bookings, 75 inspections, 40 maintenance records, 33 quality issues and 20 workflow runs.
|
||||
|
||||
See `artifacts/functional-completion/final-summary.md` for the functional-completion
|
||||
audit evidence (supersedes the design-validation summary below for integration status),
|
||||
and `artifacts/final-acceptance/summary.md` for the original M0–M7 acceptance evidence.
|
||||
This is deliberately not accounting, payments, a public reservation site, generic CRM, inventory, HR or an autonomous write agent.
|
||||
|
||||
## Repository map
|
||||
## Stack
|
||||
|
||||
- `CLAUDE.md` — binding implementation rules.
|
||||
- `MASTER_BUILD_PROMPT.md` — prompt to start an autonomous Claude run.
|
||||
- `PROJECT_STATE.md` — short persistent project memory.
|
||||
- `docs/` — product, architecture, UX and acceptance specification.
|
||||
- `contracts/` — OpenAPI, event and MCP contracts.
|
||||
- `knowledge/` — fictitious source documents for the MobilityOps RAGcore workspace.
|
||||
- `seed/` — deterministic synthetic dataset and generator.
|
||||
- `n8n/` — importable workflow definitions.
|
||||
- `backend/` — FastAPI/SQLAlchemy/Alembic API.
|
||||
- `frontend/` — React/TypeScript/Vite web app, including the Playwright end-to-end suite (`frontend/e2e/`).
|
||||
- `artifacts/evidence/` — final acceptance evidence (screenshots, architecture, `final-summary.md`).
|
||||
- `artifacts/design-validation/` — baseline audit, Stitch direction references and implemented responsive captures.
|
||||
- `docs/functional-completion/` — the functional-completion audit and pre-work server baseline.
|
||||
- `artifacts/functional-completion/` — functional-completion acceptance evidence.
|
||||
- `docs/demo-release/` — demo concept, scenarios, seed/date-anchoring strategy, guided
|
||||
tour, and runbook.
|
||||
- `artifacts/demo-release/` — demo-productization acceptance evidence.
|
||||
React, TypeScript, Vite, FastAPI, SQLAlchemy 2, PostgreSQL, Alembic, n8n, RAGcore, ITWorx MCP Hub, Docker Compose, Prometheus, Grafana and Playwright.
|
||||
|
||||
## Quickstart
|
||||
## Run locally
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
make demo
|
||||
```
|
||||
|
||||
This builds and starts the full stack (migrations run automatically) and loads the
|
||||
deterministic demo dataset. See `docs/17-runbook.md` for the one-time n8n workflow setup
|
||||
required for the automation demo, and the full operational runbook.
|
||||
|
||||
Endpoints:
|
||||
|
||||
- Web: `http://localhost:1228`
|
||||
- API readiness: `http://localhost:8128/health/ready` (liveness: `/health/live`)
|
||||
- n8n: `http://localhost:5678`
|
||||
- API readiness: `http://localhost:8128/health/ready`
|
||||
- Existing n8n server: configure `N8N_BASE_URL`; Fleet Ops does not create a second n8n instance.
|
||||
|
||||
All defaults are configurable via `.env` (see `.env.example`).
|
||||
The deterministic local knowledge provider supports clean-checkout acceptance without pretending to be the live RAGcore integration. Configuration is documented in `.env.example`; operations and recovery are in [docs/17-runbook.md](docs/17-runbook.md).
|
||||
|
||||
## Quality gates
|
||||
|
||||
```bash
|
||||
make test # backend: isolated PostgreSQL Compose project; never the live database
|
||||
make lint # backend: ruff + mypy (strict, zero errors)
|
||||
make e2e # frontend: complete Playwright acceptance (live stack required)
|
||||
make test # isolated PostgreSQL backend suite
|
||||
make lint # Ruff + strict mypy
|
||||
make e2e # complete Playwright browser acceptance
|
||||
cd frontend && npm run build
|
||||
```
|
||||
|
||||
Frontend build/typecheck: `cd frontend && npm run build` (`tsc -b && vite build`).
|
||||
Current verified results and production evidence are recorded in [artifacts/evidence/final-summary.md](artifacts/evidence/final-summary.md) and [PROJECT_STATE.md](PROJECT_STATE.md).
|
||||
|
||||
## Repository map
|
||||
|
||||
- `backend/` — FastAPI domain, API, migrations and tests
|
||||
- `frontend/` — React app and Playwright acceptance suite
|
||||
- `contracts/` — OpenAPI, event and MCP contracts
|
||||
- `knowledge/` — versioned fictional procedures
|
||||
- `n8n/` — importable workflow definitions for the existing server
|
||||
- `seed/` — deterministic synthetic dataset
|
||||
- `docs/` — architecture, security, UX, testing and runbooks
|
||||
- `artifacts/evidence/` — current acceptance evidence and screenshots
|
||||
|
||||
“MobilityOps” remains the repository/deployment identifier; **Fleet Ops** is the product name shown to users.
|
||||
|
||||
@@ -1,63 +1,68 @@
|
||||
# MobilityOps — as-built architecture
|
||||
# Fleet Ops — as-built architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Browser
|
||||
UI["MobilityOps Web<br/>React + TypeScript"]
|
||||
end
|
||||
UI["Fleet Ops Web\nReact + TypeScript + Vite"]
|
||||
|
||||
subgraph MobilityOps["MobilityOps (this repo)"]
|
||||
API["FastAPI backend<br/>/api/v1/*"]
|
||||
DISPATCH["Outbox dispatcher<br/>background thread"]
|
||||
subgraph CORE["Fleet Ops — this repository"]
|
||||
API["FastAPI /api/v1\nRBAC + domain rules"]
|
||||
OUT["Outbox dispatcher\nleases + bounded retry"]
|
||||
DB[(PostgreSQL)]
|
||||
OBS["Prometheus metrics\nGrafana dashboards"]
|
||||
API --> DB
|
||||
DISPATCH --> DB
|
||||
OUT --> DB
|
||||
API --> OBS
|
||||
end
|
||||
|
||||
subgraph External["External central services"]
|
||||
N8N["n8n<br/>return-processing workflow"]
|
||||
RAGDEMO["Demo KnowledgeProvider<br/>TF-IDF extractive, local files"]
|
||||
RAGCORE["RAGcore<br/>(adapter built, no live instance)"]
|
||||
HUB["ITWorx MCP Hub<br/>(endpoints built, no live instance)"]
|
||||
subgraph EXT["Existing external platforms"]
|
||||
N8N["Central n8n\nsecondary orchestration"]
|
||||
RAG["RAGcore\ngrounded procedure retrieval"]
|
||||
HUB["ITWorx MCP Hub\ntool transport + publication"]
|
||||
end
|
||||
|
||||
UI -->|session cookie| API
|
||||
API -->|GroundedAnswer| RAGDEMO
|
||||
API -.->|configurable, unavailable-safe| RAGCORE
|
||||
DISPATCH -->|POST vehicle.returned.v1| N8N
|
||||
N8N -->|callback, X-Service-Token| API
|
||||
HUB -.->|X-Service-Token, read-only| API
|
||||
|
||||
classDef unverified stroke-dasharray: 5 5;
|
||||
class RAGCORE,HUB unverified;
|
||||
UI -->|secure session cookie| API
|
||||
API -->|tenant/workspace adapter| RAG
|
||||
OUT -->|vehicle.returned.v1| N8N
|
||||
N8N -->|service-authenticated callback| API
|
||||
HUB -->|service-authenticated read-only tools| API
|
||||
```
|
||||
|
||||
Dashed boxes/arrows are implemented and unit/contract-tested but were never exercised
|
||||
against a live instance in this environment (no reachable RAGcore or ITWorx MCP Hub).
|
||||
Solid boxes were verified end-to-end, including a real n8n instance.
|
||||
## Ownership and trust boundaries
|
||||
|
||||
## Component responsibility (unchanged from `docs/03-architecture.md`)
|
||||
| Component | Owns | Explicitly does not own |
|
||||
|---|---|---|
|
||||
| Fleet Ops | vehicles, customers, bookings, inspections, quality issues, audit, permissions, outbox state | external workflow execution or procedure retrieval |
|
||||
| RAGcore | indexing/retrieval and grounded procedure evidence | Fleet Ops database or business state |
|
||||
| ITWorx MCP Hub | MCP transport, connector publication and central tool-call audit | Fleet Ops database or write actions |
|
||||
| n8n | post-commit workflow orchestration | critical business rules or the source-of-truth transaction |
|
||||
|
||||
| Component | Owns |
|
||||
|---|---|
|
||||
| MobilityOps | vehicles, customers, bookings, inspections, data-quality issues, audit, outbox/delivery state |
|
||||
| RAGcore | procedure retrieval and grounded answers (demo provider substitutes locally) |
|
||||
| ITWorx MCP Hub | MCP transport, tool publication, central tool-call audit |
|
||||
| n8n | post-commit secondary orchestration only — never the source of truth for vehicle state |
|
||||
## End-to-end return trace
|
||||
|
||||
## Reliability boundaries verified in this build
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor Operator
|
||||
participant Web
|
||||
participant API
|
||||
participant DB
|
||||
participant n8n
|
||||
Operator->>Web: Review and confirm return
|
||||
Web->>API: POST return with idempotency key
|
||||
API->>DB: Lock booking and validate invariants
|
||||
API->>DB: Commit inspection, state, audit and outbox atomically
|
||||
API-->>Web: Result + correlation ID
|
||||
Web-->>Operator: Human result and full processing trace
|
||||
API->>n8n: Deliver persisted outbox event
|
||||
n8n->>API: Authenticated status callback
|
||||
API->>DB: Persist delivery/audit evidence
|
||||
```
|
||||
|
||||
1. **Return commits atomically with its outbox event** — `app/services/returns.py`, one
|
||||
transaction; verified by `test_concurrent_returns_only_one_succeeds` (real Postgres row
|
||||
locking, not mocked).
|
||||
2. **Outbox delivery is at-least-once, idempotent by event ID** — verified live: the n8n
|
||||
callback checks for an existing `AuditEvent` by event ID before recording a second time.
|
||||
3. **RAGcore failure disables knowledge answers only** — `RAGcoreKnowledgeProvider` degrades
|
||||
to `unavailable`; the rest of the app is unaffected because the knowledge router is the
|
||||
only consumer.
|
||||
4. **MCP Hub failure does not affect the web application** — the four MCP provider
|
||||
endpoints are a separate authenticated surface (`X-Service-Token`), invisible to the
|
||||
browser-facing API/UI.
|
||||
5. **n8n failure leaves events pending with bounded retries** — verified live: a seeded
|
||||
`failed` event, retried through the UI, was picked up by the background dispatcher and
|
||||
delivered through the real n8n instance within one poll cycle.
|
||||
## Verified reliability properties
|
||||
|
||||
1. Concurrent returns serialize through PostgreSQL row locking; only one can commit.
|
||||
2. Local return success is independent of n8n availability. Pending delivery remains persisted and retryable.
|
||||
3. Outbox delivery is at-least-once and idempotent by event ID, with crash-recoverable leases and bounded backoff.
|
||||
4. RAGcore failure affects knowledge answers only. The UI reports unavailable/insufficient evidence and does not invent an answer.
|
||||
5. MCP endpoints are a separate tenant-bound, client-identity-validated, read-only surface; every call is audited with a correlation ID.
|
||||
6. Browser authorization is enforced again on the API. Hiding a navigation item is never the security boundary.
|
||||
|
||||
The live deployment has exercised all three external boundaries. Local clean-checkout acceptance can use the deterministic extractive knowledge provider while reporting that mode honestly.
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -164,6 +164,7 @@ class RegisterReturnResult(BaseModel):
|
||||
odometer_regression: bool
|
||||
quality_issue_ref: str | None
|
||||
workflow_event_id: str
|
||||
correlation_id: str
|
||||
next_booking_risk: NextBookingRisk | None
|
||||
|
||||
|
||||
|
||||
@@ -143,14 +143,21 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
|
||||
),
|
||||
DemoIntegrationSummaryOut(
|
||||
key="ragcore",
|
||||
status_code="operational" if knowledge_health.provider == "ragcore" else "demoMode",
|
||||
detail_code="ragcoreDetail",
|
||||
status_code=(
|
||||
"operational"
|
||||
if knowledge_health.provider == "ragcore" and knowledge_health.available
|
||||
else "demoMode"
|
||||
if knowledge_health.provider == "demo" and knowledge_health.available
|
||||
else "unavailable"
|
||||
),
|
||||
detail_code=(
|
||||
"ragcoreDetail"
|
||||
if knowledge_health.document_count is not None
|
||||
else "ragcoreSourceDetail"
|
||||
),
|
||||
detail_params={
|
||||
"count": (
|
||||
knowledge_health.document_count
|
||||
if knowledge_health.document_count is not None
|
||||
else "unknown"
|
||||
),
|
||||
"count": knowledge_health.document_count or 0,
|
||||
"source_count": knowledge_health.source_document_count,
|
||||
"collection": knowledge_health.collection,
|
||||
},
|
||||
),
|
||||
@@ -162,7 +169,9 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
|
||||
# "operational" -- same evidence rule the integration status page uses.
|
||||
status_code="operational" if mcp_hub.state == "operational" else "notConnected",
|
||||
detail_code=(
|
||||
"mcpDetailEnabled" if mcp_hub.state == "operational" else "mcpDetailNotConnected"
|
||||
"mcpDetailOperational"
|
||||
if mcp_hub.state == "operational"
|
||||
else "mcpDetailPrepared"
|
||||
),
|
||||
detail_params={},
|
||||
),
|
||||
|
||||
@@ -328,6 +328,7 @@ def register_vehicle_return(
|
||||
"odometer_regression": evaluation.odometer_regression,
|
||||
"quality_issue_ref": quality_issue_ref,
|
||||
"workflow_event_id": str(event.event_id),
|
||||
"correlation_id": str(correlation_id),
|
||||
"next_booking_risk": evaluation.next_booking_risk,
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +120,8 @@ def test_preview_performs_no_writes_and_matches_commit(ops_client):
|
||||
assert commit_body["resulting_vehicle_status"] == preview_body["resulting_vehicle_status"]
|
||||
assert commit_body["odometer_regression"] == preview_body["odometer_regression"]
|
||||
assert commit_body["next_booking_risk"] == preview_body["next_booking_risk"]
|
||||
assert commit_body["correlation_id"]
|
||||
assert len(commit_body["correlation_id"]) == 36
|
||||
|
||||
|
||||
def test_preview_detects_odometer_regression(ops_client):
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
# Portfolio case-study template
|
||||
# Fleet Ops — portfolio case study
|
||||
|
||||
## Title
|
||||
## Product
|
||||
|
||||
MobilityOps — connected operations for vehicle rental and service teams
|
||||
|
||||
## One-sentence summary
|
||||
|
||||
A working synthetic-data PoC that unifies vehicle and booking operations, automates vehicle returns, surfaces data-quality problems and exposes grounded internal knowledge through reusable RAG and MCP platforms.
|
||||
Fleet Ops is a working operations platform for a fictional Belgian mobility company. It unifies vehicle and booking work, makes data defects resolvable, automates post-return follow-up and turns internal procedures into cited answers.
|
||||
|
||||
## Problem
|
||||
|
||||
Operational information was modelled as fragmented across booking, vehicle and procedure sources, causing manual checks, inconsistent states and slow access to instructions.
|
||||
Rental operations often span booking records, vehicle state, inspections and separate procedure documents. The difficult part is not another dashboard; it is preserving one trustworthy state while people, automation and AI consume the same workflow.
|
||||
|
||||
## Approach
|
||||
## Engineering approach
|
||||
|
||||
- MobilityOps owns canonical operational data and business rules.
|
||||
- A transactional return workflow persists inspections, derives status and queues post-commit orchestration.
|
||||
- Explainable rules detect duplicates, mileage regressions, overlaps, missing fields and status conflicts.
|
||||
- RAGcore provides source-grounded procedure answers.
|
||||
- ITWorx MCP Hub exposes narrowly scoped read-only AI tools.
|
||||
- n8n orchestrates secondary processing without becoming the business-rule engine.
|
||||
- Keep business invariants in a FastAPI/SQLAlchemy domain layer backed by PostgreSQL constraints and row locks.
|
||||
- Commit vehicle returns, audit evidence and an outbox message in one transaction.
|
||||
- Let the existing central n8n execute secondary work asynchronously, with leases, retries and authenticated callbacks.
|
||||
- Detect five classes of data-quality issue and require bounded, explicit human resolution.
|
||||
- Integrate RAGcore through a provider adapter that refuses unsupported answers and preserves citation provenance.
|
||||
- Publish only four narrow read-only tools through ITWorx MCP Hub; never give external AI systems database access.
|
||||
- Expose correlation-based evidence in the product so architecture claims can be inspected, not merely read in a diagram.
|
||||
|
||||
## Demonstrable results
|
||||
## Demonstrable result
|
||||
|
||||
Use only facts actually measured in the final build, such as:
|
||||
- 50 vehicles, 180 customers and 254 bookings in a deterministic synthetic dataset;
|
||||
- one complete controlled return workflow and five persisted quality-rule types;
|
||||
- 11 authoritative Dutch procedure sources with localized grounded retrieval;
|
||||
- four tenant-bound, service-authenticated read-only MCP tools;
|
||||
- trilingual responsive UI, server-backed roles, global search, privacy controls and audit;
|
||||
- degraded-mode coverage for n8n and RAGcore, production health/metrics and verified backups;
|
||||
- complete backend and browser acceptance recorded in `artifacts/evidence/final-summary.md`.
|
||||
|
||||
- 50 deterministic synthetic vehicles and 245 bookings;
|
||||
- five automated data-quality rule types;
|
||||
- one end-to-end return workflow;
|
||||
- ten versioned source procedures;
|
||||
- four read-only MCP tools;
|
||||
- tested degraded modes for RAGcore and n8n.
|
||||
## Product decisions worth discussing
|
||||
|
||||
Do not claim real customer adoption, production use or percentage time savings.
|
||||
**Why an outbox?** A return must remain committed when n8n is unavailable. Persisting the event in the same database transaction closes the gap between business success and later orchestration.
|
||||
|
||||
**Why no generative fallback?** A fluent procedure answer without evidence is worse than a clear unavailable state. Knowledge answers require cited provider evidence.
|
||||
|
||||
**Why read-only MCP tools?** Recruiter demos should show credible AI integration without granting autonomous mutation rights. Operational writes remain explicit product workflows with authorization, validation and audit.
|
||||
|
||||
**Why synthetic but production-shaped data?** The public demo can remain safely open while still exercising realistic state transitions, failure recovery and resettable scenarios.
|
||||
|
||||
## Stack
|
||||
|
||||
React, TypeScript, FastAPI, PostgreSQL, n8n, RAGcore, ITWorx MCP Hub, Docker Compose and Playwright.
|
||||
React, TypeScript, Vite, FastAPI, SQLAlchemy 2, PostgreSQL, Alembic, n8n, RAGcore, ITWorx MCP Hub, Docker Compose, Prometheus, Grafana and Playwright.
|
||||
|
||||
## Demo disclosure
|
||||
## Honest boundary
|
||||
|
||||
All data and the represented company are fictitious. The software behaviour, integration contracts, audit trail and validation are implemented as a real proof of concept.
|
||||
Fleet Ops is a completed locked PoC, not claimed customer adoption or a general ERP. Accounting, payments, public reservations, CRM, inventory, HR and autonomous write agents are intentionally out of scope. A real personal-data rollout would additionally require the adopting organisation's identity provider, retention approvals, secrets lifecycle, alert ownership and disaster-recovery process.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
const OUT = "../artifacts/evidence/screenshots";
|
||||
|
||||
test("capture current recruiter-facing evidence", async ({ page, request }) => {
|
||||
await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } });
|
||||
await request.post("/api/v1/demo/reset");
|
||||
|
||||
await page.goto("/login");
|
||||
await expect(page.getByRole("heading", { name: "Elke overdracht. Eén helder overzicht." })).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/1-login.png`, fullPage: true });
|
||||
|
||||
await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
|
||||
await expect(page).toHaveURL(/\/highlights$/);
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/2-highlights.png`, fullPage: true });
|
||||
|
||||
await page.goto("/about");
|
||||
await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" }).first()).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/3-engineering-story.png`, fullPage: true });
|
||||
|
||||
await page.goto("/dashboard");
|
||||
await expect(page.getByRole("heading", { name: "Wagenparkstatus" })).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/4-dashboard.png`, fullPage: true });
|
||||
|
||||
await page.goto("/knowledge");
|
||||
await page.getByRole("button", { name: "Wat moet ik doen wanneer een voertuig terugkomt met schade?" }).click();
|
||||
await expect(page.getByText("Onderbouwd met geciteerde procedures")).toBeVisible({ timeout: 15_000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/5-knowledge-evidence.png`, fullPage: true });
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/highlights");
|
||||
await expect(page.locator(".highlight-card").first()).toBeVisible();
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: `${OUT}/6-highlights-mobile.png` });
|
||||
});
|
||||
@@ -5,7 +5,7 @@ test.describe.configure({ mode: "serial" });
|
||||
test("demo entry screen names the fictional org and never shows a password", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await expect(page.getByText(/Northstar Mobility/)).toBeVisible();
|
||||
await expect(page.getByText(/Synthetische demo/)).toBeVisible();
|
||||
await expect(page.getByText(/Synthetische demo · geen echte klant-/)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Start begeleide demo" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Verken als Operationsmanager" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Verken als Verhuurmedewerker" })).toBeVisible();
|
||||
|
||||
@@ -27,7 +27,7 @@ test("return flow pre-fills the suspicious odometer reading and explains why", a
|
||||
await page.getByRole("button", { name: "Retour bevestigen" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Retour geregistreerd" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Automatiseringsstatus bekijken" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Auditgeschiedenis bekijken" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Volledige verwerking volgen" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("data quality issue detail explains what's wrong and why it matters", async ({ page }) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ test("five-minute demo script end to end", async ({ page, request }) => {
|
||||
|
||||
await test.step("1. login as Operations Manager", async () => {
|
||||
await page.goto("/login");
|
||||
await expect(page.getByText(/Synthetische demo/)).toBeVisible();
|
||||
await expect(page.getByText(/Synthetische demo · geen echte klant-/)).toBeVisible();
|
||||
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
});
|
||||
@@ -90,7 +90,7 @@ test("five-minute demo script end to end", async ({ page, request }) => {
|
||||
await test.step("9. verify responsive navigation at mobile width", async () => {
|
||||
await page.setViewportSize({ width: 360, height: 800 });
|
||||
await page.goto("/dashboard");
|
||||
await expect(page.getByText(/Synthetische demo/).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /Synthetische demo/ })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Overzicht" }).first()).toBeVisible();
|
||||
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
||||
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
|
||||
|
||||
@@ -107,7 +107,7 @@ test.describe("status-recommendation flow", () => {
|
||||
await resetDemoData(request);
|
||||
await loginAsOpsManager(page, "nl-BE");
|
||||
await page.goto("/data-quality/DQ-DEMO-STATUS");
|
||||
await expect(page.getByRole("heading", { name: "DQ-DEMO-STATUS" })).toBeVisible();
|
||||
await expect(page.getByText("DQ-DEMO-STATUS", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click();
|
||||
await expect(page.getByText("Aanbevolen status")).toBeVisible();
|
||||
|
||||
@@ -185,7 +185,7 @@ test("data quality issue detail: defer and reject buttons work", async ({ page,
|
||||
expect(ref).not.toBe("");
|
||||
await firstLink.click();
|
||||
await expect(page).toHaveURL(new RegExp(`/data-quality/${ref}$`));
|
||||
await expect(page.getByRole("heading", { name: ref })).toBeVisible();
|
||||
await expect(page.getByText(ref, { exact: true })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Defer" }).click();
|
||||
await expect(page.locator(".badge.status-deferred")).toBeVisible();
|
||||
});
|
||||
@@ -193,7 +193,7 @@ test("data quality issue detail: defer and reject buttons work", async ({ page,
|
||||
test("data quality: providing missing fields resolves a vehicle issue", async ({ page, request }) => {
|
||||
await resetDemoData(request);
|
||||
await page.goto("/data-quality/DQ-DEMO-ATTENTION");
|
||||
await expect(page.getByRole("heading", { name: "DQ-DEMO-ATTENTION" })).toBeVisible();
|
||||
await expect(page.getByText("DQ-DEMO-ATTENTION", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByLabel("Registration number").fill("TST-777");
|
||||
await page.getByLabel("Make").fill("TestMake");
|
||||
@@ -207,7 +207,7 @@ test("data quality: providing missing fields resolves a vehicle issue", async ({
|
||||
test("data quality: resolving a booking overlap blocks one booking", async ({ page, request }) => {
|
||||
await resetDemoData(request);
|
||||
await page.goto("/data-quality/DQ-DEMO-OVERLAP");
|
||||
await expect(page.getByRole("heading", { name: "DQ-DEMO-OVERLAP" })).toBeVisible();
|
||||
await expect(page.getByText("DQ-DEMO-OVERLAP", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole("radio", { name: /Block BK-DEMO-OVERLAP-A/ }).check();
|
||||
await page.getByRole("button", { name: /^Block BK-DEMO-OVERLAP-A$/ }).click();
|
||||
@@ -227,7 +227,7 @@ test("data quality: applying the recommended status resolves a vehicle conflict"
|
||||
}) => {
|
||||
await resetDemoData(request);
|
||||
await page.goto("/data-quality/DQ-DEMO-STATUS");
|
||||
await expect(page.getByRole("heading", { name: "DQ-DEMO-STATUS" })).toBeVisible();
|
||||
await expect(page.getByText("DQ-DEMO-STATUS", { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Review recommendation" }).click();
|
||||
await page.getByRole("button", { name: /^Change status to/ }).click();
|
||||
@@ -248,7 +248,7 @@ test("data quality: retaining canonical resolves an odometer regression issue",
|
||||
const target = issues[0];
|
||||
|
||||
await page.goto(`/data-quality/${target.public_ref}`);
|
||||
await expect(page.getByRole("heading", { name: target.public_ref })).toBeVisible();
|
||||
await expect(page.getByText(target.public_ref, { exact: true })).toBeVisible();
|
||||
await page.getByRole("radio", { name: /Retain canonical/ }).check();
|
||||
await page.getByRole("button", { name: "Resolve issue" }).click();
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test("90-second recruiter entry exposes three verifiable engineering highlights", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/highlights$/);
|
||||
await expect(page.getByRole("heading", { name: "De sterkste keuzes in 90 seconden" })).toBeVisible();
|
||||
await expect(page.locator(".highlight-card")).toHaveCount(3);
|
||||
await expect(page.getByText("Transactie · idempotency · outbox · degradatie")).toBeVisible();
|
||||
await expect(page.getByText("RAGcore · citations · veilige degradatie")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Engineering Story exposes architecture, reliability and honest integration evidence", async ({ page }) => {
|
||||
await page.goto("/login");
|
||||
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
|
||||
await page.goto("/about");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" }).first()).toBeVisible();
|
||||
await expect(page.getByText("Commit eerst, orkestreer daarna")).toBeVisible();
|
||||
await expect(page.getByText("AI moet bewijs tonen")).toBeVisible();
|
||||
await expect(page.getByText("PostgreSQL + audittrail")).toBeVisible();
|
||||
await expect(page.getByText(/nog niet live gekoppeld/)).toHaveCount(0);
|
||||
await expect(page.getByText(/unknown procedures/)).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("mobile recruiter surfaces remain readable without horizontal overflow", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/login");
|
||||
await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
|
||||
await expect(page).toHaveURL(/\/highlights$/);
|
||||
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
expect(overflow).toBeLessThanOrEqual(1);
|
||||
await expect(page.locator(".highlight-card").first()).toBeVisible();
|
||||
});
|
||||
@@ -3,9 +3,18 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Fleet Ops synthetic-data operations demo" />
|
||||
<meta name="description" content="Fleet Ops is a production-minded vehicle operations demo with reliable workflows, explainable data quality and source-grounded AI." />
|
||||
<meta name="theme-color" content="#0f172a" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Fleet Ops — connected vehicle operations" />
|
||||
<meta property="og:description" content="Explore reliable vehicle returns, controlled data-quality resolution and source-grounded operational knowledge." />
|
||||
<meta property="og:image" content="/og-fleet-ops.svg" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Fleet Ops — connected vehicle operations" />
|
||||
<meta name="twitter:description" content="A working synthetic-data platform for reliable mobility operations." />
|
||||
<meta name="twitter:image" content="/og-fleet-ops.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>Fleet Ops</title>
|
||||
<title>Fleet Ops — connected vehicle operations</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
// _capture-screenshots.spec.ts is evidence-generation tooling, not a regression test —
|
||||
// it mutates demo data via a reset and is run explicitly, not as part of the suite.
|
||||
testIgnore: "**/_*.spec.ts",
|
||||
testIgnore: process.env.CAPTURE_EVIDENCE === "1" ? undefined : "**/_*.spec.ts",
|
||||
timeout: 30_000,
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Fleet Ops</title>
|
||||
<desc id="desc">Connected vehicle operations with reliable workflows, explainable data quality and grounded knowledge.</desc>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0f172a"/><stop offset="1" stop-color="#17394d"/></linearGradient>
|
||||
<radialGradient id="glow"><stop stop-color="#2dd4bf" stop-opacity=".34"/><stop offset="1" stop-color="#2dd4bf" stop-opacity="0"/></radialGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="630" fill="url(#bg)"/>
|
||||
<circle cx="1000" cy="92" r="360" fill="url(#glow)"/>
|
||||
<g transform="translate(78 72)">
|
||||
<path d="M42 72C16 42 7 24 7 8a35 35 0 1 1 70 0c0 16-9 34-35 64Z" fill="#fff" transform="translate(0 6) scale(.72)"/>
|
||||
<circle cx="30" cy="28" r="6" fill="#2dd4bf"/>
|
||||
<rect x="0" y="58" width="62" height="6" rx="3" fill="#2dd4bf"/>
|
||||
<text x="84" y="51" fill="#fff" font-family="Inter,Segoe UI,sans-serif" font-size="36" font-weight="750">Fleet Ops</text>
|
||||
</g>
|
||||
<text x="78" y="265" fill="#5eead4" font-family="Inter,Segoe UI,sans-serif" font-size="24" font-weight="750" letter-spacing="3">CONNECTED VEHICLE OPERATIONS</text>
|
||||
<text x="78" y="345" fill="#fff" font-family="Inter,Segoe UI,sans-serif" font-size="60" font-weight="760">Every hand-off.</text>
|
||||
<text x="78" y="413" fill="#fff" font-family="Inter,Segoe UI,sans-serif" font-size="60" font-weight="760">One clear view.</text>
|
||||
<g transform="translate(78 500)" font-family="Inter,Segoe UI,sans-serif" font-size="22" font-weight="650">
|
||||
<rect width="272" height="54" rx="27" fill="#fff" fill-opacity=".09" stroke="#fff" stroke-opacity=".16"/><text x="27" y="35" fill="#dbeafe">Reliable workflows</text>
|
||||
<rect x="292" width="288" height="54" rx="27" fill="#fff" fill-opacity=".09" stroke="#fff" stroke-opacity=".16"/><text x="319" y="35" fill="#dbeafe">Explainable quality</text>
|
||||
<rect x="600" width="290" height="54" rx="27" fill="#fff" fill-opacity=".09" stroke="#fff" stroke-opacity=".16"/><text x="627" y="35" fill="#dbeafe">Grounded knowledge</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -21,9 +21,10 @@ const AboutDemo = lazy(() => import("./pages/AboutDemo").then((module) => ({ def
|
||||
const Scenarios = lazy(() => import("./pages/Scenarios").then((module) => ({ default: module.Scenarios })));
|
||||
const Users = lazy(() => import("./pages/Users").then((module) => ({ default: module.Users })));
|
||||
const Privacy = lazy(() => import("./pages/Privacy").then((module) => ({ default: module.Privacy })));
|
||||
const Highlights = lazy(() => import("./pages/Highlights").then((module) => ({ default: module.Highlights })));
|
||||
|
||||
function deferredPage(element: ReactNode) {
|
||||
return <Suspense fallback={<div className="route-loading" role="status"><span className="spinner" /></div>}>{element}</Suspense>;
|
||||
return <Suspense fallback={<div className="route-loading" role="status" aria-label="Loading"><div className="route-skeleton"><span /><span /><span /></div></div>}>{element}</Suspense>;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
@@ -55,6 +56,7 @@ export function App() {
|
||||
<Route path="/privacy" element={deferredPage(<Privacy />)} />
|
||||
<Route path="/about" element={deferredPage(<AboutDemo />)} />
|
||||
<Route path="/scenarios" element={deferredPage(<Scenarios />)} />
|
||||
<Route path="/highlights" element={deferredPage(<Highlights />)} />
|
||||
</Route>
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
||||
|
||||
@@ -224,6 +224,7 @@ export interface RegisterReturnResult {
|
||||
odometer_regression: boolean;
|
||||
quality_issue_ref: string | null;
|
||||
workflow_event_id: string;
|
||||
correlation_id: string;
|
||||
next_booking_risk: NextBookingRisk | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ const NAV_GROUPS: Array<{ labelKey: string; items: NavItem[] }> = [
|
||||
{ to: "/audit", labelKey: "items.audit", icon: "audit", roles: ["operations_manager"] },
|
||||
{ to: "/users", labelKey: "items.users", icon: "activity", roles: ["operations_manager"] },
|
||||
{ to: "/privacy", labelKey: "items.privacy", icon: "shield", roles: ["operations_manager"] },
|
||||
{ to: "/about", labelKey: "items.engineering", icon: "spark" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ReturnResultPanel({ result }: { result: RegisterReturnResult })
|
||||
<div className="result-links">
|
||||
<Link className="button button-secondary" to={`/vehicles/${result.vehicle_ref}`}>{t("result.viewVehicle", { ref: result.vehicle_ref })}<Icon name="chevron" /></Link>
|
||||
<Link className="button button-secondary" to="/automation">{t("result.viewAutomation")}<Icon name="chevron" /></Link>
|
||||
<Link className="button button-secondary" to="/audit">{t("result.viewAudit")}<Icon name="chevron" /></Link>
|
||||
<Link className="button button-secondary" to={`/audit?correlation_id=${result.correlation_id}`}>{t("result.viewTrace")}<Icon name="chevron" /></Link>
|
||||
{guideOpen && (
|
||||
<button type="button" className="button button-primary" onClick={continueDemo}>
|
||||
{t("result.continueDemo")} <Icon name="chevron" />
|
||||
|
||||
@@ -17,20 +17,20 @@ export const DEMO_GUIDE_STEPS: DemoGuideStep[] = [
|
||||
{
|
||||
id: "open-booking",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings",
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings/BK-DEMO-RETURN",
|
||||
target: ".record-surface",
|
||||
},
|
||||
{
|
||||
id: "process-return",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings",
|
||||
manifest?.scenarios.find((s) => s.id === "return-anomaly")?.start_path ?? "/bookings/BK-DEMO-RETURN",
|
||||
target: "#return-form-heading",
|
||||
},
|
||||
{ id: "handle-quality-issue", route: () => "/data-quality", target: ".table-shell" },
|
||||
{
|
||||
id: "merge-duplicate",
|
||||
route: (manifest) =>
|
||||
manifest?.scenarios.find((s) => s.id === "duplicate-customer")?.start_path ?? "/data-quality",
|
||||
manifest?.scenarios.find((s) => s.id === "duplicate-customer")?.start_path ?? "/data-quality/DQ-DEMO-DUPLICATE",
|
||||
target: "#compare-heading",
|
||||
},
|
||||
{ id: "ask-knowledge", route: () => "/knowledge", target: "#ask-heading" },
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
"survivor": "Retained profile",
|
||||
"loser": "Merged profile"
|
||||
},
|
||||
"trace": {
|
||||
"eyebrow": "End-to-end trace",
|
||||
"title": "From return to automation",
|
||||
"description": "This view connects events sharing the same correlation ID into one verifiable operation.",
|
||||
"steps": {
|
||||
"return": { "title": "Return persisted", "complete": "Booking and inspection were processed atomically.", "pending": "Not processed yet." },
|
||||
"vehicle": { "title": "Fleet state derived", "complete": "The server persisted the resulting vehicle state.", "pending": "No state change found yet." },
|
||||
"outbox": { "title": "Automation queued", "complete": "The outbox job was created in the same transaction.", "pending": "No outbox job found." },
|
||||
"delivery": { "title": "External follow-up", "complete": "n8n confirmed the follow-up.", "pending": "Delivery is asynchronous and not yet confirmed in this audit trace." }
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"demo_login": "Logged in",
|
||||
"demo_logout": "Logged out",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"accessHeading": "Choose how to start",
|
||||
"accessIntro": "No password needed. Each role opens a scoped synthetic environment — all workflows and controls are really implemented.",
|
||||
"startGuidedDemo": "Start guided demo",
|
||||
"startRecruiterTour": "See the highlights in 90 seconds",
|
||||
"exploreAsOperationsManager": "Explore as Operations Manager",
|
||||
"exploreAsOperationsManagerDetail": "Full overview, quality resolution and retries",
|
||||
"exploreAsRentalEmployee": "Explore as Rental Employee",
|
||||
|
||||
@@ -87,6 +87,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"highlights": {
|
||||
"eyebrow": "Recruiter highlights",
|
||||
"title": "The strongest choices in 90 seconds",
|
||||
"description": "Three short views show how {{productName}} combines operational reliability, controlled corrections and source-grounded AI.",
|
||||
"duration": "about 30 sec",
|
||||
"open": "View in the platform",
|
||||
"proofLabel": "Engineering proof",
|
||||
"return": { "title": "A return that cannot fail silently", "body": "See server-side impact validation, atomic persistence and an outbox that decouples external automation from the rental desk.", "proof": "Transaction · idempotency · outbox · degradation" },
|
||||
"quality": { "title": "Data quality with human control", "body": "Compare duplicate profiles, deliberately choose the surviving record and preserve booking and audit history.", "proof": "Explainable rules · preview · audit · rollback" },
|
||||
"knowledge": { "title": "AI that must show its sources", "body": "Ask a procedure question and inspect the answer, source excerpts and honest insufficient-evidence state.", "proof": "RAGcore · citations · safe degradation" },
|
||||
"storyTitle": "Want to see the choices behind the system?",
|
||||
"storyBody": "The Engineering page connects product flows to architecture, reliability and honest integration status.",
|
||||
"storyButton": "Open Engineering Story",
|
||||
"fullTourButton": "Start the complete demo"
|
||||
},
|
||||
"scenarios": {
|
||||
"eyebrow": "Demonstration scenarios",
|
||||
"title": "Try a demonstration scenario",
|
||||
@@ -149,9 +164,29 @@
|
||||
"title": "What {{productName}} is and isn't",
|
||||
"description": "{{orgName}} is a fictional rental organisation that makes this demo tangible — not a real company.",
|
||||
"loading": "Loading demo information…",
|
||||
"ctaTitle": "Prefer to jump right in?",
|
||||
"ctaBody": "The guided demo walks through all eight steps above in practice.",
|
||||
"ctaButton": "Start guided demo",
|
||||
"ctaTitle": "Choose how much time you have",
|
||||
"ctaBody": "See the three strongest engineering moments in 90 seconds, or take the complete operational tour.",
|
||||
"ctaButton": "Start full demo",
|
||||
"quickTourButton": "See highlights · 90 sec",
|
||||
"scenarioButton": "Open scenario lab",
|
||||
"proofEyebrow": "Engineering evidence",
|
||||
"proofTitle": "Control, reliability and explainability",
|
||||
"proofDescription": "Every technical choice supports a concrete operational action.",
|
||||
"proofRulesTitle": "Rules stay with the domain",
|
||||
"proofRulesBody": "FastAPI governs returns, statuses and data-quality resolutions. n8n and prompts never make authoritative decisions.",
|
||||
"proofReliabilityTitle": "Commit first, orchestrate second",
|
||||
"proofReliabilityBody": "The operation commits atomically; an outbox then delivers to n8n with bounded retries. An outage never blocks the rental desk.",
|
||||
"proofAiTitle": "AI must show evidence",
|
||||
"proofAiBody": "RAGcore answers include sources. Without sufficient evidence the assistant does not invent an answer.",
|
||||
"architectureFlowTitle": "From user action to controlled follow-up",
|
||||
"architectureFlowDescription": "The system of record stays separate from external orchestration and AI.",
|
||||
"architectureFrontend": "Accessible React interface",
|
||||
"architectureApi": "FastAPI domain rules",
|
||||
"architectureDatabase": "PostgreSQL + audit trail",
|
||||
"architectureOutbox": "Reliable outbox",
|
||||
"architectureExternal": "External services: n8n, RAGcore and MCP Hub",
|
||||
"verificationTitle": "Built to be verified",
|
||||
"verificationBody": "Domain rules, API contracts, degraded modes and the complete demo are tested automatically. The repository contains the exact acceptance commands and evidence bundle.",
|
||||
"problemTitle": "The fictional problem",
|
||||
"problemBody": "{{orgName}} rents around 50 campers and vans from one main location. Bookings, returns, customer records and maintenance used to live in separate spreadsheets and verbal hand-offs, so problems (duplicate customers, incorrect odometer readings, double-booked vehicles) only surfaced late. {{productName}} shows how one connected system flags these problems early and lets them be resolved under control.",
|
||||
"scopeTitle": "Who it's for and its scope",
|
||||
@@ -172,7 +207,7 @@
|
||||
"resetBodyManager": "The environment can be reset to its starting state at any time. Last reset: <strong>{{when}}</strong>. Use <strong>Reset demo data</strong> in the sidebar to start over.",
|
||||
"resetBodyEmployee": "The environment can be reset to its starting state at any time. Last reset: <strong>{{when}}</strong>. An Operations Manager can reset the demo environment via the sidebar.",
|
||||
"limitationsTitle": "Limitations",
|
||||
"limitationsBody": "This is a focused proof of concept, not a full ERP. RAGcore and the ITWorx MCP Hub are not yet live-connected; the knowledge assistant uses a local, scoped demo knowledge base instead of a live RAGcore environment.",
|
||||
"limitationsBody": "This is a focused operational platform, not a full ERP. Accounting, payments, public reservations and a generic CRM are deliberately out of scope. External integrations are assessed separately and honestly using recent evidence.",
|
||||
"unknown": "unknown"
|
||||
},
|
||||
"integrationSummary": {
|
||||
@@ -183,7 +218,8 @@
|
||||
},
|
||||
"n8nDetail": "{{succeeded}} succeeded · {{failed}} failed · {{pending}} pending.",
|
||||
"ragcoreDetail": "{{count}} procedures indexed in {{collection}}.",
|
||||
"mcpDetailEnabled": "Prepared for future, controlled tool calls from the Hub.",
|
||||
"mcpDetailNotConnected": "Not yet connected — prepared for future controlled tool calls from the Hub."
|
||||
"ragcoreSourceDetail": "{{source_count}} managed source procedures available; RAGcore does not report a reliable index count.",
|
||||
"mcpDetailOperational": "Live controlled tool calls from the Hub are confirmed in the audit trail.",
|
||||
"mcpDetailPrepared": "Four read-only tools are contract-ready; no recent Hub call has been recorded."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"statusUnavailable": "Unavailable",
|
||||
"proceduresIndexed": "{{count}} procedures indexed",
|
||||
"proceduresIndexUnknown": "Index size is not available through this connection",
|
||||
"proceduresAvailable": "{{count}} managed source procedures available",
|
||||
"technicalDetails": "Technical provider details",
|
||||
"progress": { "retrieving": "Selecting sources", "checking": "Checking evidence", "composing": "Building answer" },
|
||||
"sourceTitles": {
|
||||
"damage-procedure": "Vehicle return damage",
|
||||
"vehicle-availability-procedure": "Restoring vehicle availability",
|
||||
"vehicle-return-procedure": "Registering a vehicle return",
|
||||
"odometer-anomaly-procedure": "Reviewing an odometer anomaly",
|
||||
"checkout-procedure": "Required checkout controls"
|
||||
},
|
||||
"statistics": {
|
||||
"sourceDocuments": "source documents in this language",
|
||||
"reportedSynced": "sync reported by n8n",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"integrations": "Integrations",
|
||||
"audit": "Audit trail",
|
||||
"users": "Users",
|
||||
"privacy": "Privacy"
|
||||
"privacy": "Privacy",
|
||||
"engineering": "Engineering"
|
||||
},
|
||||
"primaryNavLabel": "Primary navigation",
|
||||
"mobileNavLabel": "Mobile navigation",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"showMatchingFields": "Also show matching fields",
|
||||
"hideMatchingFields": "Hide matching fields",
|
||||
"previewTitle": "Preview of the merged record ({{ref}})",
|
||||
"mergePreview": "{{loser}} will become a tombstone linked to {{survivor}}; its bookings will be rewired.",
|
||||
"mergePreview": "{{loser}} will be safely archived and linked to {{survivor}}; its booking history moves with it.",
|
||||
"mergeInto": "Merge into {{ref}}",
|
||||
"confirmMergeTitle": "Confirm merge",
|
||||
"confirmMergeBody": "Merge {{loser}} into {{survivor}}? This cannot be undone.",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"viewVehicle": "View vehicle {{ref}}",
|
||||
"viewAutomation": "View automation status",
|
||||
"viewAudit": "View audit trail",
|
||||
"viewTrace": "Trace the complete operation",
|
||||
"continueDemo": "Continue the demo"
|
||||
},
|
||||
"scenario": {
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
"survivor": "Profil conservé",
|
||||
"loser": "Profil fusionné"
|
||||
},
|
||||
"trace": {
|
||||
"eyebrow": "Trace de bout en bout",
|
||||
"title": "Du retour à l'automatisation",
|
||||
"description": "Cette vue relie les événements partageant le même identifiant de corrélation en une opération vérifiable.",
|
||||
"steps": {
|
||||
"return": { "title": "Retour enregistré", "complete": "La réservation et l'inspection ont été traitées atomiquement.", "pending": "Pas encore traité." },
|
||||
"vehicle": { "title": "État de flotte dérivé", "complete": "Le serveur a enregistré l'état résultant du véhicule.", "pending": "Aucun changement d'état trouvé." },
|
||||
"outbox": { "title": "Automatisation planifiée", "complete": "La tâche outbox a été créée dans la même transaction.", "pending": "Aucune tâche outbox trouvée." },
|
||||
"delivery": { "title": "Suivi externe", "complete": "n8n a confirmé le suivi.", "pending": "La livraison est asynchrone et n'est pas encore confirmée dans cette trace." }
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"demo_login": "Connecté",
|
||||
"demo_logout": "Déconnecté",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"accessHeading": "Choisissez comment démarrer",
|
||||
"accessIntro": "Aucun mot de passe requis. Chaque rôle ouvre un environnement synthétique délimité — tous les workflows et contrôles sont réellement implémentés.",
|
||||
"startGuidedDemo": "Démarrer la démo guidée",
|
||||
"startRecruiterTour": "Voir les points forts en 90 secondes",
|
||||
"exploreAsOperationsManager": "Explorer en tant que Responsable des opérations",
|
||||
"exploreAsOperationsManagerDetail": "Vue d'ensemble complète, résolution qualité et nouvelles tentatives",
|
||||
"exploreAsRentalEmployee": "Explorer en tant que Collaborateur de location",
|
||||
|
||||
@@ -87,6 +87,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"highlights": {
|
||||
"eyebrow": "Points forts pour recruteurs",
|
||||
"title": "Les choix les plus forts en 90 secondes",
|
||||
"description": "Trois vues courtes montrent comment {{productName}} combine fiabilité opérationnelle, corrections contrôlées et IA fondée sur des sources.",
|
||||
"duration": "± 30 s",
|
||||
"open": "Voir dans la plateforme",
|
||||
"proofLabel": "Preuve technique",
|
||||
"return": { "title": "Un retour qui ne peut pas échouer silencieusement", "body": "Découvrez la validation d'impact côté serveur, la persistance atomique et une outbox qui découple l'automatisation externe du comptoir.", "proof": "Transaction · idempotence · outbox · dégradation" },
|
||||
"quality": { "title": "Qualité des données sous contrôle humain", "body": "Comparez les profils en double, choisissez délibérément la fiche survivante et conservez l'historique des réservations et de l'audit.", "proof": "Règles explicables · aperçu · audit · rollback" },
|
||||
"knowledge": { "title": "Une IA qui doit montrer ses sources", "body": "Posez une question de procédure et inspectez la réponse, les extraits sources et l'état honnête de preuves insuffisantes.", "proof": "RAGcore · citations · dégradation sûre" },
|
||||
"storyTitle": "Voir les choix derrière le système ?",
|
||||
"storyBody": "La page Ingénierie relie les flux produit à l'architecture, la fiabilité et l'état honnête des intégrations.",
|
||||
"storyButton": "Ouvrir l'histoire d'ingénierie",
|
||||
"fullTourButton": "Démarrer la démo complète"
|
||||
},
|
||||
"scenarios": {
|
||||
"eyebrow": "Scénarios de démonstration",
|
||||
"title": "Essayer un scénario de démonstration",
|
||||
@@ -149,9 +164,29 @@
|
||||
"title": "Ce que {{productName}} est et n'est pas",
|
||||
"description": "{{orgName}} est une organisation de location fictive qui rend cette démo concrète — pas une véritable entreprise.",
|
||||
"loading": "Chargement des informations de démo…",
|
||||
"ctaTitle": "Vous préférez vous lancer directement ?",
|
||||
"ctaBody": "La démo guidée parcourt les huit étapes ci-dessus en pratique.",
|
||||
"ctaButton": "Démarrer la démo guidée",
|
||||
"ctaTitle": "Choisissez le temps dont vous disposez",
|
||||
"ctaBody": "Découvrez les trois moments d'ingénierie les plus forts en 90 secondes, ou suivez la démo opérationnelle complète.",
|
||||
"ctaButton": "Démarrer la démo complète",
|
||||
"quickTourButton": "Voir les points forts · 90 s",
|
||||
"scenarioButton": "Ouvrir le laboratoire de scénarios",
|
||||
"proofEyebrow": "Preuves d'ingénierie",
|
||||
"proofTitle": "Contrôle, fiabilité et explicabilité",
|
||||
"proofDescription": "Chaque choix technique soutient une action opérationnelle concrète.",
|
||||
"proofRulesTitle": "Les règles restent dans le domaine",
|
||||
"proofRulesBody": "FastAPI régit les retours, les statuts et les résolutions de qualité. n8n et les prompts ne prennent jamais de décisions faisant autorité.",
|
||||
"proofReliabilityTitle": "Valider d'abord, orchestrer ensuite",
|
||||
"proofReliabilityBody": "L'opération est validée atomiquement ; une outbox livre ensuite à n8n avec des tentatives limitées. Une panne ne bloque jamais le comptoir.",
|
||||
"proofAiTitle": "L'IA doit montrer ses preuves",
|
||||
"proofAiBody": "Les réponses RAGcore incluent des sources. Sans preuves suffisantes, l'assistant n'invente pas de réponse.",
|
||||
"architectureFlowTitle": "De l'action utilisateur au suivi contrôlé",
|
||||
"architectureFlowDescription": "Le système de référence reste séparé de l'orchestration externe et de l'IA.",
|
||||
"architectureFrontend": "Interface React accessible",
|
||||
"architectureApi": "Règles métier FastAPI",
|
||||
"architectureDatabase": "PostgreSQL + piste d'audit",
|
||||
"architectureOutbox": "Outbox fiable",
|
||||
"architectureExternal": "Services externes : n8n, RAGcore et MCP Hub",
|
||||
"verificationTitle": "Construit pour être vérifiable",
|
||||
"verificationBody": "Les règles métier, contrats d'API, modes dégradés et la démo complète sont testés automatiquement. Le dépôt contient les commandes d'acceptation exactes et le dossier de preuves.",
|
||||
"problemTitle": "Le problème fictif",
|
||||
"problemBody": "{{orgName}} loue environ 50 campings-cars et fourgonnettes depuis un site principal. Les réservations, retours, fiches clients et l'entretien vivaient jusqu'ici dans des feuilles de calcul séparées et des échanges verbaux, si bien que les problèmes (clients en double, kilométrages incorrects, véhicules réservés en double) n'apparaissaient que tardivement. {{productName}} montre comment un système unique et connecté signale ces problèmes tôt et permet de les résoudre de façon contrôlée.",
|
||||
"scopeTitle": "Pour qui et avec quelle portée",
|
||||
@@ -172,7 +207,7 @@
|
||||
"resetBodyManager": "L'environnement peut être réinitialisé à son état de départ à tout moment. Dernière réinitialisation : <strong>{{when}}</strong>. Utilisez <strong>Réinitialiser les données de démo</strong> dans la barre latérale pour recommencer.",
|
||||
"resetBodyEmployee": "L'environnement peut être réinitialisé à son état de départ à tout moment. Dernière réinitialisation : <strong>{{when}}</strong>. Un Responsable des opérations peut réinitialiser l'environnement de démo via la barre latérale.",
|
||||
"limitationsTitle": "Limitations",
|
||||
"limitationsBody": "Ceci est une preuve de concept ciblée, pas un ERP complet. RAGcore et l'ITWorx MCP Hub ne sont pas encore connectés en direct ; l'assistant de connaissances utilise une base de connaissances de démo locale et délimitée au lieu d'un environnement RAGcore en direct.",
|
||||
"limitationsBody": "Il s'agit d'une plateforme opérationnelle ciblée, pas d'un ERP complet. La comptabilité, les paiements, les réservations publiques et un CRM générique sont délibérément hors périmètre. Les intégrations externes sont évaluées séparément et honnêtement à partir de preuves récentes.",
|
||||
"unknown": "inconnu"
|
||||
},
|
||||
"integrationSummary": {
|
||||
@@ -183,7 +218,8 @@
|
||||
},
|
||||
"n8nDetail": "{{succeeded}} réussi(s) · {{failed}} échoué(s) · {{pending}} en attente.",
|
||||
"ragcoreDetail": "{{count}} procédures indexées dans {{collection}}.",
|
||||
"mcpDetailEnabled": "Préparé pour de futurs appels d'outils contrôlés depuis le Hub.",
|
||||
"mcpDetailNotConnected": "Pas encore connecté — préparé pour de futurs appels d'outils contrôlés depuis le Hub."
|
||||
"ragcoreSourceDetail": "{{source_count}} procédures sources gérées sont disponibles ; RAGcore ne fournit pas de comptage d'index fiable.",
|
||||
"mcpDetailOperational": "Des appels d'outils contrôlés en direct depuis le Hub sont confirmés dans la piste d'audit.",
|
||||
"mcpDetailPrepared": "Quatre outils en lecture seule sont prêts contractuellement ; aucun appel récent du Hub n'a été enregistré."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,17 @@
|
||||
"statusAvailable": "Disponible",
|
||||
"statusUnavailable": "Indisponible",
|
||||
"proceduresIndexed": "{{count}} procédures indexées",
|
||||
"proceduresIndexUnknown": "La taille de l’index n’est pas disponible via cette connexion",
|
||||
"proceduresIndexUnknown": "La taille de l'index n'est pas disponible via cette connexion",
|
||||
"proceduresAvailable": "{{count}} procédures sources gérées disponibles",
|
||||
"technicalDetails": "Détails techniques du fournisseur",
|
||||
"progress": { "retrieving": "Sélection des sources", "checking": "Vérification des preuves", "composing": "Construction de la réponse" },
|
||||
"sourceTitles": {
|
||||
"damage-procedure": "Dommages au retour du véhicule",
|
||||
"vehicle-availability-procedure": "Rétablir la disponibilité du véhicule",
|
||||
"vehicle-return-procedure": "Enregistrer un retour de véhicule",
|
||||
"odometer-anomaly-procedure": "Examiner une anomalie kilométrique",
|
||||
"checkout-procedure": "Contrôles obligatoires au départ"
|
||||
},
|
||||
"statistics": {
|
||||
"sourceDocuments": "documents sources dans cette langue",
|
||||
"reportedSynced": "synchronisation signalée par n8n",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"integrations": "Intégrations",
|
||||
"audit": "Piste d'audit",
|
||||
"users": "Utilisateurs",
|
||||
"privacy": "Confidentialité"
|
||||
"privacy": "Confidentialité",
|
||||
"engineering": "Ingénierie"
|
||||
},
|
||||
"primaryNavLabel": "Navigation principale",
|
||||
"mobileNavLabel": "Navigation mobile",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"showMatchingFields": "Afficher aussi les champs identiques",
|
||||
"hideMatchingFields": "Masquer les champs identiques",
|
||||
"previewTitle": "Aperçu de la fiche fusionnée ({{ref}})",
|
||||
"mergePreview": "{{loser}} deviendra une fiche archivée liée à {{survivor}} ; ses réservations seront transférées.",
|
||||
"mergePreview": "{{loser}} sera archivé en toute sécurité et lié à {{survivor}} ; son historique de réservations sera transféré.",
|
||||
"mergeInto": "Fusionner avec {{ref}}",
|
||||
"confirmMergeTitle": "Confirmer la fusion",
|
||||
"confirmMergeBody": "Fusionner {{loser}} avec {{survivor}} ? Cette action est irréversible.",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"viewVehicle": "Voir le véhicule {{ref}}",
|
||||
"viewAutomation": "Voir le statut d'automatisation",
|
||||
"viewAudit": "Voir la piste d'audit",
|
||||
"viewTrace": "Suivre l'opération complète",
|
||||
"continueDemo": "Poursuivre la démo"
|
||||
},
|
||||
"scenario": {
|
||||
|
||||
@@ -69,6 +69,17 @@
|
||||
"survivor": "Behouden profiel",
|
||||
"loser": "Samengevoegd profiel"
|
||||
},
|
||||
"trace": {
|
||||
"eyebrow": "Volledige verwerking",
|
||||
"title": "Van retour tot automatisering",
|
||||
"description": "Deze weergave koppelt de gebeurtenissen met dezelfde correlatiecode tot één controleerbare verwerking.",
|
||||
"steps": {
|
||||
"return": { "title": "Retour opgeslagen", "complete": "Boeking en inspectie zijn atomair verwerkt.", "pending": "Nog niet verwerkt." },
|
||||
"vehicle": { "title": "Wagenparkstatus afgeleid", "complete": "De server heeft de resulterende voertuigstatus vastgelegd.", "pending": "Statuswijziging nog niet gevonden." },
|
||||
"outbox": { "title": "Automatisering klaargezet", "complete": "De outbox-opdracht is in dezelfde transactie aangemaakt.", "pending": "Geen outbox-opdracht gevonden." },
|
||||
"delivery": { "title": "Externe opvolging", "complete": "n8n heeft de opvolging bevestigd.", "pending": "Aflevering is asynchroon en nog niet in deze audittrace bevestigd." }
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"demo_login": "Ingelogd",
|
||||
"demo_logout": "Uitgelogd",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"accessHeading": "Kies hoe je wil starten",
|
||||
"accessIntro": "Geen wachtwoord nodig. Elke rol opent een afgebakende, synthetische omgeving — alle workflows en controles zijn echt geïmplementeerd.",
|
||||
"startGuidedDemo": "Start begeleide demo",
|
||||
"startRecruiterTour": "Bekijk de highlights in 90 seconden",
|
||||
"exploreAsOperationsManager": "Verken als Operationsmanager",
|
||||
"exploreAsOperationsManagerDetail": "Volledig overzicht, kwaliteitsoplossing en herpogingen",
|
||||
"exploreAsRentalEmployee": "Verken als Verhuurmedewerker",
|
||||
|
||||
@@ -87,6 +87,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"highlights": {
|
||||
"eyebrow": "Highlights voor recruiters",
|
||||
"title": "De sterkste keuzes in 90 seconden",
|
||||
"description": "Drie korte inkijkjes tonen hoe {{productName}} operationele betrouwbaarheid, gecontroleerde correcties en brongebaseerde AI samenbrengt.",
|
||||
"duration": "± 30 sec",
|
||||
"open": "Bekijk in het platform",
|
||||
"proofLabel": "Technisch bewijs",
|
||||
"return": { "title": "Een retour die niet stil kan falen", "body": "Bekijk serverzijdige impactvalidatie, atomaire opslag en een outbox die externe automatisering loskoppelt van de balie.", "proof": "Transactie · idempotency · outbox · degradatie" },
|
||||
"quality": { "title": "Datakwaliteit met menselijke controle", "body": "Vergelijk dubbele profielen, kies bewust het overlevende record en behoud boekings- en auditgeschiedenis.", "proof": "Uitlegbare regels · preview · audit · rollback" },
|
||||
"knowledge": { "title": "AI die zijn bronnen moet tonen", "body": "Stel een procedurevraag en inspecteer het antwoord, de bronfragmenten en de eerlijke insufficient-evidence toestand.", "proof": "RAGcore · citations · veilige degradatie" },
|
||||
"storyTitle": "Wil je de keuzes achter het systeem zien?",
|
||||
"storyBody": "De Engineering-pagina verbindt de productflows met architectuur, betrouwbaarheid en eerlijke integratiestatus.",
|
||||
"storyButton": "Open het engineeringverhaal",
|
||||
"fullTourButton": "Start de volledige demo"
|
||||
},
|
||||
"scenarios": {
|
||||
"eyebrow": "Demonstratiescenario's",
|
||||
"title": "Probeer een demonstratiescenario",
|
||||
@@ -149,9 +164,29 @@
|
||||
"title": "Wat {{productName}} wel en niet is",
|
||||
"description": "{{orgName}} is een fictieve verhuurorganisatie die dient om deze demo tastbaar te maken — geen bestaand bedrijf.",
|
||||
"loading": "Demo-informatie laden…",
|
||||
"ctaTitle": "Liever meteen aan de slag?",
|
||||
"ctaBody": "De gegidste demo doorloopt alle acht stappen hierboven in de praktijk.",
|
||||
"ctaButton": "Start begeleide demo",
|
||||
"ctaTitle": "Kies hoeveel tijd je hebt",
|
||||
"ctaBody": "Bekijk de drie sterkste engineeringmomenten in 90 seconden, of doorloop de volledige operationele demo.",
|
||||
"ctaButton": "Start volledige demo",
|
||||
"quickTourButton": "Bekijk highlights · 90 sec",
|
||||
"scenarioButton": "Open scenariolab",
|
||||
"proofEyebrow": "Engineeringbewijs",
|
||||
"proofTitle": "Controle, betrouwbaarheid en uitlegbaarheid",
|
||||
"proofDescription": "Elke technische keuze ondersteunt een concrete operationele handeling.",
|
||||
"proofRulesTitle": "Regels blijven bij het domein",
|
||||
"proofRulesBody": "FastAPI bewaakt retouren, statussen en datakwaliteitsoplossingen. n8n en prompts nemen nooit gezaghebbende beslissingen.",
|
||||
"proofReliabilityTitle": "Commit eerst, orkestreer daarna",
|
||||
"proofReliabilityBody": "De operatie commit atomair; een outbox levert nadien met begrensde herpogingen aan n8n. Een storing blokkeert de verhuurbalie niet.",
|
||||
"proofAiTitle": "AI moet bewijs tonen",
|
||||
"proofAiBody": "RAGcore-antwoorden bevatten bronnen. Zonder voldoende evidentie geeft de assistent geen verzonnen antwoord.",
|
||||
"architectureFlowTitle": "Van gebruikersactie tot gecontroleerd vervolg",
|
||||
"architectureFlowDescription": "Het systeem van record blijft gescheiden van externe orkestratie en AI.",
|
||||
"architectureFrontend": "Toegankelijke React-interface",
|
||||
"architectureApi": "FastAPI-domeinregels",
|
||||
"architectureDatabase": "PostgreSQL + audittrail",
|
||||
"architectureOutbox": "Betrouwbare outbox",
|
||||
"architectureExternal": "Externe diensten: n8n, RAGcore en MCP Hub",
|
||||
"verificationTitle": "Verifieerbaar gebouwd",
|
||||
"verificationBody": "Domeinregels, API-contracten, degradatiemodi en de volledige demonstratie worden automatisch getest. De repository bevat de exacte acceptatiecommando's en bewijsbundel.",
|
||||
"problemTitle": "Het fictieve probleem",
|
||||
"problemBody": "{{orgName}} verhuurt zo'n 50 campers en bestelwagens vanuit één hoofdlocatie. Boekingen, retours, klantgegevens en onderhoud kwamen tot nu toe uit losse spreadsheets en mondelinge afspraken, waardoor fouten (dubbele klanten, foutieve kilometerstanden, dubbel geboekte voertuigen) laat aan het licht kwamen. {{productName}} toont hoe één samenhangend systeem die problemen vroeg signaleert en gecontroleerd laat oplossen.",
|
||||
"scopeTitle": "Voor wie en met welke scope",
|
||||
@@ -172,7 +207,7 @@
|
||||
"resetBodyManager": "De omgeving is op elk moment terug te zetten naar de startsituatie. Laatste reset: <strong>{{when}}</strong>. Gebruik <strong>Demogegevens herstellen</strong> in de zijbalk om opnieuw te beginnen.",
|
||||
"resetBodyEmployee": "De omgeving is op elk moment terug te zetten naar de startsituatie. Laatste reset: <strong>{{when}}</strong>. Een Operationsmanager kan de demo-omgeving herstellen via de zijbalk.",
|
||||
"limitationsTitle": "Beperkingen",
|
||||
"limitationsBody": "Dit is een gerichte proof of concept, geen volledig ERP. RAGcore en de ITWorx MCP Hub zijn nog niet live gekoppeld; de kennisassistent gebruikt een lokale, afgebakende demokennisbank in plaats van een live RAGcore-omgeving.",
|
||||
"limitationsBody": "Dit is een doelgericht operationeel platform, geen volledig ERP. Boekhouding, betalingen, publieke reserveringen en een generieke CRM vallen bewust buiten scope. Externe integraties worden afzonderlijk en eerlijk op recent bewijs beoordeeld.",
|
||||
"unknown": "onbekend"
|
||||
},
|
||||
"integrationSummary": {
|
||||
@@ -183,7 +218,8 @@
|
||||
},
|
||||
"n8nDetail": "{{succeeded}} geslaagd · {{failed}} mislukt · {{pending}} in wachtrij.",
|
||||
"ragcoreDetail": "{{count}} procedures geïndexeerd in {{collection}}.",
|
||||
"mcpDetailEnabled": "Voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub.",
|
||||
"mcpDetailNotConnected": "Nog niet gekoppeld — voorbereid voor toekomstige, gecontroleerde tool-aanroepen vanuit de Hub."
|
||||
"ragcoreSourceDetail": "{{source_count}} beheerde bronprocedures beschikbaar; RAGcore rapporteert geen betrouwbare indextelling.",
|
||||
"mcpDetailOperational": "Live gecontroleerde tool-aanroepen vanuit de Hub zijn in de auditgeschiedenis bevestigd.",
|
||||
"mcpDetailPrepared": "Vier read-only tools zijn contractueel voorbereid; er is nog geen recente Hub-aanroep geregistreerd."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
"statusUnavailable": "Niet beschikbaar",
|
||||
"proceduresIndexed": "{{count}} procedures geïndexeerd",
|
||||
"proceduresIndexUnknown": "Indexomvang niet beschikbaar via deze koppeling",
|
||||
"proceduresAvailable": "{{count}} beheerde bronprocedures beschikbaar",
|
||||
"technicalDetails": "Technische providerdetails",
|
||||
"progress": { "retrieving": "Bronnen selecteren", "checking": "Evidentie controleren", "composing": "Antwoord opbouwen" },
|
||||
"sourceTitles": {
|
||||
"damage-procedure": "Schade bij voertuigretour",
|
||||
"vehicle-availability-procedure": "Voertuig opnieuw beschikbaar stellen",
|
||||
"vehicle-return-procedure": "Voertuigretour registreren",
|
||||
"odometer-anomaly-procedure": "Afwijkende kilometerstand beoordelen",
|
||||
"checkout-procedure": "Verplichte vertrekcontroles"
|
||||
},
|
||||
"statistics": {
|
||||
"sourceDocuments": "brondocumenten in deze taal",
|
||||
"reportedSynced": "sync door n8n gerapporteerd",
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"integrations": "Integraties",
|
||||
"audit": "Auditgeschiedenis",
|
||||
"users": "Gebruikers",
|
||||
"privacy": "Privacy"
|
||||
"privacy": "Privacy",
|
||||
"engineering": "Techniek"
|
||||
},
|
||||
"primaryNavLabel": "Hoofdnavigatie",
|
||||
"mobileNavLabel": "Mobiele navigatie",
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"showMatchingFields": "Ook overeenkomende velden tonen",
|
||||
"hideMatchingFields": "Overeenkomende velden verbergen",
|
||||
"previewTitle": "Voorbeeld van het samengevoegde record ({{ref}})",
|
||||
"mergePreview": "{{loser}} wordt een tombstone gekoppeld aan {{survivor}}; de boekingen worden herverbonden.",
|
||||
"mergePreview": "{{loser}} wordt veilig gearchiveerd en gekoppeld aan {{survivor}}; de boekingsgeschiedenis verhuist mee.",
|
||||
"mergeInto": "Samenvoegen met {{ref}}",
|
||||
"confirmMergeTitle": "Samenvoegen bevestigen",
|
||||
"confirmMergeBody": "{{loser}} samenvoegen met {{survivor}}? Dit kan niet ongedaan gemaakt worden.",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"viewVehicle": "Voertuig {{ref}} bekijken",
|
||||
"viewAutomation": "Automatiseringsstatus bekijken",
|
||||
"viewAudit": "Auditgeschiedenis bekijken",
|
||||
"viewTrace": "Volledige verwerking volgen",
|
||||
"continueDemo": "Ga verder met de demo"
|
||||
},
|
||||
"scenario": {
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { useDemoGuide } from "../context/DemoGuideContext";
|
||||
import { useDemoManifest } from "../context/DemoManifestContext";
|
||||
import { useLocaleFormat } from "../i18n/format";
|
||||
import { Icon } from "../components/Icons";
|
||||
import { Icon, type IconName } from "../components/Icons";
|
||||
import { IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
|
||||
import { PRODUCT_NAME } from "../product";
|
||||
|
||||
const INTEGRATION_ICON: Record<string, "n8n" | "rag" | "mcp"> = {
|
||||
n8n: "n8n",
|
||||
ragcore: "rag",
|
||||
mcp_hub: "mcp",
|
||||
};
|
||||
|
||||
const N8N_STATUS_LABEL_KEY: Record<string, string> = {
|
||||
disabled: "notConnected",
|
||||
unavailable: "deliveryFailed",
|
||||
degraded: "retryAvailable",
|
||||
operational: "operational",
|
||||
no_evidence: "prepared",
|
||||
};
|
||||
const INTEGRATION_ICON: Record<string, "n8n" | "rag" | "mcp"> = { n8n: "n8n", ragcore: "rag", mcp_hub: "mcp" };
|
||||
const N8N_STATUS_LABEL_KEY: Record<string, string> = { disabled: "notConnected", unavailable: "deliveryFailed", degraded: "retryAvailable", operational: "operational", no_evidence: "prepared" };
|
||||
const PROOFS: Array<{ id: "Rules" | "Reliability" | "Ai"; icon: IconName }> = [
|
||||
{ id: "Rules", icon: "shield" },
|
||||
{ id: "Reliability", icon: "integrations" },
|
||||
{ id: "Ai", icon: "knowledge" },
|
||||
];
|
||||
|
||||
export function AboutDemo() {
|
||||
const { t } = useTranslation(["demo", "integrations"]);
|
||||
@@ -27,112 +22,56 @@ export function AboutDemo() {
|
||||
const { manifest, loading } = useDemoManifest();
|
||||
const { user } = useAuth();
|
||||
const { openGuide } = useDemoGuide();
|
||||
|
||||
function statusLabelKey(key: string, statusCode: string): string {
|
||||
if (key === "n8n") return N8N_STATUS_LABEL_KEY[statusCode] ?? statusCode;
|
||||
return statusCode;
|
||||
}
|
||||
const statusLabelKey = (key: string, statusCode: string) => key === "n8n" ? N8N_STATUS_LABEL_KEY[statusCode] ?? statusCode : statusCode;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
eyebrow={t("about.eyebrow")}
|
||||
title={t("about.title", { productName: PRODUCT_NAME })}
|
||||
description={manifest ? t("about.description", { orgName: manifest.organization_name }) : undefined}
|
||||
/>
|
||||
|
||||
<div className="page engineering-story">
|
||||
<PageHeader eyebrow={t("about.eyebrow")} title={t("about.title", { productName: PRODUCT_NAME })} description={manifest ? t("about.description", { orgName: manifest.organization_name }) : undefined} />
|
||||
{loading && <LoadingState label={t("about.loading")} />}
|
||||
|
||||
{manifest && (
|
||||
<>
|
||||
{user?.role === "operations_manager" && (
|
||||
<section className="record-surface about-card about-cta">
|
||||
<div>
|
||||
<strong>{t("about.ctaTitle")}</strong>
|
||||
<p>{t("about.ctaBody")}</p>
|
||||
</div>
|
||||
<button type="button" className="button button-primary" onClick={openGuide}>
|
||||
<Icon name="spark" /> {t("about.ctaButton")}
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.problemTitle")}</h2>
|
||||
<p>{t("about.problemBody", { orgName: manifest.organization_name, productName: PRODUCT_NAME })}</p>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.scopeTitle")}</h2>
|
||||
<p>{t("about.scopeBody", { productName: PRODUCT_NAME })}</p>
|
||||
</section>
|
||||
|
||||
<div className="about-grid">
|
||||
<div className="record-surface about-card">
|
||||
<h2>{t("about.realTitle")}</h2>
|
||||
<p>{t("about.realBody")}</p>
|
||||
</div>
|
||||
|
||||
<div className="record-surface about-card">
|
||||
<h2>{t("about.syntheticTitle")}</h2>
|
||||
<p>{t("about.syntheticBody", { testDomain: ".test" })}</p>
|
||||
</div>
|
||||
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.architectureTitle")}</summary>
|
||||
<p>{t("about.architectureBody")}</p>
|
||||
</details>
|
||||
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.securityTitle")}</summary>
|
||||
<p>{t("about.securityBody")}</p>
|
||||
</details>
|
||||
|
||||
<details className="record-surface about-card about-details">
|
||||
<summary>{t("about.testingTitle")}</summary>
|
||||
<p>{t("about.testingBody")}</p>
|
||||
</details>
|
||||
{manifest && <>
|
||||
<section className="panel engineering-hero">
|
||||
<div>
|
||||
<p className="page-eyebrow">{t("about.proofEyebrow")}</p>
|
||||
<h2>{t("about.proofTitle")}</h2>
|
||||
<p>{t("about.proofDescription")}</p>
|
||||
</div>
|
||||
<div className="engineering-hero-actions">
|
||||
<Link className="button button-primary" to="/highlights"><Icon name="spark" /> {t("about.quickTourButton")}</Link>
|
||||
{user?.role === "operations_manager" && <button type="button" className="button button-secondary" onClick={openGuide}>{t("about.ctaButton")}</button>}
|
||||
<Link className="button button-secondary" to="/scenarios">{t("about.scenarioButton")}</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-label={t("about.integrationsTitle")} className="record-surface">
|
||||
<SectionHeading title={t("about.integrationsTitle")} description={t("about.integrationsDescription")} />
|
||||
<div className="integration-cards">
|
||||
{manifest.integrations.map((integration) => (
|
||||
<article key={integration.key}>
|
||||
<IntegrationMark kind={INTEGRATION_ICON[integration.key]} />
|
||||
<div>
|
||||
<span className="integration-kicker">
|
||||
{t(`integrations:statusLabels.${statusLabelKey(integration.key, integration.status_code)}`)}
|
||||
</span>
|
||||
<h2>{t(`integrationSummary.titles.${integration.key}`, { ns: "demo" })}</h2>
|
||||
<p>{t(`integrationSummary.${integration.detail_code}`, { ns: "demo", ...integration.detail_params })}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section aria-label={t("about.proofTitle")}>
|
||||
<div className="proof-grid">
|
||||
{PROOFS.map((proof) => <article className="panel proof-card" key={proof.id}><span><Icon name={proof.icon} /></span><h3>{t(`about.proof${proof.id}Title`)}</h3><p>{t(`about.proof${proof.id}Body`)}</p></article>)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card">
|
||||
<h2>{t("about.resetTitle")}</h2>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey={user?.role === "operations_manager" ? "about.resetBodyManager" : "about.resetBodyEmployee"}
|
||||
t={t}
|
||||
values={{ when: manifest.last_reset_at ? formatDateTime(manifest.last_reset_at) : t("about.unknown") }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
</section>
|
||||
<section className="panel architecture-story" aria-labelledby="architecture-heading">
|
||||
<SectionHeading title={t("about.architectureFlowTitle")} description={t("about.architectureFlowDescription")} />
|
||||
<div className="architecture-flow" id="architecture-heading">
|
||||
{["Frontend", "Api", "Database", "Outbox", "External"].map((step, index) => <div className="architecture-step" key={step}><span>{index + 1}</span><strong>{t(`about.architecture${step}`)}</strong>{index < 4 && <Icon name="chevron" />}</div>)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="access-note record-surface" aria-label={t("about.limitationsTitle")}>
|
||||
<Icon name="shield" />
|
||||
<p>
|
||||
<strong>{t("about.limitationsTitle")}</strong>
|
||||
<span>{t("about.limitationsBody")}</span>
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<div className="about-grid">
|
||||
<div className="record-surface about-card"><h2>{t("about.problemTitle")}</h2><p>{t("about.problemBody", { orgName: manifest.organization_name, productName: PRODUCT_NAME })}</p></div>
|
||||
<div className="record-surface about-card"><h2>{t("about.scopeTitle")}</h2><p>{t("about.scopeBody", { productName: PRODUCT_NAME })}</p></div>
|
||||
<div className="record-surface about-card"><h2>{t("about.realTitle")}</h2><p>{t("about.realBody")}</p></div>
|
||||
<div className="record-surface about-card"><h2>{t("about.syntheticTitle")}</h2><p>{t("about.syntheticBody", { testDomain: ".test" })}</p></div>
|
||||
<div className="record-surface about-card"><h2>{t("about.verificationTitle")}</h2><p>{t("about.verificationBody")}</p></div>
|
||||
<div className="record-surface about-card"><h2>{t("about.securityTitle")}</h2><p>{t("about.securityBody")}</p></div>
|
||||
</div>
|
||||
|
||||
<section aria-label={t("about.integrationsTitle")} className="record-surface">
|
||||
<SectionHeading title={t("about.integrationsTitle")} description={t("about.integrationsDescription")} />
|
||||
<div className="integration-cards">{manifest.integrations.map((integration) => <article key={integration.key}><IntegrationMark kind={INTEGRATION_ICON[integration.key]} /><div><span className="integration-kicker">{t(`integrations:statusLabels.${statusLabelKey(integration.key, integration.status_code)}`)}</span><h2>{t(`integrationSummary.titles.${integration.key}`, { ns: "demo" })}</h2><p>{t(`integrationSummary.${integration.detail_code}`, { ns: "demo", ...integration.detail_params })}</p></div></article>)}</div>
|
||||
</section>
|
||||
|
||||
<section className="record-surface about-card"><h2>{t("about.resetTitle")}</h2><p><Trans i18nKey={user?.role === "operations_manager" ? "about.resetBodyManager" : "about.resetBodyEmployee"} t={t} values={{ when: manifest.last_reset_at ? formatDateTime(manifest.last_reset_at) : t("about.unknown") }} components={{ strong: <strong /> }} /></p></section>
|
||||
<section className="access-note record-surface" aria-label={t("about.limitationsTitle")}><Icon name="shield" /><p><strong>{t("about.limitationsTitle")}</strong><span>{t("about.limitationsBody")}</span></p></section>
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuth } from "../context/AuthContext";
|
||||
import { useLocaleFormat } from "../i18n/format";
|
||||
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
import { Pagination } from "../components/Pagination";
|
||||
import { Icon } from "../components/Icons";
|
||||
|
||||
function humanizeField(field: string): string {
|
||||
const spaced = field.replace(/_/g, " ");
|
||||
@@ -39,6 +40,22 @@ function ChangeDiff({ before, after, t }: { before: Record<string, unknown> | nu
|
||||
|
||||
interface EventGroup { correlationId: string; primary: AuditEvent; related: AuditEvent[]; }
|
||||
|
||||
function OperationTrace({ events, t }: { events: AuditEvent[]; t: (key: string, options?: Record<string, unknown>) => string }) {
|
||||
const actions = new Set(events.map((event) => event.action));
|
||||
const isReturn = actions.has("return_registered");
|
||||
if (!isReturn) return null;
|
||||
const steps = [
|
||||
{ key: "return", complete: isReturn },
|
||||
{ key: "vehicle", complete: actions.has("vehicle_status_changed") },
|
||||
{ key: "outbox", complete: isReturn },
|
||||
{ key: "delivery", complete: actions.has("n8n_return_followup_recorded") },
|
||||
];
|
||||
return <section className="panel operation-trace" aria-labelledby="operation-trace-title">
|
||||
<div><p className="page-eyebrow">{t("trace.eyebrow")}</p><h2 id="operation-trace-title">{t("trace.title")}</h2><p>{t("trace.description")}</p></div>
|
||||
<ol>{steps.map((step, index) => <li className={step.complete ? "is-complete" : "is-pending"} key={step.key}><span>{step.complete ? <Icon name="check" /> : index + 1}</span><div><strong>{t(`trace.steps.${step.key}.title`)}</strong><small>{t(`trace.steps.${step.key}.${step.complete ? "complete" : "pending"}`)}</small></div></li>)}</ol>
|
||||
</section>;
|
||||
}
|
||||
|
||||
export function Audit() {
|
||||
const { t } = useTranslation("audit");
|
||||
const { formatDateTime } = useLocaleFormat();
|
||||
@@ -95,6 +112,7 @@ export function Audit() {
|
||||
<label>{t("fromFilterLabel")}<input type="date" value={from} onChange={(e) => updateParams({ from: e.target.value, page: 1 })} /></label>
|
||||
<label>{t("toFilterLabel")}<input type="date" value={to} onChange={(e) => updateParams({ to: e.target.value, page: 1 })} /></label>
|
||||
</form>
|
||||
{correlationId && events && <OperationTrace events={events.items} t={t} />}
|
||||
{correlationId && <p className="quiet-empty" role="status">{t("relatedFilterActive", { count: events?.total ?? 0 })} <button type="button" className="link-button" onClick={() => updateParams({ correlation_id: null, page: 1 })}>{t("clearFilter")}</button></p>}
|
||||
{error && <ErrorState message={error} />}
|
||||
{!error && !events && <LoadingState label={t("loading")} />}
|
||||
|
||||
@@ -774,7 +774,7 @@ export function DataQualityIssueDetail() {
|
||||
return (
|
||||
<div className="page">
|
||||
<Link className="back-link" to="/data-quality"><Icon name="arrow-left" /> {t("detail.backLink")}</Link>
|
||||
<PageHeader eyebrow={t("detail.eyebrow", { rule: t(`ruleTypes.${issue.rule_type}`, { defaultValue: issue.rule_type.replace(/_/g, " ") }) })} title={issue.public_ref} description={t("detail.title")} actions={<div className="status-stack"><SeverityBadge severity={issue.severity} /><StatusBadge status={issue.status} label={t(`list.status${issue.status.charAt(0).toUpperCase()}${issue.status.slice(1)}`, { defaultValue: issue.status })} /></div>} />
|
||||
<PageHeader eyebrow={issue.public_ref} title={t(`ruleTypes.${issue.rule_type}`, { defaultValue: issue.rule_type.replace(/_/g, " ") })} description={t("detail.title")} actions={<div className="status-stack"><SeverityBadge severity={issue.severity} /><StatusBadge status={issue.status} label={t(`list.status${issue.status.charAt(0).toUpperCase()}${issue.status.slice(1)}`, { defaultValue: issue.status })} /></div>} />
|
||||
<section className="record-surface" aria-label={t("detail.summary.rule")}><dl className="detail-grid">
|
||||
<div><dt>{t("detail.summary.rule")}</dt><dd>{t(`ruleTypes.${issue.rule_type}`, { defaultValue: issue.rule_type.replace(/_/g, " ") })}</dd></div>
|
||||
<div><dt>{t("detail.summary.entity")}</dt><dd>{issue.entity_type === "vehicle" ? <Link to={`/vehicles/${issue.entity_ref}`}>{issue.entity_ref}</Link> : issue.entity_ref}</dd></div>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Icon, type IconName } from "../components/Icons";
|
||||
import { PageHeader } from "../components/PageChrome";
|
||||
import { useDemoGuide } from "../context/DemoGuideContext";
|
||||
import { PRODUCT_NAME } from "../product";
|
||||
|
||||
const HIGHLIGHTS: Array<{ id: "return" | "quality" | "knowledge"; to: string; icon: IconName }> = [
|
||||
{ id: "return", to: "/bookings/BK-DEMO-RETURN", icon: "bookings" },
|
||||
{ id: "quality", to: "/data-quality/DQ-DEMO-DUPLICATE", icon: "quality" },
|
||||
{ id: "knowledge", to: "/knowledge", icon: "knowledge" },
|
||||
];
|
||||
|
||||
export function Highlights() {
|
||||
const { t } = useTranslation("demo");
|
||||
const { openGuide } = useDemoGuide();
|
||||
|
||||
return (
|
||||
<div className="page recruiter-highlights">
|
||||
<PageHeader
|
||||
eyebrow={t("highlights.eyebrow")}
|
||||
title={t("highlights.title")}
|
||||
description={t("highlights.description", { productName: PRODUCT_NAME })}
|
||||
/>
|
||||
|
||||
<ol className="highlight-grid">
|
||||
{HIGHLIGHTS.map((highlight, index) => (
|
||||
<li key={highlight.id} className="panel highlight-card">
|
||||
<div className="highlight-card-top">
|
||||
<span className="highlight-number">0{index + 1}</span>
|
||||
<span className="highlight-duration"><Icon name="clock" /> {t("highlights.duration")}</span>
|
||||
</div>
|
||||
<span className="highlight-icon"><Icon name={highlight.icon} /></span>
|
||||
<h2>{t(`highlights.${highlight.id}.title`)}</h2>
|
||||
<p>{t(`highlights.${highlight.id}.body`)}</p>
|
||||
<div className="highlight-proof">
|
||||
<strong>{t("highlights.proofLabel")}</strong>
|
||||
<span>{t(`highlights.${highlight.id}.proof`)}</span>
|
||||
</div>
|
||||
<Link className="button button-primary" to={highlight.to}>
|
||||
{t("highlights.open")} <Icon name="chevron" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<section className="panel highlights-next">
|
||||
<div>
|
||||
<p className="page-eyebrow">{t("highlights.proofLabel")}</p>
|
||||
<h2>{t("highlights.storyTitle")}</h2>
|
||||
<p>{t("highlights.storyBody")}</p>
|
||||
</div>
|
||||
<div className="highlights-next-actions">
|
||||
<Link className="button button-primary" to="/about">{t("highlights.storyButton")} <Icon name="chevron" /></Link>
|
||||
<button type="button" className="button button-secondary" onClick={openGuide}><Icon name="spark" /> {t("highlights.fullTourButton")}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,12 @@ function isOpaqueVersion(value: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(value);
|
||||
}
|
||||
|
||||
function sourceDisplayTitle(title: string, t: (key: string, options?: Record<string, unknown>) => string): string {
|
||||
const base = title.replace(/\.md$/i, "");
|
||||
const fallback = base.split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
||||
return t(`sourceTitles.${base}`, { defaultValue: fallback });
|
||||
}
|
||||
|
||||
export function Knowledge() {
|
||||
const { t, i18n } = useTranslation(["knowledge", "errors"]);
|
||||
const { formatDateTime } = useLocaleFormat();
|
||||
@@ -27,6 +33,7 @@ export function Knowledge() {
|
||||
const [statusSettled, setStatusSettled] = useState(false);
|
||||
const [question, setQuestion] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [loadingStage, setLoadingStage] = useState(0);
|
||||
const [error, setError] = useState<ApiErrorInfo | null>(null);
|
||||
const [exchanges, setExchanges] = useState<Exchange[]>([]);
|
||||
|
||||
@@ -44,6 +51,9 @@ export function Knowledge() {
|
||||
async function ask(questionText: string) {
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
setLoadingStage(0);
|
||||
const sourceTimer = window.setTimeout(() => setLoadingStage(1), 900);
|
||||
const evidenceTimer = window.setTimeout(() => setLoadingStage(2), 2600);
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
const answer = await api.post<GroundedAnswer>("/api/v1/knowledge/questions", {
|
||||
@@ -61,6 +71,8 @@ export function Knowledge() {
|
||||
} catch (err) {
|
||||
setError(describeApiError(t, err, "askFailed"));
|
||||
} finally {
|
||||
window.clearTimeout(sourceTimer);
|
||||
window.clearTimeout(evidenceTimer);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
@@ -124,11 +136,11 @@ export function Knowledge() {
|
||||
<span>
|
||||
{status.available ? t("statusAvailable") : t("statusUnavailable")} ·{" "}
|
||||
{status.document_count === null
|
||||
? t("proceduresIndexUnknown")
|
||||
? t("proceduresAvailable", { count: status.source_document_count })
|
||||
: t("proceduresIndexed", { count: status.document_count })}
|
||||
</span>
|
||||
</div>
|
||||
<small>{status.collection}</small>
|
||||
<details className="knowledge-diagnostics"><summary>{t("technicalDetails")}</summary><small>{status.collection} · {status.detail}</small></details>
|
||||
</div>
|
||||
)}
|
||||
{statusSettled && !status && (
|
||||
@@ -181,6 +193,9 @@ export function Knowledge() {
|
||||
</button>
|
||||
</div>
|
||||
<ApiErrorNotice error={error} />
|
||||
{submitting && <div className="knowledge-progress" role="status" aria-live="polite">
|
||||
{["retrieving", "checking", "composing"].map((stage, index) => <span className={index <= loadingStage ? "is-active" : ""} key={stage}>{index < loadingStage ? <Icon name="check" /> : <i />}{t(`progress.${stage}`)}</span>)}
|
||||
</div>}
|
||||
<div className="knowledge-suggestions">
|
||||
<span>{t("suggestedLabel")}</span>
|
||||
{suggestedQuestions.map((suggestion) => (
|
||||
@@ -230,7 +245,7 @@ export function Knowledge() {
|
||||
<details open={sourceIndex === 0}>
|
||||
<summary>
|
||||
<span className="source-number">{sourceIndex + 1}</span>
|
||||
<span className="source-title">{source.title}</span>
|
||||
<span className="source-title">{sourceDisplayTitle(source.title, t)}</span>
|
||||
{source.section && <span className="source-section">{source.section}</span>}
|
||||
</summary>
|
||||
<p className="source-excerpt">{source.excerpt}</p>
|
||||
|
||||
@@ -17,11 +17,11 @@ export function Login() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
|
||||
async function handleLogin(role: Role, guided = false) {
|
||||
async function handleLogin(role: Role, destination = "/dashboard") {
|
||||
setError(null);
|
||||
try {
|
||||
await loginAs(role);
|
||||
navigate(guided ? "/dashboard?guide=start" : "/dashboard");
|
||||
navigate(destination);
|
||||
} catch {
|
||||
setError(t("loginFailed"));
|
||||
}
|
||||
@@ -78,9 +78,13 @@ export function Login() {
|
||||
type="button"
|
||||
className="button button-primary login-guide-cta"
|
||||
disabled={loading}
|
||||
onClick={() => handleLogin("operations_manager", true)}
|
||||
onClick={() => handleLogin("operations_manager", "/highlights")}
|
||||
>
|
||||
<Icon name="spark" />
|
||||
{t("startRecruiterTour")}
|
||||
</button>
|
||||
|
||||
<button type="button" className="button button-secondary login-full-demo" disabled={loading} onClick={() => handleLogin("operations_manager", "/dashboard?guide=start")}>
|
||||
{t("startGuidedDemo")}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -435,6 +435,25 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details
|
||||
|
||||
.state-panel { min-height: 180px; display: flex; align-items: center; justify-content: center; gap: 12px; padding: 28px; color: var(--muted); background: white; border: 1px solid var(--line); border-radius: var(--radius); text-align: left; }.state-panel svg { width: 24px; color: var(--critical); }.state-panel strong { color: var(--ink); font-size: .82rem; }.state-panel p { margin: 4px 0 0; font-size: .73rem; }.spinner { width: 22px; height: 22px; border: 2px solid var(--line); border-top-color: var(--teal); border-radius: 50%; animation: spin .7s linear infinite; }@keyframes spin { to { transform: rotate(360deg); } }.state-empty svg { color: var(--teal-dark); }
|
||||
.route-loading { min-height: 40vh; display: grid; place-items: center; }
|
||||
.route-skeleton { width: min(760px, calc(100% - 40px)); display: grid; gap: 14px; }
|
||||
.route-skeleton span { display: block; height: 18px; background: linear-gradient(90deg, #edf1f5 25%, #f8fafc 50%, #edf1f5 75%); background-size: 220% 100%; border-radius: var(--radius); animation: skeleton-shift 1.25s ease-in-out infinite; }
|
||||
.route-skeleton span:first-child { width: 42%; height: 34px; }.route-skeleton span:nth-child(2) { width: 76%; }.route-skeleton span:last-child { height: 180px; }
|
||||
@keyframes skeleton-shift { to { background-position: -220% 0; } }
|
||||
|
||||
/* Recruiter-facing product story */
|
||||
.engineering-hero { display: flex; justify-content: space-between; align-items: center; gap: 28px; margin-bottom: 26px; padding: 28px; background: linear-gradient(135deg, var(--petrol), #18344a); color: white; overflow: hidden; }
|
||||
.engineering-hero h2 { max-width: 720px; margin: 0; font-size: clamp(1.6rem, 3vw, 2.35rem); line-height: 1.1; letter-spacing: -.035em; }.engineering-hero p:not(.page-eyebrow) { max-width: 680px; margin: 10px 0 0; color: #c4d0dc; line-height: 1.6; }.engineering-hero .page-eyebrow { color: #5eead4; }
|
||||
.engineering-hero-actions { min-width: 220px; display: grid; gap: 9px; }.engineering-hero-actions .button { width: 100%; }
|
||||
.standalone-heading { padding-inline: 0; border: 0; }.proof-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 26px; }.proof-card { padding: 22px; }.proof-card > span, .highlight-icon { width: 42px; height: 42px; display: grid; place-items: center; color: var(--teal-dark); background: var(--teal-pale); border-radius: 50%; }.proof-card svg, .highlight-icon svg { width: 21px; }.proof-card h3 { margin: 18px 0 8px; font-size: 1rem; }.proof-card p { margin: 0; color: var(--muted); font-size: var(--type-body); line-height: 1.6; }
|
||||
.architecture-story { margin-bottom: 26px; overflow: hidden; }.architecture-flow { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); padding: 22px; }.architecture-step { position: relative; min-height: 104px; display: flex; flex-direction: column; justify-content: center; gap: 9px; padding: 16px; background: var(--surface-subtle); border: 1px solid var(--line); }.architecture-step > span { color: var(--teal-dark); font-size: var(--type-meta); font-weight: 800; }.architecture-step > strong { font-size: .82rem; line-height: 1.35; }.architecture-step > svg { position: absolute; z-index: 2; right: -12px; top: calc(50% - 12px); width: 24px; height: 24px; padding: 5px; color: white; background: var(--teal-dark); border-radius: 50%; }
|
||||
.highlight-grid { list-style: none; display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 0 0 18px; padding: 0; counter-reset: highlight; }.highlight-card { min-height: 440px; display: flex; flex-direction: column; padding: 24px; }.highlight-card-top { display: flex; justify-content: space-between; align-items: center; }.highlight-number { color: var(--teal-dark); font-size: .72rem; font-weight: 800; letter-spacing: .1em; }.highlight-duration { display: flex; align-items: center; gap: 5px; color: var(--muted); font-size: var(--type-meta); }.highlight-duration svg { width: 14px; }.highlight-icon { margin-top: 32px; }.highlight-card h2 { margin: 18px 0 10px; font-size: 1.18rem; letter-spacing: -.02em; }.highlight-card > p { margin: 0; color: var(--muted); line-height: 1.65; }.highlight-proof { display: grid; gap: 4px; margin: auto 0 18px; padding: 13px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }.highlight-proof strong { color: var(--muted); font-size: var(--type-label); text-transform: uppercase; letter-spacing: .06em; }.highlight-proof span { font-size: .77rem; font-weight: 700; }.highlights-next { display: flex; justify-content: space-between; align-items: center; gap: 24px; padding: 24px; }.highlights-next h2 { margin: 0; font-size: 1.15rem; }.highlights-next p:not(.page-eyebrow) { margin: 7px 0 0; color: var(--muted); }.highlights-next-actions { display: flex; flex-wrap: wrap; gap: 9px; }
|
||||
.login-full-demo { width: 100%; min-height: 44px; margin: -8px 0 18px; }
|
||||
|
||||
/* Correlated operation trace */
|
||||
.operation-trace { margin-bottom: 14px; padding: 22px; }.operation-trace h2 { margin: 0; font-size: 1.15rem; }.operation-trace > div > p:last-child { margin: 7px 0 0; color: var(--muted); }.operation-trace ol { list-style: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin: 20px 0 0; padding: 0; }.operation-trace li { position: relative; display: grid; grid-template-columns: 34px 1fr; gap: 10px; align-items: start; padding-right: 15px; }.operation-trace li::after { content: ""; position: absolute; top: 16px; left: 34px; right: 0; border-top: 1px solid var(--line-strong); }.operation-trace li:last-child::after { display: none; }.operation-trace li > span { position: relative; z-index: 1; width: 34px; height: 34px; display: grid; place-items: center; color: var(--muted); background: white; border: 1px solid var(--line-strong); border-radius: 50%; font-size: .72rem; font-weight: 800; }.operation-trace li.is-complete > span { color: white; background: var(--success); border-color: var(--success); }.operation-trace li svg { width: 17px; }.operation-trace li div { position: relative; z-index: 1; padding-right: 6px; background: white; }.operation-trace li strong { display: block; font-size: .78rem; }.operation-trace li small { display: block; margin-top: 4px; color: var(--muted); font-size: var(--type-meta); line-height: 1.4; }
|
||||
|
||||
/* Knowledge wait and diagnostics */
|
||||
.knowledge-diagnostics { max-width: 260px; text-align: right; }.knowledge-diagnostics summary { color: var(--teal-dark); font-size: var(--type-meta); font-weight: 700; cursor: pointer; }.knowledge-diagnostics small { display: block; margin-top: 5px; overflow-wrap: anywhere; }.knowledge-progress { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; margin: 12px 0; padding: 11px; background: var(--surface-subtle); border: 1px solid var(--line); }.knowledge-progress span { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: var(--type-meta); font-weight: 700; }.knowledge-progress span.is-active { color: var(--teal-dark); }.knowledge-progress i, .knowledge-progress svg { width: 15px; height: 15px; flex: 0 0 auto; }.knowledge-progress i { border: 2px solid var(--line-strong); border-top-color: var(--teal); border-radius: 50%; animation: spin .7s linear infinite; }
|
||||
.error { color: #9f2929; font-size: .74rem; font-weight: 600; }
|
||||
.api-error-notice { display: block; padding: 12px 14px; background: #fdf1f1; border: 1px solid #f0caca; border-radius: var(--radius); }
|
||||
.api-error-notice strong { display: block; font-size: .78rem; }
|
||||
@@ -540,6 +559,19 @@ details summary { cursor: pointer; color: var(--teal-dark); }.data-table details
|
||||
.tabs { margin-inline: -2px; }.record-surface { padding: 10px; }.detail-grid { grid-template-columns: 1fr 1fr; }.detail-grid div { min-height: 70px; padding: 11px; }.return-progress { padding-inline: 12px; gap: 6px; }.return-progress b { width: 20px; }.return-progress span { font-size: .58rem; }.return-form > .section-heading { padding: 16px; }.return-capture, .return-review { padding: 16px; }.form-grid, .condition-fieldset, .review-facts { grid-template-columns: 1fr; }.condition-fieldset { display: grid; }.review-facts div { display: flex; justify-content: space-between; align-items: center; }.review-facts dd { margin: 0; }.form-actions { padding: 12px 16px; }.form-actions .button { flex: 1; }.duplicate-compare { padding: 14px; }.duplicate-compare fieldset label { display: flex !important; margin-bottom: 8px; }.integration-cards article { min-height: 140px; }.knowledge-input-row { align-items: stretch; }.knowledge-input-row .button { min-width: 72px; padding-inline: 10px; }.knowledge-empty { min-height: 250px; padding: 22px 16px; }.retrieval-flow { width: 100%; gap: 4px; }.retrieval-flow span { padding: 5px; font-size: .52rem; }.retrieval-flow i { flex: 1; min-width: 5px; }.login-story { min-height: 38vh; }.login-message > p:last-child { font-size: .8rem; }.control-illustration { display: none; }.login-access { min-height: 62vh; padding: 34px 20px 50px; }.login-options button { min-height: 72px; }.app-footer { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
:root { --type-body: .875rem; --type-meta: .75rem; --type-label: .75rem; --type-badge: .75rem; }
|
||||
.icon-button, .language-switcher select, .demo-badge-trigger, .demo-guide-trigger, .back-link, .section-heading > a, .data-table td button, .duplicate-compare > button, .resolution-actions button, .confirm-bar button, .pagination button, .toggle-matching-fields, .button-tertiary, .bulk-toolbar select, .bulk-toolbar input { min-width: 44px; min-height: 44px; }
|
||||
.demo-guide-trigger { width: 44px; justify-content: center; }.demo-badge-trigger { width: 44px; height: 44px; padding: 0; justify-content: center; font-size: 0; }.demo-badge-trigger svg { width: 16px; height: 16px; color: #48566a; }.language-switcher select { height: 44px; }
|
||||
.mobile-nav span { max-width: 54px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.engineering-hero, .highlights-next { align-items: stretch; flex-direction: column; padding: 20px; }.engineering-hero-actions { min-width: 0; }.proof-grid, .highlight-grid { grid-template-columns: 1fr; }.highlight-card { min-height: 0; }.highlight-proof { margin-top: 22px; }.architecture-flow { grid-template-columns: 1fr; gap: 8px; padding: 14px; }.architecture-step { min-height: 72px; }.architecture-step > svg { right: calc(50% - 12px); top: auto; bottom: -16px; transform: rotate(90deg); }.highlights-next-actions { display: grid; }.operation-trace ol { grid-template-columns: 1fr; gap: 13px; }.operation-trace li { min-height: 54px; }.operation-trace li::after { top: 34px; bottom: -13px; left: 16px; right: auto; border-top: 0; border-left: 1px solid var(--line-strong); }.operation-trace li div { padding-bottom: 5px; }.knowledge-status { align-items: flex-start; }.knowledge-diagnostics { max-width: none; text-align: left; }.knowledge-progress { grid-template-columns: 1fr; }.retrieval-flow span { font-size: var(--type-meta); }.return-progress span { font-size: var(--type-meta); }.duplicate-compare > button { position: sticky; bottom: 78px; z-index: 6; width: 100%; box-shadow: var(--shadow-float); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
.route-skeleton span, .knowledge-progress i { animation: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.page-actions { display: flex; min-height: 24px; }
|
||||
}
|
||||
|
||||