feat: add public h5 dashboard and publish traceability

This commit is contained in:
Wyndham ARR
2026-08-03 13:04:41 +08:00
parent 7e7470821b
commit 2107f00e32
29 changed files with 1047 additions and 71 deletions

View File

@@ -0,0 +1,29 @@
# ADR-005: Public Read-Only H5 Dashboard
## Status
Accepted — 2026-08-03
## Context
ARR Web login protects Finance operations, but the mobile H5 dashboard is a read-only aggregate view intended for
broader viewing. Making only the HTML page public would leave its data requests behind the login gate. Making generic
desktop APIs public would widen access to jobs, traces, operational health, filenames and other operator data.
## Decision
1. Publish the H5 page/assets anonymously.
2. Add purpose-built anonymous `GET /api/public/h5/months` and `GET /api/public/h5/analytics` routes.
3. Return only aggregate dashboard data and month/date coverage from those routes; omit source hashes, filenames,
traces, detailed health, source coordinates, guest/detail rows and operational metadata.
4. Keep desktop pages, generic and legacy APIs, jobs/traces, downloads, uploads, Booking/company/report operations and
mutations behind the existing server-side session/CSRF boundary.
5. Keep `/healthz` as the only public readiness signal; public JSON responses remain `no-store`.
## Consequences
- Anyone with the H5 URL can view the aggregate channel/room dashboard and all month options returned by the public H5
month index.
- Finance/operator workflows remain authenticated and the public H5 surface has no write capability.
- Deployment must treat the aggregate metrics, channel names and financial totals as intentionally public data and should
apply normal HTTPS, access logging and rate/abuse controls at the edge.

View File

@@ -4,6 +4,7 @@
| ID | Decision | Status | Date | Applies To | Detail |
|---|---|---|---|---|---|
| ADR-005 | 手机 H5 看板通过专用只读聚合接口公开访问;桌面及运营接口继续认证 | Accepted | 2026-08-03 | Web access boundary, H5, Channel BI | [ADR-005](ADR-005-public-read-only-h5-dashboard.md) |
| ADR-004 | ARR2.0 owns deterministic processing, artifact validation and terminal ingestion; no Agent/MCP in production XML flow | Accepted | 2026-07-30 | Upload, processing runtime, OSS ACL, validation, deployment | [ADR-004](ADR-004-arr-owned-programmatic-processing.md) |
| ADR-001 | 月报由独立 worker 自动触发;“更新至”取最新纳入的 `ARRIVAL`;发布后页面自动新增;`TOTAL PRICE` 为公式字段 | Implemented | 2026-07-29 | 月报触发、持久化、页面同步、XLSX 输出 | [ADR-001](ADR-001-automatic-monthly-trigger-and-total-price-formula.md) |

View File

@@ -2,9 +2,13 @@
## Web Access Boundary
1. An anonymous browser requesting `/` or `/h5` is redirected to `/login` with an allowlisted return target; anonymous API/download requests receive JSON `401 AUTH_REQUIRED`.
1. An anonymous browser requesting `/` is redirected to `/login` with an allowlisted return target. `/h5` and its H5
assets are public; anonymous access is limited to `/api/public/h5/months`, `/api/public/h5/analytics` and the
no-detail `/healthz` signal. Other anonymous API/download requests receive JSON `401 AUTH_REQUIRED`.
2. `POST /api/login` accepts strict same-origin JSON, verifies runtime-owned credentials under a bounded per-client attempt ledger and issues a random server-side session plus CSRF token in an `HttpOnly`, `SameSite=Strict` cookie.
3. Every portal page, detailed health response, read API, upload/mutation, task trace and artifact download validates the authenticated session. Mutations additionally validate the session CSRF token.
3. Every desktop portal page, detailed health response, generic read API, upload/mutation, task trace and artifact
download validates the authenticated session. Public H5 endpoints return only sanitized aggregate dashboard data;
mutations additionally validate the session CSRF token.
4. `POST /api/logout` validates CSRF, revokes the server-side session and expires the cookie. `/healthz` remains a no-detail anonymous readiness signal for container orchestration.
5. `GET /api/history-months` is an authenticated read-only discovery path. It combines daily processing/monthly publication counts from PostgreSQL with durable company-job counts; each desktop history then reads its selected month through the existing paged list endpoint.

