diff --git a/CHANGELOG.md b/CHANGELOG.md index add2789..8e953dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes are documented here. Releases follow [SemVer](https://semver.org). Image tags published to Docker Hub (`krippler52/starr`) and GHCR (`ghcr.io/krippler/starr`). +## [1.3.6] — 2026-07-17 + +### Fixed +- **The rightmost dashboard column still couldn't be dragged** — cards in the right column of a two-column layout couldn't be moved to create a third column or into the first column (only after seeding a third column from the left did the middle column become movable). Root cause: native HTML5 drag-and-drop — adding the + rail on dragstart reflowed the columns and the browser's drag hit-testing broke dragging out of the rightmost column. Rearranging is now driven by **pointer events with a floating clone** and purely coordinate-based routing, so every column behaves identically. Verified by driving a real mouse in a headless browser (right-column → new third column, right-column → first column, and in-column reorder all work). + ## [1.3.5] — 2026-07-17 ### Fixed diff --git a/README.md b/README.md index ee4ecdd..ff4b172 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ docker run -d \ -v /mnt/user/appdata:/appdata:rw \ -v /mnt/user/appdata/starr/backups:/backups \ -v /var/run/docker.sock:/var/run/docker.sock \ - krippler52/starr:1.3.5 + krippler52/starr:1.3.6 ``` **That's it for the host side.** Open the dashboard, paste each app's API key, click **Save Credentials**, and Starr remembers it for scheduled runs and reloads. URLs / DB paths / container names are auto-discovered from Docker. @@ -72,7 +72,7 @@ docker run -d \ | Tag | Use | |---|---| -| `1.3.5` | exact version — recommended pin for production | +| `1.3.6` | exact version — recommended pin for production | | `1.2` / `1` | floating minor / major | | `latest` | newest **released version** (updated on every version tag) | | `edge` | tip of `main` — newest merged code, for testing ahead of a release | diff --git a/app/server.py b/app/server.py index 60dc6b1..73d436b 100644 --- a/app/server.py +++ b/app/server.py @@ -879,7 +879,7 @@ def _repair_worker(cfg: dict) -> None: _job.history = [] _job.result = None - emit("SYS", f"Starr DB Repair v1.3.5 – job started for {cfg['app'].upper()}", "sys") + emit("SYS", f"Starr DB Repair v1.3.6 – job started for {cfg['app'].upper()}", "sys") emit("SYS", f"Dry run: {cfg.get('dry_run', False)}", "sys") db_path = None diff --git a/app/templates/index.html b/app/templates/index.html index 0ec823b..6873e7a 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -162,14 +162,20 @@ .dash-card > .panel-head, .dash-card > .terminal-bar { position: relative; padding-left: 30px; } .drag-handle { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); - cursor: grab; user-select: none; + cursor: grab; user-select: none; touch-action: none; font-size: 14px; line-height: 1; color: var(--muted); opacity: 0; transition: opacity .15s; } .dash-card:hover .drag-handle, .dashboard.dnd-active .drag-handle { opacity: .5; } .drag-handle:hover { opacity: 1; color: var(--app); } .drag-handle:active { cursor: grabbing; } -.dash-card.is-dragging { opacity: .35; } +.dash-card.is-dragging { opacity: .3; } +/* The floating clone that follows the pointer during a drag (pointer-based DnD). */ +.drag-clone { + position: fixed; left: 0; top: 0; margin: 0; z-index: 9999; + pointer-events: none; opacity: .92; border-radius: var(--radius); + box-shadow: 0 14px 44px rgba(0,0,0,.55); +} .dash-col.drop-into { outline: 2px dashed var(--app); outline-offset: -2px; border-radius: var(--radius); } .dash-card.drop-before { box-shadow: 0 -3px 0 -1px var(--app); } .dash-card.drop-after { box-shadow: 0 3px 0 -1px var(--app); } @@ -467,7 +473,7 @@

STARR DB REPAIR

STARR DB REPAIR

-

Database Diagnostic & Recovery — v1.3.5

+

Database Diagnostic & Recovery — v1.3.6

@@ -1210,7 +1216,7 @@

STARR DB REPAIR

const LAYOUT_KEY = 'starr_card_layout'; const DEFAULT_CARD_ORDER = ['connection', 'log', 'ops', 'trends', 'backups', 'schedules', 'notifications']; let _dashInited = false; -let _dragCard = null; +let _drag = null; // active pointer drag: { card, clone, dx, dy } function _columnCount() { const w = window.innerWidth; @@ -1286,11 +1292,17 @@

STARR DB REPAIR

ids.forEach(id => col.appendChild(cards[id])); dash.appendChild(col); }); - _wireDashboardDnD(); // one router on the dashboard covers all columns _injectHandles(); document.getElementById('layoutTools').classList.toggle('show', Array.isArray(saved)); } +// ── Pointer-based card dragging ──────────────────────────────────────────────── +// Driven with pointer events + a floating clone, NOT native HTML5 drag-and-drop. +// Native DnD proved unreliable here: adding the empty + rail on dragstart +// reflows the columns, and the browser's own drag hit-testing made the rightmost +// column impossible to drag out of. Pointer events give full control and purely +// coordinate-based routing (it doesn't matter what element is under the cursor), +// so every column behaves the same. function _injectHandles() { document.querySelectorAll('#dashboard .dash-card').forEach(card => { if (card._handleWired) return; @@ -1300,25 +1312,52 @@

