> **SUPERSEDED — HISTORICAL RECORD ONLY (archived 2026-07-18).** This document is part of the original "superpowers" wayleave build, whose design diverged from the projects-module patterns it should have mirrored. Do NOT use it as a pattern source for wayleave or any new module. The wayleave module is being realigned to the projects module (meta-driven fields, OpenLayers, shared JS) — see CLAUDE.md.

# Wayleave Module V4 Updates Implementation Plan

**Goal:** Resolve every issue raised in `docs/superpowers/plans/2026-04-27-wayleave_module-v3-feedback.md`. The single substantive change is replacing the always-on Select/Modify polygon-edit pattern with the explicit popup → Edit link → attributes-sidebar → Save/Cancel flow used by `project_edit_v2.js`.

**Execution:** Tasks are implemented sequentially. User tests and approves each task before the next begins. No sub-agents.

**Reference files (do not modify):**
- `www/js/project_edit_v2.js` — map editing flow reference
- `www/html/html_body_projectedit.php` — sidebar/button markup reference
- `www/css/project_edit_v2.css` — `#sidebarAttributes`, `#sidebarLayers`, `.feature-popup` reference

**Conventions:**
- No build system. Edit `.css`/`.js` directly.
- All AJAX endpoints in `www/fn/` return `echo json_encode(['success'=>true/false, ...])`.
- Do not modify save endpoint `wayleave_coverage_polygon_save.php` write logic.

---

## What changes and why

| Current behaviour | Problem | New behaviour |
|---|---|---| 
| `ol.interaction.Select` always active — clicking a boundary immediately enters edit mode | Clashes with UPRN-point clicks; clicking a point to inspect it also selects the polygon underneath | `singleclick` collects ALL features at the pixel and shows a unified popup list |
| `ol.interaction.Modify` always active | Modifyend auto-saves every drag — excessive DB calls + audit log noise | Modify is only added after the user clicks the Edit link in the popup |
| No explicit save step | User has no way to review/cancel geometry edits | Attributes sidebar with Save Map Edits + Cancel; geometry committed only on Save |
| Separate UPRN popup (OL overlay positioned to coordinate) | Two different popup systems on one map | One popup mechanism for everything; UPRN attributes listed the same way as boundary attributes |
| Delete Polygon enabled by select interaction | Confusing — can delete without being in "edit mode" | Delete Polygon only enabled while in explicit edit mode |

---

## Task 1: Unified popup for all features at a pixel

**Why first:** Every subsequent change (Edit link, sidebar, Save/Cancel) layers on top of the popup. Establishing the unified click handler first makes it easy to verify before wiring the editing flow.

**Files:**
- Modify: `www/js/wayleave_edit_v2.js`
- Modify: `www/html/html_body_wayleaveedit.php`
- Modify: `www/css/wayleave_edit_v2.css`

### Step 1.1 — Replace the OL overlay popup with a DOM-anchored popup

The current wayleave popup is an `ol.Overlay` positioned at a coordinate. The projectedit popup (`.feature-popup`) is a DOM element whose position is set by the `ol.Overlay` wrapper. Keep the `ol.Overlay` approach (it auto-follows the map), but reuse the existing `#wl-view-popup` element. No HTML change is needed for the popup itself — it stays as-is.

Tag `polyLayer` with a title and `isEditable` flag immediately after it is created in `initMap()`:

```js
polyLayer = new ol.layer.Vector({ source: viewPolySource, style: polyStyle, zIndex: 500 });
polyLayer.set('title', 'Wayleave Boundary');
polyLayer.set('isEditable', true);
```

Tag each entity point layer with a title (no `isEditable`):

```js
layerEntityPolygon   = new ol.layer.Vector({ ..., title: 'UPRNs – boundary',  zIndex: 511 });
layerEntityDirect    = new ol.layer.Vector({ ..., title: 'UPRNs – direct',     zIndex: 512 });
layerEntityStocklist = new ol.layer.Vector({ ..., title: 'UPRNs – stocklists', zIndex: 513 });
layerEntityTitle     = new ol.layer.Vector({ ..., title: 'UPRNs – titles',     zIndex: 514 });
```

(Use `new ol.layer.Vector({ source: ..., style: ..., zIndex: ..., title: '...' })` — OL accepts arbitrary options that are stored and retrievable via `.get('title')`.)

### Step 1.2 — Rewrite `singleclick` to the projectedit multi-feature pattern

Remove the existing `singleclick` handler (currently checks `layer === entityLayers`) and replace it with:

```js
var popupEnabled = true;

viewMap.on('singleclick', function (evt) {
    if (!popupEnabled) return;

    var featuresAtPixel = [];
    viewMap.forEachFeatureAtPixel(evt.pixel, function (feature, lyr) {
        featuresAtPixel.push({ feature: feature, layer: lyr });
    }, { hitTolerance: 6 });

    if (!featuresAtPixel.length) { hidePopup(); return; }

    // Build popup HTML — one <li> per feature found.
    var html = '<ul style="list-style:none;padding:0;margin:0;">';
    featuresAtPixel.forEach(function (item) {
        var f   = item.feature;
        var lyr = item.layer;
        var props = f.get('props') || f.getProperties();
        var layerTitle = (lyr && lyr.get('title')) || 'Feature';
        var isEditable = lyr && lyr.get('isEditable');

        html += '<li style="padding:6px 0;border-bottom:1px solid #eee;">';
        html += '<strong>' + WL.escapeHtml(layerTitle) + '</strong>';

        if (isEditable) {
            // Boundary polygon — show label + edit link
            var label = f.get('label') || ('ID ' + (f.get('wl_polygon_id') || ''));
            html += ' — ' + WL.escapeHtml(label);
            html += ' <span class="float-end"><a href="#" class="wl-edit-feature-link text-dark text-decoration-none">'
                  + '<i class="fas fa-pencil-ruler"></i> Edit</a></span>';
        } else {
            // UPRN point — show UPRN + address + sources
            var uprn    = props.uprn    || '';
            var address = props.address || '';
            var sources = (props.sources || []).join(', ');
            if (uprn)    html += '<br><small>UPRN: '    + WL.escapeHtml(uprn)    + '</small>';
            if (address) html += '<br><small>'          + WL.escapeHtml(address) + '</small>';
            if (sources) html += '<br><small class="text-muted">Sources: ' + WL.escapeHtml(sources) + '</small>';
            if (props.released) html += '<br><small class="text-success">Released</small>';
        }

        html += '</li>';
    });
    html += '</ul>';

    document.getElementById('wl-view-popup-content').innerHTML = html;
    document.getElementById('wl-view-popup').style.display = 'block';
    popupOverlay.setPosition(evt.coordinate);

    // Wire Edit links after innerHTML is set.
    document.querySelectorAll('.wl-edit-feature-link').forEach(function (link, i) {
        var item = featuresAtPixel.filter(function (x) { return x.layer && x.layer.get('isEditable'); })[i];
        if (!item) return;
        link.addEventListener('click', function (e) {
            e.preventDefault();
            hidePopup();
            startEditingPolygon(item.feature);
        });
    });
});
```

### Step 1.3 — Verify

- Click a UPRN point → popup shows UPRN/address/sources, no edit link.
- Click a polygon boundary → popup shows boundary label + Edit link.
- Click empty map → popup hides.
- At a location where a boundary and a UPRN overlap → both entries appear in the popup.

**Commit:** `feat(wayleave): unified feature-at-pixel popup matching projectedit pattern`

---

## Task 2: Explicit edit mode — attributes sidebar + Save / Cancel

**Why:** With Task 1 providing the Edit link entry point, now wire the full edit flow: enter edit mode → modify geometry → Save (commit to DB) or Cancel (restore).

**Files:**
- Modify: `www/html/html_body_wayleaveedit.php`
- Modify: `www/js/wayleave_edit_v2.js`
- Modify: `www/css/wayleave_edit_v2.css`

### Step 2.1 — Add `#wl-sidebar-attributes` and update the button panel in HTML

Inside `#wl-tab-map > .wl-map-wrap`, directly after `#wl-view-sidebar-layers`, add:

```html
<div class="wl-map-sidebar" id="wl-sidebar-attributes">
    <div class="sidebar-content p-3">
        <h6 id="wl-attr-title" class="mb-3">Editing Polygon</h6>
        <p class="m-1">
            <button type="button" id="wl-save-map-edits" class="btn btn-xs btn-success" disabled>
                <i class="fa fa-save"></i> Save Map Edits
            </button>
        </p>
        <p class="m-1">
            <button type="button" id="wl-cancel-edit" class="btn btn-xs btn-secondary">
                <i class="bi bi-x-circle"></i> Cancel
            </button>
        </p>
        <hr>
        <p class="m-1">
            <button type="button" id="wl-map-delete" class="btn btn-xs btn-danger" disabled>
                <i class="bi bi-trash"></i> Delete Polygon
            </button>
            <button type="button" id="wl-map-delete-yes" class="btn btn-xs btn-danger d-none">Yes, Delete</button>
            <button type="button" id="wl-map-delete-no"  class="btn btn-xs btn-success d-none">No, Cancel</button>
        </p>
    </div>
</div>
```