View File

@@ -15,10 +15,10 @@
| `arr_storage/aliyun_oss_v2.py` | Encrypted/unversioned OSS adapter | Writes all objects private |
| `arr_web/downloads.py` | OSS daily + controlled local report download routing | Rechecks metadata, size and SHA-256 |
| `arr_web/job_trace.py` | Programmatic persisted-fact trace | No external trace store |
| `arr_web/app.py`, `arr_web/repository.py`, `arr_web/company_jobs.py` | Authenticated portal routes, paged history reads and read-only history-month discovery | Default-deny login gate protects pages/APIs/downloads; daily/monthly counts and rows share a repeatable-read snapshot; `/api/history-months` merges daily/monthly database counts with company job-state counts; company totals/slices share one lock |
| `arr_web/app.py`, `arr_web/repository.py`, `arr_web/company_jobs.py` | Authenticated portal routes plus public H5 aggregate routes, paged history reads and read-only history-month discovery | Default-deny login gate protects the desktop/API/download surface; purpose-built `/api/public/h5/*` exposes only sanitized aggregate metrics; daily/monthly counts and rows share a repeatable-read snapshot; `/api/history-months` merges daily/monthly database counts with company job-state counts; company totals/slices share one lock |
| `arr_web/server.py` | Standard-library HTTP transport | Dispatches GET/POST/PATCH/DELETE with one bounded body reader; real socket tests cover review update/delete and missing/oversized lengths |
| `arr_web/static/login.html`, `login.css`, `login.js` | Responsive ARR login gateway | Labeled form, password visibility, generic inline failures, safe desktop/H5 return target and reduced-motion support |
| `arr_web/static/app.js`, `arr_web/static/h5.js` | Authenticated client state, rendering and polling | Desktop daily/monthly/company histories own independent viewing-month state and default to the latest non-empty month; company generation month remains separate. Desktop also includes 50-row Booking draft review/edit, the draft's validated uploaded filename below the review title, individual/all-visible selection, count-aware in-page delete confirmation and activation; company generation keeps the fixed five-company context beside the page title, a four-card upload/period setup row, cumulative CO display labels, short centered period actions and an in-page generation confirmation dialog; session-expiry redirect and CSRF logout remain shared; monthly versions auto-refresh the selected viewing month every four seconds |
| `arr_web/static/app.js`, `arr_web/static/h5.js` | Authenticated desktop and anonymous-capable H5 client state, rendering and polling | Desktop daily/monthly/company histories own independent viewing-month state and default to the latest non-empty month; company generation month remains separate. H5 reads only public aggregate endpoints and may retain optional logout for an authenticated session. Desktop also includes 50-row Booking draft review/edit, the draft's validated uploaded filename below the review title, individual/all-visible selection, count-aware in-page delete confirmation and activation; company generation keeps the fixed five-company context beside the page title, a four-card upload/period setup row, cumulative CO display labels, short centered period actions and an in-page generation confirmation dialog; session-expiry redirect and CSRF logout remain shared; monthly versions auto-refresh the selected viewing month every four seconds |
| `monthly_reports/worker.py` | Dedicated outbox consumer | Lease/reclaim, retry/dead-letter, success acknowledgement after activation |
| `monthly_reports/repository.py` | Monthly snapshot and publication repository | Derives scope from `ARRIVAL`; persists metadata/lineage/artifact identities |
| `monthly_reports/xlsx/build_workbook.mjs` | Monthly XLSX builder and reopen validator | Exact row-relative `TOTAL PRICE` formulas only in column S |
@@ -26,7 +26,7 @@
| `database/012_monthly_report_publication.sql` | Additive metadata-only publication schema | Applied after immutable 008011 baseline |
| `database/014_booking_current_source_batch.sql` | Booking full-workbook current-source pointer and view scoping | Formally applied on 2026-07-31; batch 1 remains selected |
| `database/015_booking_excel_review_drafts.sql` | Item-level Booking extraction draft state | Formally applied and empty; basic latest-state review only, with no actor/reason/revision history and no DB-enforced zero-pending activation |
| `compose.yaml`, `deploy/` | Web-login + Caddy-HTTPS template and worker deployment boundary | Requires Web credentials; only `/healthz` is anonymously probed; no MCP port/domain/service |
| `compose.yaml`, `deploy/` | Web-login + Caddy-HTTPS template and worker deployment boundary | Requires Web credentials for the desktop/operational surface; public H5 aggregate routes and `/healthz` remain anonymously reachable; no MCP port/domain/service |
| `tests/test_arr_programmatic.py` | Real success/failure vertical slices | Primary ARR2.0 acceptance proof |
Historical ARR1 compatibility modules and migrations remain for audit/tests but are not imported, installed or deployed

