feat(ui): wire data.jsx to real API; add loading gate in app.jsx: data.jsx
This commit is contained in:
parent
068e3a0828
commit
98025001e8
1 changed files with 160 additions and 112 deletions
|
|
@ -1,127 +1,175 @@
|
|||
// data.jsx - mock data for Z-AMPP prototype
|
||||
// exposed on window so all babel scripts can read it
|
||||
// data.jsx — API client; populates window.ZAMPP_DATA from real endpoints
|
||||
|
||||
const PROJECTS = [
|
||||
{ id: "p1", name: "Protour 2026", assets: 142, updated: "2h ago", color: "#5B7CFA", thumbs: 4 },
|
||||
{ id: "p2", name: "Studio A — Live", assets: 28, updated: "12m ago", color: "#FF3B30", thumbs: 3 },
|
||||
{ id: "p3", name: "Sponsor Spots Q2", assets: 67, updated: "yesterday", color: "#2DD4A8", thumbs: 5 },
|
||||
{ id: "p4", name: "Archive — 2025", assets: 1204, updated: "3w ago", color: "#B57CFA", thumbs: 6 },
|
||||
{ id: "p5", name: "Documentary Cut", assets: 89, updated: "4d ago", color: "#F5A623", thumbs: 4 },
|
||||
{ id: "p6", name: "Wild Dragon Promo", assets: 23, updated: "1h ago", color: "#FF8E5B", thumbs: 2 },
|
||||
];
|
||||
const API = '/api/v1';
|
||||
|
||||
const BINS = [
|
||||
{ id: "b1", name: "All assets", count: 142, icon: "grid" },
|
||||
{ id: "b2", name: "Live captures", count: 18, icon: "live" },
|
||||
{ id: "b3", name: "Master files", count: 67, icon: "film" },
|
||||
{ id: "b4", name: "Proxies", count: 142, icon: "proxy" },
|
||||
{ id: "b5", name: "Audio stems", count: 24, icon: "audio" },
|
||||
{ id: "b6", name: "Final deliverables", count: 8, icon: "package" },
|
||||
];
|
||||
window.ZAMPP_DATA = {
|
||||
PROJECTS: [],
|
||||
ASSETS: [],
|
||||
RECORDERS: [],
|
||||
JOBS: [],
|
||||
NODES: [],
|
||||
USERS: [],
|
||||
BINS: [],
|
||||
COMMENTS: [],
|
||||
};
|
||||
|
||||
// generate a deterministic-ish thumb color
|
||||
function thumbGrad(seed) {
|
||||
const hues = [220, 280, 340, 30, 150, 200, 260, 320];
|
||||
const h1 = hues[seed % hues.length];
|
||||
const h2 = hues[(seed + 3) % hues.length];
|
||||
return `linear-gradient(135deg, hsl(${h1} 45% 22%) 0%, hsl(${h2} 38% 12%) 100%)`;
|
||||
async function apiFetch(path, opts = {}) {
|
||||
const res = await fetch(API + path, {
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) },
|
||||
...opts,
|
||||
});
|
||||
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
const ASSETS = [
|
||||
{ id: "a1", name: "Stage_Cam_A_Master.mov", type: "video", duration: "01:14:22", res: "3840×2160", fps: "59.94", codec: "ProRes 422", size: "187 GB", updated: "12m ago", status: "live", project: "Protour 2026", bin: "Live captures", comments: 3, seed: 1 },
|
||||
{ id: "a2", name: "Sponsor_Reel_v3_FINAL.mp4", type: "video", duration: "00:00:30", res: "1920×1080", fps: "29.97", codec: "H.264", size: "412 MB", updated: "1h ago", status: "ready", project: "Sponsor Spots Q2", bin: "Final deliverables", comments: 8, seed: 2 },
|
||||
{ id: "a3", name: "Interview_Driver_07.mxf", type: "video", duration: "00:23:45", res: "1920×1080", fps: "25", codec: "XDCAM HD", size: "8.4 GB", updated: "3h ago", status: "processing", project: "Documentary Cut", bin: "Master files", comments: 0, seed: 3, progress: 67 },
|
||||
{ id: "a4", name: "Wide_Cam_B_proxy.mp4", type: "video", duration: "00:45:10", res: "1280×720", fps: "59.94", codec: "H.264", size: "1.2 GB", updated: "12m ago", status: "ready", project: "Protour 2026", bin: "Proxies", comments: 2, seed: 4 },
|
||||
{ id: "a5", name: "Drone_Aerial_Lap_3.mov", type: "video", duration: "00:04:22", res: "3840×2160", fps: "59.94", codec: "ProRes 422 HQ", size: "12 GB", updated: "4h ago", status: "ready", project: "Wild Dragon Promo", bin: "Master files", comments: 5, seed: 5 },
|
||||
{ id: "a6", name: "Audio_FOH_Mix.wav", type: "audio", duration: "01:14:22", res: "—", fps: "—", codec: "PCM 48kHz", size: "1.6 GB", updated: "12m ago", status: "live", project: "Protour 2026", bin: "Audio stems", comments: 1, seed: 6 },
|
||||
{ id: "a7", name: "Trophy_Ceremony.mov", type: "video", duration: "00:18:32", res: "3840×2160", fps: "29.97", codec: "ProRes 422", size: "42 GB", updated: "yesterday", status: "ready", project: "Protour 2026", bin: "Master files", comments: 12, seed: 7 },
|
||||
{ id: "a8", name: "Pit_Lane_GoPro_1.mp4", type: "video", duration: "00:12:08", res: "2704×1520", fps: "59.94", codec: "H.265", size: "3.8 GB", updated: "yesterday", status: "ready", project: "Protour 2026", bin: "All assets", comments: 0, seed: 8 },
|
||||
{ id: "a9", name: "Sponsor_Logo_Lower3rd.mov", type: "video", duration: "00:00:08", res: "1920×1080", fps: "30", codec: "ProRes 4444", size: "82 MB", updated: "2d ago", status: "ready", project: "Sponsor Spots Q2", bin: "All assets", comments: 0, seed: 9 },
|
||||
{ id: "a10", name: "Backstage_Walkthrough.mxf", type: "video", duration: "00:08:14", res: "1920×1080", fps: "25", codec: "XDCAM HD", size: "2.9 GB", updated: "3d ago", status: "error", project: "Documentary Cut", bin: "Master files", comments: 1, seed: 10 },
|
||||
{ id: "a11", name: "Pre_Race_Press.mov", type: "video", duration: "00:34:50", res: "3840×2160", fps: "25", codec: "ProRes 422", size: "78 GB", updated: "3d ago", status: "ready", project: "Documentary Cut", bin: "Master files", comments: 4, seed: 11 },
|
||||
{ id: "a12", name: "Wide_Cam_C_Master.mov", type: "video", duration: "01:12:00", res: "3840×2160", fps: "59.94", codec: "ProRes 422", size: "172 GB", updated: "5d ago", status: "ready", project: "Protour 2026", bin: "Master files", comments: 0, seed: 12 },
|
||||
];
|
||||
function fmtDuration(ms) {
|
||||
if (!ms) return '—';
|
||||
const s = Math.round(ms / 1000);
|
||||
const h = Math.floor(s / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
const sec = s % 60;
|
||||
if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0');
|
||||
return m + ':' + String(sec).padStart(2, '0');
|
||||
}
|
||||
|
||||
const RECORDERS = [
|
||||
{ id: "r1", name: "Stage Cam A", source: "SDI", url: "DeckLink Duo 2 · Port 1", node: "zampp2", status: "recording", elapsed: "01:14:22", bitrate: "120 Mbps", res: "2160p59.94", codec: "ProRes 422 HQ", health: 98 },
|
||||
{ id: "r2", name: "Wide Cam B", source: "SRT", url: "srt://10.0.4.18:4201", node: "zampp1", status: "recording", elapsed: "00:45:10", bitrate: "45 Mbps", res: "1080p59.94", codec: "H.265", health: 91 },
|
||||
{ id: "r3", name: "FOH Audio Feed", source: "SRT", url: "srt://10.0.4.18:4202", node: "zampp1", status: "recording", elapsed: "01:14:22", bitrate: "1.5 Mbps", res: "—", codec: "AAC 48k", health: 100, audio: true },
|
||||
{ id: "r4", name: "Pit Lane RTMP", source: "RTMP", url: "rtmp://stream.local/pit", node: "zampp1", status: "idle", elapsed: "00:00:00", bitrate: "—", res: "1080p30", codec: "H.264", health: 0 },
|
||||
{ id: "r5", name: "Trophy Cam", source: "SDI", url: "DeckLink Duo 2 · Port 2", node: "zampp2", status: "armed", elapsed: "00:00:00", bitrate: "—", res: "1080p59.94", codec: "ProRes 422", health: 100 },
|
||||
{ id: "r6", name: "Drone Downlink", source: "SRT", url: "srt://10.0.4.18:4204", node: "zampp1", status: "error", elapsed: "00:02:14", bitrate: "—", res: "—", codec: "H.265", health: 12 },
|
||||
];
|
||||
function fmtSize(bytes) {
|
||||
if (!bytes) return '—';
|
||||
if (bytes >= 1e12) return (bytes / 1e12).toFixed(1) + ' TB';
|
||||
if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB';
|
||||
if (bytes >= 1e6) return Math.round(bytes / 1e6) + ' MB';
|
||||
return Math.round(bytes / 1e3) + ' KB';
|
||||
}
|
||||
|
||||
const JOBS = [
|
||||
{ id: "j1", kind: "Proxy", asset: "Stage_Cam_A_Master.mov", node: "zampp1", progress: 67, eta: "08:42", status: "running", priority: "high", started: "12m ago" },
|
||||
{ id: "j2", kind: "Transcode", asset: "Sponsor_Reel_v3.mp4", node: "zampp1", progress: 34, eta: "02:18", status: "running", priority: "normal", started: "4m ago" },
|
||||
{ id: "j3", kind: "Thumbnail", asset: "Interview_Driver_07.mxf", node: "zampp2", progress: 92, eta: "00:08", status: "running", priority: "normal", started: "1m ago" },
|
||||
{ id: "j4", kind: "Proxy", asset: "Wide_Cam_C_Master.mov", node: "zampp1", progress: 100, eta: "—", status: "done", priority: "normal", started: "18m ago" },
|
||||
{ id: "j5", kind: "AMPP Sync", asset: "Trophy_Ceremony.mov", node: "zampp1", progress: 100, eta: "—", status: "done", priority: "high", started: "1h ago" },
|
||||
{ id: "j6", kind: "Transcode", asset: "Backstage_Walkthrough.mxf", node: "zampp2", progress: 0, eta: "—", status: "failed", priority: "normal", started: "2h ago", error: "Codec mismatch" },
|
||||
{ id: "j7", kind: "Proxy", asset: "Pre_Race_Press.mov", node: "zampp1", progress: 0, eta: "—", status: "queued", priority: "normal", started: "queued" },
|
||||
{ id: "j8", kind: "Proxy", asset: "Drone_Aerial_Lap_3.mov", node: "zampp1", progress: 0, eta: "—", status: "queued", priority: "low", started: "queued" },
|
||||
];
|
||||
function fmtRelative(iso) {
|
||||
if (!iso) return '—';
|
||||
const diff = (Date.now() - new Date(iso)) / 1000;
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
const NODES = [
|
||||
{ id: "zampp1", role: "primary", ip: "172.18.91.216", cpu: 12, mem: 21, memTotal: 96, gpus: ["NVIDIA RTX A4000", "NVIDIA RTX A4000", "NVIDIA RTX A4000"], version: "1.2.0", status: "online", uptime: "13d 4h", x: 0.5, y: 0.5 },
|
||||
{ id: "zampp2", role: "worker", ip: "172.18.91.217", cpu: 34, mem: 6.8, memTotal: 19, gpus: ["NVIDIA T1000"], version: "1.2.0", status: "online", uptime: "13d 4h", devices: ["DeckLink Duo 2"], x: 0.22, y: 0.28 },
|
||||
{ id: "zampp3", role: "capture", ip: "172.18.91.218", cpu: 8, mem: 4.2, memTotal: 32, gpus: ["NVIDIA T400"], version: "1.2.0", status: "online", uptime: "2d 8h", devices: ["DeckLink Quad HDMI"], x: 0.22, y: 0.72 },
|
||||
{ id: "zampp4", role: "worker", ip: "172.18.91.219", cpu: 0, mem: 0.4, memTotal: 32, gpus: [], version: "1.1.4", status: "offline", uptime: "—", x: 0.78, y: 0.28 },
|
||||
{ id: "zampp5", role: "worker", ip: "172.18.91.220", cpu: 18, mem: 9.1, memTotal: 64, gpus: ["NVIDIA RTX A2000"], version: "1.2.0", status: "online", uptime: "5h 12m", x: 0.78, y: 0.72 },
|
||||
];
|
||||
const PROJECT_COLORS = ['#5B7CFA', '#2DD4A8', '#FF5B5B', '#F5A623', '#B57CFA', '#6B7280'];
|
||||
|
||||
const CONTAINERS = [
|
||||
{ id: "c1", name: "wild-dragon-mam-api", image: "wild-dragon-mam-api:1.2.0", state: "running", uptime: "1h 12m", ports: "47432:3000/tcp", cpu: 4.2, mem: 312 },
|
||||
{ id: "c2", name: "wild-dragon-web-ui", image: "wild-dragon-web-ui:1.2.0", state: "running", uptime: "4h 18m", ports: "47434:80/tcp", cpu: 0.4, mem: 48 },
|
||||
{ id: "c3", name: "wild-dragon-editor", image: "wild-dragon-editor:0.4.1", state: "running", uptime: "28h", ports: "47435:80/tcp", cpu: 0.1, mem: 32 },
|
||||
{ id: "c4", name: "wild-dragon-capture", image: "wild-dragon-capture:1.2.0", state: "running", uptime: "28h", ports: "47433:3001, 49000:9000/udp, 41935:1935", cpu: 8.7, mem: 421 },
|
||||
{ id: "c5", name: "wild-dragon-worker", image: "wild-dragon-worker:1.2.0", state: "running", uptime: "13h", ports: "—", cpu: 12.4, mem: 824 },
|
||||
{ id: "c6", name: "wild-dragon-queue", image: "redis:7-alpine", state: "running", uptime: "13h", ports: "—", cpu: 0.2, mem: 18 },
|
||||
{ id: "c7", name: "wild-dragon-db", image: "postgres:16", state: "running", uptime: "13h", ports: "—", cpu: 1.1, mem: 184, healthy: true },
|
||||
];
|
||||
function normalizeAsset(a, projectMap) {
|
||||
return {
|
||||
...a,
|
||||
name: a.display_name || a.filename || 'Untitled',
|
||||
type: a.media_type || 'video',
|
||||
duration: fmtDuration(a.duration_ms),
|
||||
size: fmtSize(a.file_size),
|
||||
res: a.resolution || '—',
|
||||
updated: fmtRelative(a.updated_at),
|
||||
project: (projectMap && projectMap[a.project_id]) || '',
|
||||
comments: 0,
|
||||
progress: 0,
|
||||
tc: a.start_tc || null,
|
||||
seed: a.id ? a.id.charCodeAt(0) % 6 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
const USERS = [
|
||||
{ id: "u1", username: "admin", display: "Zach Gaetano", role: "admin", groups: ["broadcast"], last: "now", avatar: "ZG" },
|
||||
{ id: "u2", username: "k.morales", display: "Kira Morales", role: "editor", groups: ["broadcast", "post"], last: "8m ago", avatar: "KM" },
|
||||
{ id: "u3", username: "j.tran", display: "Jules Tran", role: "ingest", groups: ["broadcast"], last: "1h ago", avatar: "JT" },
|
||||
{ id: "u4", username: "m.okafor", display: "Mara Okafor", role: "viewer", groups: ["clients"], last: "2d ago", avatar: "MO" },
|
||||
{ id: "u5", username: "svc.ampp", display: "AMPP Service", role: "service", groups: ["system"], last: "now", avatar: "AM" },
|
||||
];
|
||||
function normalizeRecorder(r) {
|
||||
let elapsed = '—';
|
||||
if (r.status === 'recording' && r.started_at) {
|
||||
const s = Math.floor((Date.now() - new Date(r.started_at)) / 1000);
|
||||
elapsed = String(Math.floor(s / 3600)).padStart(2, '0') + ':' +
|
||||
String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' +
|
||||
String(s % 60).padStart(2, '0');
|
||||
}
|
||||
const cfg = r.source_config || {};
|
||||
return {
|
||||
...r,
|
||||
source: r.source_type || '—',
|
||||
url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '—',
|
||||
codec: r.recording_codec || '—',
|
||||
res: r.recording_resolution || '—',
|
||||
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
||||
elapsed,
|
||||
bitrate: '—',
|
||||
health: 100,
|
||||
audio: false,
|
||||
};
|
||||
}
|
||||
|
||||
const TOKENS_LIST = [
|
||||
{ id: "t1", name: "AMPP Sync — Production", scope: "assets:write, sync:write", created: "2026-04-12", last: "2m ago", owner: "svc.ampp" },
|
||||
{ id: "t2", name: "Editor CI", scope: "editor:render", created: "2026-03-01", last: "1h ago", owner: "k.morales" },
|
||||
{ id: "t3", name: "Read-only audit", scope: "assets:read", created: "2026-02-18", last: "yesterday", owner: "admin" },
|
||||
{ id: "t4", name: "Old token (revoked)", scope: "—", created: "2025-11-04", last: "—", owner: "admin", revoked: true },
|
||||
];
|
||||
function normalizeJob(j) {
|
||||
const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' };
|
||||
const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode' };
|
||||
const meta = j.metadata || {};
|
||||
return {
|
||||
...j,
|
||||
status: statusMap[j.status] || j.status,
|
||||
kind: kindMap[j.type] || j.type || 'Job',
|
||||
asset: j.asset_name || meta.filename || '—',
|
||||
eta: '—',
|
||||
node: meta.node || '—',
|
||||
priority: meta.priority || 'normal',
|
||||
error: j.error || null,
|
||||
progress: j.progress || 0,
|
||||
};
|
||||
}
|
||||
|
||||
const ACTIVITY = [
|
||||
{ id: "ac1", who: "Kira Morales", what: "added a comment on", target: "Stage_Cam_A_Master.mov", time: "2m ago", kind: "comment" },
|
||||
{ id: "ac2", who: "Stage Cam A", what: "started recording", target: "01:14:22 elapsed", time: "1h 14m ago", kind: "record" },
|
||||
{ id: "ac3", who: "Proxy job", what: "completed for", target: "Wide_Cam_C_Master.mov", time: "18m ago", kind: "job" },
|
||||
{ id: "ac4", who: "Jules Tran", what: "uploaded", target: "Drone_Aerial_Lap_3.mov", time: "4h ago", kind: "upload" },
|
||||
{ id: "ac5", who: "AMPP Service", what: "synced", target: "Trophy_Ceremony.mov", time: "1h ago", kind: "sync" },
|
||||
{ id: "ac6", who: "Drone Downlink", what: "errored —", target: "signal lost", time: "12m ago", kind: "error" },
|
||||
{ id: "ac7", who: "Kira Morales", what: "marked as final", target: "Sponsor_Reel_v3_FINAL.mp4", time: "1h ago", kind: "approve" },
|
||||
];
|
||||
async function loadData() {
|
||||
const [projectsR, assetsR, recordersR, jobsR, clusterR, usersR, binsR] = await Promise.allSettled([
|
||||
apiFetch('/projects'),
|
||||
apiFetch('/assets?limit=500'),
|
||||
apiFetch('/recorders'),
|
||||
apiFetch('/jobs'),
|
||||
apiFetch('/cluster'),
|
||||
apiFetch('/users'),
|
||||
apiFetch('/bins'),
|
||||
]);
|
||||
|
||||
const COMMENTS = [
|
||||
{ id: "cm1", who: "Kira Morales", avatar: "KM", time: "00:12:04", real: "12m ago", text: "Hit point on the engine roar feels late — can we slip the cut 4 frames earlier?", resolved: false, frame: 12 * 60 * 30 + 4 * 30 },
|
||||
{ id: "cm2", who: "Zach Gaetano", avatar: "ZG", time: "00:24:18", real: "8m ago", text: "Sponsor logo is clipping into safe area. Bump 6px inward.", resolved: false, frame: 24 * 60 * 30 + 18 * 30 },
|
||||
{ id: "cm3", who: "Mara Okafor", avatar: "MO", time: "00:41:55", real: "5m ago", text: "Approved from sponsor side. Ready to publish ✓", resolved: true, frame: 41 * 60 * 30 + 55 * 30 },
|
||||
{ id: "cm4", who: "Kira Morales", avatar: "KM", time: "01:02:30", real: "2m ago", text: "Audio dip here — FOH mix dropped 6dB. Pulling stem fix from r3.", resolved: false, frame: 62 * 60 * 30 + 30 * 30 },
|
||||
];
|
||||
const projectMap = {};
|
||||
if (projectsR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.PROJECTS = (projectsR.value || []).map((p, i) => ({
|
||||
...p,
|
||||
color: PROJECT_COLORS[i % PROJECT_COLORS.length],
|
||||
assets: 0,
|
||||
updated: fmtRelative(p.updated_at),
|
||||
}));
|
||||
window.ZAMPP_DATA.PROJECTS.forEach(p => { projectMap[p.id] = p.name; });
|
||||
}
|
||||
|
||||
const SDI_PORTS_zampp2 = [
|
||||
{ idx: 1, label: "Stage Cam A", active: true, signal: "2160p59.94", level: 0.84, recording: true },
|
||||
{ idx: 2, label: "Stage Cam B", active: true, signal: "2160p59.94", level: 0.77, recording: false },
|
||||
{ idx: 3, label: "Trophy Cam", active: true, signal: "1080p59.94", level: 0.68, recording: false },
|
||||
{ idx: 4, label: "—", active: false, signal: "no signal", level: 0, recording: false },
|
||||
];
|
||||
if (assetsR.status === 'fulfilled') {
|
||||
const raw = assetsR.value;
|
||||
const list = Array.isArray(raw) ? raw : (raw.assets || []);
|
||||
window.ZAMPP_DATA.ASSETS = list.map(a => normalizeAsset(a, projectMap));
|
||||
const counts = {};
|
||||
list.forEach(a => { if (a.project_id) counts[a.project_id] = (counts[a.project_id] || 0) + 1; });
|
||||
window.ZAMPP_DATA.PROJECTS = window.ZAMPP_DATA.PROJECTS.map(p => ({ ...p, assets: counts[p.id] || 0 }));
|
||||
}
|
||||
|
||||
Object.assign(window, {
|
||||
ZAMPP_DATA: {
|
||||
PROJECTS, BINS, ASSETS, RECORDERS, JOBS, NODES, CONTAINERS, USERS,
|
||||
TOKENS_LIST, ACTIVITY, COMMENTS, SDI_PORTS_zampp2, thumbGrad,
|
||||
},
|
||||
});
|
||||
if (recordersR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.RECORDERS = (recordersR.value || []).map(normalizeRecorder);
|
||||
}
|
||||
|
||||
if (jobsR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.JOBS = (jobsR.value || []).map(normalizeJob);
|
||||
}
|
||||
|
||||
if (clusterR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.NODES = clusterR.value || [];
|
||||
}
|
||||
|
||||
if (usersR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.USERS = (usersR.value || []).map(u => ({
|
||||
...u,
|
||||
name: u.display_name || u.username || u.email || 'Unknown',
|
||||
initials: (u.display_name || u.username || '?').slice(0, 2).toUpperCase(),
|
||||
role: u.role || 'viewer',
|
||||
joined: fmtRelative(u.created_at),
|
||||
lastSeen: fmtRelative(u.last_seen || u.updated_at),
|
||||
}));
|
||||
}
|
||||
|
||||
if (binsR.status === 'fulfilled') {
|
||||
window.ZAMPP_DATA.BINS = (binsR.value || []).map(b => ({
|
||||
...b,
|
||||
count: b.asset_count || 0,
|
||||
icon: b.type || 'grid',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
window.ZAMPP_API = { fetch: apiFetch, loadData, fmtDuration, fmtSize, fmtRelative };
|
||||
|
|
|
|||
Loading…
Reference in a new issue