Four changes that together get a signed-in officer back to working data.
The SSO validation in index.php and embed.php called the public hostname from
inside the server. The Lands Commission's NAT rule blocks traffic that leaves
the server and tries to come back in, so that curl stalled for its full
30-second timeout and then failed validation, bouncing the user back to the
login. Both now call http://l4lmainbackend/sso/validate, overridable with
LUPMIS_SSO_BASE. Measured from inside the spdrawing container: 25 ms with no
redirect, against a 10-second timeout on the public name.
API_ROOT is now the relative path /api. The PWA host already proxies it to the
ApiEngine container — a deliberate route, not a catch-all: /api answers on pwa.
and api. but not on permits., minio. or minioapi. Same-origin removes the
mixed-content, CORS and CORP constraints that Cross-Origin-Embedder-Policy:
require-corp imposes on a cross-origin call, and no API hostname is compiled
into the bundle any more. The dev server proxies /api to the public host, which
is right there because a developer's machine is outside the server and only
callers inside it are blocked from the public name.
A direct request for /index.html bypassed index.php, which is what injects
window.LUPMIS_SESSION, so the app started with no session at all and said so in
the menu. The LUSPA landing page links to exactly that URL, and the SPA
fallback could not help because its RewriteCond !-f fails precisely when the
file exists. .htaccess now routes it through index.php; index.php reads
index.html from the filesystem, so there is no loop. This one is inert until
AllowOverride is relaxed on the PWA container, where it is currently None.
Service worker v29, and v28 before it, which carries the GIS Tools rename to
devices that already have the app installed. The tile cache stays pinned at
v24, so downloaded map areas survive both updates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Permit-iframe hardening:
- public/embed.php — replace the 302 redirect on unauthenticated visits
with an in-iframe HTML "Sign in to view the map" card (HTTP 401)
whose primary button uses target="_top" to break the iframe and send
the parent window to the SSO portal. The 302 was broken UX inside an
iframe because the LUSPA portal refuses to be framed.
- public/embed.php + public/.htaccess — strip X-Frame-Options at the
embed endpoint (defence in depth). Apache's <Files "embed.php">
Header always unset X-Frame-Options + PHP's header_remove() both
ensure the only iframe-policy header on the response is our CSP
frame-ancestors (which already allows the permits subdomain). Fixes
Safari's "Refused to display ... because it set 'X-Frame-Options'
to 'SAMEORIGIN'" when the container's reverse proxy injects it.
Import UX refinements:
- Spinner overlay (index.html #import-spinner-overlay + main.js
showImportSpinner/hideImportSpinner) shown during the file-drop →
mapping-modal gap. Wired at the top of each handle*Import and at
every error / early-return path; hidden by stageImport() just before
openImportMappingModal() so it spans both the JS parse and the
SQLocal staging insert.
- Per-feature client_uuid tagging — each imported OL feature now
carries _externalImportId + _clientUuid set in stageImport(). These
tags are the link that lets later edits find the matching staging
row, and they are passed through to addExternalImportFeatures.
- Geometry-edit persistence — new public callback registry
MapView.onFeatureModified(cb) fired from a modifyend listener on
_modifyInteraction. main.js handler writes the new WKT (EPSG:4326)
back to external_import_features.geometry_wkt via new helper
updateExternalImportFeatureGeometry(clientUuid, wkt). Non-imported
features carry no tags, so the handler is a no-op for them.
- Delete persistence — removefeature listener on each imported layer's
source. New helper deleteExternalImportFeature(clientUuid) runs an
atomic DELETE + decrement of external_imports.feature_count and
broadcasts the changes so the LayerSwitcher badge can recount.
- Field-mapping dropdown — sample values + bold field names.
New helpers sampleSourceValues(fc) in import-detect.js (picks first
non-empty value per attribute, JSON-stringifies objects, collapses
whitespace, truncates to 35 chars) and toBoldUnicode(s) in
import-modal.js (ASCII letters/digits → Mathematical Alphanumeric
Symbols block). Options now read as "𝐮𝐩𝐧 — [12345-6789]";
HTML/CSS bold doesn't render inside <option> elements, so Unicode
bold codepoints are the cross-browser way.
Workshop deliverables:
- LUPMIS2_Improvements_Mar_to_Jun_2026.docx — handout mirroring the
slide deck one-to-one (160 paragraphs, branded styling).
- LUPMIS2_Workshop_Mar_to_Jun_2026.pptx — 16-slide pptxgenjs deck
(16:9 widescreen, brand palette, hero + content + closing masters,
embedded staged-upload diagram on slide 9).
- LUPMIS2_Staged_Upload_Flow.svg + .png — three swim-lane diagram of
the staged-upload pipeline with a dedicated "Client QA Gate"
callout. Hand-crafted SVG + 2400 px PNG.
save_gps_trail.php diagnosis (no code change, on the database team):
the reported "CORS" error is a missing endpoint — Apache returns 404
with no CORS headers and the browser surfaces it as access-control.
Once the endpoint is deployed the API server's global CORS handling
attaches the right headers and the GPS-trail sync will work without
client changes.
dist/ rebuilt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Iframe embed for the Permitting app (LUPMIS2_Reusable_Mapping_Concept §3.2):
- public/embed.php — SSO + production gate + frame-ancestors CSP +
whitelisted URL params (mode, lon/lat/zoom, upn, basemap,
application_code); injects window.LUPMIS_SESSION + window.LUPMIS_EMBED.
- public/.htaccess — clean /embed URL (rewrite before the SPA fallback).
- src/embed-bridge.js — postMessage protocol: out ready / parcel:select /
parcel:cleared / error; in set:view / set:selected / clear:selected /
set:basemap. Visual highlight via a dedicated VectorLayer; pending-UPN
queue resolved as parcels stream in.
- main.js — reads window.LUPMIS_EMBED, gates the normal click/dblclick
handlers in permit mode, exposes parcelsLayer to module scope, makes
it visible and hands it to the bridge after loadParcels().
- index.html — CSS for body.embed-mode-permit hides navbar/dock/offcanvas
and lets the map fill the iframe.
- LUPMIS2_Permit_Map_Integration.docx — integration instructions for the
Permitting team (contract, show.blade.php changes, phasing).
Local lu_parcels structural refactor:
- src/database.js — parcels table now mirrors spatial.lu_parcels with
explicit columns (upn, style, landuse, zone_code/name, sector, block,
parcel_no, prop_no, st_name, prop_add, fac_name, min/max_height,
eff_date, lp_name, locality, mmda, last_update, remarks, geom→geometry_wkt,
created_at, updated_at, districtid) plus local-only status/fetched_at.
Drop-and-recreate migration off `upn` presence. saveParcels wraps the
~25k inserts in a transaction; numeric coercion via numOrNull.
updateParcel/insertNewParcel write individual columns.
- main.js parcelsToGeoJSON — handles GeoJSON `geom` object (API) and
`geometry_wkt` string (local cache); skips housekeeping fields.
Production access guard + no-district overlay:
- public/index.php — on *.lupmis4luspa.org, redirect to the SSO portal
if no session.
- src/remotedb.js resolveDistrictId — no silent fallback to '1' for an
authenticated user; dev mode (no session at all) keeps the fallback.
- main.js — blocking overlay if the session lacks district_id; init
aborts so no API call is made with the wrong scope.
LayerSwitcher ordering fix:
- MapView.initEditBar + MapTools — find the Overlays group by reference
/ title instead of assuming it's the last layer (the GPS layers
add-layered on top in the constructor broke that assumption).
Service Worker v8 → v9 to evict stale shell/module caches on deploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>