View File

@@ -31,7 +31,9 @@ from committed `ARRIVAL` facts, publishes a validated workbook, and records meta
## Important Boundaries
- The uploaded source is re-materialized from committed object storage before processing.
- Login documents/assets, `POST /api/login` and minimal `/healthz` readiness are the only anonymous Web routes. Portal documents redirect to login; business APIs, detailed health, uploads, traces and downloads return `401` without an authenticated server-side session.
- Login documents/assets, the H5 read-only page/assets, purpose-built public H5 aggregate endpoints, `POST /api/login`
and minimal `/healthz` readiness are anonymous Web routes. The desktop portal redirects to login; generic business
APIs, detailed health, uploads, traces and downloads return `401` without an authenticated server-side session.
- Operator credentials come only from runtime `ARR_WEB_USERNAME` / `ARR_WEB_PASSWORD`; the application fails closed when either is absent. Authenticated mutations also require the per-session CSRF token.
- Processor exit code and JSON status must agree; output paths are confined to an isolated temporary directory.
- Validation completes before the database transaction; any transaction failure leaves no partial active version.

View File

@@ -10,7 +10,38 @@ download identity.
The company-channel detail generator now separates generation permission from calendar completeness: a current-month
or historical C/O period can be generated from the current committed Finance snapshot, while future report months stay
blocked. A not-yet-ended period keeps its fixed C/O cutoff and must be rerun after later source facts arrive if the final
workbook needs those facts.
workbook needs those facts. Duplicate publication of the same semantic snapshot is idempotent: the first validated
archive/result pair remains authoritative even if a retry rebuilds different XLSX bytes.
## Completed On 2026-08-03
- Made the mobile H5 dashboard publicly readable without weakening the desktop/operator boundary. Anonymous users can
load `/h5`, its H5 assets and purpose-built `/api/public/h5/months` plus `/api/public/h5/analytics` aggregate routes;
the public projection omits source hashes and operational metadata. Generic analytics, legacy H5 APIs, desktop pages,
detailed health, jobs/traces, downloads and all write paths remain authenticated. H5 keeps optional authenticated
logout, uses no-detail `/healthz` for its connection indicator, and 71 `test_arr_web*` tests plus syntax checks pass.
No live service restart or public deployment was performed.
- Clarified ARR trace semantics for internet-facing deployment. `artifact_callback` traces now expose additive
`execution_scope=arr_runtime`, `processor_mode=fixed_processor` and `remote_dispatch=none`; legacy `direct_mcp`
traces identify remote Agent/MCP delivery. User-visible messages no longer call ARR output “本地处理”, and the
queued downstream message explicitly says Finance database commit is complete while the outbox waits for its
consumer. Persisted `local-...` delivery keys and existing trace fields remain unchanged. Trace/Web tests (27) plus
Python/JavaScript syntax and diff checks pass; no live task or business data was mutated.
- Fixed company-report duplicate publication. A retry now validates the existing archive/result pair against the
deterministic reservation and builder `semantic_sha256`, verifies the archive hash against the result JSON, and
reuses the first successful artifact metadata instead of comparing the new XLSX binary SHA. Partial, corrupt or
semantically conflicting pairs fail closed; an existing `current` file is not overwritten, while a missing one can be
repaired from the authoritative archive. First-publication rollback behavior and monthly publication code are
unchanged. Publisher tests pass 6/6, company-report tests pass 32 with five optional ArtifactTool skips, Web
company/trace tests pass 13/13 and Python compilation is clean. No live report rerun or business-data mutation was
performed; the historical 2026-07 `11-20` retry remains to be rerun as a separate acceptance action.
- Added Booking Excel to company-report traceability. New report jobs persist a safe source summary captured server-side
at submission (batch id, filename, activation time and counts); the company page now shows the active source, job
detail shows the submission-time source, and history adds a source column. Re-uploading the same active workbook is
treated as an informational refresh that keeps the existing source and report history; it does not re-extract or
auto-generate a report. Historical jobs without source metadata remain readable and show that the source was not
recorded. Full discovery passes 360 tests with 10 optional skips; no live upload, activation, report generation or
service restart was performed because the available browser session was unauthenticated.
## Completed On 2026-08-02
@@ -318,6 +349,10 @@ workbook needs those facts.
- Booking review persistence passed a real-PostgreSQL transaction-only vertical slice: draft creation exposed two items
with one pending, edit confirmed the pending item, activation produced one source row with quantity three, and the outer
rollback restored batch 1 with no synthetic draft, source or artifact residue.
- A 2026-08-03 diagnosis reproduced the 2026-07 `11-20` company-report retry failure: all five companies failed at
`publish` after an earlier same-period success because repeated XLSX builds had identical semantic SHA-256 values but
different binary SHA-256 values. Publisher idempotency now reuses a complete matching archive/result pair and fails
closed on partial or conflicting state; the historical retry still needs a deliberate live rerun.
- Isolated authenticated browser checks covered the upload-first desktop flow, editable review table, fixed-company text
and responsive 375/390-pixel layouts with no horizontal overflow or console errors.
@@ -351,4 +386,4 @@ workbook needs those facts.
## Last Updated
2026-07-31
2026-08-03

