feat: layout hamburger, media browser filters+previews, billing fixes

- Layout: mobile hamburger menu + overlay backdrop + close button; content area always full-width
- Media browser: filter chips (default still+turntable); advanced toggle for GLB/STL; thumbnail_url previews for non-image types; video hover-play for turntable
- Backend: asset_types multi-filter, thumbnail_url in MediaAssetOut, download proxy endpoint for MinIO/local files
- Admin: "Import Existing Media" button → POST /api/admin/import-media-assets
- Billing: fix invoice create 500 (MissingGreenlet — use selectinload after commit); PDF download uses axios blob instead of bare <a href> (auth header missing); fix storage.upload() accepting str|Path
- SSE task logs: task_logs.py core + router, LiveRenderLog component
- CadPreview: fix infinite loop when no gltf_geometry assets; loading screen before ThreeDViewer render
- render-worker: add trimesh layer to Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 00:09:27 +01:00
parent 9bf6e72718
commit f5ca91ee02
25 changed files with 792 additions and 299 deletions
+8 -2
View File
@@ -60,6 +60,12 @@ export async function deleteInvoice(id: string): Promise<void> {
await api.delete(`/billing/invoices/${id}`)
}
export function getInvoicePdfUrl(id: string): string {
return `/api/billing/invoices/${id}/pdf`
export async function downloadInvoicePdf(id: string): Promise<void> {
const res = await api.get(`/billing/invoices/${id}/pdf`, { responseType: 'blob' })
const url = URL.createObjectURL(res.data)
const a = document.createElement('a')
a.href = url
a.download = `invoice-${id}.pdf`
a.click()
URL.revokeObjectURL(url)
}
+4 -3
View File
@@ -28,13 +28,14 @@ export interface MediaAsset {
is_archived: boolean
created_at: string
download_url: string | null
thumbnail_url: string | null
}
export interface MediaFilter {
product_id?: string
order_line_id?: string
cad_file_id?: string
asset_type?: MediaAssetType
asset_types?: MediaAssetType[]
skip?: number
limit?: number
}
@@ -44,10 +45,10 @@ export const getMediaAssets = (filters: MediaFilter = {}): Promise<MediaAsset[]>
if (filters.product_id) params.set('product_id', filters.product_id)
if (filters.order_line_id) params.set('order_line_id', filters.order_line_id)
if (filters.cad_file_id) params.set('cad_file_id', filters.cad_file_id)
if (filters.asset_type) params.set('asset_type', filters.asset_type)
if (filters.asset_types?.length) filters.asset_types.forEach(t => params.append('asset_types', t))
if (filters.skip !== undefined) params.set('skip', String(filters.skip))
if (filters.limit !== undefined) params.set('limit', String(filters.limit))
return api.get(`/media?${params}`).then(r => r.data)
return api.get(`/media/?${params}`).then(r => r.data)
}
export const getMediaAsset = (id: string): Promise<MediaAsset> =>