Update `#wl-map-buttons` to remove Delete (now in sidebar) and add Attributes toggle + Save Map Edits:

```html
<div id="wl-map-buttons" class="wl-map-button-box">
    <p class="m-1">
        <button type="button" id="wl-view-layers-toggle"     class="btn btn-xs btn-light"><i class="fa fa-layer-group"></i> Map Layers</button>
    </p>
    <p class="m-1">
        <button type="button" id="wl-attributes-toggle"      class="btn btn-xs btn-light"><i class="fa fa-layer-group"></i> Attributes</button>
    </p>
    <p class="m-1">
        <button type="button" id="wl-map-draw"               class="btn btn-xs btn-success"><i class="bi bi-pencil-square"></i> Draw Polygon</button>
    </p>
    <p class="m-1">
        <button type="button" id="wl-save-map-edits-panel"   class="btn btn-xs btn-success" disabled><i class="fa fa-save"></i> Save Map Edits</button>
    </p>
</div>
```

> **Note:** `#wl-save-map-edits-panel` in the button panel and `#wl-save-map-edits` in the sidebar both call the same `saveMapEdits()` function. The panel button follows the projectedit `#mapSaveEdits` button position; the sidebar button provides a contextual Save next to Cancel.

### Step 2.2 — Add sidebar CSS

In `wayleave_edit_v2.css`, add after the existing `.wl-map-sidebar` block:

```css
/* Attributes sidebar — same glass treatment as the layers sidebar */
#wl-sidebar-attributes {
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 80%;
    max-width: 260px;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
    z-index: 9999;
    background: rgba(255, 255, 255, 0.4);
    backdrop-filter: blur(2px) saturate(180%);
    -webkit-backdrop-filter: blur(2px) saturate(180%);
    box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2),
                inset 0 4px 20px rgba(255, 255, 255, 0.3);
    display: flex;
    flex-direction: column;
}
#wl-sidebar-attributes.open { transform: translateX(0); }
#wl-sidebar-attributes .btn { width: 100%; }
```

### Step 2.3 — Remove always-on Select/Modify; add `startEditingPolygon` / `cancelEditing` / `saveMapEdits`

**Remove from `initMap()`:**
- The `selectInteraction` and `modifyInteraction` variable declarations and `map.addInteraction` calls.
- The `selectInteraction.on('select', ...)` handler that enabled/disabled `#wl-map-delete`.
- The `modifyInteraction.on('modifyend', ...)` handler that auto-called `savePolygon`.

**Keep:**
- `drawInteraction` and `startDraw()` — the Draw Polygon button is unchanged.
- `popupEnabled` flag from Task 1.

**Add edit state variables (IIFE scope):**

```js
var editingFeature = null;   // the feature currently being modified
var editModify     = null;   // the active ol.interaction.Modify (null when not editing)
```

**Add `editStyle` — mirrors projectedit's `editstyles`:**

```js
var editStyle = [
    new ol.style.Style({
        stroke: new ol.style.Stroke({ color: 'blue', width: 3 }),
        fill:   new ol.style.Fill({   color: 'rgba(185,199,255,0.4)' })
    }),
    new ol.style.Style({
        image: new ol.style.Circle({
            radius: 6,
            stroke: new ol.style.Stroke({ color: 'black', width: 1 }),
            fill:   new ol.style.Fill({   color: 'rgb(255,221,0)' })
        }),
        zIndex: 9000,
        geometry: function (feature) {
            var coords = [];
            try { coords = feature.getGeometry().getCoordinates()[0]; } catch (e) {}
            return new ol.geom.MultiPoint(coords);
        }
    })
];
```

**Add `startEditingPolygon(feature)`:**

```js
function startEditingPolygon(feature) {
    if (editingFeature) cancelEditing();   // guard: finish any prior edit

    editingFeature = feature;
    feature.set('originalGeometry', feature.getGeometry().clone());
    feature.setStyle(editStyle);

    editModify = new ol.interaction.Modify({ features: new ol.Collection([feature]) });
    viewMap.addInteraction(editModify);

    // Mark as edited on any vertex drag.
    editModify.on('modifyend', function () { feature.set('edited', true); });

    var label = feature.get('label') || ('Polygon ' + (feature.get('wl_polygon_id') || ''));
    $('#wl-attr-title').text('Editing: ' + label);
    openAttrSidebar();
    $('#wl-save-map-edits, #wl-save-map-edits-panel').prop('disabled', false);
    $('#wl-map-delete').prop('disabled', false).removeClass('d-none');
    $('#wl-map-delete-yes, #wl-map-delete-no').addClass('d-none');
    popupEnabled = false;
}
```

