Field officers reported three problems with the drawing palette: it had no
background and disappeared over light imagery, it sat centred at the top across
the area being drawn, and its icons did not match one another. All three had
distinct causes.
Layout. The bar is now a two-column rail docked to the left edge on a solid
panel, with its controls re-ordered into labelled Draw / Edit / Shape / File
bands by MapView._buildToolbarRail(). Two columns because a single file of
thirteen buttons plus labels runs past the bottom of a laptop map view; the
rail measures 75 x 363 px. Adding ol-ext's own `ol-left` class also makes
option bars (Select's Delete/Info, Split's sub-tools) fly out to the right
rather than dropping downwards.
Icons. ol-ext ships no icon font or sprite sheet — it draws each built-in tool
as CSS ::before/::after shapes from borders and box-shadows, which is why our
own Bootstrap-Icon tools looked foreign next to them. The generated shapes are
suppressed and every button gets an icon from a single TOOLBAR_ICONS map. The
glyph is injected as a <span>, not an <i>, because ol-ext styles
`.ol-transform > button i::before` and would otherwise out-specify Bootstrap's
`.bi::before` and blank the icon.
Phones. The rail becomes a horizontally scrolling strip above the dock, which
retires the two-row flex-break workaround and its `.ol-editbar-break` element.
Touch targets stay at 36 px, and option bars open upward.
Two things worth recording for anyone changing this later. The rail must not
set `overflow` on either axis: the other axis then computes to `auto` too and
clips the option bars that fly out past its right edge. And `display` in the
phone media query must stay free of `!important`, since ol-ext shows and hides
the whole bar with an inline style.
Also picks up ol-offset, an EditBar default that was never explicitly enabled
and had been sitting in the bar unlabelled; it is grouped under Shape.
Verified in a headless browser at desktop and phone widths, in light and dark
mode: icons, grouping, active-tool tint, option-bar placement, and that the bar
still hides when Draw mode is off.
Service Worker v14 -> v15.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reference notes for transforming Accra / Ghana National Grid geometry to WGS 84
in PostGIS: ST_Transform, ST_SetSRID for columns reporting SRID 0, updating a
column in place, and altering a column whose type modifier locks it to 2136.
Relevant wherever data arrives on the national grid while the API and the
application work in 4326 throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two documents, both for work the database team will carry out. No application
code changes.
OSM re-import runbook
---------------------
The roads endpoint returns geom, districtid, osm_id, surface, oneway and name
for district 1 — no `highway`. The road class was discarded at import, which is
what osm2pgsql's default.style does to any tag not on its list, so roads cannot
be styled or filtered by class. A discarded tag cannot be recovered by querying;
the only fix is to import again, and if that is happening anyway it is worth
doing on terms that allow the data to be kept current afterwards.
The runbook covers a flex import with --slim and --extra-attributes, producing
four tables — roads, lines, points, polygons — plus daily replication. Two
decisions are specific to LUPMIS:
* The import goes into its own `osm` schema. osm2pgsql --create drops and
recreates every table it owns, and `spatial` holds lu_parcels; the API is
given views in `spatial` instead, so the existing get_osm_roads.php keeps
working and simply gains `highway`.
* districtid is stamped by a spatial join after each import, since osm2pgsql
has no concept of a district.
Executed against osm2pgsql 2.2.0 and a scratch PostGIS database with a hand-made
extract covering every shape the config handles. All four tables imported
cleanly; updatable and attributes both true; amenities mapped as node, closed
way and multipolygon relation all reached the points table, as did a
healthcare-tagged node with no amenity — three cases the current layout cannot
serve. An --append then applied a diff and left districtid NULL on the changed
row while untouched rows kept theirs, which is the behaviour the incremental
re-stamp in section 5.2 depends on.
Not verified: anything needing the LUPMIS database itself — the current import
state, the district boundary table's real name, row counts. Section 1 is the
set of checks to run first.
External layer tables
---------------------
DDL for storing layers added through the Add External Layer dialog:
spatial.hlp_layer_types available layer types (wms, wfs, xyz, cog)
spatial.es_external_layers one row per layer a user has added
Layer types live in a table rather than a CHECK constraint so a new kind can be
introduced by inserting a row. The rule that WMS and WFS require a layer name
moved there too as requires_layer_name — left in the schema it would have meant
a migration for every new type anyway. Validity is enforced by a foreign key and
the conditional rule by a trigger, since a CHECK cannot read another table; the
trigger raises check_violation so existing API error handling still applies.
Beyond the dialog's four fields the table carries districtid (NULL = every
district), userid and is_shared, plus the columns needed to represent the
external layers the application already hard-codes — style, opacity, z_index,
attribution, legend_url, online_only. Without those, moving the existing
DEAfrica slope layer into the database would lose information the map relies on;
it is included as a seed row to prove the schema can hold it.
Executed against PostgreSQL 16. Nine cases pass, including: a new type added as
data with no DDL and usable immediately, that type enforcing its own
requires_layer_name, a retired type blocked for new layers while existing ones
stay editable, and deletion of a type still in use being refused.
The application still hard-codes the four types and will until
get_layer_types.php exists, so the dialog and hlp_layer_types must be changed
together until then — noted in the file, as the two fail in opposite directions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preventive, not incident-driven. No LUPMIS host is known to have been
compromised; these rules exist so the most common automated attack against a
PHP deployment cannot succeed.
Scanners probe every public host for /.env (database passwords, API keys) and
/.git/. The second is the more damaging: given /.git/index together with
/.git/objects an attacker can reconstruct the whole repository, including its
history — so any credential ever committed is exposed even after rotation.
That is directly relevant here, because the minio-uploads integration has had
access keys written into its PHP files and those files are tracked in Gitea.
- public/.htaccess: deny hidden paths and files that should never be served.
- docs/apache-hardening.conf: the same rules for vhost/server config, which is
where they belong — .htaccess depends on AllowOverride. Also covers logging
the real client address through the openresty proxy (mod_remoteip), and
blocking at the proxy so requests never reach the application.
Two details that are easy to get wrong, and are handled:
- <FilesMatch "^\."> does NOT stop /.git/config. FilesMatch tests the basename
only, and there the basename is "config". Blocking a hidden directory needs a
rule that sees the whole path: mod_rewrite in .htaccess, <DirectoryMatch> in
server config.
- The SPA fallback only rewrites paths that do not exist (!-f), so a real .env
on disk skips it and is served as a plain file. The deny rules therefore run
before the fallback, not after.
.well-known is exempt, or ACME certificate renewal would silently break.
Verified against a live Apache instance with a planted .env and .git tree:
/.env and /./.env return 403; /.git/config, /.git/HEAD, /.git/objects/... and
/backup.sql return 404; /.well-known/acme-challenge/..., /assets/*.js and
/manifest.json still return 200. Nothing in the document root is caught — the
only dotfile there is .htaccess, which Apache never serves.
These rules are damage limitation. The fix is to keep .git and .env out of a
document root: deploy build output rather than a working copy, and hold secrets
in environment variables outside the served tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contours hillshade layer
- Wired to the new endpoint POST /api/contours/get_by_district.php. It sits in
its own /contours module rather than /spatial_planning, so remotedb gained a
resolveEndpoint() helper: a bare filename still resolves inside
/spatial_planning, a path resolves against the API root. The endpoint also
names the district parameter `districtid` (numeric, no underscore) unlike
every other endpoint, which is an easy trap — documented at the call site.
- Each contour is labelled with its `elev` value. Labels are decluttered
(addGeoJSONLayer gained a declutter passthrough, since OpenLayers only allows
it at construction time) and gated by resolution, so the map shows an
occasional elevation value rather than one per segment.
- Verified against the live endpoint: district 1 returns 2,676 features, 11
elevation values at 10 m intervals, median feature ~50 m. The response uses
the standard {success, data:[…]} envelope; the reader also tolerates a bare
array.
Analyse: fail loudly instead of silently
- Overlays are polygon-only, but line inputs were being filtered out silently,
producing an empty result and the misleading "the layers may not overlap".
They now raise a message naming the geometry actually found and pointing at
Zonal statistics. Union checks each layer separately, so a line layer paired
with a polygon layer can no longer be dropped unnoticed.
- Zonal statistics returns warnings alongside the result — shown in the panel
and written into the Excel workbook — for figures that are valid but easy to
misread: total area over non-polygon inputs (always 0), and mean over line
inputs, which is not an area-weighted average of the surface. The mean
warning is restricted to lines; averaging over points is a normal statistic.
Measurements are read-only
- The Measurements layer carries selectable:false, honoured by both the Select
interaction and box-select. Previously a measured circle showed vertex
handles while the Digitise tool was active, and dragging one reshaped the
measurement: the intersection test followed the new shape while the reported
radius and area did not. Double-click analysis is unaffected — it hit-tests
the map directly rather than going through Select.
- Circle Analysis now reads the radius from the geometry instead of the
`_radius` snapshot taken at draw time, so the reported figure cannot drift
from the area analysed (that figure also feeds the PDF export). The property
is kept but marked as a snapshot, not a source of truth.
Documents
- New Buffer (Circle) tool user guide: the tool is named Circle rather than
Buffer, it takes two clicks rather than a drag, and the analysis only appears
on a double-click — the three things users were getting stuck on.
- New technician training plan: 12 topics over 60 days, derived from the
technologies actually used in this code base, including Docker.
- Analyse guide: elevation per zone from the contours layer, with the
min/max-versus-mean caveat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Excel export (from TWG feedback)
- src/analysis/xlsx.js: a dependency-free XLSX writer — an .xlsx is a ZIP of
XML, so this packs the required parts with a small stored-ZIP writer. Avoids
SheetJS (stale npm package with advisories) and ExcelJS (heavy for an
offline-first field app), and does not rely on the JSZip that only reaches us
transitively via shp-write. Lazy-loaded as a ~5.6 kB chunk.
- After a zonal run the Analysis panel offers "Export table (Excel)", writing a
two-sheet workbook: Results (figures as real numbers) and Parameters (zone
and input layers with feature counts, the "Apply to" scope, membership rule,
statistics, numeric field and export time) — so a table can be verified or
reproduced later rather than being an unattributed set of numbers.
The button is hidden for overlay runs and cleared when the mode changes.
- Verified against two independent readers: openpyxl loads it with zero
warnings and correct numeric types, and LibreOffice Calc opens it as a
spreadsheet. Also exercised end-to-end through the real zonal pipeline.
COG raster entry point
- Add External Layer gains a COG type alongside WMS/WFS/XYZ, with a URL
pre-flight check that distinguishes a web page, a 404, a CORS block and a
server without byte-range support — geotiff.js otherwise reports these only
as an opaque "AggregateError: Request failed".
Digital Earth Africa ETL
- etl/deafrica_dem_to_minio.py exports a DE Africa DEM for a district as a COG
and uploads it to the LUSPA MinIO bucket raster-objects, then verifies the
object is anonymously readable and range-capable. Credentials come from the
environment; the existing PHP integration hardcodes them and an earlier key
pair reached Gitea. NOTE: not yet run against the Sandbox — start with
--list-products to confirm the DEM product name.
Documents
- Concept note: Digital Earth Africa added as a raster source (§6.3), separating
the live WMS route from the batch Sandbox export; corrected the in-house
contour table's provenance to OpenTopography (gdal_contour over an SRTM 30 m /
Copernicus 30 m DEM), and added the ToR §2.4.2 / FS §2 alignment chapter.
- TWG presentation, architecture and two integration workflow charts (SVG
sources kept in the repo so they stay editable), and a user guide for the
Analyse tools.
Service worker v13 -> v14. .gitignore: exclude Python bytecode from etl/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 1 of the GIS Analytical Tools concept — client-side spatial analysis:
- src/analysis/overlay.js: vector overlays (intersect / clip / difference /
union-dissolve) on Turf.js, reprojecting to WGS84 and merging attributes
(intersect keeps both layers', clip keeps only A's).
- src/analysis/zonal.js: vector-in-vector zonal statistics — count / sum /
mean / min / max / total area per zone, with centroid-in-zone (default) or
any-overlap membership.
- Bounding-box pre-filtering in both: only genuinely overlapping pairs reach
the expensive geometry test. 43 zones x 25,004 parcels now completes in
~106 ms; previously it was refused as too large.
- src/analysis-modal.js + markup: Analysis panel with "Apply to" scoping —
whole layer, current map view, selected features, or the catch of a drawn
Circle/Area. Reached from a new "Analyse" dock button.
- MapView.addCOGLayer() for Cloud-Optimized GeoTIFF display (WebGLTile +
GeoTIFF source, imported lazily); listVectorLayers(); getSelectedFeatures().
- Circle/Area analysis popup: one "Export" button (PDF folded into the export
modal as a fourth format, field-rename table hidden for it) plus an
"Analyse" button that opens the panel pre-scoped to the intersecting
features.
- vite.config.js: code-split turf, geotiff and pako so the eager bundle is
unchanged (~283 kB). Giving pako its own chunk also fixes a circular chunk
between jspdf and geotiff, which share it via fast-png.
Drawing-tool fixes carried in the same working tree:
- Delete/Backspace key deletes the selection via the EditBar's Delete
interaction (same undoable block as the button).
- Multi-select: shift-click toggles, Ctrl/Cmd-drag box-selects; helper layers
(vertex overlay, GPS) excluded.
- Undo: split/merge/divide wrapped in undo blocks so one press reverses the
whole operation; vertex-overlay churn no longer pollutes the undo stack;
sources are re-scanned and the stack cleared when edit mode is entered, so
deletes on sub-grouped layers are undoable. NOTE: the undo behaviour is not
yet confirmed on-device.
- ol-ext TouchCursor gated to touch-only devices, so hybrid touchscreen
laptops keep the normal cursor.
Service worker v12 -> v13.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The landing page is the dispatch hub for the other LUSPA modules. Add a plain
navigation link to https://lupmis4luspa.org/ that keeps the SSO session intact
(no /?logout=1, no cache wipe), shown only when authenticated. Logout remains
the full SSO sign-out via /user-logout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /?logout=1 endpoint destroyed only the PWA's own PHP session and then
redirected to the bare landing page — which has no session and immediately
blocks access, so the user was never actually logged out of SSO.
- public/index.php: after session_destroy(), redirect to
https://lupmis4luspa.org/user-logout (the portal's full SSO logout) instead
of the landing page. Crucially, no longer clear sso_auth_token here —
/user-logout needs that cookie to identify which SSO session to terminate
(and it clears the cookie itself). The production access-guard bounce to the
landing page is unchanged.
- main.js: drop the now-redundant best-effort client call to /sso/logout; the
server redirect chain (/?logout=1 → /user-logout) owns the SSO logout. Offline
guard and district-cache wipe unchanged.
- sw.js: update the v12 changelog note (still v12; not yet deployed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Session / district correctness
- public/index.php: add a /?logout=1 endpoint that destroys the PWA's own PHP
session (session_destroy + expire PHPSESSID + clear sso_auth_token, then
redirect to the SSO portal). Logout previously cleared only the SSO cookie,
leaving the PHPSESSID session — and its frozen district_id — intact, which is
why a reassigned user kept loading the old district across logout/login.
- SSO token is validated once per session, at login (unchanged first-login
logic). A district transfer is now picked up on the next logout→login, which
is correct precisely because logout finally tears the session down. No
periodic SSO polling.
- main.js: the menu Logout button routes through /?logout=1 and wipes
district-scoped local caches first. Logout is blocked while offline — a
session can only be created online, so an offline logout would strand the
user with no way back in (and would not actually reach the server).
- main.js: enforceDistrictConsistency() clears district-scoped caches when the
session district changes between loads; the district boundary is cached under
a per-district key (district_boundary_<id>) so one district's geometry can
never be served for another.
GPS coordinate format
- New "GPS Coordinate Format" setting (Lat/Lon · UTM · Both) in the Settings
panel; the navbar read-out renders the chosen format and repaints the current
fix immediately on change. Self-contained WGS84→UTM converter in
geo-utils.js, verified against an independent Redfearn-series implementation.
ol-ext touch cursor
- MapView gates the TouchCursor to genuine touch-only devices via matchMedia
(any-pointer: fine / any-hover: hover); hybrid touchscreen laptops keep the
normal cursor. Reactive to pointer-capability changes.
- Service worker v11 → v12 (new shell). docs/SSO_Session_Refresh_Proposal.md
documents the implemented approach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Default interaction mode changed from 'addLocation' to 'none' so that
opening the app does not silently arm the Add-Location cursor; users
must explicitly click the Add button to enter that mode.
- Add button now toggles: a second click returns to 'none' mode, matching
the behaviour of the Measure and Draw buttons.
- All other tool buttons (Circle, Line, Area, Draw) now return to 'none'
on toggle-off instead of re-activating Add-Location.
- Removed the hard-coded 'active' CSS class from the Add button in
index.html, consistent with the new default state.
- Added INSPIRE_HILUCS_Workshop.pptx (13 slides) as a workshop reference
deck covering INSPIRE Land Use Data Specifications, the HILUCS hierarchy
(3 levels, 13 top-level classes), example HILUCS↔LUPMIS2 zone-code
mappings, and a structured PROs/CONs analysis for LUSPA adoption,
including a pragmatic localisation recommendation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Evicts the stale shell + module caches at the next deploy so existing
clients pick up the permit-iframe auth-required card, the new
X-Frame-Options strip, and the import-UX refinements (parse spinner,
client_uuid tagging, geometry/delete persistence, sample values +
Unicode-bold field names in the mapping dropdown).
Co-Authored-By: Claude Opus 4.7 (1M context) <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>
UPN-grid layer:
- src/database.js — new upn_grid SQLocal table (id, districtid, upn_prefix,
geometry_wkt) + saveUpnGrid / getLocalUpnGrid; cache-once-per-district.
- src/remotedb.js — getUpnGrid → get_upn_grid_per_district.php.
- main.js loadUpnGrid + upnGridToGeoJSON in the Administration group, with
a zoom-aware style: white casing under a bolder violet dashed stroke
(visible against parcels) and upn_prefix labels rendered only when
resolution ≤ 7 m/px (≈ scale ≤ 1:25,000).
- main.js click handler: single click on a UPN-grid cell opens an info
popup showing the upn_prefix.
External-dataset import → staging → upload (client-side complete):
- src/database.js — external_imports + external_import_features tables,
plus createExternalImport / addExternalImportFeatures /
updateExternalImport / getExternalImport / getExternalImportFeatures /
listExternalImports / remapImportedFeatureProperties /
deleteExternalImport. Status enum: imported/mapped/other/uploading/
submitted/migrated/failed (aligned with the database team's staged-
upload model — lu_parcels_upload_tmp + supervisor review).
- src/import-detect.js — pure helpers: detectTargetType(),
autoMapFields(), applyFieldMapping(), listSourceFields() + TARGET_TYPES
/ TARGET_FIELDS registries.
- src/import-modal.js — Bootstrap mapping modal: target dropdown,
field-rename table, three actions (Cancel / Save / Save + Upload now).
- main.js — stageImport hooked into addImportedGeoJSON (the single
convergence point for shp/GeoJSON/KML drops); handleImportModalResult
applies the mapping in one transaction; runUpload builds the real
payload (district_id + api_token from remotePost, user_id_upload from
SSO session, per-feature client_uuid/geom/props) and currently logs +
toasts — the upload_<target>.php endpoints are not yet live.
- index.html — #importMappingModal markup.
- MapView._decorateLayerListItem — import-state chip (Upload N /
spinner / ✓ submitted / ✓ live / N errors) dispatching
lupmis:import-chip-click; src/styles/layerswitcher.css — chip variants.
GIS export from Area / Circle Analysis popups:
- MapView._showAnalysisPopup now accepts an exportContext (clipGeometry +
parcelFeatures + zoneFeatures + otherByLayer) and renders an "Export
GIS" button next to "Export PDF". Click dispatches lupmis:export-gis.
- index.html — #exportGisModal markup.
- src/export-gis-modal.js — Bootstrap modal: format toggle (GeoJSON
default / Shapefile / KML), filename, field-rename table with SHP
10-char DBF warning.
- src/gis-export.js — writers: GeoJSON via Blob, KML via OL KMLFormat,
Shapefile via shp-write (with DBF-safe name sanitiser).
- Adds shp-write@0.3.2 dependency.
MapView style options:
- addGeoJSONLayer now accepts strokeDash for line-dash patterns (used by
the UPN-grid layer and available for any future contextual overlay).
Service Worker v9 → v10 to evict the stale shell/module caches on the
next deploy.
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>
Major:
- GPS trail recording: reusable, dependency-free engine in src/geotracker/
(GeoTracker + geo-utils) with pluggable storage/sync adapters; LUPMIS
wiring in src/geotracker-lupmis.js. Expandable My Location control
(Locate Me + Record Trail), live navbar GPS readout, on-map trail/position
rendering, gps_trails/gps_trail_points SQLocal tables, and store-and-forward
sync via pushGpsTrail() -> save_gps_trail.php (server side documented, not
yet built).
- SSO authentication: public/index.php entry point validates the LUSPA SSO
cookie and injects window.LUPMIS_SESSION; remotedb district_id is now a
session-resolved getter. Adds public/.htaccess (DirectoryIndex).
- Account menu offcanvas (navbar burger) with sign-in/out states.
UI / fixes:
- LayerSwitcher modernisation; base-map "None" option in picker + settings.
- Mobile drawing toolbar wraps to two rows below 576px and shows only in
Draw mode; second row right-aligned and clears the Select option bar.
- Safari bottom-dock clipping fixed (app-container 100dvh -> 100svh).
- Rename public/icons -> app-icons to dodge Apache's default /icons/ alias.
- Service Worker bumped to v8 (network-first HTML, per-provider tile clear).
Docs: reusable-mapping and OSM-3D-buildings concept notes; ignore Office
lock files (~$*). Rebuilt dist/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comprehensive .gitignore covering:
* node_modules/ (re-installable from package-lock.json) — also untracks
the 5 679 files that were carried over from the initial commit
* Vite dependency cache (.vite/) — pure build churn
* IDE state: BBEdit (*.bbprojectd/), VS Code, JetBrains, Zed, Sublime,
Vim swap files
* OS metadata: macOS .DS_Store / ._*, Windows Thumbs.db, etc.
* Interim Word-document backups (*-v[0-9].docx pattern), env files,
test coverage, common cache directories
dist/ deliberately NOT ignored — the repo currently serves the built
output directly. If you switch to a CI deploy later, uncomment the
dist/ lines in .gitignore.
After this commit, `git status` will be empty until real source changes
are made (no more node_modules / .vite cache noise).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>