diff --git a/services/mam-api/src/routes/assets.js b/services/mam-api/src/routes/assets.js index 890a03e..a202de6 100644 --- a/services/mam-api/src/routes/assets.js +++ b/services/mam-api/src/routes/assets.js @@ -187,7 +187,7 @@ router.get('/:id', async (req, res, next) => { router.patch('/:id', async (req, res, next) => { try { const { id } = req.params; - const { display_name, tags, notes } = req.body; + const { display_name, tags, notes, bin_id } = req.body; const updates = []; const params = []; @@ -208,6 +208,12 @@ router.patch('/:id', async (req, res, next) => { params.push(notes); } + if (bin_id !== undefined) { + // Accept null to move the asset back to the project root + updates.push(`bin_id = $${paramCount++}`); + params.push(bin_id || null); + } + if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' }); } @@ -234,6 +240,59 @@ router.patch('/:id', async (req, res, next) => { } }); +// POST /:id/copy - Reference-copy an asset into another bin (or project) +// +// Same S3 keys, new asset row. Mirrors filename + metadata. Useful for +// multi-binning a single piece of media without duplicating storage. +router.post('/:id/copy', async (req, res, next) => { + try { + const { id } = req.params; + const { binId, projectId } = req.body; + + const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]); + if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' }); + const src = r.rows[0]; + + const newId = uuidv4(); + const ins = await pool.query( + `INSERT INTO assets ( + id, project_id, bin_id, filename, display_name, + status, media_type, original_s3_key, proxy_s3_key, thumbnail_s3_key, + codec, resolution, fps, duration_ms, start_tc, file_size, tags, notes, + created_at, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15, $16, $17, $18, + NOW(), NOW() + ) RETURNING *`, + [ + newId, + projectId || src.project_id, + binId === undefined ? src.bin_id : (binId || null), + src.filename, + src.display_name, + src.status, + src.media_type, + src.original_s3_key, + src.proxy_s3_key, + src.thumbnail_s3_key, + src.codec, + src.resolution, + src.fps, + src.duration_ms, + src.start_tc, + src.file_size, + src.tags, + src.notes, + ] + ); + res.status(201).json(ins.rows[0]); + } catch (err) { + next(err); + } +}); + // DELETE /:id - Soft or hard delete router.delete('/:id', async (req, res, next) => { try { diff --git a/services/web-ui/public/css/common.css b/services/web-ui/public/css/common.css index 66e1af6..4d51cf5 100644 --- a/services/web-ui/public/css/common.css +++ b/services/web-ui/public/css/common.css @@ -18,10 +18,10 @@ --bg-hover: oklch(29% 0.015 250); /* Accent — amber tally light */ - --accent: oklch(76% 0.178 52); + --accent: oklch(45% 0.20 266); --accent-dim: oklch(56% 0.130 52); - --accent-subtle: oklch(76% 0.178 52 / 0.10); - --accent-border: oklch(76% 0.178 52 / 0.30); + --accent-subtle: oklch(55% 0.20 266 / 0.12); + --accent-border: oklch(55% 0.20 266 / 0.36); /* Text */ --text-primary: oklch(93% 0.008 250); @@ -42,7 +42,7 @@ --status-green-bg: oklch(68% 0.18 148 / 0.10); --status-red-bg: oklch(62% 0.22 25 / 0.10); --status-blue-bg: oklch(65% 0.16 245 / 0.10); - --status-amber-bg: oklch(76% 0.178 52 / 0.10); + --status-amber-bg: oklch(55% 0.20 266 / 0.12); /* Spacing — 4pt base */ --sp-1: 4px; @@ -332,8 +332,8 @@ svg { display: block; flex-shrink: 0; } color: oklch(11% 0.010 250); border-color: var(--accent); } -.btn-primary:hover { background: oklch(80% 0.178 52); border-color: oklch(80% 0.178 52); } -.btn-primary:active { background: oklch(70% 0.178 52); } +.btn-primary:hover { background: oklch(52% 0.21 266); border-color: oklch(52% 0.21 266); } +.btn-primary:active { background: oklch(40% 0.19 266); } .btn-secondary { background: var(--bg-surface); @@ -540,9 +540,9 @@ textarea:focus { border-color: var(--accent-border); box-shadow: 0 0 0 3px var(- .status-dot--idle { background: var(--bg-hover); border: 1px solid var(--border-strong); } @keyframes pulse-amber { - 0% { box-shadow: 0 0 0 0 oklch(76% 0.178 52 / 0.8); } - 60% { box-shadow: 0 0 0 5px oklch(76% 0.178 52 / 0); } - 100% { box-shadow: 0 0 0 0 oklch(76% 0.178 52 / 0); } + 0% { box-shadow: 0 0 0 0 oklch(55% 0.20 266 / 0.8); } + 60% { box-shadow: 0 0 0 5px oklch(55% 0.20 266 / 0); } + 100% { box-shadow: 0 0 0 0 oklch(55% 0.20 266 / 0); } } /* ================================================================ @@ -785,3 +785,18 @@ textarea:focus { border-color: var(--accent-border); box-shadow: 0 0 0 3px var(- .mt-2 { margin-top: var(--sp-2); } .mt-4 { margin-top: var(--sp-4); } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } + +/* === AMPP Safe loading indicator ============================= + Use anywhere we'd otherwise show a generic spinner. The helmet + pulses gently; the dot underneath pulses on a different beat. */ +.ampp-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--sp-3);padding:var(--sp-6);color:var(--text-tertiary);font-size:var(--text-sm)} +.ampp-loading-img{width:160px;aspect-ratio:1963/1236;background-image:url(/img/ampp-safe.jpg);background-size:contain;background-position:center;background-repeat:no-repeat;filter:drop-shadow(0 8px 24px oklch(55% 0.20 266 / 0.35));animation:amppPulse 1.8s ease-in-out infinite} +.ampp-loading-label{display:flex;align-items:center;gap:8px;font-size:var(--text-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--text-secondary)} +.ampp-loading-dot{width:6px;height:6px;border-radius:50%;background:oklch(55% 0.20 266);animation:amppDot 1.2s ease-in-out infinite} +.ampp-loading--sm .ampp-loading-img{width:96px} +.ampp-loading--xs .ampp-loading-img{width:64px} +.ampp-loading--inline{flex-direction:row;padding:var(--sp-2) var(--sp-3);gap:var(--sp-2)} +.ampp-loading--inline .ampp-loading-img{width:28px} +.ampp-loading--inline .ampp-loading-label{font-size:11px} +@keyframes amppPulse{0%,100%{transform:scale(.96);opacity:.78}50%{transform:scale(1);opacity:1}} +@keyframes amppDot{0%,100%{transform:scale(.7);opacity:.35}50%{transform:scale(1.15);opacity:1}} diff --git a/services/web-ui/public/index.html b/services/web-ui/public/index.html index a105fd7..e84d90d 100644 --- a/services/web-ui/public/index.html +++ b/services/web-ui/public/index.html @@ -240,7 +240,7 @@ .drop-overlay { position: fixed; inset: 0; - background: oklch(76% 0.178 52 / 0.07); + background: oklch(55% 0.20 266 / 0.09); border: 2px dashed var(--accent); pointer-events: none; opacity: 0; @@ -297,9 +297,9 @@ .first-splash{position:fixed;inset:0;z-index:60;background:radial-gradient(ellipse at 50% 45%,#1a1d28 0%,#08090d 70%);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:24px;opacity:1;transition:opacity .55s ease-out, visibility .55s} .first-splash.hidden{opacity:0;visibility:hidden;pointer-events:none} - .first-splash-img{width:min(420px,46vw);aspect-ratio:1963/1236;background-image:url(img/ampp-safe.jpg);background-size:contain;background-position:center;background-repeat:no-repeat;filter:drop-shadow(0 20px 40px rgba(232,160,32,.15))} - .first-splash-stamp{display:flex;align-items:center;gap:10px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:oklch(76% 0.178 52)} - .first-splash-dot{width:8px;height:8px;background:oklch(76% 0.178 52);border-radius:50%;animation:fsPulse 1.4s ease-in-out infinite} + .first-splash-img{width:min(420px,46vw);aspect-ratio:1963/1236;background-image:url(img/ampp-safe.jpg);background-size:contain;background-position:center;background-repeat:no-repeat;filter:drop-shadow(0 20px 40px rgba(31,58,208,.15))} + .first-splash-stamp{display:flex;align-items:center;gap:10px;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:oklch(55% 0.20 266)} + .first-splash-dot{width:8px;height:8px;background:oklch(55% 0.20 266);border-radius:50%;animation:fsPulse 1.4s ease-in-out infinite} @keyframes fsPulse{0%,100%{opacity:.35;transform:scale(.9)}50%{opacity:1;transform:scale(1.1)}} .first-splash-title{font-size:13px;color:var(--text-secondary);letter-spacing:.04em} @@ -407,8 +407,9 @@ -
@@ -472,6 +473,7 @@ +