gonna break stuff now
Container image / image (push) Failing after 51s

This commit is contained in:
legop3
2026-09-24 18:36:24 -04:00
parent 8ca714cf1a
commit 7231809140
30 changed files with 292 additions and 388 deletions
+14 -56
View File
@@ -1,61 +1,19 @@
import { useCallback, useEffect, useState } from 'react';
import { useControlActions } from '../../controls/index.js';
import { useSessionActions } from '../../context/SessionContext.jsx';
import { useCallback, useEffect } from 'react';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import { triggerTouchHaptic } from '../../lib/touchHaptics.js';
import CardFrame from '../../components/CardFrame/index.jsx';
import ControlPadPanel from '../../components/MobileControls/ControlPadPanel.jsx';
import GPIOToggleControl from '../../components/GPIOToggleControl/index.jsx';
import KeyPill from '../../components/vip/VipAudioUploadCard/KeyPill.jsx';
import ControlHint from '../../components/ControlHint/index.jsx';
function isSpotlightOn(light = {}) {
if (typeof light?.on === 'boolean') return light.on;
const raw = light?.state;
if (typeof raw === 'string') {
const normalized = raw.trim().toLowerCase();
return !['', '0', 'off', 'false'].includes(normalized);
}
return Boolean(Number(raw));
}
function normalizeIrMode(mode) {
const normalized = String(mode || '').trim().toLowerCase();
if (normalized === 'on') return 'On';
if (normalized === 'off') return 'Off';
return 'Auto';
}
function nextIrMode(currentMode) {
const current = normalizeIrMode(currentMode);
if (current === 'Auto') return 'On';
if (current === 'On') return 'Off';
return 'Auto';
}
function PtzLightingControls({ ptz, disabled = false }) {
const { ptzSpotlight, ptzIr } = useSessionActions();
const [busy, setBusy] = useState('');
const spotlightOn = isSpotlightOn(ptz?.light);
const irMode = normalizeIrMode(ptz?.ir?.state);
const toggleSpotlight = async (nextOn) => {
if (disabled) return;
setBusy('spotlight');
try {
await ptzSpotlight({ state: nextOn ? 1 : 0 });
} finally {
setBusy('');
}
};
const cycleIr = async () => {
if (disabled) return;
function PtzLightingControls({ disabled = false }) {
const { setHeadlight, setLaser } = useControlActions();
const { spotlightOn, irMode, pending } = useControlSelector((control) => control.ptzControls.lighting);
const canControl = useControlSelector((control) => control.ptzControls.isActive);
const blocked = disabled || !canControl;
const cycleIr = () => {
triggerTouchHaptic('button');
setBusy('ir');
try {
await ptzIr({ state: nextIrMode(irMode) });
} finally {
setBusy('');
}
setLaser();
};
return (
@@ -63,14 +21,14 @@ function PtzLightingControls({ ptz, disabled = false }) {
<GPIOToggleControl
label="Spotlight"
on={spotlightOn}
disabled={disabled || busy === 'spotlight'}
onToggle={toggleSpotlight}
disabled={blocked || pending.spotlight}
onToggle={setHeadlight}
heightClass="min-h-14"
/>
<button
type="button"
className="mobile-touch-control flex min-h-14 flex-col items-center justify-center gap-0.5 rounded-xl border-2 border-cyan-300/70 bg-cyan-900 px-1 py-0.75 text-center text-cyan-50 disabled:opacity-50"
disabled={disabled || busy === 'ir'}
disabled={blocked || pending.ir}
onClick={cycleIr}
>
<span className="text-sm font-semibold">Infrared</span>
@@ -158,7 +116,7 @@ function PtzMobileZoomButtons({ disabled = false }) {
);
}
function PtzMobileControlsPanel({ ptz, disabled = false }) {
function PtzMobileControlsPanel({ disabled = false }) {
return (
<div className="mobile-touch-control space-y-0.5">
<PtzMobileZoomButtons disabled={disabled} />
@@ -170,7 +128,7 @@ function PtzMobileControlsPanel({ ptz, disabled = false }) {
*/}
<ControlPadPanel compact disabled={disabled} />
</div>
<PtzLightingControls ptz={ptz} disabled={disabled} />
<PtzLightingControls disabled={disabled} />
</div>
);
}
+3 -11
View File
@@ -1,10 +1,9 @@
import CardFrame from '../../components/CardFrame/index.jsx';
import ChatPanel from '../../components/ChatPanel/index.jsx';
import HomeAssistantControls from '../../components/HomeAssistantControls/index.jsx';
import ReplaySourcesPanel from '../../components/ReplaySourcesPanel/index.jsx';
import { PTZ_CAMERA_ID } from '../../components/PtzLiveVideo/index.jsx';
import { PtzQueueSummary } from './PtzQueuePanel.jsx';
import { PtzLightingControls, PtzMobileControlsPanel, PtzControlReference } from './PtzControls.jsx';
import { PtzMobileControlsPanel, PtzControlReference } from './PtzControls.jsx';
import { PtzPresetPanel } from './PtzPresetPanel.jsx';
import { PtzMediaPane } from './PtzMediaPane.jsx';
function PtzDesktopFullscreen({ ptz, releasePending }) {
@@ -19,13 +18,6 @@ function PtzDesktopFullscreen({ ptz, releasePending }) {
layout instead of being covered by one solid sidebar rectangle. */}
<aside className="flex min-h-0 min-w-56 flex-1 flex-col gap-0.5 overflow-y-auto text-sm">
<PtzQueueSummary ptz={ptz} />
{ptz?.permissions?.canControl ? (
<PtzLightingControls ptz={ptz} />
) : (
<CardFrame title="Controls" bodyClassName="p-1 text-xs text-slate-400">
Live PTZ controls unlock when your camera turn is active.
</CardFrame>
)}
<PtzControlReference />
<ReplaySourcesPanel panelId="ptz-controller-replay" defaultSelectedKey={`ptz:${PTZ_CAMERA_ID}`} />
{/*
@@ -97,7 +89,7 @@ function PtzMobileLandscape({ ptz, onClose, releasePending = false }) {
the same scroll column.
*/}
<PtzQueueSummary ptz={ptz} />
<PtzMobileControlsPanel ptz={ptz} disabled={!ptz?.permissions?.canControl} />
<PtzMobileControlsPanel disabled={!ptz?.permissions?.canControl} />
<ReplaySourcesPanel
panelId="ptz-controller-replay-mobile-landscape"
defaultSelectedKey={`ptz:${PTZ_CAMERA_ID}`}
@@ -133,7 +125,7 @@ function PtzMobilePortrait({ ptz, onClose, releasePending = false }) {
same shared PTZ controls used in landscape.
*/}
<section className="mobile-touch-control">
<PtzMobileControlsPanel ptz={ptz} disabled={!ptz?.permissions?.canControl} />
<PtzMobileControlsPanel disabled={!ptz?.permissions?.canControl} />
</section>
<section className="space-y-0.5">
{/*
@@ -0,0 +1,19 @@
import { FaLightbulb, FaEye } from 'react-icons/fa';
import { useControlActions, useControlSelector } from '../../controls/index.js';
import ControlHint from '../../components/ControlHint/index.jsx';
import PeripheralPod from '../../components/HudOverlays/newgen/CornerPods/PeripheralPod.jsx';
import RoundControl from '../../components/HudOverlays/newgen/CornerPods/RoundControl.jsx';
import usePodVisibility from '../../components/HudOverlays/newgen/CornerPods/usePodVisibility.js';
export default function PtzLightingPod() {
const [open, setOpen] = usePodVisibility('ptzLighting', true);
const { setHeadlight, setLaser } = useControlActions();
const canControl = useControlSelector((control) => control.ptzControls.isActive);
const { spotlightOn, irMode, pending } = useControlSelector((control) => control.ptzControls.lighting);
return (
<PeripheralPod open={open} onOpenChange={setOpen} label="camera lights" className="h-24 w-44">
<RoundControl label="Spotlight" icon={FaLightbulb} keyLabel={<ControlHint actionId="headlightToggle" />} active={spotlightOn} disabled={!canControl || pending.spotlight} onClick={() => setHeadlight(!spotlightOn)} className="absolute left-5 top-1" />
<RoundControl label={`Infrared: ${irMode}`} icon={FaEye} value={irMode} keyLabel={<ControlHint actionId="laserToggle" />} active={irMode === 'On'} disabled={!canControl || pending.ir} onClick={() => setLaser()} className="absolute right-1 bottom-1" />
</PeripheralPod>
);
}
+8 -4
View File
@@ -1,8 +1,9 @@
import PtzLightingPod from './PtzLightingPod.jsx';
import PtzLiveVideo, { PTZ_CAMERA_ID } from '../../components/PtzLiveVideo/index.jsx';
import ChatExpansion from '../../components/HudOverlays/newgen/CornerPods/ChatExpansion.jsx';
import TopLeftPod from '../../components/HudOverlays/newgen/CornerPods/TopLeftPod.jsx';
import { usePtzCameraSnapshots } from '../../hooks/usePtzCameraSnapshot.js';
function PtzSnapshotPreview({ feed, label = 'PTZ Camera', className = 'h-full w-full' }) {
function PtzSnapshotPreview({ feed, label = 'PTZ Camera', className = 'h-full w-full', statusClassName = 'pointer-events-none absolute left-1 bottom-1 z-20 font-medium text-slate-100 text-[0.65rem]' }) {
return (
<div className={`relative overflow-hidden bg-black ${className}`}>
{feed?.objectUrl ? (
@@ -15,7 +16,7 @@ function PtzSnapshotPreview({ feed, label = 'PTZ Camera', className = 'h-full w-
camera name belongs to the surrounding card/menu, while the media pane
only exposes stream health in the small bottom-left diagnostic overlay.
*/}
<div className="pointer-events-none absolute left-1 bottom-1 z-20 font-medium text-slate-100 text-[0.65rem]">
<div className={statusClassName}>
<div className="flex flex-col gap-0.5 leading-none">
<span>Status: {feed?.error ? `Error: ${feed.error}` : feed?.status || 'connecting'}</span>
</div>
@@ -28,14 +29,17 @@ function PtzMediaPane({ ptz, open, framed = true, compact = false }) {
const shouldUseLiveVideo = ptz?.viewMode === 'live';
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !shouldUseLiveVideo });
const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null;
const statusClassName = `pointer-events-none absolute left-1 ${compact ? 'bottom-1' : 'bottom-24'} z-20 text-[0.65rem] font-medium text-slate-100`;
const media = (
<>
{shouldUseLiveVideo ? (
<PtzLiveVideo enabled={open} startMuted={false} statusClassName="pointer-events-none absolute left-1 bottom-1 z-20 text-[0.65rem] font-medium text-slate-100" />
<PtzLiveVideo enabled={open} startMuted={false} statusClassName={statusClassName} />
) : (
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} statusClassName={statusClassName} />
)}
{ptz?.turn ? <TopLeftPod compact={compact} turns={ptz.turn} /> : null}
{/* Mobile already has lighting controls beside/below the video. */}
{!compact ? <PtzLightingPod /> : null}
<ChatExpansion podOpen={false} />
</>
);
+5 -5
View File
@@ -3,11 +3,11 @@ import { useSessionActions, useSessionSelector } from '../../context/SessionCont
import CardFrame from '../../components/CardFrame/index.jsx';
import { QueueUserChips } from '../../components/QueueTargetRow/index.jsx';
import { PTZ_CAMERA_ID } from '../../components/PtzLiveVideo/index.jsx';
import { normalizePtzQueue, usePtzQueueLookup } from '../queue.js';
import useQueueUserLookup from '../../hooks/useQueueUserLookup.js';
function PtzQueueSummary({ ptz, title = 'PTZ queue' }) {
const { ptzRequestTurn, ptzRelease, pushAlert } = useSessionActions();
const [pending, setPending] = useState(false);
const participating = Boolean(ptz?.isOperator || ptz?.queuedPosition);
const participating = Boolean(ptz?.turn?.isActive || ptz?.turn?.turnsAhead);
const changeTurn = async () => {
if (pending) return;
setPending(true);
@@ -21,8 +21,8 @@ function PtzQueueSummary({ ptz, title = 'PTZ queue' }) {
}
};
const selfId = useSessionSelector((state) => state.session?.socketId || null);
const lookupUser = usePtzQueueLookup(ptz);
const { queue, currentId, nextId } = normalizePtzQueue(ptz);
const lookupUser = useQueueUserLookup(ptz?.turn);
const { queue, currentId, nextId } = ptz?.turn || {};
return (
<CardFrame title={title} bodyClassName="space-y-0.5 p-1 text-sm">
@@ -35,7 +35,7 @@ function PtzQueueSummary({ ptz, title = 'PTZ queue' }) {
lookupUser={lookupUser}
/>
<button type="button" className="button-dark w-full text-xs" disabled={pending || !(participating ? ptz?.permissions?.canReleaseTurn : ptz?.permissions?.canRequestTurn)} onClick={changeTurn}>
{pending ? 'Please wait…' : ptz?.isOperator ? 'Release turn' : ptz?.queuedPosition ? 'Leave queue' : 'Request turn'}
{pending ? 'Please wait…' : ptz?.turn?.isActive ? 'Release turn' : ptz?.turn?.turnsAhead ? 'Leave queue' : 'Request turn'}
</button>
</CardFrame>
);
-42
View File
@@ -1,42 +0,0 @@
import { useCallback } from 'react';
import { useSessionSelector } from '../context/SessionContext.jsx';
function normalizePtzQueue(ptz = null) {
/*
The PTZ service exposes the current operator separately from the waiting
queue, while the rover queue row expects one ordered queue plus a current id.
Normalizing once keeps every PTZ surface consistent with the shared queue
renderer without making that renderer understand PTZ service internals.
*/
const currentId = ptz?.operatorSocketId || null;
const waiting = Array.isArray(ptz?.queue)
? ptz.queue.map((entry) => entry?.socketId || entry).filter(Boolean)
: [];
const queue = currentId ? [currentId, ...waiting.filter((id) => id !== currentId)] : waiting;
const nextId = currentId ? waiting[0] || null : queue[0] || null;
return { queue, currentId, nextId };
}
function usePtzQueueLookup(ptz = null) {
const users = useSessionSelector((state) => state.session?.users ?? []);
return useCallback(
(socketId) => {
const fromUsers = users.find((entry) => entry.socketId === socketId);
if (fromUsers) return fromUsers;
if (ptz?.operatorSocketId === socketId) {
return { socketId, nickname: ptz?.operatorLabel || null, role: null };
}
const fromQueue = Array.isArray(ptz?.queue)
? ptz.queue.find((entry) => (entry?.socketId || entry) === socketId)
: null;
return {
socketId,
nickname: fromQueue?.label || null,
role: null,
};
},
[ptz, users],
);
}
export { normalizePtzQueue, usePtzQueueLookup };