View File

@@ -4,6 +4,11 @@
| Date | Task | Outcome | Docs Updated |
|---|---|---|---|
| 2026-08-03 | Make the mobile H5 dashboard publicly readable | Added public H5 shell/assets and purpose-built sanitized aggregate month/analytics endpoints; desktop, generic APIs, legacy H5 APIs, detailed health, jobs/traces, downloads and writes remain protected. Anonymous H5 loading plus authenticated compatibility is covered by 71 Web tests and syntax checks; no live restart or deployment occurred | Current state/history, architecture, business rules, ADR-005, H5 public-access evidence/index, scoped planning record |
| 2026-08-03 | Clarify ARR trace semantics for internet deployment | Added additive execution-scope, processor-mode and remote-dispatch fields; replaced misleading “本地处理” copy and clarified that a pending downstream event follows a completed Finance commit. Existing delivery IDs/trace fields remain compatible; 27 trace/Web tests plus Python/JavaScript syntax and diff checks pass, with no live mutation | Current state/history, ARR trace-semantics evidence/index, scoped planning record |
| 2026-08-03 | Add Booking Excel to company-report traceability | New company-report jobs store the server-captured active source summary; the page shows the current source, job detail/history show the source Excel, and identical active uploads become an informational refresh instead of a generic extraction error. Old jobs remain compatible without source metadata. Focused Web/company tests and the 360-test suite pass with 10 optional skips; no live mutation or restart was performed | Current state, source-to-report traceability evidence/index, scoped planning record |
| 2026-08-03 | Fix duplicate company-report publication | Made same-reservation/same-semantic retries reuse the first validated archive/result identity even when a rebuilt XLSX has different binary bytes. Partial, corrupt or semantic-conflict states fail closed; an existing current file is preserved and a missing one can be repaired from the archive. Publisher tests pass 6/6, company-report tests pass 32 with five optional ArtifactTool skips, Web company/trace tests pass 13/13 and compilation is clean. No live rerun or business-data mutation was performed | Current state, company-report retry evidence/index, scoped plan |
| 2026-08-03 | Diagnose 2026-07 company-report `正式表格发布失败` | Found the historical `11-20` retry failed for all five companies at publication after an earlier same-period success. A read-only double-build probe showed identical semantic workbook identities but different binary XLSX SHA-256 values; the immutable archive/result identity rejects that retry. No code, report, database or output state was changed | Current state, company-report retry evidence/index |
| 2026-08-02 | Restore prior-month history across Daily Report, monthly processing and company-channel detail | Added a read-only merged history-month count endpoint and three independent month navigators with latest-non-empty defaults, month-aware empty/pagination states and Chinese/English/Thai responsive copy. Company generation/history months are separate and monthly polling follows only the viewing month. A live read confirms July 30/4/4 records, 356 tests pass, isolated 1440/900/390 QA is overflow-free, and the restarted 8766 runtime is ready without business writes | Current state/history, history-visibility evidence/index, stale item, scoped planning record |
| 2026-08-02 | Diagnose missing prior-month history and design its display | Proved the data was retained: July has 30 daily jobs, 4 monthly publications and 4 company jobs, while the August-default frontend view is empty. Daily/monthly loaders hard-code the Bangkok current month; company history hides its month filtering inside generation setup. Proposed visible per-history month navigation, recent-record empty states and separation of company generation/history months; no product code, runtime or business data changed | History-visibility evidence/index, stale item, scoped planning record |
| 2026-07-31 | Tighten Channel BI top spacing | Raised the desktop BI panel by 20px toward the four-tab bar and applied a lighter 10px mobile adjustment. Other panels, tab behavior and BI data are unchanged; JavaScript syntax and 39 focused Web tests pass | Current state/history, Channel BI utility evidence |

View File

@@ -2,7 +2,10 @@
## Durable Rules
- Human access to desktop/H5 pages, detailed health, business APIs, uploads, traces and downloads requires an authenticated ARR Web session. Only login assets, `POST /api/login` and no-detail `/healthz` readiness are anonymous.
- Human access to the desktop Finance workspace, detailed health, generic business APIs, uploads, traces and downloads
requires an authenticated ARR Web session. The H5 mobile dashboard is an intentional anonymous read-only exception:
only its page/assets, `/api/public/h5/months`, `/api/public/h5/analytics` and no-detail `/healthz` are public; the H5
projection contains aggregate channel/room metrics and omits source hashes and operational metadata.
- The single-operator login identity is injected as `ARR_WEB_USERNAME` / `ARR_WEB_PASSWORD`; real credentials are never stored in source or returned/logged. Missing credentials fail Web startup closed.
- Login failures use a generic message and bounded attempt blocking. Logout revokes the server-side session; authenticated mutations continue to require CSRF.
- The user uploads XML; the user does not submit a report year, report month, cutoff date, or separate monthly-generation request in the primary workflow.

View File

@@ -4,6 +4,10 @@ Use this index for searchable, traceable evidence records.
| Date | Topic | Status | Source | Detail |
|---|---|---|---|---|
| 2026-08-03 | Public mobile H5 dashboard boundary | Implemented; deployment/restart pending | [Evidence topic](topics/2026-08-03-public-h5-dashboard.md) | Anonymous H5 page/assets and purpose-built `/api/public/h5/*` aggregate routes are allowed. The projection omits `source_monthly_sha256`; desktop, generic/legacy APIs, detailed health, jobs/traces, downloads and writes remain protected. `test_arr_web*` discovery passes 71 tests; no live service or business data was changed. |
| 2026-08-03 | ARR job trace semantics for internet deployment | Implemented; live deployment acceptance pending | [Evidence topic](topics/2026-08-03-arr-job-trace-semantics.md) | Trace projection adds `execution_scope`, `processor_mode` and `remote_dispatch`; `artifact_callback` is identified as ARR fixed-processor runtime execution and legacy `direct_mcp` as remote Agent/MCP delivery. “本地处理” copy is removed, pending downstream explicitly follows Finance commit, and persisted `local-...` delivery identities are retained. Focused trace/Web tests pass; no live task or business write occurred. |
| 2026-08-03 | Booking Excel to company-report traceability | Implemented; authenticated browser acceptance pending | [Evidence topic](topics/2026-08-03-company-source-report-traceability.md) | New report jobs persist a safe source summary captured at submission; current source, job detail and history expose the filename/batch context. Identical active uploads refresh context without re-extraction or automatic generation. Full discovery passes 360 tests with 10 optional skips; no live mutation or restart occurred. |
| 2026-08-03 | 2026-07 company-report retry publication failure | Implemented; live historical rerun pending | [Evidence topic](topics/2026-08-03-company-report-publish-retry.md) | July `11-20` job `60bcda9c…` failed for all five companies at `publish` after an earlier same-period success. Publisher idempotency now validates and reuses a complete matching archive/result pair despite different rebuilt XLSX bytes, while partial or conflicting state fails closed. |
| 2026-08-02 | Cross-month history visibility | Implemented and runtime-active | [Evidence topic](topics/2026-08-02-history-month-visibility.md) | Three independent history-month navigators now default to the latest non-empty month through a read-only count endpoint; company generation/history months are separated. July resolves to 30 daily/4 monthly/4 company jobs, 356 tests pass, 1440/900/390 browser QA is overflow-free, port 8766 is ready and no business write occurred. |
| 2026-07-31 | Channel BI header utilities and compact month selector | Implemented; focused static checks pass | [Evidence topic](topics/2026-07-31-channel-bi-header-utilities.md) | The visible BI panel heading and ready-state 数据库已连接 copy are gone from desktop/H5 while the navigation label, browser metadata and status dot remain. The month selector is compact; task-log/logout use low-emphasis accessible links, and the desktop BI panel now sits 20px closer to the tab bar with a 10px mobile adjustment. No API or business data changed. |
| 2026-07-31 | Channel BI copy and data coverage | Implemented; focused checks pass | [Evidence topic](topics/2026-07-31-channel-bi-copy-and-data-coverage.md) | Desktop and H5 remove the BI-only CHANNEL PERFORMANCE, snapshot watermark and visible BI TOTAL PRICE labels. The company-sales panels show localized 数据范围 from additive min_arrival_date plus existing max_arrival_date values returned by the same repeatable-read current-version snapshot. |

