diff --git a/API_CONTRACT.md b/API_CONTRACT.md index ad72e72..8a3dc44 100644 --- a/API_CONTRACT.md +++ b/API_CONTRACT.md @@ -140,23 +140,6 @@ always returns normalized refs. Pinning ("Pin Version") holds a container at its current version: it's never flagged for updates and is grouped into a separate section, but can still be updated by hand. -### `GET /api/hidden` - -- Auth: cookie. -- Response: `200` — array of hidden container names. - -### `POST /api/hide` - -- Auth: cookie. -- Body: `{ "name": "string" }` — container name to hide from the dashboard. -- Response: `200 { "ok": true }`. Idempotent. - -### `DELETE /api/hide/:name` - -- Auth: cookie. -- Path param: `name` — container name to unhide (URL-encoded). -- Response: `200 { "ok": true }`. Idempotent. - ### `GET /api/settings` - Auth: cookie. @@ -196,7 +179,6 @@ separate section, but can still be updated by hand. "updateAvailable": true, "availableDigest": "sha256:...", "pinned": false, - "hidden": false, "state": "running", "composeFile": "/opt/stacks/web/compose.yaml", "composeFileMissing": false, @@ -227,8 +209,6 @@ Field notes: - `pinned` — `true` if the image ref is in the `pinned` table ("Pin Version": update indicator is suppressed and the container is grouped separately, but a manual update is still allowed). -- `hidden` — `true` if the container name is in the `hidden` table; the - dashboard omits it (restore from Settings). - `state` — Docker container state (`running`, `exited`, etc.). - `composeFile` / `workingDir` — derived from `com.docker.compose.project.config_files` / diff --git a/client/src/Dashboard.jsx b/client/src/Dashboard.jsx index 198d700..9c098a4 100644 --- a/client/src/Dashboard.jsx +++ b/client/src/Dashboard.jsx @@ -163,8 +163,8 @@ export default function Dashboard({ onPendingCountChange }) { }); }, []); - // Visible = not hidden. Pinned go to their own bottom section. - const visible = useMemo(() => containers.filter((c) => !c.hidden), [containers]); + // Pinned go to their own bottom section; everything else is the main list. + const visible = containers; const pinnedItems = useMemo( () => visible.filter((c) => c.pinned).sort((a, b) => a.name.localeCompare(b.name)), [visible] @@ -324,7 +324,6 @@ export default function Dashboard({ onPendingCountChange }) { container={container} onSettled={handleSettled} onPinChange={load} - onHidden={load} registerRunner={registerRunner} /> ))} @@ -349,7 +348,6 @@ export default function Dashboard({ onPendingCountChange }) { container={container} onSettled={handleSettled} onPinChange={load} - onHidden={load} registerRunner={registerRunner} /> ))} diff --git a/client/src/api.js b/client/src/api.js index 6f47fe6..51e1701 100644 --- a/client/src/api.js +++ b/client/src/api.js @@ -111,20 +111,6 @@ export function unpin(ref) { return del(`/pin/${encodeURIComponent(ref)}`); } -// --- Hiding --- - -export function getHidden() { - return get('/hidden'); -} - -export function hideContainer(name) { - return post('/hide', { name }); -} - -export function unhideContainer(name) { - return del(`/hide/${encodeURIComponent(name)}`); -} - // --- Settings --- export function getSettings() { diff --git a/client/src/components/Header.jsx b/client/src/components/Header.jsx index eef1f2f..793c0d0 100644 --- a/client/src/components/Header.jsx +++ b/client/src/components/Header.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, NavLink } from 'react-router-dom'; import { logout } from '../api.js'; import { useTheme } from '../hooks/useTheme.js'; @@ -69,6 +69,17 @@ export default function Header({ pendingCount = 0, onLoggedOut }) { Diun Updater {pendingCount > 0 && {pendingCount}} +
- - )} - - {!hiddenLoading && !hiddenError && hidden.length === 0 && ( -
-

No hidden containers.

-
- )} - - {!hiddenLoading && !hiddenError && hidden.length > 0 && ( - - )} - -

About

Diun Updater

