chore: snapshot before HartOMat rebrand

This commit is contained in:
2026-04-06 12:41:44 +02:00
parent 7d27ffc116
commit fa7093307a
25 changed files with 247 additions and 92 deletions
+9
View File
@@ -65,6 +65,15 @@ export async function reprocessCadFile(cad_file_id: string): Promise<void> {
await api.post(`/worker/activity/${cad_file_id}/reprocess`)
}
export async function dismissAllFailed(): Promise<{ dismissed_renders: number; dismissed_cad: number }> {
const res = await api.post<{ dismissed_renders: number; dismissed_cad: number }>('/worker/activity/dismiss-failed')
return res.data
}
export async function dismissSingleRender(orderLineId: string): Promise<void> {
await api.post(`/worker/activity/dismiss-render/${orderLineId}`)
}
export interface RenderLogEntry {
ts: number
t: string
+3 -3
View File
@@ -62,7 +62,7 @@ export default function LiveRenderLog({
<div className="mt-1">
<button
onClick={() => setExpanded((v) => !v)}
className="text-[10px] text-gray-400 hover:text-gray-600 flex items-center gap-1"
className="text-[10px] text-content-muted hover:text-content-secondary flex items-center gap-1"
>
<Terminal size={10} />
Log ({entries.length})
@@ -79,11 +79,11 @@ export default function LiveRenderLog({
<div className="mt-2">
<button
onClick={() => setExpanded((v) => !v)}
className="flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-700 mb-1"
className="flex items-center gap-1.5 text-xs text-content-muted hover:text-content-secondary mb-1"
>
<Terminal size={12} />
<span className="font-medium">Render Log</span>
<span className="text-gray-400">({entries.length} entries)</span>
<span className="text-content-muted">({entries.length} entries)</span>
{expanded ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
</button>
{expanded && (
+3 -2
View File
@@ -316,8 +316,9 @@ export default function ChatPanel({ open, onClose, contextType, contextId }: Cha
{/* Error state */}
{sendMut.isError && (
<div className="flex justify-center">
<p className="text-xs text-red-500 bg-red-50 px-3 py-1.5 rounded-full">
Failed to send. Please try again.
<p className="text-xs text-red-500 bg-red-50 dark:bg-red-900/20 px-3 py-1.5 rounded-full text-center max-w-[80%]">
{(sendMut.error as { response?: { data?: { detail?: string } } })?.response?.data?.detail
|| 'Failed to send. Please try again.'}
</p>
</div>
)}
+16 -16
View File
@@ -41,51 +41,51 @@ function UploadModal({ onClose }: { onClose: () => void }) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg flex flex-col">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Upload Asset Library</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">
<div className="bg-surface rounded-xl shadow-2xl w-full max-w-lg flex flex-col">
<div className="px-6 py-4 border-b border-border-default flex items-center justify-between">
<h2 className="text-lg font-semibold text-content">Upload Asset Library</h2>
<button onClick={onClose} className="text-content-muted hover:text-content-secondary text-xl leading-none">
&times;
</button>
</div>
<div className="px-6 py-4 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-content-secondary mb-1">
Name <span className="text-red-500">*</span>
</label>
<input
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className="input-base"
placeholder="e.g. Schaeffler Materials v2"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<label className="block text-sm font-medium text-content-secondary mb-1">Description</label>
<input
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className="input-base"
placeholder="Optional description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-content-secondary mb-1">
.blend File <span className="text-red-500">*</span>
</label>
<div
{...getRootProps()}
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-blue-400'
isDragActive ? 'border-accent bg-accent-light' : 'border-border-default hover:border-accent'
}`}
>
<input {...getInputProps()} />
{file ? (
<p className="text-sm text-gray-700 font-medium">{file.name}</p>
<p className="text-sm text-content-secondary font-medium">{file.name}</p>
) : (
<>
<Upload size={24} className="text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-500">
<Upload size={24} className="text-content-muted mx-auto mb-2" />
<p className="text-sm text-content-muted">
{isDragActive ? 'Drop the .blend file here' : 'Drag & drop a .blend file, or click to browse'}
</p>
</>
@@ -93,10 +93,10 @@ function UploadModal({ onClose }: { onClose: () => void }) {
</div>
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
<div className="px-6 py-4 border-t border-border-default flex justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors"
className="btn-secondary"
>
Cancel
</button>
@@ -183,7 +183,7 @@ function LibraryCard({ lib }: { lib: AssetLibrary }) {
disabled={toggleMut.isPending}
title={lib.is_active ? 'Deactivate' : 'Activate'}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${
lib.is_active ? 'bg-green-500' : 'bg-gray-300'
lib.is_active ? 'bg-green-500' : 'bg-surface-muted'
} disabled:opacity-50`}
>
<span
+1 -1
View File
@@ -48,7 +48,7 @@ function Toggle({
onClick={() => !disabled && onChange(!enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${
disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'
} ${enabled ? 'bg-blue-600' : 'bg-gray-200'}`}
} ${enabled ? 'bg-accent' : 'bg-surface-muted'}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
+4 -1
View File
@@ -71,6 +71,7 @@ export default function OrderDetailPage() {
const [rejectModalOpen, setRejectModalOpen] = useState(false)
const [rejectReason, setRejectReason] = useState('')
const [rejectNotifyClient, setRejectNotifyClient] = useState(true)
const [dispatchedAt, setDispatchedAt] = useState<number | null>(null)
// Table state
const [filters, setFilters] = useState<TableFilters>(EMPTY_FILTERS)
@@ -81,9 +82,10 @@ export default function OrderDetailPage() {
const { data: order, isLoading } = useQuery({
queryKey: ['order', id],
queryFn: () => getOrder(id!),
// Poll while renders are active (pending/processing) — stop when all terminal
// Poll while renders are active, or for 15s after dispatch to catch initial queuing
refetchInterval: (query) => {
const rp = query.state.data?.render_progress
if (dispatchedAt && Date.now() - dispatchedAt < 15000) return 2000
if (!rp) return false
return (rp.pending > 0 || rp.processing > 0) ? 3000 : false
},
@@ -113,6 +115,7 @@ export default function OrderDetailPage() {
mutationFn: () => dispatchRenders(id!),
onSuccess: (data) => {
toast.success(`${data.dispatched} render${data.dispatched !== 1 ? 's' : ''} dispatched`)
setDispatchedAt(Date.now())
qc.invalidateQueries({ queryKey: ['order', id] })
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Dispatch failed'),
+68 -14
View File
@@ -10,6 +10,7 @@ import { Link } from 'react-router-dom'
import {
getWorkerActivity, reprocessCadFile, CadActivityEntry, RenderLog, RenderJobEntry,
getQueueStatus, purgeQueue, cancelTask, QueueTask,
dismissAllFailed, dismissSingleRender,
} from '../api/worker'
import LiveRenderLog from '../components/LiveRenderLog'
import ConfirmModal from '../components/ConfirmModal'
@@ -37,6 +38,25 @@ export default function WorkerActivityPage() {
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'),
})
const dismissAllMut = useMutation({
mutationFn: dismissAllFailed,
onSuccess: (res) => {
const total = res.dismissed_renders + res.dismissed_cad
toast.success(`${total} failed job${total !== 1 ? 's' : ''} cleared`)
qc.invalidateQueries({ queryKey: ['worker-activity'] })
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to dismiss'),
})
const dismissOneMut = useMutation({
mutationFn: dismissSingleRender,
onSuccess: () => {
toast.success('Render dismissed')
qc.invalidateQueries({ queryKey: ['worker-activity'] })
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to dismiss'),
})
const lastUpdated = dataUpdatedAt
? new Date(dataUpdatedAt).toLocaleTimeString('de-DE')
: '—'
@@ -84,15 +104,32 @@ export default function WorkerActivityPage() {
{/* Summary */}
{data && (
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="CAD active" value={data.active_count}
color={data.active_count > 0 ? 'text-status-info-text' : 'text-content-secondary'} />
<StatCard label="CAD failed" value={data.failed_count}
color={data.failed_count > 0 ? 'text-red-600' : 'text-content-secondary'} />
<StatCard label="Rendering" value={data.render_active_count}
color={data.render_active_count > 0 ? 'text-status-info-text' : 'text-content-secondary'} />
<StatCard label="Render failed" value={data.render_failed_count}
color={data.render_failed_count > 0 ? 'text-red-600' : 'text-content-secondary'} />
<div className="space-y-2">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard label="CAD active" value={data.active_count}
color={data.active_count > 0 ? 'text-status-info-text' : 'text-content-secondary'} />
<StatCard label="CAD failed" value={data.failed_count}
color={data.failed_count > 0 ? 'text-red-600' : 'text-content-secondary'} />
<StatCard label="Rendering" value={data.render_active_count}
color={data.render_active_count > 0 ? 'text-status-info-text' : 'text-content-secondary'} />
<StatCard label="Render failed" value={data.render_failed_count}
color={data.render_failed_count > 0 ? 'text-red-600' : 'text-content-secondary'} />
</div>
{(data.failed_count + data.render_failed_count) > 0 && (
<div className="flex justify-end">
<button
disabled={dismissAllMut.isPending}
onClick={() => dismissAllMut.mutate()}
className="text-xs px-3 py-1.5 rounded-lg border border-red-200 text-red-600 hover:bg-red-50 transition-colors flex items-center gap-1.5 disabled:opacity-50"
>
{dismissAllMut.isPending
? <Loader2 size={11} className="animate-spin" />
: <XCircle size={11} />
}
Clear all failed ({data.failed_count + data.render_failed_count})
</button>
</div>
)}
</div>
)}
@@ -124,7 +161,12 @@ export default function WorkerActivityPage() {
<div className="card overflow-hidden divide-y divide-border-light">
{events.map((ev) =>
ev.kind === 'render' ? (
<RenderJobRow key={`render-${ev.job.order_line_id}`} job={ev.job} />
<RenderJobRow
key={`render-${ev.job.order_line_id}`}
job={ev.job}
onDismiss={ev.job.render_status === 'failed' ? () => dismissOneMut.mutate(ev.job.order_line_id) : undefined}
dismissPending={dismissOneMut.isPending}
/>
) : (
<CadFileRow
key={`cad-${ev.entry.cad_file_id}`}
@@ -421,7 +463,7 @@ function QueuePanel() {
// ── Render job row ───────────────────────────────────────────────────────────
function RenderJobRow({ job }: { job: RenderJobEntry }) {
function RenderJobRow({ job, onDismiss, dismissPending }: { job: RenderJobEntry; onDismiss?: () => void; dismissPending?: boolean }) {
const elapsed = job.render_started_at && job.render_completed_at
? ((new Date(job.render_completed_at).getTime() - new Date(job.render_started_at).getTime()) / 1000).toFixed(1)
: null
@@ -473,9 +515,21 @@ function RenderJobRow({ job }: { job: RenderJobEntry }) {
</div>
</div>
<div className="text-xs text-content-muted shrink-0 text-right hidden sm:block">
<p>{new Date(job.updated_at).toLocaleDateString('de-DE')}</p>
<p>{new Date(job.updated_at).toLocaleTimeString('de-DE')}</p>
<div className="flex items-center gap-2 shrink-0">
{onDismiss && (
<button
disabled={dismissPending}
onClick={onDismiss}
className="text-xs px-2 py-1 rounded border border-red-200 text-red-500 hover:bg-red-50 transition-colors flex items-center gap-1 disabled:opacity-50"
title="Dismiss failed render"
>
<XCircle size={11} /> Dismiss
</button>
)}
<div className="text-xs text-content-muted text-right hidden sm:block">
<p>{new Date(job.updated_at).toLocaleDateString('de-DE')}</p>
<p>{new Date(job.updated_at).toLocaleTimeString('de-DE')}</p>
</div>
</div>
</div>
<div className="px-4 pb-1">
+3 -3
View File
@@ -64,15 +64,15 @@ function BaseNode({ label, icon, color, description, selected, hasSource = true,
}`}
>
{hasTarget && (
<Handle type="target" position={Position.Left} className="w-3 h-3 bg-gray-400 border-2 border-white" />
<Handle type="target" position={Position.Left} className="w-3 h-3 bg-content-muted border-2 border-surface" />
)}
<div className={`flex items-center gap-2 mb-1 text-${color}-600`}>
{icon}
<span className="font-medium text-sm">{label}</span>
</div>
{description && <p className="text-xs text-gray-500">{description}</p>}
{description && <p className="text-xs text-content-muted">{description}</p>}
{hasSource && (
<Handle type="source" position={Position.Right} className="w-3 h-3 bg-gray-400 border-2 border-white" />
<Handle type="source" position={Position.Right} className="w-3 h-3 bg-content-muted border-2 border-surface" />
)}
</div>
)