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
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:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "netfelix-audio-fix",
|
"name": "netfelix-audio-fix",
|
||||||
"version": "2026.04.15.4",
|
"version": "2026.04.15.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||||
"dev:client": "vite",
|
"dev:client": "vite",
|
||||||
|
|||||||
@@ -23,6 +23,25 @@ export function ProcessingColumn({ items, progress, queueStatus, onMutate }: Pro
|
|||||||
return () => clearInterval(t);
|
return () => clearInterval(t);
|
||||||
}, [job]);
|
}, [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
|
// 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.
|
// a previous job would otherwise show wrong numbers until the new job emits.
|
||||||
const liveProgress = job && progress && progress.id === job.id ? progress : null;
|
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}
|
actions={job ? [{ label: "Stop", onClick: stop, danger: true }] : undefined}
|
||||||
>
|
>
|
||||||
{queueStatus && queueStatus.status !== "running" && (
|
{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 === "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</>}
|
{queueStatus.status === "idle" && <>Idle</>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -60,6 +60,49 @@ function LockedInput({ locked, ...props }: { locked: boolean } & React.InputHTML
|
|||||||
|
|
||||||
// ─── Secret input (password-masked with eye-icon reveal) ──────────────────────
|
// ─── 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
|
* Input for API keys / passwords. Shows "***" masked when the server returns
|
||||||
* a secret value (the raw key never reaches this component by default). Eye
|
* a secret value (the raw key never reaches this component by default). Eye
|
||||||
@@ -101,31 +144,33 @@ function SecretInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className={`relative ${className ?? ""}`}>
|
||||||
<Input
|
<Input
|
||||||
type={revealed || !isMasked ? "text" : "password"}
|
type={revealed || !isMasked ? "text" : "password"}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={locked}
|
disabled={locked}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={`pr-16 ${className ?? ""}`}
|
className="pr-9"
|
||||||
/>
|
/>
|
||||||
<button
|
{locked ? (
|
||||||
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 && (
|
|
||||||
<span
|
<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"
|
title="Set via environment variable — edit your .env file to change this value"
|
||||||
>
|
>
|
||||||
🔒
|
🔒
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -300,7 +345,7 @@ function ConnSection({
|
|||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
placeholder={urlPlaceholder}
|
placeholder={urlPlaceholder}
|
||||||
className="mt-0.5 max-w-sm"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block text-sm text-gray-700 mb-1 mt-3">
|
<label className="block text-sm text-gray-700 mb-1 mt-3">
|
||||||
@@ -311,7 +356,7 @@ function ConnSection({
|
|||||||
value={key}
|
value={key}
|
||||||
onChange={setKey}
|
onChange={setKey}
|
||||||
placeholder="your-api-key"
|
placeholder="your-api-key"
|
||||||
className="mt-0.5 max-w-xs"
|
className="mt-0.5"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2 mt-3">
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user