View File

@@ -0,0 +1,43 @@
# ARR Job Trace Semantics for Internet Deployment
## Question
Whether the ARR task trace is understandable when ARR is deployed on a server reachable through the internet, rather
than being interpreted as if processing occurred on the operator's computer.
## Findings
- The current `artifact_callback` path is owned by ARR's fixed processor. It does not create a remote Agent/MCP run,
so `remote_run_id=null` is expected and is not a failure.
- Persisted delivery keys such as `local-arrjob-...-a1` are delivery/idempotency identities. They are retained for
compatibility and are not a claim about the user's machine or the server's geographic location.
- A successful job with `current_stage=downstream` and `DOWNSTREAM_EVENT_QUEUED` has already committed the Finance
version. `publish_status=pending` means the durable outbox event is waiting for its downstream consumer.
## Change
The trace job summary now adds:
| Field | `artifact_callback` | legacy `direct_mcp` |
|---|---|---|
| `execution_scope` | `arr_runtime` | `remote_agent` |
| `processor_mode` | `fixed_processor` | `legacy_direct_mcp` |
| `remote_dispatch` | `none` | `mcp` |
User-visible messages now say `ARR 固定处理器输出` and explicitly state `Finance 数据库提交已完成` before the
outbox wait. Existing `delivery_mode`, `remote_run_id`, trace version, persisted delivery IDs and privacy allowlist
remain compatible.
## Verification
- `python3 -m unittest tests.test_arr_web_job_trace tests.test_arr_web_task_log_ui tests.test_arr_web` — 27 passed.
- `python3 -m py_compile arr_web/job_trace.py` — passed.
- `node --check arr_web/static/app.js` — passed.
- `git diff --check` — passed.
- No live task was rerun, no service was restarted and no Finance/report/Booking business data was changed.
## Follow-up
After deployment, inspect one successful `artifact_callback` trace and confirm the additive header fields appear in the
deployed CLI/API/UI. This is a display/observability acceptance step; it is separate from rerunning the historical
2026-07 company-report `11-20` publication.