STARR DB REPAIR

h.className = 'drag-handle'; h.title = 'Drag to rearrange'; h.textContent = '⠳'; // ⠳ braille grip - h.setAttribute('draggable', 'true'); head.insertBefore(h, head.firstChild); - h.addEventListener('dragstart', e => { - _dragCard = card; - card.classList.add('is-dragging'); - document.getElementById('dashboard').classList.add('dnd-active'); - e.dataTransfer.effectAllowed = 'move'; - try { e.dataTransfer.setData('text/plain', card.dataset.card); } catch (_) {} - }); - h.addEventListener('dragend', () => { - card.classList.remove('is-dragging'); - document.getElementById('dashboard').classList.remove('dnd-active'); - _dragCard = null; - _persistLayout(); - }); + h.addEventListener('pointerdown', e => _startDrag(e, card)); card._handleWired = true; }); } +function _startDrag(e, card) { + if (e.pointerType === 'mouse' && e.button !== 0) return; // left button only + e.preventDefault(); + e.stopPropagation(); + const r = card.getBoundingClientRect(); + const clone = card.cloneNode(true); + clone.classList.add('drag-clone'); + clone.classList.remove('is-dragging'); + clone.style.width = r.width + 'px'; + clone.style.transform = `translate(${r.left}px, ${r.top}px)`; + document.body.appendChild(clone); + card.classList.add('is-dragging'); + document.getElementById('dashboard').classList.add('dnd-active'); + _drag = { card, clone, dx: e.clientX - r.left, dy: e.clientY - r.top }; + window.addEventListener('pointermove', _onDragMove); + window.addEventListener('pointerup', _endDrag, { once: true }); + window.addEventListener('pointercancel', _endDrag, { once: true }); +} + +function _onDragMove(e) { + if (!_drag) return; + _drag.clone.style.transform = `translate(${e.clientX - _drag.dx}px, ${e.clientY - _drag.dy}px)`; + const col = _columnAtX(e.clientX); + if (!col) return; + const card = _drag.card; + const after = _cardAfterPoint(col, e.clientY); // live-move the real card to preview the drop + if (after == null) { if (col.lastElementChild !== card) col.appendChild(card); } + else if (after !== card) col.insertBefore(card, after); +} + +function _endDrag() { + if (!_drag) return; + window.removeEventListener('pointermove', _onDragMove); + _drag.clone.remove(); + _drag.card.classList.remove('is-dragging'); + document.getElementById('dashboard').classList.remove('dnd-active'); + _drag = null; + _persistLayout(); +} + function _cardAfterPoint(col, y) { const cards = Array.from(col.querySelectorAll(':scope > .dash-card:not(.is-dragging)')); for (const c of cards) { @@ -1331,10 +1370,8 @@

STARR DB REPAIR

// Pick the visible column whose horizontal band contains x. Bands tile the whole // dashboard width with no dead gaps: a column owns everything up to its right // edge; the rightmost column (often the empty + rail while dragging) owns -// everything to its right too. This is what makes dropping into an adjacent -// column — or the far-right new-column rail — reliable, instead of depending on -// landing the cursor precisely inside a thin column or falling into the gap -// between two columns. +// everything to its right too — so dropping into an adjacent column or the +// far-right new-column rail is reliable, not dependent on hitting a thin target. function _columnAtX(x) { const cols = Array.from(document.querySelectorAll('#dashboard .dash-col')) .filter(c => getComputedStyle(c).display !== 'none'); @@ -1345,26 +1382,6 @@

STARR DB REPAIR

return cols[cols.length - 1]; } -// One dragover/drop router on the dashboard (not per-column), so the entire -// width is a live drop surface. Routes by X-band, then positions within the -// target column by Y. -function _wireDashboardDnD() { - const dash = document.getElementById('dashboard'); - if (dash._dndWired) return; - dash._dndWired = true; - dash.addEventListener('dragover', e => { - if (!_dragCard) return; - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - const col = _columnAtX(e.clientX); - if (!col) return; - const after = _cardAfterPoint(col, e.clientY); - if (after == null) { if (col.lastElementChild !== _dragCard) col.appendChild(_dragCard); } - else if (after !== _dragCard) col.insertBefore(_dragCard, after); - }); - dash.addEventListener('drop', e => { if (_dragCard) e.preventDefault(); }); -} - function _persistLayout() { const n = _columnCount(); const cols = Array.from(document.querySelectorAll('#dashboard .dash-col')).map(col => @@ -1687,7 +1704,7 @@

STARR DB REPAIR

// Header text document.getElementById('appTitle').textContent = cfg.name + ' DB REPAIR'; - document.getElementById('appSub').textContent = cfg.sub + ' — v1.3.5'; + document.getElementById('appSub').textContent = cfg.sub + ' — v1.3.6'; document.getElementById('logoSvg').innerHTML = cfg.icon; document.getElementById('termTitle').textContent = cfg.name + ' REPAIR LOG';