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
136 lines
4.7 KiB
JavaScript
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;
|