From 7cce59d68788212f147847adf28b4937040ab211 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:26:49 +0000 Subject: [PATCH 1/9] feat: name leftover-map cross share on leftover pairs (v2.12.29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Persist leftover-map cross share x = 2 R̂_c U_c / R̃² of centered leftover on leftover post–criterion pairs (ADR 0185). After make seed, closest and farthest leftover pairs sit above the member list with 2R̂U/R̃² next to leftover-map distance d; click opens that post. A signed remainder is shown, never clamped. Do not invent a leftover score. Do not persist explained leftover share e, unexplained leftover share s, unexplained leftover U, or reconstruction R̂. --- AGENTS.md | 9 +- ARCHITECTURE.md | 5 +- .../2.12.29-leftover-map-cross-share.md | 10 ++ CHANGELOG.md | 13 ++ CLAUDE.md | 4 +- backend/app/report_ingestion.py | 14 +- backend/tests/test_api.py | 17 ++ docker/postgres-init/migrate.sh | 2 +- .../0003-fast-mlsirm-report-integration.md | 10 +- docs/adr/0048-persist-lsirm-leftover-pairs.md | 9 +- docs/adr/0049-leftover-pair-report-ui.md | 9 +- docs/adr/0185-leftover-map-cross-share.md | 110 +++++++++++++ frontend/package.json | 2 +- frontend/src/App.test.tsx | 8 +- frontend/src/App.tsx | 37 ++++- frontend/src/api.ts | 1 + frontend/src/components/AdminPanel.tsx | 6 +- frontend/src/i18n.test.ts | 34 ++++ frontend/src/i18n.ts | 44 +++++ frontend/src/leftoverMapCrossShare.test.ts | 18 ++ frontend/src/leftoverMapCrossShare.ts | 13 ++ lineageweave/leftover_pairs.py | 155 ++++++++++++++---- .../0185_report_leftover_map_cross_share.sql | 16 ++ .../0185_report_leftover_map_cross_share.sql | 4 + pyproject.toml | 2 +- scripts/seed_demo_data.py | 7 +- tests/test_leftover_pairs.py | 140 +++++++++++++++- tests/test_migration_replay.py | 1 + tests/test_period_report.py | 6 + tests/test_schema.py | 35 ++++ uv.lock | 2 +- 31 files changed, 680 insertions(+), 63 deletions(-) create mode 100644 CHANGELOG.d/2.12.29-leftover-map-cross-share.md create mode 100644 docs/adr/0185-leftover-map-cross-share.md create mode 100644 frontend/src/leftoverMapCrossShare.test.ts create mode 100644 frontend/src/leftoverMapCrossShare.ts create mode 100644 migrations/0185_report_leftover_map_cross_share.sql create mode 100644 migrations/rollback/0185_report_leftover_map_cross_share.sql diff --git a/AGENTS.md b/AGENTS.md index 1728f9e61..78dcbd340 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,12 +186,15 @@ in the same spirit) -- never against real data, per the hard rule above. against a live local stack (`make up`) and self-skip without one -- see [README.md](README.md#local-product-stack-docker-compose). -Period leftover pairs (ADR 0017 / 0018) are computed in +Period leftover pairs (ADR 0048 / 0049 / 0185) are computed in `lineageweave/leftover_pairs.py` from the residual after a real GRM/GPCM score, never invented. Missing cells stay out of the Gabriel factorization. Closest and farthest post–criterion pairs -persist to `report_leftover_pair` and sit above the member list so -a click opens that post. +persist to `report_leftover_pair` and sit above the member list with +leftover-map cross share `x = 2 R̂_c U_c / R̃²` of centered leftover +next to leftover-map distance `d` so a click opens that post. +Two-axis reconstruction `R̂_c` and unexplained leftover `U_c` are +not persisted. `frontend/` has its own toolchain (Node pinned via `frontend/mise.toml`, pnpm via Corepack -- do not add a second Node package manager or a diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d0280ff97..31c466935 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -588,8 +588,9 @@ on those same fixed parameters (Kim, 2006 FIPC). After scoring, `information_polytomous` ranks the shared-bank items by Fisher information at the group's mean θ (Lord, 1980 max-info CAT). Rankings persist to `report_item_information`. After those IRT main effects, -residual SVD leftover pairs (Jeon et al., 2021; ADR 0017) persist to -`report_leftover_pair`. Results persist to +residual SVD leftover pairs (Jeon et al., 2021; ADR 0048 / 0185) persist to +`report_leftover_pair` with leftover-map cross share `x = 2 R̂_c U_c / R̃²` +of centered leftover named on the pair row. Results persist to `report_period_score` / `report_member_score`. `GET /api/reports/{grouping}` lists the trend; `GET /api/reports/{grouping}/{period}` is ABAC-filtered; diff --git a/CHANGELOG.d/2.12.29-leftover-map-cross-share.md b/CHANGELOG.d/2.12.29-leftover-map-cross-share.md new file mode 100644 index 000000000..9e4b9b419 --- /dev/null +++ b/CHANGELOG.d/2.12.29-leftover-map-cross-share.md @@ -0,0 +1,10 @@ +## 2.12.29 — Leftover-map cross share + +- Persist leftover-map cross share `x = 2 R̂_c U_c / R̃²` of centered + leftover on leftover post–criterion pairs (ADR 0185). After + `make seed`, closest and farthest leftover pairs sit above the + member list with `2R̂U/R̃²` next to leftover-map distance `d`; click + opens that post. Omit the badge when the share is missing. A signed + remainder is shown, never clamped. Never invent a leftover score. Do + not persist leftover-map explained leftover share `e`, unexplained + leftover share `s`, unexplained leftover `U`, or reconstruction `R̂`. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed1a099..c4ebca32b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ All notable changes to this project are documented here. Format follows environment, so local OIDC and synthetic-data workflows resolve the same pinned dependencies as CI. +## [2.12.29] - 2026-08-24 + +### Added + +- Period leftover pair rows now name leftover-map cross share + `x = 2 R̂_c U_c / R̃²` of centered leftover next to leftover-map + distance `d`, then open that post (Gabriel, 1971; Jeon et al., 2021, + eq. 3; ADR 0185). A missing share omits the badge rather than + inventing a leftover score. A signed remainder is shown, never + clamped. Two-axis reconstruction `R̂_c` and unexplained leftover + `U_c` stay internal and are not persisted. Explained leftover share + `e` and unexplained leftover share `s` are not persisted here. + ## [2.12.6] - 2026-08-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 1bcf50763..469800593 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,9 @@ chip name contains `Corporate entity: Demo Corp` and the persisted mean θ. The period-report panel says Demo Corp is the opened grouping and to read its mean θ and member posts, then open a post. Those members land immediately under that next action, ahead of Other Corp -and the week strip. Opening Public post names the next action: read +and the week strip. After `make seed`, leftover closest/farthest pairs +sit above the member list with leftover-map cross share `2R̂U/R̃²` +next to leftover-map distance `d`. Opening Public post names the next action: read Event Lineage, Keyman, and evaluation on that post. The popup Event Lineage DAG marks that post current. After that current node, the popup names Keyman and evaluation as the next read. After landed diff --git a/backend/app/report_ingestion.py b/backend/app/report_ingestion.py index 50614b0ad..28787dad3 100644 --- a/backend/app/report_ingestion.py +++ b/backend/app/report_ingestion.py @@ -443,8 +443,9 @@ async def persist_period_report( """ insert into report_leftover_pair ( grouping_kind, grouping_key, period_code, rubric_version, - pair_kind, post_id, criterion_code, leftover_distance, leftover_residual - ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9) + pair_kind, post_id, criterion_code, leftover_distance, leftover_residual, + leftover_map_cross_share + ) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) """, grouping_kind, grouping_key, @@ -455,6 +456,7 @@ async def persist_period_report( pair.criterion_code, pair.leftover_distance, pair.leftover_residual, + pair.leftover_map_cross_share, ) @@ -601,7 +603,8 @@ async def fetch_period_reports( leftover = await conn.fetch( # nosemgrep: python.lang.security.audit.sqli.asyncpg-sqli.asyncpg-sqli f""" select lp.grouping_key, lp.pair_kind, lp.post_id, lp.criterion_code, - lp.leftover_distance, lp.leftover_residual, p.post_title, + lp.leftover_distance, lp.leftover_residual, lp.leftover_map_cross_share, + p.post_title, p.visibility_code, p.corporate_entity_id, ({_SOURCE_CONTEXT_PRESENT_SQL}) as has_real_source_context from report_leftover_pair lp @@ -698,6 +701,11 @@ async def fetch_period_reports( "criterion_code": str(row["criterion_code"]), "leftover_distance": float(row["leftover_distance"]), "leftover_residual": float(row["leftover_residual"]), + "leftover_map_cross_share": ( + None + if row["leftover_map_cross_share"] is None + else float(row["leftover_map_cross_share"]) + ), "visibility_code": row["visibility_code"], "corporate_entity_id": str(row["corporate_entity_id"]), "has_real_source_context": bool(row["has_real_source_context"]), diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index 438b4786a..dece4ca51 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -113,6 +113,11 @@ / "migrations" / "0102_project_bound_summary_event.sql" ) +_LEFTOVER_MAP_CROSS_SHARE_MIGRATION = ( + Path(__file__).resolve().parents[2] + / "migrations" + / "0185_report_leftover_map_cross_share.sql" +) def _postgres_available() -> bool: @@ -226,6 +231,7 @@ def seeded_db(demo_analyst_token): cur.execute(_MAJOR_EVENT_ACTION_MIGRATION.read_text()) cur.execute(_PROJECT_BOUND_ACTION_MIGRATION.read_text()) cur.execute(_PROJECT_BOUND_EVENT_MIGRATION.read_text()) + cur.execute(_LEFTOVER_MAP_CROSS_SHARE_MIGRATION.read_text()) cur.execute( "insert into common_lookup_value (lookup_category, lookup_code, lookup_label) values " "('corporate_entity_level', 'group', 'Group'), " @@ -4518,6 +4524,17 @@ def test_seed_period_report_surfaces_on_get_reports(client, demo_analyst_token, assert leftover_kinds <= {"closest", "farthest"} assert all(pair["post_title"] for pair in high_report.get("leftover_pairs", [])) assert all(pair["leftover_distance"] >= 0 for pair in high_report.get("leftover_pairs", [])) + for pair in high_report.get("leftover_pairs", []): + share = pair.get("leftover_map_cross_share") + assert share is None or isinstance(share, (int, float)) + if share is not None: + assert isinstance(share, (int, float)) + assert share == share + assert abs(share) != float("inf") + assert "leftover_map_explained_share" not in pair + assert "leftover_map_unexplained_share" not in pair + assert "leftover_map_unexplained" not in pair + assert "leftover_map_reconstruction" not in pair week3 = client.get( "/api/reports/process_unit/2026-W03", diff --git a/docker/postgres-init/migrate.sh b/docker/postgres-init/migrate.sh index f329117d6..cd59fcb10 100644 --- a/docker/postgres-init/migrate.sh +++ b/docker/postgres-init/migrate.sh @@ -18,7 +18,7 @@ for migration in /opt/lineageweave/migrations/*.sql; do migration_name=${migration##*/} case "$migration_name" in 0012_*|0013_*|0014_*|0015_*|0016_*|0017_*|0018_*|0019_*|0020_*|0021_*|0022_*|0023_*|0024_*|0025_*|0026_*|0027_*|0028_*|0029_*|0030_*|0031_*|0032_*|0033_*|0034_*|0035_*|0036_*|0037_*|0038_*|0039_*|0040_*|0041_*|0042_*|0043_*|0044_*|0045_*|0046_*|0047_*|0048_*|0049_*|0050_*) ;; - 0060_*|0100_*|0101_*|0102_*) ;; + 0060_*|0100_*|0101_*|0102_*|0185_*) ;; *) continue ;; esac printf 'Applying %s\n' "$migration_name" diff --git a/docs/adr/0003-fast-mlsirm-report-integration.md b/docs/adr/0003-fast-mlsirm-report-integration.md index bdf234b65..c08919c6c 100644 --- a/docs/adr/0003-fast-mlsirm-report-integration.md +++ b/docs/adr/0003-fast-mlsirm-report-integration.md @@ -100,9 +100,15 @@ than one large PR: `information_polytomous` (Lord, 1980 max-info). Persist the ranking (`report_item_information`) and show the rank-1 item on the Period reports panel. Do not reimplement an information function here. -7. **Leftover-pair slice** (shipped in 0.71.2; ADR 0017 / 0018): after +7. **Leftover-pair slice** (shipped in 0.71.2; ADR 0048 / 0049 / 0185): after IRT main effects, persist closest and farthest post–criterion pairs - from the residual leftover map. Do not fork LSIRM; do not invent a + from the residual leftover map, and name leftover-map cross share + `x = 2 R̂_c U_c / R̃²` of centered leftover when Gabriel coordinates + exist so the identity remainder after two-axis reconstruction is not + read as leftover residual, leftover-map distance, explained leftover + share, or unexplained leftover share. Do not persist leftover-map + explained leftover share `e`, unexplained leftover share `s`, or + two-axis reconstruction `R̂`. Do not fork LSIRM; do not invent a leftover-pair API inside `fast-mlsirm` in this slice. **TEPP boundary.** [ARCHITECTURE.md](../../ARCHITECTURE.md) already diff --git a/docs/adr/0048-persist-lsirm-leftover-pairs.md b/docs/adr/0048-persist-lsirm-leftover-pairs.md index 8e87383f0..722cf6bde 100644 --- a/docs/adr/0048-persist-lsirm-leftover-pairs.md +++ b/docs/adr/0048-persist-lsirm-leftover-pairs.md @@ -30,7 +30,14 @@ and one `farthest` observed cell per period report in `report_leftover_pair` (3NF, two-or-more-word `snake_case`). The biplot lives in `lineageweave/leftover_pairs.py` so leftover -tests do not import `period_report` or `fast_mlsirm`. +tests do not import `period_report` or `fast_mlsirm`. Each leftover +row also names leftover-map cross share `x = 2 R̂_c U_c / R̃²` of +centered leftover when Gabriel coordinates exist so the identity +remainder after two-axis reconstruction is not read as leftover +residual `R`, leftover-map distance `d`, explained leftover share +`e`, or unexplained leftover share `s` (ADR 0185). Two-axis +reconstruction `R̂_c` and unexplained leftover `U_c` are computed +internally and are not persisted. Cascade the rows with `report_period_score`. A leftover post must also be a `report_member_score` row, and the leftover criterion diff --git a/docs/adr/0049-leftover-pair-report-ui.md b/docs/adr/0049-leftover-pair-report-ui.md index a93985b40..9da16226e 100644 --- a/docs/adr/0049-leftover-pair-report-ui.md +++ b/docs/adr/0049-leftover-pair-report-ui.md @@ -19,8 +19,13 @@ On each period-report group, render leftover pairs **above** the member list. Each pair is a button: closest or farthest label, post title, criterion short label, leftover-map distance, and the next action (“Open this post to read the criterion it sat closest to / -farthest from after main effects.”). Clicking the button opens that -post with the same handler as a member row. +farthest from after main effects.”). Leftover-map cross share next +action: two leftover-map axes leave identity remainder `x` of +centered leftover after IRT main effects; open this post to read the +named criterion. A missing share keeps the existing closest/farthest +next action. Clicking the button opens that post with the same +handler as a member row. Leftover-map cross share naming is +[ADR 0185](0185-leftover-map-cross-share.md). After `make seed`, closest and farthest leftover pairs sit above the member list. Click a pair to open that post. diff --git a/docs/adr/0185-leftover-map-cross-share.md b/docs/adr/0185-leftover-map-cross-share.md new file mode 100644 index 000000000..3699d0947 --- /dev/null +++ b/docs/adr/0185-leftover-map-cross-share.md @@ -0,0 +1,110 @@ +# ADR 0185 — Name leftover-map cross share on period-report pair rows + +**Decision status:** Draft +**Date:** 2026-08-24 + +Amends [ADR 0048](0048-persist-lsirm-leftover-pairs.md) and +[ADR 0049](0049-leftover-pair-report-ui.md). + +## Context + +ADR 0048 already persists leftover-map distance `d = ‖ξ_p − ζ_i‖` and +leftover residual `R = Y − E[Y|θ, item]` on `report_leftover_pair`. +ADR 0049 already renders closest and farthest pairs above the member +list and opens the named post. Distance is the Jeon et al. (2021, +eq. 3) map gap. Gabriel (1971) reconstructs a *centered* matrix from +the biplot as the inner product of person and item coordinates. The +leftover map buyers read is two-axis: unused axes pad with zero, and +hidden SVD axes after the second are dropped. Two-axis reconstruction +`R̂_c = ξ_{1:2} · ζ_{1:2}` therefore recovers centered leftover +`R̃ = R − center`, not raw residual `R`. Truncated two-axis +reconstruction of a higher-rank cell keeps unexplained leftover +`U_c = R̃ − R̂_c` and a cross term `2 R̂_c U_c`. The cell identity +`R̃² = R̂_c² + U_c² + 2 R̂_c U_c` therefore yields +`e + s + x = 1` with explained leftover share `e = R̂_c² / R̃²`, +unexplained leftover share `s = U_c² / R̃²`, and leftover-map cross +share `x = 2 R̂_c U_c / R̃²`. Hiding `x` lets a buyer read `e + s` +as a complete leftover partition even though the truncated map leaves +an identity remainder. `x` may be negative when reconstruction and +unexplained leftover have opposite signs; a nonnegative CHECK would +reject a mathematically honest cell. + +This increment does not persist leftover-map reconstruction `R̂` or +`R̂_c`, does not persist leftover-map unexplained leftover `U` or +`U_c`, does not persist leftover-map unexplained leftover share `s`, +does not persist leftover-map explained leftover share `e`, does not +persist leftover-map coordinates, does not name leftover-map inner +product, cosine, or length, does not name observed `Y` / expected +`E`, does not name leftover-map rank, does not split leftover-map +distance onto two axes, and does not land Post quality on the leftover +criterion. Leftover-map distance stays full-rank Euclidean. +Reconstruction `R̂_c` and unexplained leftover `U_c` are computed +internally so `x` is honest, then discarded. + +The unprotected-stack reconstructions for neighbouring leftover facts +use 0162–0184. This protected-main increment uses **0185** so it does +not collide with leftover-map explained leftover share (0184), +leftover-map unexplained leftover share (0183), leftover-map +unexplained leftover (0182), leftover-map reconstruction (0181), +leftover-map length (0181 on the length stack), leftover-map cosine +(0180), leftover-map inner product (0179), leftover residual +disclosure (0178), leftover observed `Y` / expected `E` (0170), +leftover-map rank (0172), two-axis leftover-map distance (0166), +leftover coverage (0165 / 0168), leftover-map axis share (0148), or +leftover interaction-map persistence (0121). + +## Decision + +Each leftover pair names `leftover_map_cross_share` — leftover-map +cross share `x = 2 R̂_c U_c / R̃²` of centered leftover after +two-axis Gabriel reconstruction `R̂_c = ξ_{1:2} · ζ_{1:2}` and +unexplained leftover `U_c = R̃ − R̂_c`. Migration `0185` is the +single source of the column on every install path, fresh or existing +-- shipped migrations (`0001` / `0012`) are never edited after the +fact. The column is nullable so older leftover rows keep distance and +residual without fabricating a share. Fallback pairs that have no +complete-case leftover map omit the value rather than inventing one. +A rank-0 origin map stores `0.0` (`R̃ = 0`, `R̂_c = 0`, `U_c = 0`), +not a missing value. A fully reconstructed rank-1 cell stores `0.0` +because `U_c = 0`. A non-finite share stores null rather than +inventing a leftover score. A finite negative share is stored; do not +add a nonnegative CHECK. Do not persist +`leftover_map_explained_share`, `leftover_map_unexplained_share`, +`leftover_map_unexplained`, or `leftover_map_reconstruction`. + +The pair button shows `2R̂U/R̃² {share}` next to leftover-map +distance `d` when the value is a finite number, including a signed +negative remainder. Next action: two leftover-map axes leave identity +remainder `x` of centered leftover after IRT main effects; open this +post to read the named criterion. A missing or non-finite share omits +the badge and keeps the existing closest/farthest next action. Do not +invent a leftover score. Do not invent a theta. + +## Consequences + +`GET /api/reports/{grouping}/{period}` returns +`leftover_map_cross_share`. After `make seed`, closest and farthest +leftover pairs sit above the member list with named `2R̂U/R̃²` next +to `d`; click opens that post. Hidden posts stay hidden. + +## Related + +Independent of leftover interaction-map persistence, leftover-criterion +evaluation landing, leftover residual disclosure, leftover observed +`Y` / expected `E`, leftover-map complete-case coverage, leftover-map +axis share, leftover pairs on the grouping comparison strip, two-axis +leftover-map distance, leftover-map rank, leftover-map inner product, +leftover-map cosine, leftover-map length, leftover-map reconstruction, +leftover-map unexplained leftover, leftover-map unexplained leftover +share, and leftover-map explained leftover share. + +## References + +Gabriel, K. R. (1971). The biplot graphic display of matrices with +application to principal component analysis. *Biometrika, 58*(3), +453–467. https://doi.org/10.1093/biomet/58.3.453 + +Jeon, M., Jin, I. H., Schweinberger, M., & Baugh, S. (2021). Mapping +unobserved item–respondent interactions: A latent space item response +model with interaction map. *Psychometrika, 86*(2), 378–403. +https://doi.org/10.1007/s11336-021-09762-5 diff --git a/frontend/package.json b/frontend/package.json index e2e996bbe..52fc90c93 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "2.12.6", + "version": "2.12.29", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7462abd2c..75867a64f 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -932,6 +932,7 @@ describe("App, authenticated", () => { criterion_code: "sales_lead_specificity", leftover_distance: 0.12, leftover_residual: 0.4, + leftover_map_cross_share: 0.12, }, { pair_kind: "farthest", @@ -940,6 +941,7 @@ describe("App, authenticated", () => { criterion_code: "general_sentiment_negative", leftover_distance: 1.84, leftover_residual: -1.1, + leftover_map_cross_share: -0.24, }, ], members: [ @@ -3357,13 +3359,15 @@ describe("App, authenticated", () => { }); expect(closestPair).toHaveTextContent("Closest leftover: Public post · sales-lead"); expect(closestPair).toHaveTextContent( - "Open this post to read the criterion it sat closest to after main effects.", + "Two leftover-map axes leave identity remainder 0.12 of centered leftover after IRT main effects. Open this post to read sales-lead.", ); + expect(closestPair).toHaveTextContent("2R̂U/R̃² 0.12"); expect(closestPair).toHaveTextContent("d 0.12"); expect(farthestPair).toHaveTextContent("Farthest leftover: Specification revision requested · negative"); expect(farthestPair).toHaveTextContent( - "Open this post to read the criterion it sat farthest from after main effects.", + "Two leftover-map axes leave identity remainder -0.24 of centered leftover after IRT main effects. Open this post to read negative.", ); + expect(farthestPair).toHaveTextContent("2R̂U/R̃² -0.24"); expect(farthestPair).toHaveTextContent("d 1.84"); const memberButton = screen.getByRole("button", { name: /open report post: public post/i }); expect(closestPair.compareDocumentPosition(memberButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6fba0dd41..8e73b2012 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -101,7 +101,10 @@ import { tf, useLocale, } from "./i18n"; -import { rememberOidcReturnUrl, returnUrlFromLocation } from "./oidcReturnUrl"; +import { + formatLeftoverMapCrossShare, + LEFTOVER_MAP_CROSS_SHARE_ACTION, +} from "./leftoverMapCrossShare"; import "./App.css"; function orchestratorUnavailableMessage(err: unknown, action: string): string { @@ -3390,15 +3393,28 @@ function ReportsPanel({ )} {report.leftover_pairs && report.leftover_pairs.length > 0 && ( -