diff --git a/services/mam-api/src/routes/bins.js b/services/mam-api/src/routes/bins.js
index 94c6063..8344bce 100644
--- a/services/mam-api/src/routes/bins.js
+++ b/services/mam-api/src/routes/bins.js
@@ -7,18 +7,28 @@ const router = express.Router();
router.use(requireAuth);
-// GET / - List bins for a project_id
+// GET / - List bins. Filter by project_id when supplied; otherwise return
+// every bin across every project so the Library / asset-context-menu can
+// present a global "move to bin" picker.
router.get('/', async (req, res, next) => {
try {
const { project_id } = req.query;
- if (!project_id) {
- return res.status(400).json({ error: 'project_id is required' });
+ const params = [];
+ let where = '';
+ if (project_id) {
+ where = 'WHERE b.project_id = $1';
+ params.push(project_id);
}
const result = await pool.query(
- `SELECT * FROM bins WHERE project_id = $1 ORDER BY created_at DESC`,
- [project_id]
+ `SELECT b.*, p.name AS project_name,
+ (SELECT COUNT(*)::int FROM assets a WHERE a.bin_id = b.id) AS asset_count
+ FROM bins b
+ LEFT JOIN projects p ON p.id = b.project_id
+ ${where}
+ ORDER BY b.created_at DESC`,
+ params
);
res.json(result.rows);
diff --git a/services/web-ui/public/data.jsx b/services/web-ui/public/data.jsx
index a083d5d..6b92630 100644
--- a/services/web-ui/public/data.jsx
+++ b/services/web-ui/public/data.jsx
@@ -51,6 +51,7 @@ function fmtRelative(iso) {
}
const PROJECT_COLORS = ['#5B7CFA', '#2DD4A8', '#FF5B5B', '#F5A623', '#B57CFA', '#6B7280'];
+window.PROJECT_COLORS = PROJECT_COLORS;
function normalizeAsset(a, projectMap) {
return {
diff --git a/services/web-ui/public/screens-jobs.jsx b/services/web-ui/public/screens-jobs.jsx
index e643eec..44c8aa0 100644
--- a/services/web-ui/public/screens-jobs.jsx
+++ b/services/web-ui/public/screens-jobs.jsx
@@ -51,6 +51,17 @@ function Jobs({ navigate }) {
.catch(e => alert('Delete failed: ' + e.message));
}, []);
+ // Retry every failed job at once. Useful after a transient infra issue
+ // (S3 outage, hung worker) — one click per job is painful with 20+ failures.
+ const handleRetryAll = React.useCallback(() => {
+ const failedJobs = jobs.filter(j => j.status === 'failed');
+ if (failedJobs.length === 0) return;
+ if (!window.confirm(`Re-queue all ${failedJobs.length} failed jobs?`)) return;
+ Promise.allSettled(
+ failedJobs.map(j => window.ZAMPP_API.fetch('/jobs/' + j.id + '/retry', { method: 'POST' }))
+ ).then(refresh);
+ }, [jobs, refresh]);
+
const counts = {
all: jobs.length,
running: jobs.filter(j => j.status === 'running').length,
@@ -66,6 +77,11 @@ function Jobs({ navigate }) {
Jobs
Proxy generation, transcoding, and processing queue
+ {counts.failed > 0 && (
+
+ )}
@@ -142,7 +158,15 @@ function JobRow({ job, onRetry, onDelete }) {
)}
{job.status === 'done' && Complete}
{job.status === 'queued' && Waiting…}
- {job.status === 'failed' && {job.error || 'Failed'}}
+ {job.status === 'failed' && (
+
+
+
+ {(job.error || 'Failed').slice(0, 120)}
+
+
+ )}
{job.eta}
{job.priority}
diff --git a/services/web-ui/public/screens-library.jsx b/services/web-ui/public/screens-library.jsx
index 3bd01b8..8b7a740 100644
--- a/services/web-ui/public/screens-library.jsx
+++ b/services/web-ui/public/screens-library.jsx
@@ -240,14 +240,18 @@ function AssetContextMenu({ asset, x, y, bins, onClose, onChanged, onOpen }) {
{(bins && bins.length > 0) ? (
<>
Move to bin
- {bins.slice(0, 10).map(function(b) {
- const isCurrent = asset.bin_id === b.id;
- return (
-
- );
- })}
+ {bins
+ .filter(function(b) { return !asset.project_id || b.project_id === asset.project_id; })
+ .slice(0, 10)
+ .map(function(b) {
+ const isCurrent = asset.bin_id === b.id;
+ return (
+
+ );
+ })}
{asset.bin_id && (