View File

@@ -0,0 +1,46 @@
# Evidence Topic: 2026-07 company-report retry publication failure
## Metadata
- Date: 2026-08-03
- Status: Implemented; live historical rerun pending
- Scope: 2026-07 company-channel report, C/O `11-20`, local Web job `60bcda9c8dae49758fb69dc47e0ff9e7`
- Confidence: Fact for recorded job/build observations; inference for the exact publisher guard
- Source: job JSON, prior successful job JSON, read-only controlled PostgreSQL snapshot, source-code inspection and temporary-directory double-build probe
- Last verified: 2026-08-03
- Stale trigger: changes to the company XLSX builder, artifact identity, reservation-version derivation or publisher idempotency
## Question
Why did the Web show `正式表格发布失败` for 2026-07?
## Evidence
- The failed job is for report month `2026-07`, period `11-20`, and as-of date `2026-07-20`. All five companies failed at stage `publish` with `COMPANY_REPORT_PUBLISH_FAILED`; there are no record IDs and no source/Booking/business validation errors.
- The immediately preceding job for the same month and period succeeded for all five companies and created archive versions. The failure is therefore a retry of an already published semantic snapshot, not a first-build data failure.
- `company_reports.repository.PostgresReportRepository.reserve_report()` derives the version directory from a deterministic semantic identity containing the company, period, as-of date, processor/rule/schema versions, daily-version pins and Booking-version pins. The retry therefore targets the same archive version path.
- A read-only double-build probe loaded the current controlled July snapshot and built each of the five valid `11-20` workbooks twice in separate temporary directories. For every company, the two binary SHA-256 values differed while the builder's `semantic_sha256` was identical.
- `AtomicReportPublisher._install_once()` rejects an existing archive path when its binary SHA-256 differs from the newly built file, raising `COMPANY_REPORT_PUBLISH_FAILED`. The result JSON also embeds the binary SHA-256, so allowing only the archive copy would still leave the same retry-identity conflict at result validation.
- No database write, report rerun or output repair was performed during this diagnosis.
## Implemented Fix
- `AtomicReportPublisher` now checks for an existing archive/result pair before creating legacy/current side effects. A
retry is reusable only when both files are regular files, the result JSON matches the deterministic report identity and
current semantic SHA, and the archive's actual SHA-256 matches the result JSON's stored artifact SHA.
- A matching retry returns the existing archive/result metadata and activates the repository without copying the new
XLSX bytes. An existing current file is left untouched; a missing current file is repaired from the existing archive.
- Partial, corrupt, hash-inconsistent or semantically conflicting publication state fails closed with
`PUBLISH_FAILED`. First publication and activation-failure rollback paths remain covered.
- Regression coverage passed for same-semantic/different-binary retry, semantic mismatch, partial publication and
corrupted archive. The real ArtifactTool integration regression is present but was skipped locally because the
optional `@oai/artifact-tool` dependency is unavailable.
- No live report rerun, output repair or business-data mutation was performed after the code change.
## Conclusion
The 2026-07 `11-20` failure was caused by a non-byte-deterministic XLSX build colliding with the publisher's deterministic archive/version identity on retry. The workbook contents were semantically the same, but changing XLSX bytes made the immutable archive and result identity appear conflicting. The existing successful 11-20 artifacts remain valid; the failed retry did not replace them. The publisher now handles this case idempotently by reusing that stable identity.
## Fix direction
The publisher-side idempotency fix is implemented and is the active remedy: reuse/validate the existing artifact by semantic identity and preserve one stable binary artifact identity. Making the XLSX builder byte-deterministic remains an optional future optimization, not a prerequisite for retry success. The historical job still needs a deliberate live rerun to verify the deployed path.

