fix(demo): anchor seeded dates to the real reset moment
Booking/inspection/maintenance/outbox dates were authored as absolute timestamps around a fixed 2026-08-01 anchor and never re-anchored at seed/reset time, so demo scenarios (e.g. BK-DEMO-RETURN) silently drifted into the past. Every reset now shifts seeded dates by (today - authored anchor); dashboard's "today" filter uses real wall-clock time instead of the now-removed frozen demo_today setting. Adds seed-validation tests proving scenarios S1/S2/S4/S5 are present and internally consistent after every reset.
This commit is contained in:
@@ -8,7 +8,6 @@ POSTGRES_DB=mobilityops
|
|||||||
POSTGRES_USER=mobilityops
|
POSTGRES_USER=mobilityops
|
||||||
POSTGRES_PASSWORD=mobilityops
|
POSTGRES_PASSWORD=mobilityops
|
||||||
APP_SECRET=replace-in-production
|
APP_SECRET=replace-in-production
|
||||||
DEMO_TODAY=2026-08-01
|
|
||||||
TZ=Europe/Brussels
|
TZ=Europe/Brussels
|
||||||
# Session cookie Secure flag. Keep false for LAN/plain-HTTP deployments (including the
|
# Session cookie Secure flag. Keep false for LAN/plain-HTTP deployments (including the
|
||||||
# current Unraid review environment); set true only once MobilityOps is served over HTTPS,
|
# current Unraid review environment); set true only once MobilityOps is served over HTTPS,
|
||||||
|
|||||||
@@ -507,3 +507,38 @@ any change at `docs/functional-completion/server-baseline.md`.
|
|||||||
re-verified against the live server after every batch. See
|
re-verified against the live server after every batch. See
|
||||||
`artifacts/functional-completion/final-summary.md` for the definitive acceptance
|
`artifacts/functional-completion/final-summary.md` for the definitive acceptance
|
||||||
evidence.
|
evidence.
|
||||||
|
|
||||||
|
## Demo productization (in progress, same branch `feat/mobilityops-functional-completion`)
|
||||||
|
|
||||||
|
Follows the functional-completion work above; turns the now feature-complete PoC into a
|
||||||
|
guided, honestly-labelled demo (fictional org "Northstar Mobility", guided tour, 5 named
|
||||||
|
scenarios, demo manifest, About page). Gap audit: `docs/demo-release/current-demo-gap-audit.md`.
|
||||||
|
|
||||||
|
### Batch 1 — seed date anchoring (complete)
|
||||||
|
|
||||||
|
- **Real bug fixed**: `seed/bookings.csv` etc. store absolute ISO timestamps authored
|
||||||
|
around a fixed anchor (`2026-08-01`). Nothing previously re-anchored them at seed/reset
|
||||||
|
time, so scenario bookings (e.g. `BK-DEMO-RETURN`) silently drifted into the past every
|
||||||
|
day the environment wasn't reset. `dashboard.py::_today()` compounded this by filtering
|
||||||
|
"today's movements" against the same frozen `demo_today` setting instead of real time.
|
||||||
|
- Fix: `seed_loader.py` now computes `shift = today - SEED_AUTHORED_ANCHOR` once per
|
||||||
|
`load_seed()` call and applies it to every seeded booking/inspection/maintenance/outbox
|
||||||
|
datetime column, so scenarios stay "today"/"near-future" relative to the actual reset
|
||||||
|
moment. `SeedResult` now also carries `anchor_date`/`seeded_at`; `POST /api/v1/demo/reset`
|
||||||
|
returns them; a `demo_data_seeded` audit event records the anchor for traceability.
|
||||||
|
`dashboard.py::_today()` switched from the frozen `demo_today` setting to real wall-clock
|
||||||
|
UTC date. The now-dead `demo_today` setting/env var was removed from `config.py`,
|
||||||
|
`compose.yaml`, `.env`, `.env.example` (nothing else referenced it).
|
||||||
|
- Added seed-validation tests (`backend/tests/test_seed.py`) proving S1 (`BK-DEMO-RETURN`/
|
||||||
|
`MO-024`), S2 (`CUS-0012`/`CUS-0178`/`DQ-DEMO-DUPLICATE`), S4 (`MO-016`/
|
||||||
|
`BK-DEMO-OVERLAP-A`/`-B`/`DQ-DEMO-OVERLAP`) and S5 (seeded failed outbox event
|
||||||
|
`00000000-0000-4000-8000-000000000020`, confirmed genuinely `failed` immediately after a
|
||||||
|
fresh reset, not silently auto-healed by the background dispatcher since it only claims
|
||||||
|
`pending` rows) are fully present after every reset, plus a dedicated anchoring test
|
||||||
|
asserting the shift and the audit marker.
|
||||||
|
- Live-verified locally: reseeded and confirmed via `psql` that `BK-DEMO-RETURN` now ends
|
||||||
|
today and `BK-DEMO-NEXT`/overlap bookings sit in the near future (today = 2026-08-03).
|
||||||
|
- Evidence: `pytest` **122 passed** (117 + 5 new/expanded seed tests), `ruff check .`
|
||||||
|
clean, `mypy app` clean (46 files, canonical `make` scope).
|
||||||
|
- Exact next action: `GET /api/v1/demo/manifest` + Dutch demo entry screen + permanent
|
||||||
|
demo badge (task #30), then the Demo Guide + scenario overview (task #31).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import UTC, date, datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
@@ -30,7 +30,9 @@ _SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2}
|
|||||||
|
|
||||||
|
|
||||||
def _today() -> date:
|
def _today() -> date:
|
||||||
return datetime.fromisoformat(settings.demo_today).date()
|
# Seeded dates are shifted to the real reset moment by `seed_loader.py`'s anchor
|
||||||
|
# shift, so "today" must be real wall-clock time, not the frozen `demo_today` setting.
|
||||||
|
return datetime.now(UTC).date()
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=DashboardOut)
|
@router.get("", response_model=DashboardOut)
|
||||||
|
|||||||
@@ -99,8 +99,16 @@ def demo_reset(
|
|||||||
actor_label=user.display_name,
|
actor_label=user.display_name,
|
||||||
action="demo_reset",
|
action="demo_reset",
|
||||||
entity_type="system",
|
entity_type="system",
|
||||||
metadata={"counts": result.counts},
|
metadata={
|
||||||
|
"counts": result.counts,
|
||||||
|
"anchor_date": result.anchor_date.isoformat(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
response.delete_cookie(settings.session_cookie_name)
|
response.delete_cookie(settings.session_cookie_name)
|
||||||
return {"status": "reset", "counts": result.counts}
|
return {
|
||||||
|
"status": "reset",
|
||||||
|
"counts": result.counts,
|
||||||
|
"anchor_date": result.anchor_date.isoformat(),
|
||||||
|
"seeded_at": result.seeded_at.isoformat(),
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class Settings(BaseSettings):
|
|||||||
mcp_hub_service_token: str = "replace-me-mcp-hub-token"
|
mcp_hub_service_token: str = "replace-me-mcp-hub-token"
|
||||||
mcp_hub_registration_enabled: bool = False
|
mcp_hub_registration_enabled: bool = False
|
||||||
cors_allow_origins: str = "http://localhost:1228"
|
cors_allow_origins: str = "http://localhost:1228"
|
||||||
demo_today: str = "2026-08-01"
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import csv
|
import csv
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, date, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sqlalchemy import delete, insert, update
|
from sqlalchemy import delete, insert, update
|
||||||
@@ -20,6 +20,7 @@ from app.models.maintenance import MaintenanceRecord
|
|||||||
from app.models.outbox import OutboxEvent
|
from app.models.outbox import OutboxEvent
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.vehicle import Vehicle
|
from app.models.vehicle import Vehicle
|
||||||
|
from app.services.audit import record_audit_event
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
|
||||||
@@ -36,6 +37,17 @@ DEMO_USERS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# seed/generate_seed.py authored the committed CSVs relative to this fixed date
|
||||||
|
# (`--anchor 2026-08-01`, matching Settings.demo_today). Every reset shifts every
|
||||||
|
# seeded date by (today - SEED_AUTHORED_ANCHOR) so "today" / "near-future" / "overlaps
|
||||||
|
# right now" scenarios stay true to the actual reset moment instead of decaying as real
|
||||||
|
# time passes between resets -- a fixed anchor with no shift goes stale within days.
|
||||||
|
SEED_AUTHORED_ANCHOR = date(2026, 8, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_anchor_shift(today: date) -> timedelta:
|
||||||
|
return today - SEED_AUTHORED_ANCHOR
|
||||||
|
|
||||||
|
|
||||||
def _parse_dt(value: str) -> datetime:
|
def _parse_dt(value: str) -> datetime:
|
||||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
@@ -53,6 +65,8 @@ def _parse_optional_int(value: str) -> int | None:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class SeedResult:
|
class SeedResult:
|
||||||
counts: dict[str, int]
|
counts: dict[str, int]
|
||||||
|
anchor_date: date
|
||||||
|
seeded_at: datetime
|
||||||
|
|
||||||
|
|
||||||
def _seed_dir() -> Path:
|
def _seed_dir() -> Path:
|
||||||
@@ -83,6 +97,8 @@ def clear_all(db: Session) -> None:
|
|||||||
|
|
||||||
def load_seed(db: Session) -> SeedResult:
|
def load_seed(db: Session) -> SeedResult:
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
|
today = datetime.now(UTC).date()
|
||||||
|
shift = _seed_anchor_shift(today)
|
||||||
|
|
||||||
user_rows = [
|
user_rows = [
|
||||||
{"id": uuid.uuid4(), **user, "active": True} for user in DEMO_USERS
|
{"id": uuid.uuid4(), **user, "active": True} for user in DEMO_USERS
|
||||||
@@ -154,8 +170,8 @@ def load_seed(db: Session) -> SeedResult:
|
|||||||
"public_ref": row["public_ref"],
|
"public_ref": row["public_ref"],
|
||||||
"customer_id": customer_id_by_ref[row["customer_ref"]],
|
"customer_id": customer_id_by_ref[row["customer_ref"]],
|
||||||
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
||||||
"starts_at": _parse_dt(row["starts_at"]),
|
"starts_at": _parse_dt(row["starts_at"]) + shift,
|
||||||
"ends_at": _parse_dt(row["ends_at"]),
|
"ends_at": _parse_dt(row["ends_at"]) + shift,
|
||||||
"status": row["status"],
|
"status": row["status"],
|
||||||
"start_odometer_km": _parse_optional_int(row["start_odometer_km"]),
|
"start_odometer_km": _parse_optional_int(row["start_odometer_km"]),
|
||||||
"end_odometer_km": _parse_optional_int(row["end_odometer_km"]),
|
"end_odometer_km": _parse_optional_int(row["end_odometer_km"]),
|
||||||
@@ -179,7 +195,7 @@ def load_seed(db: Session) -> SeedResult:
|
|||||||
"damage_reported": _parse_bool(row["damage_reported"]),
|
"damage_reported": _parse_bool(row["damage_reported"]),
|
||||||
"technical_warning": _parse_bool(row["technical_warning"]),
|
"technical_warning": _parse_bool(row["technical_warning"]),
|
||||||
"odometer_km": int(row["odometer_km"]),
|
"odometer_km": int(row["odometer_km"]),
|
||||||
"completed_at": _parse_dt(row["completed_at"]),
|
"completed_at": _parse_dt(row["completed_at"]) + shift,
|
||||||
"completed_by": None,
|
"completed_by": None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -193,7 +209,7 @@ def load_seed(db: Session) -> SeedResult:
|
|||||||
"id": uuid.uuid4(),
|
"id": uuid.uuid4(),
|
||||||
"public_ref": row["public_ref"],
|
"public_ref": row["public_ref"],
|
||||||
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
"vehicle_id": vehicle_id_by_ref[row["vehicle_ref"]],
|
||||||
"occurred_at": _parse_dt(row["occurred_at"]),
|
"occurred_at": _parse_dt(row["occurred_at"]) + shift,
|
||||||
"odometer_km": int(row["odometer_km"]),
|
"odometer_km": int(row["odometer_km"]),
|
||||||
"category": row["category"],
|
"category": row["category"],
|
||||||
"summary": row["summary"],
|
"summary": row["summary"],
|
||||||
@@ -266,7 +282,7 @@ def load_seed(db: Session) -> SeedResult:
|
|||||||
},
|
},
|
||||||
"aggregate_ref": row["aggregate_ref"],
|
"aggregate_ref": row["aggregate_ref"],
|
||||||
},
|
},
|
||||||
"occurred_at": _parse_dt(row["occurred_at"]),
|
"occurred_at": _parse_dt(row["occurred_at"]) + shift,
|
||||||
"delivery_status": row["status"],
|
"delivery_status": row["status"],
|
||||||
"attempts": int(row["attempts"]),
|
"attempts": int(row["attempts"]),
|
||||||
"next_attempt_at": None,
|
"next_attempt_at": None,
|
||||||
@@ -277,7 +293,21 @@ def load_seed(db: Session) -> SeedResult:
|
|||||||
db.execute(insert(OutboxEvent), outbox_rows)
|
db.execute(insert(OutboxEvent), outbox_rows)
|
||||||
counts["workflow_runs"] = len(outbox_rows)
|
counts["workflow_runs"] = len(outbox_rows)
|
||||||
|
|
||||||
return SeedResult(counts=counts)
|
seeded_at = datetime.now(UTC)
|
||||||
|
record_audit_event(
|
||||||
|
db,
|
||||||
|
actor_type="system",
|
||||||
|
actor_label="seed loader",
|
||||||
|
action="demo_data_seeded",
|
||||||
|
entity_type="system",
|
||||||
|
metadata={
|
||||||
|
"anchor_date": today.isoformat(),
|
||||||
|
"seed_authored_anchor": SEED_AUTHORED_ANCHOR.isoformat(),
|
||||||
|
"counts": counts,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return SeedResult(counts=counts, anchor_date=today, seeded_at=seeded_at)
|
||||||
|
|
||||||
|
|
||||||
def reset_and_seed(db: Session) -> SeedResult:
|
def reset_and_seed(db: Session) -> SeedResult:
|
||||||
|
|||||||
+118
-1
@@ -1,13 +1,16 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
|
|
||||||
from app.core.db import SessionLocal
|
from app.core.db import SessionLocal
|
||||||
|
from app.models.audit import AuditEvent
|
||||||
from app.models.booking import Booking
|
from app.models.booking import Booking
|
||||||
from app.models.customer import Customer
|
from app.models.customer import Customer
|
||||||
from app.models.data_quality import DataQualityIssue
|
from app.models.data_quality import DataQualityIssue
|
||||||
from app.models.outbox import OutboxEvent
|
from app.models.outbox import OutboxEvent
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.vehicle import Vehicle
|
from app.models.vehicle import Vehicle
|
||||||
from app.seed_loader import reset_and_seed
|
from app.seed_loader import SEED_AUTHORED_ANCHOR, reset_and_seed
|
||||||
|
|
||||||
|
|
||||||
def test_seed_counts_match_deterministic_dataset():
|
def test_seed_counts_match_deterministic_dataset():
|
||||||
@@ -52,3 +55,117 @@ def test_seed_demo_scenarios_present():
|
|||||||
assert failed_run is not None
|
assert failed_run is not None
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _by_ref(db, model, ref):
|
||||||
|
return db.scalar(select(model).where(model.public_ref == ref))
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_scenario_s1_odometer_regression_return():
|
||||||
|
"""S1: BK-DEMO-RETURN on MO-024 is an active booking ready for a return with a
|
||||||
|
below-canonical odometer reading, using the vehicle's own current odometer."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
reset_and_seed(db)
|
||||||
|
booking = _by_ref(db, Booking, "BK-DEMO-RETURN")
|
||||||
|
vehicle = _by_ref(db, Vehicle, "MO-024")
|
||||||
|
assert booking is not None and vehicle is not None
|
||||||
|
assert booking.vehicle_id == vehicle.id
|
||||||
|
assert booking.status == "active"
|
||||||
|
assert booking.end_odometer_km is None
|
||||||
|
# A demo return reading must sit below the vehicle's canonical odometer to
|
||||||
|
# reproduce the odometer-regression anomaly deterministically.
|
||||||
|
assert vehicle.odometer_km > 0
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_scenario_s2_duplicate_customer_pair():
|
||||||
|
"""S2: CUS-0012/CUS-0178 form a possible-duplicate pair with a matching open issue."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
reset_and_seed(db)
|
||||||
|
primary = _by_ref(db, Customer, "CUS-0012")
|
||||||
|
duplicate = _by_ref(db, Customer, "CUS-0178")
|
||||||
|
assert primary is not None and duplicate is not None
|
||||||
|
assert primary.email == duplicate.email
|
||||||
|
assert duplicate.merged_into_customer_id is None
|
||||||
|
|
||||||
|
issue = _by_ref(db, DataQualityIssue, "DQ-DEMO-DUPLICATE")
|
||||||
|
assert issue is not None
|
||||||
|
assert issue.rule_type == "possible_duplicate_customer"
|
||||||
|
assert issue.status == "open"
|
||||||
|
related = issue.evidence_json.get("related_refs", [])
|
||||||
|
assert "CUS-0012" in related or "CUS-0178" in related
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_scenario_s4_booking_overlap():
|
||||||
|
"""S4: MO-016 carries two overlapping reservations plus a matching open issue."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
reset_and_seed(db)
|
||||||
|
vehicle = _by_ref(db, Vehicle, "MO-016")
|
||||||
|
booking_a = _by_ref(db, Booking, "BK-DEMO-OVERLAP-A")
|
||||||
|
booking_b = _by_ref(db, Booking, "BK-DEMO-OVERLAP-B")
|
||||||
|
assert vehicle is not None and booking_a is not None and booking_b is not None
|
||||||
|
assert booking_a.vehicle_id == vehicle.id
|
||||||
|
assert booking_b.vehicle_id == vehicle.id
|
||||||
|
assert booking_a.starts_at < booking_b.ends_at
|
||||||
|
assert booking_b.starts_at < booking_a.ends_at
|
||||||
|
|
||||||
|
issue = _by_ref(db, DataQualityIssue, "DQ-DEMO-OVERLAP")
|
||||||
|
assert issue is not None
|
||||||
|
assert issue.rule_type == "booking_overlap"
|
||||||
|
assert issue.status == "open"
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_scenario_s5_failed_workflow_run():
|
||||||
|
"""S5: one seeded outbox event is durably 'failed' (terminal, retryable), not merely
|
||||||
|
pending, so the background dispatcher never silently auto-heals it away."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
reset_and_seed(db)
|
||||||
|
failed = db.scalar(
|
||||||
|
select(OutboxEvent).where(
|
||||||
|
OutboxEvent.event_id == "00000000-0000-4000-8000-000000000020"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert failed is not None
|
||||||
|
assert failed.delivery_status == "failed"
|
||||||
|
assert failed.attempts >= 1
|
||||||
|
assert failed.last_error
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_dates_are_anchored_to_reset_moment():
|
||||||
|
"""Every reset shifts seeded dates by (real today - authored anchor), so scenario
|
||||||
|
bookings stay 'today'/'near-future' relative to whenever the reset actually ran,
|
||||||
|
instead of decaying back to the fixed 2026-08-01 authoring date."""
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
result = reset_and_seed(db)
|
||||||
|
today = datetime.now(UTC).date()
|
||||||
|
assert result.anchor_date == today
|
||||||
|
|
||||||
|
shift = today - SEED_AUTHORED_ANCHOR
|
||||||
|
booking = _by_ref(db, Booking, "BK-DEMO-RETURN")
|
||||||
|
assert booking is not None
|
||||||
|
# Authored ends_at was 2026-08-01T09:00Z; after shifting it must land on the
|
||||||
|
# real reset date, not the frozen authoring date (unless shift is exactly zero).
|
||||||
|
assert booking.ends_at.date() == today or shift.days == 0
|
||||||
|
|
||||||
|
marker = db.scalar(
|
||||||
|
select(AuditEvent)
|
||||||
|
.where(AuditEvent.action == "demo_data_seeded")
|
||||||
|
.order_by(AuditEvent.occurred_at.desc())
|
||||||
|
)
|
||||||
|
assert marker is not None
|
||||||
|
assert marker.metadata_json["anchor_date"] == today.isoformat()
|
||||||
|
assert marker.metadata_json["seed_authored_anchor"] == SEED_AUTHORED_ANCHOR.isoformat()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ services:
|
|||||||
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops}
|
DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops}
|
||||||
TZ: ${TZ:-Europe/Brussels}
|
TZ: ${TZ:-Europe/Brussels}
|
||||||
APP_SECRET: ${APP_SECRET:-replace-in-production}
|
APP_SECRET: ${APP_SECRET:-replace-in-production}
|
||||||
DEMO_TODAY: ${DEMO_TODAY:-2026-08-01}
|
|
||||||
CORS_ALLOW_ORIGINS: ${MOBILITYOPS_PUBLIC_URL:-http://localhost:1228}
|
CORS_ALLOW_ORIGINS: ${MOBILITYOPS_PUBLIC_URL:-http://localhost:1228}
|
||||||
KNOWLEDGE_PROVIDER: ${KNOWLEDGE_PROVIDER:-demo}
|
KNOWLEDGE_PROVIDER: ${KNOWLEDGE_PROVIDER:-demo}
|
||||||
RAGCORE_BASE_URL: ${RAGCORE_BASE_URL:-http://ragcore-api:8000}
|
RAGCORE_BASE_URL: ${RAGCORE_BASE_URL:-http://ragcore-api:8000}
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ One seeded outbox/workflow record is failed with a safe simulated connection err
|
|||||||
|
|
||||||
Question: “What must I do when a vehicle returns with damage?” Expected: answer cites damage handling and return inspection procedures.
|
Question: “What must I do when a vehicle returns with damage?” Expected: answer cites damage handling and return inspection procedures.
|
||||||
|
|
||||||
|
## Date anchoring
|
||||||
|
|
||||||
|
The committed CSVs store absolute ISO timestamps authored around a fixed anchor date
|
||||||
|
(`SEED_AUTHORED_ANCHOR = 2026-08-01` in `backend/app/seed_loader.py`, matching the
|
||||||
|
`--anchor` used to generate them). Every seed/reset shifts every seeded booking,
|
||||||
|
inspection, maintenance and outbox timestamp by `today − SEED_AUTHORED_ANCHOR`, so
|
||||||
|
"today"/"near-future"/"currently overlapping" scenarios stay true to the real moment the
|
||||||
|
environment was (re)seeded instead of decaying as real time passes between resets. Public
|
||||||
|
refs and entity relationships are untouched by the shift — only datetime columns move.
|
||||||
|
`load_seed()` returns the resolved `anchor_date`/`seeded_at`, and records a
|
||||||
|
`demo_data_seeded` audit event carrying both the resolved anchor and the original
|
||||||
|
authoring anchor, so the shift applied on any given reset stays traceable.
|
||||||
|
|
||||||
## Demo reset
|
## Demo reset
|
||||||
|
|
||||||
Reset must:
|
Reset must:
|
||||||
@@ -49,6 +62,7 @@ Reset must:
|
|||||||
- require Operations Manager;
|
- require Operations Manager;
|
||||||
- rebuild the deterministic dataset;
|
- rebuild the deterministic dataset;
|
||||||
- re-establish scenario references;
|
- re-establish scenario references;
|
||||||
|
- re-anchor scenario dates to the real reset moment (see above);
|
||||||
- clear non-seed audit/workflow state;
|
- clear non-seed audit/workflow state;
|
||||||
- complete safely and visibly;
|
- complete safely and visibly;
|
||||||
- be covered by a test.
|
- be covered by a test.
|
||||||
|
|||||||
Reference in New Issue
Block a user