diff --git a/client/src/styles/app.css b/client/src/styles/app.css index 03fccba..f9a5873 100644 --- a/client/src/styles/app.css +++ b/client/src/styles/app.css @@ -1151,3 +1151,36 @@ a { .version-value.is-available { color: var(--color-success); } + +/* ---------- Header nav (desktop only) ---------- */ + +.header-nav { + display: none; +} + +@media (min-width: 768px) { + .header-nav { + display: flex; + gap: 4px; + margin-right: auto; + margin-left: 20px; + } +} + +.header-nav-link { + padding: 6px 12px; + border-radius: var(--radius-md); + color: var(--color-text-muted); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; +} + +.header-nav-link:hover { + color: var(--color-text); +} + +.header-nav-link.is-active { + color: var(--color-accent); + background: var(--color-elevated); +} diff --git a/server/src/containers-service.js b/server/src/containers-service.js index 3bf82a1..bfe2abb 100644 --- a/server/src/containers-service.js +++ b/server/src/containers-service.js @@ -22,14 +22,12 @@ import { isUpdateAvailable, digestsEqual } from './reconcile.js'; * - returns the latest unresolved event row for a normalized ref, or * undefined if there is none. * @param {(normalizedRef: string) => boolean} params.isPinned - * @param {(containerName: string) => boolean} [params.isHidden] - whether a - * container is hidden from the dashboard. Defaults to "never hidden". * @returns {{ * items: Array, * refsToResolve: string[] * }} */ -export function buildContainerItems({ containers, lookupEvent, isPinned, isHidden = () => false }) { +export function buildContainerItems({ containers, lookupEvent, isPinned }) { const items = []; const refsToResolve = []; @@ -63,7 +61,6 @@ export function buildContainerItems({ containers, lookupEvent, isPinned, isHidde updateAvailable, availableDigest, pinned: isPinned(c.normalizedRef), - hidden: isHidden(c.name), state: c.state, composeFile: c.composeFile, composeFileMissing: c.composeFileMissing ?? false, diff --git a/server/src/db.js b/server/src/db.js index b713708..6ec933c 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -36,10 +36,6 @@ CREATE TABLE IF NOT EXISTS pinned ( ref TEXT PRIMARY KEY, created_at TEXT DEFAULT (datetime('now')) ); -CREATE TABLE IF NOT EXISTS hidden ( - container_name TEXT PRIMARY KEY, - created_at TEXT DEFAULT (datetime('now')) -); CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT @@ -90,19 +86,6 @@ const stmts = { isPinned: db.prepare(` SELECT 1 FROM pinned WHERE ref = ? LIMIT 1 `), - hide: db.prepare(` - INSERT INTO hidden (container_name) VALUES (?) - ON CONFLICT(container_name) DO NOTHING - `), - unhide: db.prepare(` - DELETE FROM hidden WHERE container_name = ? - `), - getHidden: db.prepare(` - SELECT container_name FROM hidden ORDER BY created_at DESC - `), - isHidden: db.prepare(` - SELECT 1 FROM hidden WHERE container_name = ? LIMIT 1 - `), getSetting: db.prepare(` SELECT value FROM settings WHERE key = ? LIMIT 1 `), @@ -167,22 +150,6 @@ export function isPinned(ref) { return stmts.isPinned.get(ref) !== undefined; } -export function hide(containerName) { - return stmts.hide.run(containerName); -} - -export function unhide(containerName) { - return stmts.unhide.run(containerName); -} - -export function getHidden() { - return stmts.getHidden.all().map((row) => row.container_name); -} - -export function isHidden(containerName) { - return stmts.isHidden.get(containerName) !== undefined; -} - export function getSetting(key) { const row = stmts.getSetting.get(key); return row ? row.value : undefined; diff --git a/server/src/routes/api.js b/server/src/routes/api.js index efc901b..d24df1f 100644 --- a/server/src/routes/api.js +++ b/server/src/routes/api.js @@ -46,7 +46,6 @@ apiRouter.get('/api/containers', async (req, res) => { containers, lookupEvent: db.latestUnresolvedEventForRef, isPinned: (ref) => db.isPinned(ref), - isHidden: (name) => db.isHidden(name), }); for (const ref of refsToResolve) { @@ -125,29 +124,6 @@ apiRouter.delete('/api/pin/:ref', (req, res) => { return res.status(200).json({ ok: true }); }); -// --- Hidden containers (keyed by container name) --- - -apiRouter.get('/api/hidden', (req, res) => { - return res.status(200).json(db.getHidden()); -}); - -apiRouter.post('/api/hide', (req, res) => { - const name = req.body?.name; - if (typeof name !== 'string' || name.trim() === '') { - return res.status(400).json({ error: 'invalid_payload' }); - } - db.hide(name.trim()); - broadcastGlobal({ type: 'containers-changed' }); - return res.status(200).json({ ok: true }); -}); - -apiRouter.delete('/api/hide/:name', (req, res) => { - const name = decodeURIComponent(req.params.name); - db.unhide(name); - broadcastGlobal({ type: 'containers-changed' }); - return res.status(200).json({ ok: true }); -}); - // --- Settings --- apiRouter.get('/api/settings', (req, res) => { diff --git a/server/test/containers-service.test.js b/server/test/containers-service.test.js index a88809b..394ecd9 100644 --- a/server/test/containers-service.test.js +++ b/server/test/containers-service.test.js @@ -89,7 +89,6 @@ describe('buildContainerItems', () => { updateAvailable: false, availableDigest: null, pinned: false, - hidden: false, state: 'running', composeFile: '/stacks/web/docker-compose.yml', composeFileMissing: false, @@ -97,16 +96,6 @@ describe('buildContainerItems', () => { }); }); - test('isHidden marks the item hidden', () => { - const containers = [makeContainer()]; - const { items } = buildContainerItems({ - containers, - lookupEvent: () => undefined, - isPinned: () => false, - isHidden: (name) => name === 'nginx', - }); - assert.equal(items[0].hidden, true); - }); test('handles multiple containers independently', () => { const containers = [ diff --git a/server/test/settings.test.js b/server/test/settings.test.js index 9b7f524..aa478d9 100644 --- a/server/test/settings.test.js +++ b/server/test/settings.test.js @@ -10,7 +10,6 @@ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'diun-settings-')); process.env.DATA_DIR = tmp; const { getSettings, updateSettings } = await import('../src/settings.js'); -const db = await import('../src/db.js'); test('settings: defaults when nothing stored', () => { assert.deepEqual(getSettings(), { defaultFilter: 'updates', autoCheckOnOpen: true }); @@ -31,13 +30,3 @@ test('settings: ignores unknown keys', () => { assert.doesNotThrow(() => updateSettings({ somethingUnknown: 'x' })); }); -test('hidden: hide/isHidden/unhide roundtrip', () => { - assert.equal(db.isHidden('cadvisor'), false); - db.hide('cadvisor'); - assert.equal(db.isHidden('cadvisor'), true); - assert.deepEqual(db.getHidden(), ['cadvisor']); - db.hide('cadvisor'); // idempotent - assert.equal(db.getHidden().length, 1); - db.unhide('cadvisor'); - assert.equal(db.isHidden('cadvisor'), false); -});