pipeline: live sleep countdown; settings: full-width fields, eye inside input
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m48s

ProcessingColumn now anchors a local deadline when a 'sleeping' queue
status arrives and ticks a 1s timer. "Sleeping 60s between jobs"
becomes "Next job in 59s, 58s, …".

Settings: API key inputs now span the card's width (matching the URL
field), and the reveal affordance is a GNOME-style eye glyph sitting
inside the input's right edge. Uses an inline SVG so it inherits
currentColor and doesn't fight emoji rendering across OSes. When the
field is env-locked, the lock glyph takes the slot (eye hidden — no
edit possible anyway).

v2026.04.15.5

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 14:29:35 +02:00
parent ab65909e6e
commit c22642630d
3 changed files with 82 additions and 18 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "netfelix-audio-fix",
"version": "2026.04.15.4",
"version": "2026.04.15.5",
"scripts": {
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
"dev:client": "vite",

View File

@@ -23,6 +23,25 @@ export function ProcessingColumn({ items, progress, queueStatus, onMutate }: Pro
return () => clearInterval(t);
}, [job]);
// Local sleep countdown. Server emits the sleep duration once when the
// pause begins; the client anchors "deadline = receivedAt + seconds*1000"
// and ticks a 1s timer so the UI shows a live countdown, not a static number.
const [sleepDeadline, setSleepDeadline] = useState<number | null>(null);
const [sleepNow, setSleepNow] = useState(() => Date.now());
useEffect(() => {
if (queueStatus?.status === "sleeping" && typeof queueStatus.seconds === "number") {
setSleepDeadline(Date.now() + queueStatus.seconds * 1000);
} else {
setSleepDeadline(null);
}
}, [queueStatus?.status, queueStatus?.seconds]);
useEffect(() => {
if (sleepDeadline == null) return;
const t = setInterval(() => setSleepNow(Date.now()), 1000);
return () => clearInterval(t);
}, [sleepDeadline]);
const sleepRemaining = sleepDeadline != null ? Math.max(0, Math.ceil((sleepDeadline - sleepNow) / 1000)) : null;
// Only trust progress if it belongs to the current job — stale events from
// a previous job would otherwise show wrong numbers until the new job emits.
const liveProgress = job && progress && progress.id === job.id ? progress : null;
@@ -55,9 +74,9 @@ export function ProcessingColumn({ items, progress, queueStatus, onMutate }: Pro
actions={job ? [{ label: "Stop", onClick: stop, danger: true }] : undefined}
>
{queueStatus && queueStatus.status !== "running" && (
<div className="mb-2 text-xs text-gray-500 bg-white rounded border p-2">
<div className="mb-2 text-xs text-gray-500 bg-white rounded border p-2 tabular-nums">
{queueStatus.status === "paused" && <>Paused until {queueStatus.until}</>}
{queueStatus.status === "sleeping" && <>Sleeping {queueStatus.seconds}s between jobs</>}
{queueStatus.status === "sleeping" && <>Next job in {sleepRemaining ?? queueStatus.seconds ?? 0}s</>}
{queueStatus.status === "idle" && <>Idle</>}
</div>
)}

View File

@@ -60,6 +60,49 @@ function LockedInput({ locked, ...props }: { locked: boolean } & React.InputHTML
// ─── Secret input (password-masked with eye-icon reveal) ──────────────────────
function EyeIcon({ open }: { open: boolean }) {
// GNOME-style eye / crossed-eye glyphs as inline SVG so they inherit
// currentColor instead of fighting emoji rendering across OSes.
if (open) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94" />
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19" />
<path d="M14.12 14.12a3 3 0 1 1-4.24-4.24" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
);
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
}
/**
* Input for API keys / passwords. Shows "***" masked when the server returns
* a secret value (the raw key never reaches this component by default). Eye
@@ -101,31 +144,33 @@ function SecretInput({
};
return (
<div className="relative">
<div className={`relative ${className ?? ""}`}>
<Input
type={revealed || !isMasked ? "text" : "password"}
value={value}
disabled={locked}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className={`pr-16 ${className ?? ""}`}
className="pr-9"
/>
<button
type="button"
onClick={toggle}
disabled={locked}
className="absolute right-2 top-1/2 -translate-y-1/2 text-[0.9rem] opacity-60 hover:opacity-100 disabled:opacity-30"
title={revealed ? "Hide" : "Reveal"}
>
{revealed ? "🙈" : "👁"}
</button>
{locked && (
{locked ? (
<span
className="absolute right-9 top-1/2 -translate-y-1/2 text-[0.9rem] opacity-40 pointer-events-none select-none"
className="absolute inset-y-0 right-0 flex items-center pr-2.5 text-[0.9rem] opacity-40 pointer-events-none select-none"
title="Set via environment variable — edit your .env file to change this value"
>
🔒
</span>
) : (
<button
type="button"
onClick={toggle}
tabIndex={-1}
className="absolute inset-y-0 right-0 flex items-center px-2.5 text-gray-400 hover:text-gray-700 focus:outline-none focus-visible:text-gray-700"
title={revealed ? "Hide" : "Reveal"}
aria-label={revealed ? "Hide secret" : "Reveal secret"}
>
<EyeIcon open={revealed} />
</button>
)}
</div>
);
@@ -300,7 +345,7 @@ function ConnSection({
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder={urlPlaceholder}
className="mt-0.5 max-w-sm"
className="mt-0.5"
/>
</label>
<label className="block text-sm text-gray-700 mb-1 mt-3">
@@ -311,7 +356,7 @@ function ConnSection({
value={key}
onChange={setKey}
placeholder="your-api-key"
className="mt-0.5 max-w-xs"
className="mt-0.5"
/>
</label>
<div className="flex items-center gap-2 mt-3">