88dca32d3c
Milestone 72 phase D — the dashboard view IS the edit surface (operator ask): - /d/<id> renders the grid via Gridstack in STATIC mode — positioned exactly like before, live HTMX widget bodies keep polling. An "Edit" button flips the same grid interactive (grid.setStatic(false)) in place, so you drag/resize over real data. "Done" flips it back. Layout autosaves on change. - The widget picker is now a bottom drawer (position:fixed overlay) revealed by body.dash-editing — so the dashboard width is identical entering/leaving edit. - Add: POST returns a single grid item; JS inserts it + grid.makeWidget + htmx.process so it loads live data. Remove: POST 204 + grid.removeWidget. Per-panel drag handle + remove ✕ are in the DOM for editors, shown only while editing. - Gridstack loads for everyone (viewers get the static grid); edit wiring is gated on can_edit. Mobile collapses to one column. - Removed the separate /d/<id>/edit route + edit.html + _edit_panels.html (rule 22). Dashboard-list Edit and new-dashboard create now deep-link /d/<id>?edit=1 which opens edit mode on load. Browser-only behaviour — CI can't exercise it; needs an operator visual check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 lines
1.4 KiB
HTML
26 lines
1.4 KiB
HTML
{# One dashboard widget as a Gridstack item — shared by index.html (initial render)
|
|
and the add-widget endpoint (single-item insert). Edit chrome (drag handle +
|
|
remove) is in the DOM only for editors and shown via body.dash-editing CSS. #}
|
|
<div class="grid-stack-item" gs-id="{{ item.db.id }}"
|
|
gs-x="{{ item.db.grid_x }}" gs-y="{{ item.db.grid_y }}"
|
|
gs-w="{{ item.db.grid_w }}" gs-h="{{ item.db.grid_h }}">
|
|
<div class="grid-stack-item-content">
|
|
<div class="widget-header">
|
|
<span style="display:flex;align-items:center;gap:0.4rem;min-width:0;">
|
|
{% if can_edit %}<span class="panel-chrome panel-drag" title="Drag to move">⠿</span>{% endif %}
|
|
<span class="widget-title" title="{{ item.title }}">{{ item.title }}</span>
|
|
</span>
|
|
<span style="display:flex;align-items:center;gap:0.5rem;flex-shrink:0;">
|
|
<a href="{{ item.defn.detail_url }}" class="widget-link">Details →</a>
|
|
{% if can_edit %}<button type="button" class="panel-chrome btn btn-danger btn-sm"
|
|
data-remove="{{ item.db.id }}" title="Remove widget"
|
|
style="padding:0.05rem 0.4rem;line-height:1.3;">✕</button>{% endif %}
|
|
</span>
|
|
</div>
|
|
<div class="dash-cell-body" id="widget-{{ item.db.id }}"
|
|
hx-get="{{ item.hx_url }}"
|
|
hx-trigger="load{% if item.defn.poll %}, every {{ poll_interval }}s{% endif %}"
|
|
hx-swap="innerHTML"></div>
|
|
</div>
|
|
</div>
|