From aa03ea75ef8a977c931ca47a40a6ffe57b1ce755 Mon Sep 17 00:00:00 2001 From: crzykidd Date: Sun, 17 May 2026 10:09:34 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20v0.6.5=20=E2=80=94=20edit=20page=20poli?= =?UTF-8?q?sh,=20universal=20delete,=20widget=20modal,=20refresh=20pause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 43 +++++ CLAUDE.md | 6 +- README.md | 4 +- routes_dashboard.py | 24 +++ static/css/dashboard.css | 218 +++++++++++++++++++++++- static/js/dashboard.js | 222 +++++++++++++++++++++---- templates/base.html | 2 + templates/dashboard.html | 32 +++- templates/edit_entry.html | 152 +++++++++-------- templates/partials/delete_popover.html | 9 + templates/partials/widget_modal.html | 12 ++ templates/tiled_dash.html | 26 ++- 12 files changed, 629 insertions(+), 121 deletions(-) create mode 100644 templates/partials/delete_popover.html create mode 100644 templates/partials/widget_modal.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c5ce10..f1f1ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.5] — 2026-05-17 + +UI overhaul, part 2 (release 3 of 3): edit page polish, a universal delete +pattern, click-to-view widget data, and an auto-refresh that knows not to wipe +out your work while you're looking at it. + +### Added + +- **Trash icon on Tiled tiles and Dashboard rows.** One click deletes a dynamic + entry, with a small inline confirm popover anchored to the icon. Static + (Locked) entries show a lock icon in the same slot instead — they can only be + deleted from the edit page, which is the deliberate path for protected entries. +- **Widget data modal.** Clicking the chart icon on a tile now opens a modal + showing just that service's widget data. Faster than expanding the full drawer, + especially on mobile. The chevron drawer still works as before. +- **Auto-refresh pauses while you're interacting.** Open a drawer, the widget + modal, or the changelog popup, and the page won't refresh out from under you. + The refresh timer shows "Refresh paused" while interaction is active and + resumes (counter reset to 0) when you close everything. + +### Changed + +- **Edit page visual polish.** Tighter input padding and border-radius, section + headings (Identity / URLs & Health / Grouping & Display / Widget), tighter + spacing within sections and looser between, muted helper text. URL fields now + group their health-check checkbox inline below the input instead of floating + it to the side. The "Select Existing / Add New" group selector is now a + tab-style toggle instead of bullet radios. +- **Universal delete UI.** All delete affordances — drawer Delete button, edit + page Delete button, and the new trash icons — use the same small inline + popover with a Confirm button. The typed-name confirmation form is gone; + static entries are protected by being deletable only from the edit page. +- **Lock icon replaces 🔒 emoji on Dashboard rows.** Consistent visual language + with the new Tiled lock indicator. Same meaning: "this entry is locked from + notifier updates; delete from the edit page." +- **Drawer Delete button hidden for static entries.** The tile-level lock icon + signals the constraint; the drawer no longer duplicates it. + +### Removed + +- **Danger Zone section on the edit page.** Replaced by a single Delete button + at the bottom of the form, using the new popover. + ## [0.6.4] — 2026-05-17 UI overhaul, part 2 (release 2 of 3): mobile usability and tile restructure. diff --git a/CLAUDE.md b/CLAUDE.md index ffb5996..3e5fa40 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ ## Build Status -Current shipped release: **v0.6.3** (latest tag on `main`) +Current shipped release: **v0.6.4** (latest tag on `main`) Status: @@ -56,8 +56,8 @@ Status: - v0.6.1 — UI overhaul part 1 (tiled redesign, drawer, shared CSS/JS): DONE - v0.6.2 — hotfix: restore `toggleRestoreSource`, upload filename feedback: DONE - v0.6.3 — UI overhaul part 2 foundations (orphan sweep, inline-style cleanup, what's new popup): DONE -- v0.6.4 — UI overhaul part 2: mobile usability + tile icon restructure: IN PROGRESS (on dev) -- v0.6.5 — UI overhaul part 2 (edit page polish + universal delete popover): TBD +- v0.6.4 — UI overhaul part 2: mobile usability + tile icon restructure: DONE +- v0.6.5 — UI overhaul part 2 (edit page polish, universal delete, widget modal, refresh pause): IN PROGRESS (on dev) - v0.7.0+ — TBD (no scoped features at present) ## Git Workflow diff --git a/README.md b/README.md index bc32559..dd4a759 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ through the web UI or directly via API. ## What It Does -- Three dashboard views (table, tiled, compact). Tiled view shows each service as a tile with a per-tile expand drawer for full host, URL, Docker, network, port, exposure, and widget detail. Icons use [Tabler Icons](https://tabler.io/icons) v3.34.0 (loaded via CDN). +- Three dashboard views (table, tiled, compact). Tiled view shows each service as a tile with a per-tile expand drawer for full host, URL, Docker, network, port, exposure, and widget detail. Icons use [Tabler Icons](https://tabler.io/icons) v3.34.0 (loaded via CDN). Clicking the chart icon on a tile opens a widget-data modal without expanding the full drawer. - **Dashboard view controls (v0.6.0+).** A `Group by` axis selector (`group` / `stack` / `host`) and a `Show URL-less` filter render above the service grid on all three views. State is URL-driven, so @@ -52,7 +52,7 @@ through the web UI or directly via API. - Register endpoint for pushing container metadata from external tools. - SQLite backing store, file-based, no external DB server. - Daily YAML backups with retention. -- Manual add / edit / delete through the web UI. +- Manual add / edit / delete through the web UI. Tiled tiles and Dashboard rows have a one-click trash icon with an inline confirm popover. Static (Locked) entries show a lock icon and can only be deleted from the edit page. - Optional Dozzle log link integration. - Local user accounts with session-based auth. - Per-entry sort priority within groups; grouping and group sort. diff --git a/routes_dashboard.py b/routes_dashboard.py index 1b6c7a0..811027b 100644 --- a/routes_dashboard.py +++ b/routes_dashboard.py @@ -976,6 +976,30 @@ def edit_entry(id): selected_widget=selected_widget) +@dashboard_bp.route('/api/v1/entries//delete', methods=['POST']) +@login_required +@is_admin_required +def delete_entry_json(id): + """JSON delete endpoint for JS-triggered deletes (tile trash icon, drawer delete button). + + Returns {ok: true} on success. The caller is responsible for DOM cleanup or redirect. + """ + entry = ServiceEntry.query.get_or_404(id) + + if entry.widget_id: + other_services = ServiceEntry.query.filter( + ServiceEntry.widget_id == entry.widget_id, + ServiceEntry.id != entry.id + ).count() + if other_services == 0: + WidgetValue.query.filter_by(widget_id=entry.widget_id).delete() + Widget.query.filter_by(id=entry.widget_id).delete() + + db.session.delete(entry) + db.session.commit() + return jsonify({'ok': True}) + + @dashboard_bp.route('/api/v1/changelog') @login_required def changelog_api(): diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 80229b3..996fbe5 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -53,8 +53,8 @@ background-color: var(--color-bg-surface); color: var(--color-text-secondary); border: 1px solid #4a5568; - padding: 0.625rem 1rem; - border-radius: var(--radius-input); + padding: 0.5rem 0.75rem; + border-radius: 0.375rem; font-size: 0.875rem; width: 100%; transition: border-color 0.2s ease, box-shadow 0.2s ease; @@ -707,3 +707,217 @@ padding: 8px 14px; } } + +/* ── Delete popover ───────────────────────────────────────── */ +.delete-popover { + position: fixed; + z-index: 300; + max-width: 280px; + background-color: var(--color-bg-raised); + border: 1px solid var(--color-border-input); + border-radius: var(--radius-tile); + padding: 10px 14px 12px; + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.65); +} +.delete-popover.hidden { display: none; } +.delete-popover-message { + font-size: 0.85rem; + color: var(--color-text-secondary); + margin-bottom: 10px; +} +.delete-popover-message strong { color: var(--color-text-primary); } +.delete-popover-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} +.delete-popover-cancel { + font-size: 0.78rem; + padding: 4px 10px; + border-radius: 4px; + border: 1px solid var(--color-border-input); + background: none; + color: var(--color-text-muted); + cursor: pointer; + transition: color 0.15s ease, border-color 0.15s ease; +} +.delete-popover-cancel:hover { + color: var(--color-text-secondary); + border-color: var(--color-text-muted); +} +.delete-popover-confirm { + font-size: 0.78rem; + padding: 4px 10px; + border-radius: 4px; + border: 1px solid var(--color-status-bad); + background-color: var(--color-status-bad); + color: #fff; + cursor: pointer; + transition: opacity 0.15s ease; +} +.delete-popover-confirm:hover { opacity: 0.85; } +@media (max-width: 639px) { + .delete-popover { max-width: calc(100vw - 32px); } + .delete-popover-cancel, + .delete-popover-confirm { padding: 8px 14px; } +} + +/* ── Trash / lock icons on tiles and rows ─────────────────── */ +.status-icon-trash { color: var(--color-status-muted); } +.tile-status-row button.status-icon-trash:hover { + color: var(--color-status-bad); + opacity: 1; +} + +/* ── Widget data modal ────────────────────────────────────── */ +.widget-modal { + position: fixed; + inset: 0; + z-index: 500; + display: flex; + align-items: center; + justify-content: center; +} +.widget-modal.hidden { display: none; } + +.widget-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); +} + +.widget-modal-panel { + position: relative; + z-index: 501; + width: min(480px, calc(100vw - 32px)); + max-height: 80vh; + display: flex; + flex-direction: column; + background-color: var(--color-bg-surface); + border: 1px solid var(--color-accent); + border-radius: var(--radius-tile); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + overflow: hidden; +} + +.widget-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 18px; + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} +.widget-modal-title { + font-size: 0.95rem; + font-weight: 600; + color: var(--color-text-primary); + margin: 0; +} +.widget-modal-close { + background: none; + border: none; + color: var(--color-text-muted); + font-size: 1.4rem; + line-height: 1; + cursor: pointer; + padding: 4px; + min-width: 32px; + min-height: 32px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.15s ease; +} +.widget-modal-close:hover { color: var(--color-text-secondary); } + +.widget-modal-body { + flex: 1; + overflow-y: auto; + padding: 16px 18px; +} +.widget-modal-body .drawer-widget-grid { + grid-template-columns: repeat(3, 1fr); + margin-top: 0; +} +.widget-modal-no-data { + font-size: 0.85rem; + color: var(--color-text-muted); + font-style: italic; + text-align: center; + padding: 20px 0; +} + +@media (max-width: 480px) { + .widget-modal-header, + .widget-modal-body { padding: 10px 14px; } + .widget-modal-close { min-width: 44px; min-height: 44px; font-size: 1.6rem; } + .widget-modal-body .drawer-widget-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 360px) { + .widget-modal-body .drawer-widget-grid { grid-template-columns: 1fr; } +} + +/* ── Edit page section headings ───────────────────────────── */ +.edit-section-heading { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-text-muted); + margin-top: 1.75rem; + margin-bottom: 0.75rem; + padding-bottom: 4px; + border-bottom: 1px solid var(--color-border); + letter-spacing: 0.03em; +} +.edit-section-heading:first-child { margin-top: 0; } + +.edit-section-fields { display: flex; flex-direction: column; gap: 0.75rem; } + +.edit-helper { + font-size: 0.75rem; + color: var(--color-text-muted); + font-style: italic; + margin-top: 3px; +} + +/* ── Group mode tab toggle ────────────────────────────────── */ +.group-mode-tabs { + display: inline-flex; + border: 1px solid var(--color-border-input); + border-radius: 6px; + overflow: hidden; +} +.group-tab { + display: inline-block; + padding: 5px 16px; + font-size: 0.82rem; + color: var(--color-text-muted); + cursor: pointer; + background: none; + transition: background-color 0.15s ease, color 0.15s ease; + user-select: none; + line-height: 1.4; +} +.group-tab + .group-tab { border-left: 1px solid var(--color-border-input); } + +#select_existing:checked + .group-tab { background-color: var(--color-accent); color: #fff; } +#add_new:checked + .group-tab { background-color: var(--color-accent); color: #fff; } + +/* ── Edit page delete button ──────────────────────────────── */ +.edit-delete-btn { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 0.82rem; + padding: 5px 14px; + border-radius: 4px; + border: 1px solid var(--color-border-input); + background: none; + color: var(--color-text-muted); + cursor: pointer; + transition: border-color 0.15s ease, color 0.15s ease; +} +.edit-delete-btn:hover { + border-color: var(--color-status-bad); + color: var(--color-status-bad); +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 176b34b..c70ab0d 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -2,7 +2,8 @@ Service Tracker Dashboard — shared JavaScript Covers: auto-refresh, group-collapse, view-controls, filter-input, tiled tile-click, tile drawers, tools popover, - dashboard group-toggle, clipboard copy. + dashboard group-toggle, clipboard copy, delete popover, + widget modal, refresh-pause while interacting. ============================================================ */ (function () { @@ -12,6 +13,17 @@ let secondsSinceRefresh = 0; let refreshInterval = 60; + function isInteracting() { + if (currentOpenDrawer) return true; + const widgetModal = document.getElementById('widget-modal'); + const changelogModal = document.getElementById('changelog-modal'); + const deletePopover = document.getElementById('delete-popover'); + if (widgetModal && !widgetModal.classList.contains('hidden')) return true; + if (changelogModal && !changelogModal.classList.contains('hidden')) return true; + if (deletePopover && !deletePopover.classList.contains('hidden')) return true; + return false; + } + function initRefresh() { const refreshLabel = document.getElementById('refreshTimer'); const refreshDropdown = document.getElementById('refreshInterval'); @@ -27,6 +39,10 @@ }); } setInterval(() => { + if (isInteracting()) { + if (refreshLabel) refreshLabel.textContent = 'Refresh paused (drawer open)'; + return; + } secondsSinceRefresh++; if (refreshLabel) { const m = Math.floor(secondsSinceRefresh / 60); @@ -155,6 +171,8 @@ } currentOpenDrawer = null; currentOpenTile = null; + // Reset refresh timer so next cycle starts fresh + secondsSinceRefresh = 0; } } @@ -216,6 +234,83 @@ }); } + /* ── Delete popover ──────────────────────────────────── */ + let _deleteOutsideHandler = null; + let _deleteEscHandler = null; + + function hideDeletePopover() { + const pop = document.getElementById('delete-popover'); + if (!pop) return; + pop.classList.add('hidden'); + if (_deleteOutsideHandler) { + document.removeEventListener('click', _deleteOutsideHandler, true); + _deleteOutsideHandler = null; + } + if (_deleteEscHandler) { + document.removeEventListener('keydown', _deleteEscHandler); + _deleteEscHandler = null; + } + } + + function showDeletePopover(triggerEl, containerName, onConfirm) { + hideDeletePopover(); + + const pop = document.getElementById('delete-popover'); + if (!pop) return; + + pop.querySelector('.delete-popover-target').textContent = containerName; + pop.classList.remove('hidden'); + + // Position: fixed, relative to viewport + const rect = triggerEl.getBoundingClientRect(); + const popW = Math.min(280, window.innerWidth - 32); + pop.style.maxWidth = popW + 'px'; + pop.style.width = 'auto'; + + // Default below; shift above if not enough room + const popH = pop.offsetHeight; + let top = rect.bottom + 4; + if (top + popH > window.innerHeight - 8) top = rect.top - popH - 4; + if (top < 8) top = 8; + + // Align left edge with trigger, shift left if it clips right edge + let left = rect.left; + if (left + popW > window.innerWidth - 8) left = window.innerWidth - popW - 8; + if (left < 8) left = 8; + + pop.style.top = top + 'px'; + pop.style.left = left + 'px'; + + // Wire buttons (clone to drop any previous listeners) + const oldConfirm = pop.querySelector('.delete-popover-confirm'); + const oldCancel = pop.querySelector('.delete-popover-cancel'); + const newConfirm = oldConfirm.cloneNode(true); + const newCancel = oldCancel.cloneNode(true); + oldConfirm.replaceWith(newConfirm); + oldCancel.replaceWith(newCancel); + + newConfirm.addEventListener('click', () => { hideDeletePopover(); onConfirm(); }); + newCancel.addEventListener('click', hideDeletePopover); + + // Click-outside (capture phase so it fires before other handlers) + setTimeout(() => { + _deleteOutsideHandler = function (e) { + if (!pop.contains(e.target) && e.target !== triggerEl) hideDeletePopover(); + }; + document.addEventListener('click', _deleteOutsideHandler, true); + }, 0); + + _deleteEscHandler = function (e) { if (e.key === 'Escape') hideDeletePopover(); }; + document.addEventListener('keydown', _deleteEscHandler); + } + + /* Shared fetch-delete helper used by tile, drawer, and dashboard row. */ + function fetchDelete(entryId, onSuccess, onError) { + fetch(`/api/v1/entries/${entryId}/delete`, { method: 'POST' }) + .then(r => r.ok ? onSuccess() : onError()) + .catch(onError); + } + /* ── Tiled drawer: delete action ────────────────────── */ function initDrawerDelete() { document.querySelectorAll('.drawer-btn-delete').forEach(btn => { @@ -223,37 +318,49 @@ e.stopPropagation(); const entryId = this.dataset.entryId; const containerName = this.dataset.containerName; - const isStatic = this.dataset.isStatic === 'true'; - - let confirmed = false; - if (isStatic) { - const typed = prompt( - `STATIC ENTRY: Type the container name to confirm deletion.\n\n` + - `Container: "${containerName}"\n\nThis cannot be undone.` - ); - confirmed = (typed === containerName); - } else { - confirmed = confirm(`Delete "${containerName}"? This cannot be undone.`); - } - - if (!confirmed) return; - - const form = document.createElement('form'); - form.method = 'POST'; - form.action = `/edit/${entryId}`; + showDeletePopover(this, containerName, () => { + fetchDelete(entryId, () => { + // Remove the tile-wrapper from the DOM + const drawer = document.getElementById(`drawer-${entryId}`); + const wrapper = drawer ? drawer.closest('.tile-wrapper') : null; + if (wrapper) wrapper.remove(); + closeCurrentDrawer(); + }, () => alert('Delete failed. Please try again.')); + }); + }); + }); + } - const addField = (name, value) => { - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = name; - input.value = value; - form.appendChild(input); - }; - addField('delete', '1'); - addField('delete_confirmation', containerName); + /* ── Tiled: trash icon on tile ───────────────────────── */ + function initTileTrash() { + document.querySelectorAll('.tile-trash-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.stopPropagation(); + const entryId = this.dataset.entryId; + const containerName = this.dataset.containerName; + showDeletePopover(this, containerName, () => { + fetchDelete(entryId, () => { + const wrapper = this.closest('.tile-wrapper'); + if (wrapper) wrapper.remove(); + }, () => alert('Delete failed. Please try again.')); + }); + }); + }); + } - document.body.appendChild(form); - form.submit(); + /* ── Dashboard: trash icon on row ────────────────────── */ + function initDashboardTrash() { + document.querySelectorAll('.row-trash-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.stopPropagation(); + const entryId = this.dataset.entryId; + const containerName = this.dataset.containerName; + showDeletePopover(this, containerName, () => { + fetchDelete(entryId, () => { + const row = this.closest('tr'); + if (row) row.remove(); + }, () => alert('Delete failed. Please try again.')); + }); }); }); } @@ -309,6 +416,53 @@ }); } + /* ── Widget modal ────────────────────────────────────── */ + function initWidgetModal() { + const modal = document.getElementById('widget-modal'); + const content = document.getElementById('widget-modal-content'); + const titleEl = modal ? modal.querySelector('.widget-modal-title') : null; + const closeBtn = modal ? modal.querySelector('.widget-modal-close') : null; + const backdrop = modal ? modal.querySelector('.widget-modal-backdrop') : null; + if (!modal) return; + + function showWidgetModal(containerName, widgetGrid) { + titleEl.textContent = containerName; + content.innerHTML = ''; + if (widgetGrid && widgetGrid.children.length > 0) { + content.appendChild(widgetGrid.cloneNode(true)); + } else { + const msg = document.createElement('p'); + msg.className = 'widget-modal-no-data'; + msg.textContent = 'No widget data available yet.'; + content.appendChild(msg); + } + modal.classList.remove('hidden'); + document.addEventListener('keydown', onModalEsc); + } + + function hideWidgetModal() { + modal.classList.add('hidden'); + secondsSinceRefresh = 0; + document.removeEventListener('keydown', onModalEsc); + } + + function onModalEsc(e) { if (e.key === 'Escape') hideWidgetModal(); } + + if (closeBtn) closeBtn.addEventListener('click', hideWidgetModal); + if (backdrop) backdrop.addEventListener('click', hideWidgetModal); + + document.querySelectorAll('.tile-widget-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.stopPropagation(); + const wrapper = this.closest('.tile-wrapper'); + if (!wrapper) return; + const containerName = (wrapper.querySelector('.container-name')?.textContent || '').trim(); + const widgetGrid = wrapper.querySelector('.drawer-widget-grid'); + showWidgetModal(containerName, widgetGrid); + }); + }); + } + /* ── Changelog "What's new" modal ───────────────────── */ function initChangelogModal() { const modal = document.getElementById('changelog-modal'); @@ -338,6 +492,7 @@ function dismiss() { localStorage.setItem(STORAGE_KEY, currentVersion); modal.classList.add('hidden'); + secondsSinceRefresh = 0; document.removeEventListener('keydown', onEsc); } @@ -361,7 +516,6 @@ .then(r => r.json()) .then(data => { if (!data.sections || data.sections.length === 0) { - // Downgrade case or already current — silently update. localStorage.setItem(STORAGE_KEY, currentVersion); } else { showModal(data.sections); @@ -371,6 +525,9 @@ } } + /* Expose popover API for pages with inline script (e.g. edit_entry.html) */ + window.showDeletePopover = showDeletePopover; + /* ── Bootstrap ───────────────────────────────────────── */ document.addEventListener('DOMContentLoaded', function () { initRefresh(); @@ -386,8 +543,11 @@ initDrawers(); initToolsPopovers(); initDrawerDelete(); + initTileTrash(); + initWidgetModal(); } else if (view === 'dashboard') { initDashboardGroupCollapse(); + initDashboardTrash(); } // compact has no extra init beyond refresh/filter/view-controls }); diff --git a/templates/base.html b/templates/base.html index f886e65..8c7dad8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -50,6 +50,8 @@ {% include 'partials/changelog_modal.html' %} + {% include 'partials/delete_popover.html' %} + {% include 'partials/widget_modal.html' %} {% block extra_scripts %}{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 5e5e201..f65be3a 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -228,16 +228,30 @@ {% endif %} - {# Actions column — pencil icon edit + static lock indicator #} + {# Actions column — pencil icon edit + trash/lock #} - - - - {% if entry.is_static %} - 🔒 - {% endif %} +
+ + + + {% if entry.is_static %} + + + + {% else %} + + {% endif %} +
{% endfor %} diff --git a/templates/edit_entry.html b/templates/edit_entry.html index fbd5a23..13419a9 100644 --- a/templates/edit_entry.html +++ b/templates/edit_entry.html @@ -7,7 +7,9 @@

