chore: snapshot before HartOMat rebrand
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 .claude/hooks/pre_tool_use.py"
|
||||
"command": "python3 /home/hartmut/Documents/Copilot/schaefflerautomat/.claude/hooks/pre_tool_use.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -99,12 +99,21 @@ async def send_message(
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
|
||||
except Exception as exc:
|
||||
error_msg = str(exc)
|
||||
error_code = None
|
||||
# Extract meaningful error from OpenAI exceptions
|
||||
if hasattr(exc, 'message'):
|
||||
error_msg = exc.message
|
||||
elif hasattr(exc, 'body') and isinstance(exc.body, dict):
|
||||
error_msg = exc.body.get('error', {}).get('message', error_msg)
|
||||
if hasattr(exc, 'body') and isinstance(exc.body, dict):
|
||||
err = exc.body.get('error', {})
|
||||
error_code = err.get('code')
|
||||
error_msg = err.get('message', error_msg)
|
||||
logger.error("Chat error: %s", error_msg)
|
||||
# Content filter violation → return 422 with user-friendly message
|
||||
if error_code == 'content_filter':
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Deine Nachricht wurde vom Azure Content Filter blockiert. Bitte formuliere sie um.",
|
||||
)
|
||||
raise HTTPException(status_code=500, detail=f"AI error: {error_msg[:500]}")
|
||||
except Exception as exc:
|
||||
logger.exception("Chat error for user %s", user.id)
|
||||
|
||||
@@ -212,6 +212,50 @@ async def stream_render_log(
|
||||
from fastapi import status as http_status
|
||||
|
||||
|
||||
@router.post("/activity/dismiss-failed", status_code=http_status.HTTP_200_OK)
|
||||
async def dismiss_all_failed(
|
||||
user: User = Depends(require_admin_or_pm),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Reset all failed render jobs and CAD files so they stop showing as failures."""
|
||||
from sqlalchemy import update as sql_update
|
||||
|
||||
render_result = await db.execute(
|
||||
sql_update(OrderLine)
|
||||
.where(OrderLine.render_status == "failed")
|
||||
.values(render_status="cancelled")
|
||||
)
|
||||
cad_result = await db.execute(
|
||||
sql_update(CadFile)
|
||||
.where(CadFile.processing_status == ProcessingStatus.failed)
|
||||
.values(processing_status=ProcessingStatus.pending)
|
||||
)
|
||||
await db.commit()
|
||||
return {"dismissed_renders": render_result.rowcount, "dismissed_cad": cad_result.rowcount}
|
||||
|
||||
|
||||
@router.post("/activity/dismiss-render/{order_line_id}", status_code=http_status.HTTP_200_OK)
|
||||
async def dismiss_single_failed_render(
|
||||
order_line_id: str,
|
||||
user: User = Depends(require_admin_or_pm),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Dismiss a single failed render job by setting its status to 'cancelled'."""
|
||||
result = await db.execute(
|
||||
select(OrderLine).where(
|
||||
OrderLine.id == order_line_id,
|
||||
OrderLine.render_status == "failed",
|
||||
)
|
||||
)
|
||||
line = result.scalar_one_or_none()
|
||||
if not line:
|
||||
raise HTTPException(404, detail="Failed render job not found")
|
||||
|
||||
line.render_status = "cancelled"
|
||||
await db.commit()
|
||||
return {"dismissed": order_line_id}
|
||||
|
||||
|
||||
@router.post("/activity/{cad_file_id}/reprocess", status_code=http_status.HTTP_202_ACCEPTED)
|
||||
async def reprocess_cad_file(
|
||||
cad_file_id: str,
|
||||
|
||||
@@ -443,8 +443,9 @@ def render_order_line_task(self, order_line_id: str):
|
||||
|
||||
if is_cinematic:
|
||||
# ── Cinematic highlight animation path ──────────────────────
|
||||
_cine_fps = 24
|
||||
_cine_frames = 480
|
||||
# Use frame_count/fps from output_type.render_settings (already extracted above)
|
||||
_cine_fps = fps # extracted from render_settings, default 25
|
||||
_cine_frames = frame_count # extracted from render_settings, default 24
|
||||
emit(order_line_id, f"Starting cinematic render: {_cine_frames} frames @ {_cine_fps}fps, {render_width or 1920}x{render_height or 1080}{tmpl_info}")
|
||||
pl.step_start("blender_cinematic", {"frame_count": _cine_frames, "fps": _cine_fps})
|
||||
from app.services.render_blender import is_blender_available, render_cinematic_to_file
|
||||
|
||||
Generated
+1
-1
@@ -13,7 +13,7 @@
|
||||
"@tanstack/react-query": "^5.28.4",
|
||||
"@tanstack/react-table": "^8.14.0",
|
||||
"@xyflow/react": "^12.0.0",
|
||||
"axios": "^1.6.8",
|
||||
"axios": "1.13.6",
|
||||
"clsx": "^2.1.0",
|
||||
"get-stream": "^9.0.1",
|
||||
"lucide-react": "^0.363.0",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@tanstack/react-query": "^5.28.4",
|
||||
"@tanstack/react-table": "^8.14.0",
|
||||
"@xyflow/react": "^12.0.0",
|
||||
"axios": "^1.6.8",
|
||||
"axios": "1.13.6",
|
||||
"clsx": "^2.1.0",
|
||||
"get-stream": "^9.0.1",
|
||||
"lucide-react": "^0.363.0",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
×
|
||||
</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
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
@@ -1,45 +0,0 @@
|
||||
# Review Report: Full UI/UX Cleanup & Simplification
|
||||
Date: 2026-03-15
|
||||
|
||||
## Result: ✅ Approved
|
||||
|
||||
## Checklist Results
|
||||
|
||||
### Frontend / TypeScript
|
||||
- [x] TypeScript compiles clean (`npx tsc --noEmit` — zero errors)
|
||||
- [x] No `bg-surface/50` Tailwind opacity syntax with CSS vars
|
||||
- [x] Loading states preserved (all existing `isPending` usage untouched)
|
||||
- [x] Error feedback preserved (all existing `toast.error` calls untouched)
|
||||
- [x] Role-dependent UI elements unchanged
|
||||
- [x] No new API interfaces needed (frontend-only visual refactor)
|
||||
|
||||
### Backend / Database / Render Pipeline
|
||||
- [x] N/A — all changes are frontend-only (6 component/page files + plan.md)
|
||||
|
||||
### Security
|
||||
- [x] No credentials in code
|
||||
- [x] No hardcoded tokens or secrets
|
||||
- [x] English variable names and comments
|
||||
|
||||
## Minor Notes (non-blocking)
|
||||
|
||||
### `as any` usage in val()/set() helpers
|
||||
**Severity**: Low
|
||||
The RenderTemplateTable uses `(editDraft as any)[field]` and `(form as any)[field]` in the shared `renderEditFormGrid()` helper — same pattern as the reference OutputTypeTable implementation. This is a TypeScript limitation with dynamic field access on union types. Non-blocking; could be improved with generics later but works correctly.
|
||||
|
||||
## Positives
|
||||
|
||||
1. **Consistent pattern across all 4 admin tables**: OutputTypeTable, RenderTemplateTable, PricingTierTable, and GlobalRenderPositionsPanel all now use the identical expandable edit row pattern — display row always visible, edit form as full-width colSpan row below with accent border.
|
||||
|
||||
2. **RenderTemplateTable column reduction**: Consolidated 4 boolean columns (Mat Replace, Lighting Only, Shadow Catcher, Camera Orbit) into a single "Flags" column with compact badges — reduces visual width from 11 to 8 columns.
|
||||
|
||||
3. **Shared renderEditFormGrid()**: Both RenderTemplateTable and PricingTierTable use a shared helper for add/edit forms, keeping the pattern DRY.
|
||||
|
||||
4. **OrderDetail material override UX**: The per-line override now shows a compact "+ override" link instead of always-visible dropdown — significantly reduces visual noise while keeping functionality accessible.
|
||||
|
||||
5. **WorkerManagement controls**: Larger touch targets (p-2 rounded-lg) make the scale controls usable on touch devices.
|
||||
|
||||
6. **Billing status indicator**: ChevronDown icon next to the status select makes the interactivity obvious without changing the badge aesthetic.
|
||||
|
||||
## Recommendation
|
||||
Approved — ready to merge.
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Parse flags
|
||||
BUILD=""
|
||||
SERVICE=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--build) BUILD="--build" ;;
|
||||
*) SERVICE="$arg" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$SERVICE" ]; then
|
||||
echo "Restarting $SERVICE${BUILD:+ (with rebuild)}..."
|
||||
docker compose up -d $BUILD "$SERVICE"
|
||||
else
|
||||
echo "Restarting all services${BUILD:+ (with rebuild)}..."
|
||||
docker compose down
|
||||
docker compose up -d $BUILD
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Waiting for health checks..."
|
||||
sleep 3
|
||||
|
||||
BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/docs 2>/dev/null || echo "000")
|
||||
FRONTEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5173 2>/dev/null || echo "000")
|
||||
|
||||
echo ""
|
||||
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
||||
echo ""
|
||||
|
||||
if [ "$BACKEND" = "200" ] && [ "$FRONTEND" = "200" ]; then
|
||||
echo "All services up."
|
||||
else
|
||||
echo "WARNING: Some services may not be ready yet."
|
||||
[ "$BACKEND" != "200" ] && echo " Backend returned $BACKEND"
|
||||
[ "$FRONTEND" != "200" ] && echo " Frontend returned $FRONTEND"
|
||||
fi
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Starting Schaeffler Automat..."
|
||||
docker compose up -d
|
||||
|
||||
echo ""
|
||||
echo "Waiting for health checks..."
|
||||
sleep 3
|
||||
|
||||
# Check services
|
||||
BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/docs 2>/dev/null || echo "000")
|
||||
FRONTEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5173 2>/dev/null || echo "000")
|
||||
|
||||
echo ""
|
||||
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
||||
echo ""
|
||||
|
||||
if [ "$BACKEND" = "200" ] && [ "$FRONTEND" = "200" ]; then
|
||||
echo "All services up."
|
||||
echo " Backend: http://localhost:8888/docs"
|
||||
echo " Frontend: http://localhost:5173"
|
||||
else
|
||||
echo "WARNING: Some services may not be ready yet."
|
||||
[ "$BACKEND" != "200" ] && echo " Backend returned $BACKEND"
|
||||
[ "$FRONTEND" != "200" ] && echo " Frontend returned $FRONTEND"
|
||||
echo " Run 'docker compose logs -f' to investigate."
|
||||
fi
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "Stopping Schaeffler Automat..."
|
||||
docker compose down
|
||||
|
||||
echo ""
|
||||
echo "All services stopped."
|
||||
Reference in New Issue
Block a user