**Add `cancelEditing()`:**

```js
function cancelEditing() {
    if (!editingFeature) return;
    var orig = editingFeature.get('originalGeometry');
    if (orig) { editingFeature.setGeometry(orig); editingFeature.unset('originalGeometry'); }
    editingFeature.setStyle(null);   // revert to layer style
    editingFeature.unset('edited');
    if (editModify) { viewMap.removeInteraction(editModify); editModify = null; }
    editingFeature = null;
    closeAttrSidebar();
    $('#wl-save-map-edits, #wl-save-map-edits-panel').prop('disabled', true);
    $('#wl-map-delete').prop('disabled', true);
    popupEnabled = true;
}
```

**Add `saveMapEdits()`:**

```js
function saveMapEdits() {
    if (!editingFeature) return;
    var polygonId = editingFeature.get('wl_polygon_id');
    if (!polygonId) { WL.toast('Cannot save: polygon has no server ID', 'err'); return; }
    savePolygon('update', polygonId, featureToGeojsonGeom(editingFeature));
    // On save, clean up edit state (savePolygon fires a toast on completion).
    editingFeature.setStyle(null);
    editingFeature.unset('originalGeometry');
    editingFeature.unset('edited');
    if (editModify) { viewMap.removeInteraction(editModify); editModify = null; }
    editingFeature = null;
    closeAttrSidebar();
    $('#wl-save-map-edits, #wl-save-map-edits-panel').prop('disabled', true);
    $('#wl-map-delete').prop('disabled', true);
    popupEnabled = true;
}
```

**Add sidebar helpers (mirror `openSidebar`/`closeSidebar` from projectedit):**

```js
function openAttrSidebar()  { $('#wl-sidebar-attributes').addClass('open'); }
function closeAttrSidebar() { $('#wl-sidebar-attributes').removeClass('open'); }
```

**Update `wireMapButtons()`:**

```js
function wireMapButtons() {
    $('#wl-map-draw').on('click', startDraw);
    WL.wireSidebarToggle('wl-attributes-toggle', 'wl-sidebar-attributes');

    $('#wl-save-map-edits, #wl-save-map-edits-panel').on('click', saveMapEdits);
    $('#wl-cancel-edit').on('click', cancelEditing);

    // Delete confirm swap (same as before, but now inside sidebar).
    $('#wl-map-delete').on('click', function () {
        $('#wl-map-delete').addClass('d-none');
        $('#wl-map-delete-yes, #wl-map-delete-no').removeClass('d-none');
    });
    $('#wl-map-delete-no').on('click', function () {
        $('#wl-map-delete-yes, #wl-map-delete-no').addClass('d-none');
        $('#wl-map-delete').removeClass('d-none');
    });
    $('#wl-map-delete-yes').on('click', function () {
        if (editingFeature) {
            var polygonId = editingFeature.get('wl_polygon_id');
            if (polygonId) savePolygon('delete', polygonId, null);
            viewPolySource.removeFeature(editingFeature);
        }
        cancelEditing();
    });
}
```

### Step 2.4 — Update Draw flow to disable popup while drawing

In `startDraw()`, add `popupEnabled = false;` after adding the draw interaction. In the `drawend` handler, add `popupEnabled = true;` after removing the draw interaction.

### Step 2.5 — Verify

- Cold-load map tab. No Select/Modify interactions are active by default.
- Click a polygon boundary → popup lists the boundary with an Edit link. Click away → popup closes.
- Click Edit → popup hides; attributes sidebar slides in with label + Save/Cancel/Delete.
- Drag a vertex → vertex turns yellow (edit style applied). Click Save → geometry saved; sidebar closes; polygon returns to normal blue style.
- Click Cancel → polygon returns to its original shape (original geometry restored); sidebar closes.
- Click Delete → Yes/No appears. Yes → polygon deleted from DB + map; No → reverts.
- Click a UPRN point → popup shows UPRN/address/sources with no Edit link.
- At a mixed location (boundary + UPRN) → both appear in one popup.
- Draw Polygon button still draws new polygons; sidebar does not open during draw.

**Commit:** `feat(wayleave): explicit polygon edit mode with attributes sidebar matching projectedit`

---

## Feedback → Task matrix

| Feedback item | Task |
|---|---|
| Editing a boundary by clicking it enters edit immediately, interfering with UPRN popup clicks | T1 — unified popup replaces both handlers |
| Auto-save on every drag creates excessive DB writes + audit log entries | T2 — geometry committed only on Save |
| Map editing style/flow does not match `project_edit_v2.js` | T1 + T2 |