Edit Service Entry

-
+ {# ── Identity ────────────────────────────────────────── #} +

Identity

+
@@ -17,36 +19,33 @@

Edit Service Entry

+
- {# URL source provenance (v0.6.0). Render a small badge next to - each URL field telling the operator which actor last set it. - "Edit" + "Save" clears the badge to "ui_edit". Clearing the - field resets source to NULL so synthesis can re-fill. #} - {% macro url_source_badge(source) %} - {% if source == 'ui_edit' %} - Edited in UI - {% elif source == 'explicit_label' %} - Explicit label - {% elif source == 'synthesized' %} - Synthesized - {% endif %} - {% endmacro %} + {# URL source provenance badge macro #} + {% macro url_source_badge(source) %} + {% if source == 'ui_edit' %} + Edited in UI + {% elif source == 'explicit_label' %} + Explicit label + {% elif source == 'synthesized' %} + Synthesized + {% endif %} + {% endmacro %} + {# ── URLs & Health ────────────────────────────────────── #} +

URLs & Health

+
-
- -
- - -
+ +
+ +
-

- Clearing this field resets provenance and re-allows synthesis from exposure observations. -

+

Clearing this field resets provenance and re-allows synthesis from exposure observations.

@@ -54,24 +53,25 @@

Edit Service Entry

External URL {{ url_source_badge(entry.externalurl_source) }} -
- -
- - -
+ +
+ +
+
+ {# ── Grouping & Display ───────────────────────────────── #} +

Grouping & Display

+
- + -
- - - - - +
+ + + +
-

Optional. Lower numbers show up higher in group listings.

+

Optional. Lower numbers show up higher in group listings.

-

Enter the filename of the SVG icon (e.g., service.svg). Will try to match automatically from application above.

+

Filename of the SVG icon (e.g., service.svg). Will try to match automatically from the application name above.

- +
+
-
-

Widget Settings

+ {# ── Widget ───────────────────────────────────────────── #} +

Widget

+
+
+
-
- - -
- -
- - -
+
+ + +
-
+
+ +
+ +
-
+ {# ── Form actions ─────────────────────────────────────── #} +
+

+ {# ── Reported by notifier (read-only) ─────────────────── #}

Reported by notifier

@@ -213,22 +225,6 @@

Exposure Observati {% endif %}

- -
- -
-

Danger Zone

-

Deleting this entry is permanent and cannot be undone.

-
- -
- - -
-
-
{% endblock %} @@ -270,7 +266,7 @@

Danger Zone

container.appendChild(label); }); }) - .catch(err => console.error("❌ Error loading widget config:", err)); + .catch(err => console.error('Error loading widget config:', err)); } if (widgetSelect) { @@ -295,7 +291,23 @@

Danger Zone

selectExisting.addEventListener('change', toggleGroupMode); addNew.addEventListener('change', toggleGroupMode); } + + // === Edit-page Delete button → popover === + const deleteBtn = document.getElementById('edit-delete-btn'); + if (deleteBtn) { + deleteBtn.addEventListener('click', function () { + const entryId = this.dataset.entryId; + const containerName = this.dataset.containerName; + const redirectUrl = this.dataset.redirect || '/'; + if (typeof showDeletePopover === 'function') { + showDeletePopover(this, containerName, () => { + fetch(`/api/v1/entries/${entryId}/delete`, { method: 'POST' }) + .then(r => { if (r.ok) window.location.href = redirectUrl; }) + .catch(() => alert('Delete failed. Please try again.')); + }); + } + }); + } }); {% endblock %} - diff --git a/templates/partials/delete_popover.html b/templates/partials/delete_popover.html new file mode 100644 index 0000000..e06d70e --- /dev/null +++ b/templates/partials/delete_popover.html @@ -0,0 +1,9 @@ + diff --git a/templates/partials/widget_modal.html b/templates/partials/widget_modal.html new file mode 100644 index 0000000..07aee02 --- /dev/null +++ b/templates/partials/widget_modal.html @@ -0,0 +1,12 @@ + diff --git a/templates/tiled_dash.html b/templates/tiled_dash.html index af40dba..e18d31b 100644 --- a/templates/tiled_dash.html +++ b/templates/tiled_dash.html @@ -176,9 +176,10 @@

+ {% endif %}

{# /tile-icon-row-status #} @@ -202,6 +203,22 @@