Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions API_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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` /
Expand Down
6 changes: 2 additions & 4 deletions client/src/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -324,7 +324,6 @@ export default function Dashboard({ onPendingCountChange }) {
container={container}
onSettled={handleSettled}
onPinChange={load}
onHidden={load}
registerRunner={registerRunner}
/>
))}
Expand All @@ -349,7 +348,6 @@ export default function Dashboard({ onPendingCountChange }) {
container={container}
onSettled={handleSettled}
onPinChange={load}
onHidden={load}
registerRunner={registerRunner}
/>
))}
Expand Down
14 changes: 0 additions & 14 deletions client/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
13 changes: 12 additions & 1 deletion client/src/components/Header.jsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -69,6 +69,17 @@ export default function Header({ pendingCount = 0, onLoggedOut }) {
<span>Diun Updater</span>
{pendingCount > 0 && <span className="badge">{pendingCount}</span>}
</Link>
<nav className="header-nav" aria-label="Primary">
<NavLink to="/" end className={({ isActive }) => `header-nav-link${isActive ? ' is-active' : ''}`}>
Updates
</NavLink>
<NavLink to="/history" className={({ isActive }) => `header-nav-link${isActive ? ' is-active' : ''}`}>
History
</NavLink>
<NavLink to="/settings" className={({ isActive }) => `header-nav-link${isActive ? ' is-active' : ''}`}>
Settings
</NavLink>
</nav>
<div className="header-actions">
<button
type="button"
Expand Down
22 changes: 2 additions & 20 deletions client/src/components/UpdateCard.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { pin, unpin, hideContainer } from '../api.js';
import { pin, unpin } from '../api.js';
import { useUpdateRunner } from '../hooks/useUpdateRunner.js';
import StatusMessage from './StatusMessage.jsx';
import StreamLog from './StreamLog.jsx';
Expand Down Expand Up @@ -66,15 +66,13 @@ const ExternalIcon = () => (
* - container: item shape from GET /api/containers
* - onSettled(name) — called once an update for this container finishes
* - onPinChange() — called after a pin/unpin so the dashboard can refresh
* - onHidden() — called after hiding so the dashboard can refresh
* - registerRunner(name, runFn) — handle for "Update all"
*/
export default function UpdateCard({ container, onSettled, onPinChange, onHidden, registerRunner }) {
export default function UpdateCard({ container, onSettled, onPinChange, registerRunner }) {
const { name, project, service, image, currentDigest, availableVersion, availableDigest, updateAvailable, pinned, sourceUrl } =
container;

const [pinBusy, setPinBusy] = useState(false);
const [hideBusy, setHideBusy] = useState(false);
const [actionError, setActionError] = useState('');

const { run, busy, startError, status, lines } = useUpdateRunner(name, onSettled);
Expand Down Expand Up @@ -108,18 +106,6 @@ export default function UpdateCard({ container, onSettled, onPinChange, onHidden
}
}, [pinned, image, onPinChange]);

const handleHide = useCallback(async () => {
setHideBusy(true);
setActionError('');
try {
await hideContainer(name);
if (onHidden) onHidden();
} catch (err) {
setActionError(err.message || 'Failed to hide container');
setHideBusy(false);
}
}, [name, onHidden]);

const showUpdateAvailable = updateAvailable && !pinned;
const link = sourceLink(sourceUrl);

Expand Down Expand Up @@ -184,10 +170,6 @@ export default function UpdateCard({ container, onSettled, onPinChange, onHidden
<ExternalIcon />
</a>
)}
<button type="button" className="btn-ghost" onClick={handleHide} disabled={hideBusy}>
{hideBusy && <span className="spinner" aria-hidden="true" />}
Hide
</button>
</div>
<button
type="button"
Expand Down
83 changes: 0 additions & 83 deletions client/src/pages/SettingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import {
get,
getPinned,
unpin,
getHidden,
unhideContainer,
getSettings,
updateSettings,
} from '../api.js';
Expand All @@ -18,11 +16,6 @@ export default function SettingsPage() {
const [pinnedError, setPinnedError] = useState('');
const [unpinningRef, setUnpinningRef] = useState('');

const [hidden, setHidden] = useState([]);
const [hiddenLoading, setHiddenLoading] = useState(true);
const [hiddenError, setHiddenError] = useState('');
const [unhidingName, setUnhidingName] = useState('');

const [settings, setSettings] = useState(null);
const [settingsError, setSettingsError] = useState('');

Expand All @@ -38,26 +31,11 @@ export default function SettingsPage() {
}
}, []);

const loadHidden = useCallback(async () => {
setHiddenError('');
try {
const data = await getHidden();
setHidden(Array.isArray(data) ? data : []);
} catch (err) {
setHiddenError(err.message || 'Failed to load hidden containers');
}
}, []);

useEffect(() => {
setPinnedLoading(true);
loadPinned().finally(() => setPinnedLoading(false));
}, [loadPinned]);

useEffect(() => {
setHiddenLoading(true);
loadHidden().finally(() => setHiddenLoading(false));
}, [loadHidden]);

useEffect(() => {
getSettings()
.then((s) => setSettings(s))
Expand Down Expand Up @@ -101,22 +79,6 @@ export default function SettingsPage() {
[loadPinned]
);

const handleUnhide = useCallback(
async (name) => {
setUnhidingName(name);
setHiddenError('');
try {
await unhideContainer(name);
await loadHidden();
} catch (err) {
setHiddenError(err.message || 'Failed to unhide container');
} finally {
setUnhidingName('');
}
},
[loadHidden]
);

return (
<div className="settings-page">
<h2>Settings</h2>
Expand Down Expand Up @@ -243,51 +205,6 @@ export default function SettingsPage() {
)}
</section>

<section className="settings-section">
<h3>Hidden containers</h3>
{hiddenLoading && (
<div className="dashboard-list" aria-busy="true" aria-label="Loading hidden containers">
<div className="skeleton-card" style={{ height: 52 }} />
</div>
)}

{!hiddenLoading && hiddenError && (
<div className="error-state">
<p>{hiddenError}</p>
<button type="button" className="btn btn-primary" onClick={loadHidden}>
Retry
</button>
</div>
)}

{!hiddenLoading && !hiddenError && hidden.length === 0 && (
<div className="empty-state">
<p>No hidden containers.</p>
</div>
)}

{!hiddenLoading && !hiddenError && hidden.length > 0 && (
<ul className="pinned-list">
{hidden.map((name) => (
<li key={name} className="pinned-row">
<span className="pinned-ref truncate" title={name}>
{name}
</span>
<button
type="button"
className="btn btn-sm"
onClick={() => handleUnhide(name)}
disabled={unhidingName === name}
>
{unhidingName === name && <span className="spinner" aria-hidden="true" />}
Unhide
</button>
</li>
))}
</ul>
)}
</section>

<section className="settings-section">
<h3>About</h3>
<p className="about-app-name">Diun Updater</p>
Expand Down
33 changes: 33 additions & 0 deletions client/src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
5 changes: 1 addition & 4 deletions server/src/containers-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>,
* refsToResolve: string[]
* }}
*/
export function buildContainerItems({ containers, lookupEvent, isPinned, isHidden = () => false }) {
export function buildContainerItems({ containers, lookupEvent, isPinned }) {
const items = [];
const refsToResolve = [];

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading