End of 2026-05-19 cutover-debug session. Saves the full state + 2 outstanding bugs + Faza G/H plan into docs/plans/ so the next session can resume without re-investigating. - prompt-handoff-2026-05-19.md: short prompt for the next session to amend + resume. - audit-2026-05-19.md: auditor-agent output (~30 findings). - 004-faza-h-runbook.md: pg_dump + REVOKE + DROP runbook with prereqs (eterra.live shares DB, unidentified writer, CfExtract schema drift). Memory entries also written: - feedback/authentik-token-endpoint-shared - project/architools-cutover-state-2026-05-19 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
ArchiTools → api.gis.ac cutover — audit 2026-05-19
Scope: every commit on main from 6b3d56e (pre-cutover baseline) through 68355ef (HEAD).
Reviewer: Claude Opus 4.7 — read-only audit; no code changes.
Pilot user: m.tarau@beletage.ro (gated via GIS_AC_PILOT_USERS).
Commits in scope:
68355ef fix(geoportal-v2): UAT click deep-links to eterra.live + revert debug
1c6efb9 debug(auth): expose session.debug={hasRefreshToken, expiresIn}
3829401 feat(ops): VersionWatcher — toast prompt when a new deploy is live
64bccdb feat(ops): /api/version endpoint with git SHA + build time
6054d08 fix(faza-e): refresh dedup, fetch timeout, error surfacing
47ca366 fix(auth): Authentik access_token refresh flow
e0610b0 fix(geoportal-v2): handle PMTiles features without uuid id
7a22b11 debug(gis-search): log session presence + access token presence
99a673d feat(geoportal): Faza E v2 thin client (PMTiles + gis.ac)
fc2bdfb feat(gis-api): Faza D thin client lib (src/lib/gis-api-client.ts)
977db6d feat(cutover): Faza C feature-flag infra for api.gis.ac
403b6b3 feat(auth): Faza B NextAuth Authentik scope=enrichment + forward access_token
54b78c2 feat(deploy): Faza A Infisical runtime migration
Files changed (19): Dockerfile, docker-compose.yml, docker-entrypoint.sh, src/middleware.ts, src/app/providers.tsx, src/core/auth/auth-options.ts, src/core/feature-flags/use-gis-ac.ts, src/core/version/version-watcher.tsx, src/lib/gis-api-client.ts, src/app/api/version/route.ts, src/app/api/gis/{search,parcela/[id],parcel/tech}/route.ts, src/modules/geoportal/components/geoportal-module.tsx, src/modules/geoportal/v2/*.tsx (5 files).
1. Production safety (flag=0 path)
[ship]Legacyparcel-sync/eterra/ancpisource trees are untouched by this session's commits.git diff 6b3d56e..HEAD --name-only -- src/modules/parcel-sync/ src/modules/eterra/ src/modules/ancpi/ src/app/api/ancpi/ src/app/api/eterra/returns empty. Non-pilot users see exactly the same code path as before.[ship]src/config/{modules,navigation,flags,companies,external-tools,nas-paths}.tsandsrc/core/feature-flags/index.tsare unchanged. No regression risk in module registration or sidebar/feature-flag wiring.[ship]geoportal-module.tsxis the only legacy module file touched: it importsuseSession, readssession.useGisAc, branches toGeoportalV2only when true. The legacy renderer (GeoportalV1Legacy) is the verbatim previous body. For flag=0 users the render output is identical to baseline.[ship]middleware.tsaddsapi/versionto the exclusion regex. No other matcher change. The version route is intentionally public (no auth), as is correct for a build-ID endpoint.[ship]auth-options.tschange is backwards-compatible: callbacks still mint the samerole/companyclaims; new fields (accessToken,error,useGisAc) are additive on the session object. Users withoutaccount.access_token(rare, but possible on cached sessions) skip the refresh branch via thetoken.accessToken && token.refreshTokenguard — no hot-loop risk.[cosmetic]VersionWatchertoast component is global (mounted inproviders.tsx). It fires for everyone, not just pilot. For non-pilot users this is fine — they get a "new build available" nudge after each redeploy, which is desirable.
2. Secret hygiene
[ship]No hard-coded secrets in any committed file.AUTHENTIK_CLIENT_ID/SECRET/ISSUER,GIS_API_URL,NEXTAUTH_SECRET, etc. are allprocess.env.Xwith safe||fallbacks (""for secrets, public URLs for non-secrets).[ship]docker-entrypoint.shuses--datawith$INFISICAL_CLIENT_ID/$INFISICAL_CLIENT_SECRETonly in a JSON body sent to Infisical over HTTPS; nothing echoed. The[infisical] exported env -line only reports presence (${VAR:+set}), never values. Good.[ship]Hard-coded URLs present and acceptable:https://api.gis.ac(gis-api-client.ts:18, env-overridable)https://pmtiles.gis.ac/overview.pmtiles(map-viewer.tsx:39, env-overridable viaNEXT_PUBLIC_PMTILES_URL)https://eterra.live/harta?...(geoportal-v2.tsx:39, feature-info-panel.tsx:157) — intentional cross-app deep link, fine.https://tools.beletage.rofallback (middleware.ts) — pre-existing, not introduced this session.
[cosmetic]src/app/api/version/route.tsexposesuseGisAcDefault(boolean) andpilotUsers(count only) — no email leakage. Safe.[ship]console.log("[auth] refresh OK expires_in=%d", body.expires_in)logs only the integer (seconds-until-expiry), never the token itself. Theconsole.warnpaths logres.status+body.error(Authentik error code likeinvalid_grant), no token bodies. Safe.
3. TypeScript correctness
[ship]NODE_ENV=development npx tsc --noEmit— exits 0, zero errors, zero warnings.
4. Dead / orphan code
[future]gisApi.parcel.unitsFetch,gisApi.parcel.immApps,gisApi.building.tech,gisApi.building.condoOwners,gisApi.enrichment.cf.{list,get,create,patch,uploadPdf,getPdf},gisApi.enrichment.catalog,gisApi.me— declared insrc/lib/gis-api-client.ts, no callers undersrc/. These are Faza F surface (CF ordering + parcel detail tabs). Acceptable to land ahead of consumers since they share helper infrastructure. Bundle cost: lib is server-side only and tree-shaken in production builds.[future]GisApiCallOpts.accessTokenoverride path: every public method acceptsopts.accessTokenbut no caller passes it. Intended for future background jobs/service accounts. Keep.[cosmetic]objectIdfield onClickedFeatureLiteis computed fromp.object_idinmap-viewer.tsx:248-254, set on the panel feature, but never read byfeature-info-panel.tsx. Minor — costs nothing but should be removed if no consumer materializes by Faza F.[future](session as any).erroris set when refresh fails; no client component currently readssession.errorto force re-login. The map-viewer fetches will silently 401 and the user will see "Eroare: gis_api_401" in the panel without auto-redirect. Add a session-error watcher in Faza F polish.
5. Faza E gaps vs plan 003
Plan 003 §Faza E specified five rewrites: map-viewer.tsx, search-bar.tsx, feature-info-panel.tsx, basemap-switcher.tsx, plus the v2 wrapper. All five exist under src/modules/geoportal/v2/. Gaps vs spec:
[future]UAT bounds / flyTo: Plan §Faza E implicitly assumedgisApi.searchwould return UAT bounds for clientfitBounds. Today gis-api returns{siruta, name, county}only. Workaround ingeoportal-v2.tsx:34-41: click on UAT openseterra.live/harta?siruta=…in a new tab. Acceptable as documented; tracked as "addGET /api/v1/uat/:siruta/boundsto gis-api". Severity = future, not blocker.[future]Layer panel removed entirely from v2. v1 hadLayerPanel+getDefaultVisibilitycontrolling per-layer visibility (UATs/cladiri/terenuri toggles). v2 hard-codes all four layers on. Pilot user may complain about not being able to toggle building outlines. Minor UX regression vs v1.[future]Selection toolbar removed entirely from v2. v1 hadSelectionToolbar+selectionMode(off/point/rectangle/polygon) for multi-select export workflows. v2 has no multi-select. Plan §Faza E was silent on this; reasonable to defer until usage is observed.[future]SetupBannerremoved. Was a no-op in v1 once setup completed, so no regression. Skip.[future]boundary-check/cf-status/export/pad/pizbuttons in the panel. Plan §Faza E: "defer scoping per 002." v2'sfeature-info-panel.tsxhas only three actions:Citește din ANCPI(parcel/tech),Export GeoPackage(eterra.live deep-link),Comandă CF(disabled — "Va fi disponibil la Faza F"). Aligned with plan; not a gap.[blocker for global rollout, ship for pilot]Map starts at Romania-wide zoom 6 with no auto-fit on click. When user clicks an UAT in search, behavior = open eterra.live in new tab (not flyTo). Pilot probably aware; broader rollout needs the bounds API or a fallbackfitBoundsfrom the parcel polygon's bbox.[cosmetic]"gis.ac · v2" badge bottom-right (geoportal-v2.tsx:83-85) — intentional, fine. Remove on global rollout.
6. Infisical drift (satra .env vs /architools prod)
Cross-checked ssh satra "grep ^[A-Z] /opt/architools/.env" (41 keys) vs Infisical /architools prod (39 keys).
Drift category A — both places (31 keys, app-secret duplication):
ADDRESSBOOK_API_KEY, ANCPI_* (5), AUTHENTIK_CLIENT_ID, AUTHENTIK_CLIENT_SECRET, AUTHENTIK_ISSUER, BREVO_API_KEY, DATABASE_URL, DWG2DXF_URL, ENCRYPTION_SECRET, ETERRA_PASSWORD, ETERRA_USERNAME, MANICTIME_TAGS_PATH, MINIO_* (6), NEXTAUTH_SECRET, NEXTAUTH_URL, NOTIFICATION_* (3), PORTAL_ONLY_USERS, STIRLING_PDF_* (2).
[ship]Behaviour is correct because the newdocker-compose.ymlno longer references these keys in theenvironment:block (onlyINFISICAL_CLIENT_ID/SECRET,NODE_ENV,PORT,HOSTNAMEremain).docker-entrypoint.shfetches them from Infisical at boot andexports them. The .env duplicates are dead bytes for the running container — never read by compose.[future]Drift is still a maintenance hazard. If a future operator rotates a secret in .env and forgets Infisical, nothing breaks (compose ignores the .env value), but reading/opt/architools/.envto "find the current value" gives a false answer. Recommendation: prune.envto bootstrap-only keys (INFISICAL_CLIENT_ID,INFISICAL_CLIENT_SECRET,NODE_ENV, and the build-timeNEXT_PUBLIC_MARTIN_URL/NEXT_PUBLIC_PMTILES_URL) after the cutover stabilizes.
Drift category B — only in .env (10 keys):
INFISICAL_CLIENT_ID,INFISICAL_CLIENT_SECRET— bootstrap, must stay in .env.NODE_ENV— compose reads${NODE_ENV:-production}, can stay or be removed (default kicks in).DB_HOST,DB_NAME,DB_PASS,DB_PORT,DB_USER— only used by thetippecanoeservice (compose line 84-88), which hasprofiles: ["tools"]and is started ad-hoc. Belongs in .env. Acceptable.NEXT_PUBLIC_MARTIN_URL,NEXT_PUBLIC_PMTILES_URL— build-time args (Dockerfile lines 26-30) baked into the static client bundle. They must be in .env atdocker compose buildtime. Cannot move to Infisical without entrypoint-side build (not viable forNEXT_PUBLIC_*). Acceptable.
Drift category C — only in Infisical (8 keys):
AI_PROVIDER,AI_MAX_TOKENS— referenced in compose pre-refactor; no longer in composeenvironment:block but still fetched by entrypoint and present in Infisical. AI module reads them viaprocess.env.AI_*. OK.AUTHENTIK_JWKS_URL— set in Infisical but not consumed by current code. NextAuthAuthentikProviderusesissuerto derive JWKS via discovery. Dead key (until/unless gis-api side validates with explicit JWKS URL — that's a gis-api concern, not architools). Cosmetic.AUTHENTIK_SCOPES="openid email profile enrichment offline_access"— verified present; this is howoffline_accessis actually getting into the auth flow (overrides the code default"openid email profile enrichment"). Critical for refresh_token issuance from Authentik. The fact that code default doesn't includeoffline_accessis an audit finding — if someone deletesAUTHENTIK_SCOPESfrom Infisical, refresh stops working silently. See §7 finding.GIS_AC_PILOT_USERS— Faza C pilot list. Live and used.GIS_API_URL— Faza D thin client target. Live.N8N_WEBHOOK_URL— PMTiles rebuild webhook (existing functionality, was on satra .env before). Compose no longer references it; entrypoint pulls from Infisical andparcel-syncreads fromprocess.env. Working.USE_GIS_AC— Faza C cutover global toggle. Currently expected to be""(only pilot user enabled).
7. Diagnostic logs left in code
All emitted only on real errors (.warn / .error) except the one .log:
[cosmetic]console.log("[auth] refresh OK expires_in=%d", ...)atauth-options.ts:69— fires every ~5min for every active session. Will be high-volume in container logs. Suggest demote toconsole.debugor gate behindprocess.env.AUTH_DEBUG === "1". Not a blocker — values are integers, not tokens.[ship]console.warn("[auth] refresh failed: …")/console.warn("[auth] refresh error: …")— fires on failure only. Keep as-is, this is real ops signal.[ship]console.error("[gis-search]", "[gis-parcela]", "[gis-parcel-tech]" "internal error: …")— error-path only, no token leakage (message truncated to 200 chars viamsg.slice(0, 200)). Keep.[ship]console.warn("[v2-click] tile props missing siruta/cadastral_ref:", p)— fires only when PMTiles emits an unexpected feature shape. Useful diag, low volume. Keep.[ship]The earlier7a22b11debug log ingis-search/route.tsand1c6efb9session.debugexposure were both reverted in6054d08and68355efrespectively. Verified — neither leaks into HEAD.
Code default scope omits offline_access. Plan 003 §Faza B specified scope: "openid profile email enrichment". The code reads process.env.AUTHENTIK_SCOPES || "openid email profile enrichment". The production-required offline_access (for refresh_token issuance) is only present because the Infisical value overrides the default. [future] Tighten the default to include offline_access so a missing Infisical key doesn't break refresh.
8. UX gotchas the pilot user might hit
[ship]Search-dropdown UAT click opens new tab to eterra.live (geoportal-v2.tsx:34-41). Intentional, documented in code comment. Pilot has been told. Track: addGET /api/v1/uat/:siruta/boundsto gis-api so v2 canfitBoundsin-place.[ship]Pilot users (only) see V2; everyone else sees V1. Confirmed ingeoportal-module.tsx:30-40:useSession()→Boolean(session.useGisAc)→ render V2 or V1Legacy. Server-sideuseGisAcFlagreadsUSE_GIS_ACenv (currently unset ="") andGIS_AC_PILOT_USERS(only the pilot email). Non-pilot users land on V1 with byte-identical render output to pre-cutover.[future]Search results dropdown shows UATs and parcele but does not snap the map. UAT click → external tab (above). Parcel click → opens info panel immediately viasetClicked({…})withsiruta: ""(empty, line 47), so the "Citește din ANCPI" button will fail withmissing_siruta_or_cadbecausefeature.sirutais empty for search-originated features. The map-click path passes siruta correctly; the search-click path doesn't. Severity: blocker for pilot productivity if they expect to find parcels by cadastral search then enrich them. Test plan should cover this case.[cosmetic]"gis.ac · v2" badge in bottom-right corner. Aesthetic. Will be removed at global rollout.[cosmetic]"Comandă CF" button in v2 panel is disabled with tooltip "Va fi disponibil la Faza F". Pilot may try to click, see disabled state. Acceptable.[future]No re-login UX whensession.error === "RefreshAccessTokenError". The session keeps a stale token, every gis-api call returns 401, panel shows raw error code. Adding a session-error watcher inproviders.tsx(or extendingVersionWatcher) would close the loop.[future]Map basemap switch tears down and rebuilds the whole MapLibre instance (map-viewer.tsx:198-283—useEffectdepends onbasemap). PMTiles source has to re-fetch metadata each time. On slow networks this is noticeable. v1 had the same issue; not a regression.[future]PMTiles overview tile coverage. v2 uses a singleoverview.pmtilesfrompmtiles.gis.ac. If that file's contents don't include UATs the pilot needs (e.g. a UAT not yet in the central DB), the map is silently empty for that area. v1 was backed by satra Martin and had different sync state. Worth a sanity check before broad rollout.
Verdict
Ready to enable for the pilot user broadly: YES (with one caveat).
The flag=0 path is byte-identical to pre-cutover; legacy code is untouched. TypeScript clean. Secret hygiene clean. No console leaks. The only pilot-blocking finding is §8 third bullet — clicking a parcel in the search dropdown sets siruta: "", which breaks the "Citește din ANCPI" button. That should be patched before relying on cadastral-search workflows. Either:
(a) gis-api's /search response carries siruta per feature (preferred), or
(b) handleFeatureSelect in geoportal-v2.tsx does a follow-up parcela.get(f.id) to hydrate siruta before showing the panel.
Ready for global rollout (flip USE_GIS_AC=1): NO. Three blockers/gaps need to land first:
- The siruta-empty-from-search bug above.
- UAT-bounds endpoint in gis-api so search → flyTo works in-place (no eterra.live tab).
- Faza F shipped (CF ordering parity) — currently the v2 panel's "Comandă CF" button is disabled. The work is half-done in the working tree (
src/app/api/cf/*,cf-api-base.ts, modifiedepay-tab.tsx) but not committed to main.
Operational improvements (no rollout impact):
- Demote
[auth] refresh OKto debug-level. - Add
offline_accessto the code-defaultAUTHENTIK_SCOPES. - Prune duplicate keys from
/opt/architools/.envafter a few weeks of stable Infisical bootstrap. - Wire
session.error === "RefreshAccessTokenError"to a re-login UX. - Remove unused
objectIdfield onClickedFeatureLiteif nothing materializes by Faza F.