View File

@@ -0,0 +1,41 @@
# Booking Excel to company-report traceability
## Scope
The Booking Excel upload page could tell the operator that an identical workbook was already active, but the company-report
history and job detail did not show which source was associated with a generated result. This change adds source identity
without exposing workbook rows, guest data or full artifact hashes.
## Diagnosis
- `booking_ingestion/excel_review_postgres.py` deduplicates an identical activated artifact and raises
`BOOKING_EXCEL_SOURCE_ALREADY_ACTIVATED` with `这份 Excel 已经启用,无需重复提取`.
- `/api/company-reports/source` already returned a privacy-safe source summary, but the frontend used it only as a
readiness boolean.
- `POST /api/company-reports/jobs` accepted only month and period, and persisted no source metadata in the durable job
state. The frontend API helper also discarded error codes, preventing an informational duplicate-upload branch.
## Implemented behavior
- The server captures the current active source when a report job is submitted; the browser cannot choose or spoof the
source identity.
- New job records expose only source batch id/type, safe filename, activation time, disposition and summary counts.
- The company page shows the current active workbook and summary counts.
- Job detail and history show the source Excel captured at submission; old job files without this field render
`历史任务未记录来源`/an equivalent localized label.
- Re-uploading the same active workbook clears the selected file, refreshes the current-source and report-history views,
and shows an informational message. It does not create a second extraction or automatically generate a report.
- Report generation remains an explicit month/period action. Upload controls stay blocked while a report job is active, so
the submission-time source remains the trace shown for that job.
## Verification
- `./.venv/bin/python -m unittest tests.test_arr_web_company_reports tests.test_arr_web`: 28 tests passed.
- `./.venv/bin/python -m unittest discover -s tests -p 'test_arr_web*.py'`: 69 tests passed.
- `./.venv/bin/python -m unittest discover -s tests`: 360 tests passed, 10 optional skips.
- `node --check arr_web/static/app.js` and Python compilation passed.
- `git diff --check` passed.
The available in-app browser session landed on the login page without an authenticated session. No login, upload,
activation, report generation, service restart or business-data mutation was attempted; authenticated browser acceptance
remains the deployment follow-up.

View File

@@ -0,0 +1,30 @@
# Public Mobile H5 Dashboard
## Request
Make the mobile dashboard publicly viewable while preserving the authenticated Finance/operator surface.
## Public contract
- Anonymous `GET /h5`, `/h5.html`, `/assets/h5.css` and `/assets/h5.js` are allowed. `i18n.js` was already public as a
login asset.
- Anonymous `GET /api/public/h5/months` returns only month key, update time and maximum ARRIVAL watermark.
- Anonymous `GET /api/public/h5/analytics?month=YYYY-MM` returns aggregate channel/room metrics and date coverage.
The public projection omits `source_monthly_sha256`, filenames and operational metadata; JSON responses are
`Cache-Control: no-store`.
- H5 uses anonymous `/healthz` for its connection indicator and does not require a CSRF token for read-only data. When a
logged-in operator opens H5, the existing session is still detected and the optional logout control remains available.
## Protected contract retained
Anonymous desktop `/`, `/api/health`, generic `/api/months`, `/api/analytics`, legacy `/api/h5/months`, legacy
`/api/monthly/{month}/analytics`, jobs, traces, downloads, uploads, Booking/company/report routes and mutations remain
protected by the existing session/CSRF boundary.
## Verification
- `python3 -m unittest tests.test_arr_web_auth tests.test_arr_web` — 26 passed.
- `python3 -m unittest discover -s tests -p 'test_arr_web*.py'` — 71 passed.
- `node --check arr_web/static/h5.js`, `node --check arr_web/static/app.js` and `python3 -m py_compile arr_web/app.py` — passed.
- `git diff --check` — passed.
- No live service restart, public deployment, database write or business/report mutation was performed.