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",
|
||||
"version": "2026.04.15.4",
|
||||
"version": "2026.04.15.5",
|
||||
"scripts": {
|
||||
"dev:server": "NODE_ENV=development bun --hot server/index.tsx",
|
||||
"dev:client": "vite",
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user