diff --git a/services/arbiter-3.0/src/routes/admin/branding-assets.js b/services/arbiter-3.0/src/routes/admin/branding-assets.js
new file mode 100644
index 0000000..7eaa3fc
--- /dev/null
+++ b/services/arbiter-3.0/src/routes/admin/branding-assets.js
@@ -0,0 +1,113 @@
+const express = require('express');
+const router = express.Router();
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+const sharp = require('sharp');
+
+const OPS_ROOT = '/opt/firefrost-ops-manual';
+const CACHE_DIR = '/var/cache/arbiter/branding-thumbs';
+const THUMB_SIZE = 256;
+const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
+
+// Scan roots — both /branding and /docs/branding
+const SCAN_ROOTS = [
+ { label: 'branding', rel: 'branding' },
+ { label: 'docs/branding', rel: 'docs/branding' }
+];
+
+function walkImages(dir, baseRel) {
+ const out = [];
+ let entries;
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
+ catch { return out; }
+ for (const entry of entries) {
+ if (entry.name.startsWith('.')) continue;
+ const full = path.join(dir, entry.name);
+ const rel = path.posix.join(baseRel, entry.name);
+ if (entry.isDirectory()) {
+ out.push(...walkImages(full, rel));
+ } else if (entry.isFile()) {
+ const ext = path.extname(entry.name).toLowerCase();
+ if (IMAGE_EXTS.has(ext)) {
+ try {
+ const st = fs.statSync(full);
+ out.push({
+ rel, full,
+ name: entry.name,
+ category: path.posix.dirname(rel),
+ size: st.size,
+ mtime: st.mtimeMs
+ });
+ } catch { /* skip */ }
+ }
+ }
+ }
+ return out;
+}
+
+function listAllAssets() {
+ const all = [];
+ for (const root of SCAN_ROOTS) {
+ const abs = path.join(OPS_ROOT, root.rel);
+ all.push(...walkImages(abs, root.rel));
+ }
+ // Sort by category, then name
+ all.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
+ return all;
+}
+
+// GET /admin/branding-assets/list — modal body
+router.get('/list', (req, res) => {
+ try {
+ const assets = listAllAssets();
+ // Group by category
+ const byCategory = {};
+ for (const a of assets) {
+ if (!byCategory[a.category]) byCategory[a.category] = [];
+ byCategory[a.category].push(a);
+ }
+ res.render('admin/social-calendar/_assets', { byCategory, total: assets.length, layout: false });
+ } catch (err) {
+ console.error('Asset list error:', err);
+ res.status(500).send("
Error loading assets.
");
+ }
+});
+
+// GET /admin/branding-assets/thumb?path=branding/logos/firefrost-emblem-512.png
+router.get('/thumb', async (req, res) => {
+ try {
+ const rel = String(req.query.path || '');
+ // Security: must be under one of our scan roots, no path traversal
+ const normalized = path.posix.normalize(rel);
+ if (normalized.includes('..') || path.isAbsolute(normalized)) {
+ return res.status(400).send('Bad path');
+ }
+ const allowed = SCAN_ROOTS.some(r => normalized.startsWith(r.rel + '/'));
+ if (!allowed) return res.status(400).send('Out of scope');
+
+ const source = path.join(OPS_ROOT, normalized);
+ if (!fs.existsSync(source)) return res.status(404).send('Not found');
+ const stat = fs.statSync(source);
+
+ // Cache key: sha1 of path + mtime so edits bust cache automatically
+ const key = crypto.createHash('sha1').update(normalized + ':' + stat.mtimeMs).digest('hex');
+ const cachePath = path.join(CACHE_DIR, key + '.webp');
+
+ if (!fs.existsSync(cachePath)) {
+ await sharp(source)
+ .resize(THUMB_SIZE, THUMB_SIZE, { fit: 'inside', withoutEnlargement: true })
+ .webp({ quality: 80 })
+ .toFile(cachePath);
+ }
+
+ res.set('Content-Type', 'image/webp');
+ res.set('Cache-Control', 'private, max-age=86400');
+ fs.createReadStream(cachePath).pipe(res);
+ } catch (err) {
+ console.error('Thumb generation error:', err);
+ res.status(500).send('Thumb error');
+ }
+});
+
+module.exports = router;
diff --git a/services/arbiter-3.0/src/routes/admin/index.js b/services/arbiter-3.0/src/routes/admin/index.js
index 30e81ab..e3a4624 100644
--- a/services/arbiter-3.0/src/routes/admin/index.js
+++ b/services/arbiter-3.0/src/routes/admin/index.js
@@ -17,6 +17,7 @@ const discordAuditRouter = require('./discord-audit');
const systemRouter = require('./system');
const socialRouter = require('./social');
const socialCalendarRouter = require('./social-calendar');
+const brandingAssetsRouter = require('./branding-assets');
const infrastructureRouter = require('./infrastructure');
const aboutRouter = require('./about');
const mcpLogsRouter = require('./mcp-logs');
@@ -125,6 +126,7 @@ router.use('/discord', discordAuditRouter);
router.use('/system', systemRouter);
router.use('/social', socialRouter);
router.use('/social-calendar', socialCalendarRouter);
+router.use('/branding-assets', brandingAssetsRouter);
router.use('/infrastructure', infrastructureRouter);
router.use('/about', aboutRouter);
router.use('/mcp-logs', mcpLogsRouter);
diff --git a/services/arbiter-3.0/src/views/admin/social-calendar/_assets.ejs b/services/arbiter-3.0/src/views/admin/social-calendar/_assets.ejs
new file mode 100644
index 0000000..f78efa8
--- /dev/null
+++ b/services/arbiter-3.0/src/views/admin/social-calendar/_assets.ejs
@@ -0,0 +1,36 @@
+
+
+
+
🎨 Branding Assets
+
<%= total %> images across <%= Object.keys(byCategory).length %> categories. Click to insert filename into media notes.
+
+
+
+
+ <% if (total === 0) { %>
+
+
📭
+
No branding assets found. Add files to branding/ in the ops manual repo.
+
+ <% } %>
+
+ <% Object.keys(byCategory).sort().forEach(function(cat) { %>
+
+
<%= cat %>
+
+ <% byCategory[cat].forEach(function(asset) { %>
+
')">
+
+
 %>)
+
+
<%= asset.name %>
+
+ <% }); %>
+
+
+ <% }); %>
+
diff --git a/services/arbiter-3.0/src/views/admin/social-calendar/_form.ejs b/services/arbiter-3.0/src/views/admin/social-calendar/_form.ejs
index 7cfdf3b..82062ee 100644
--- a/services/arbiter-3.0/src/views/admin/social-calendar/_form.ejs
+++ b/services/arbiter-3.0/src/views/admin/social-calendar/_form.ejs
@@ -48,7 +48,17 @@
-
+
+
+
+
diff --git a/services/arbiter-3.0/src/views/admin/social-calendar/index.ejs b/services/arbiter-3.0/src/views/admin/social-calendar/index.ejs
index 0762f82..0feb89d 100644
--- a/services/arbiter-3.0/src/views/admin/social-calendar/index.ejs
+++ b/services/arbiter-3.0/src/views/admin/social-calendar/index.ejs
@@ -30,6 +30,13 @@
+
+
+