Files
firefrost-services/services/arbiter-3.0/src/routes/admin/index.js
Claude d22ff8c3c9 feat(admin): Task #125 Phase 2 — Branding asset browser
Adds a visual asset library to the social calendar form, backed by
the firefrost-operations-manual branding/ directory with on-the-fly
thumbnail generation via sharp.

- src/routes/admin/branding-assets.js: /list scans both branding/ and
  docs/branding/ recursively, groups by category directory. /thumb
  generates 256px webp thumbnails from source, caches to disk keyed
  by sha1(path + mtime) so edits bust the cache automatically. Path
  traversal protection + scope check on thumb requests.
- src/views/admin/social-calendar/_assets.ejs: modal body with
  category-grouped grid of lazy-loaded thumbnails, click-to-insert.
- Form modal gets a 'Browse assets' button next to Media Notes.
- Calendar shell gets a second modal (higher z-index) and a
  sppInsertAsset() helper that appends the clicked filename to the
  media_notes textarea.

Infrastructure (not in repo):
- /opt/firefrost-ops-manual clone added to Command Center
- /etc/systemd/system/firefrost-ops-sync.{service,timer} pulls
  every 15 minutes to keep assets fresh
- /var/cache/arbiter/branding-thumbs created for thumb cache
- sharp added as Arbiter dependency (libvips 8.17.3)

Chronicler #81
2026-04-12 01:10:09 +00:00

136 lines
4.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const { requireTrinityAccess } = require('./middleware');
const { getMinecraftServers } = require('../../panel/discovery');
const db = require('../../database');
// Sub-routers
const playersRouter = require('./players');
const serversRouter = require('./servers');
const financialsRouter = require('./financials');
const graceRouter = require('./grace');
const appealsRouter = require('./appeals');
const auditRouter = require('./audit');
const rolesRouter = require('./roles');
const schedulerRouter = require('./scheduler');
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');
const tasksRouter = require('./tasks');
router.use(requireTrinityAccess);
// Make CSRF token available to all admin views
router.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
router.get('/', (req, res) => {
res.redirect('/admin/dashboard');
});
router.get('/dashboard', async (req, res) => {
try {
// Fetch server count from Pterodactyl
const servers = await getMinecraftServers();
const serversOnline = servers.length;
// Fetch subscriber stats from database
const { rows: subStats } = await db.query(`
SELECT
COUNT(*) FILTER (WHERE status IN ('active', 'grace_period') OR is_lifetime = true) as active_count,
COALESCE(SUM(mrr_value) FILTER (WHERE status = 'active'), 0) as mrr
FROM subscriptions
`);
const activeSubscribers = parseInt(subStats[0]?.active_count || 0);
const totalMRR = parseFloat(subStats[0]?.mrr || 0);
// Fetch most recent successful sync time
const { rows: syncRows } = await db.query(`
SELECT MAX(last_successful_sync) as last_sync
FROM server_sync_log
WHERE is_online = true
`);
const lastSyncTime = syncRows[0]?.last_sync || null;
// Fetch social stats across all platforms
const { rows: socialStats } = await db.query(`
SELECT
platform,
COUNT(*) as post_count,
COALESCE(SUM(views), 0) as total_views,
COALESCE(SUM(likes), 0) as total_likes,
COALESCE(SUM(comments), 0) as total_comments
FROM social_posts
GROUP BY platform
`);
const socialTotals = {
posts: 0,
views: 0,
likes: 0,
comments: 0,
platforms: {}
};
for (const row of socialStats) {
socialTotals.posts += parseInt(row.post_count);
socialTotals.views += parseInt(row.total_views);
socialTotals.likes += parseInt(row.total_likes);
socialTotals.comments += parseInt(row.total_comments);
socialTotals.platforms[row.platform] = {
posts: parseInt(row.post_count),
views: parseInt(row.total_views),
likes: parseInt(row.total_likes)
};
}
res.render('admin/dashboard', {
title: 'Command Bridge',
serversOnline,
activeSubscribers,
totalMRR,
lastSyncTime,
socialTotals
});
} catch (error) {
console.error('Dashboard data fetch error:', error);
// Fallback to zeros on error
res.render('admin/dashboard', {
title: 'Command Bridge',
serversOnline: 0,
activeSubscribers: 0,
totalMRR: 0,
lastSyncTime: null,
socialTotals: { posts: 0, views: 0, likes: 0, comments: 0, platforms: {} }
});
}
});
router.use('/players', playersRouter);
router.use('/servers', serversRouter);
router.use('/financials', financialsRouter);
router.use('/grace', graceRouter);
router.use('/appeals', appealsRouter);
router.use('/audit', auditRouter);
router.use('/roles', rolesRouter);
router.use('/scheduler', schedulerRouter);
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);
router.use('/tasks', tasksRouter);
module.exports = router;