Files
website/bot/src/server.js
wtclaude 3d6b2e23a7 feat(theming): server-resolved theme engine and admin appearance UI
Phases 3-4 of docs/website/THEMING_AND_NAV.md. Three presets, the curated font
shortlist, and /admin/appearance to drive them.

The design put the presets in theme.css as [data-theme] blocks. That does not
work: SiteContext writes --accent as an inline style on <html>, which beats any
attribute-selector block, so a preset's accent would have been painted over by
BRAND_ACCENT_COLOR while getPublic().brand.accent -- the value the Android app
themes itself from -- reported the other one.

Presets now live in server/src/config/themePresets.js. themeResolve.js layers
:root <- preset <- custom per field into a token map, getPublic() returns it as
`theme`, and the client writes it onto <html>. One authority for the merge, and
brand.accent is by construction the accent the site paints. theme.css's :root is
untouched, so an instance with no row gets no theme block and renders as today.

Also: presets carry the full 15-token palette (eight would have left Fantasy
with blue-grey borders); the option catalog is served from
GET /settings/theme/options so the form cannot offer what the server rejects;
validation is strict on write and forgiving on read; and the Discord bot now
fetches the effective accent instead of its boot-time env copy.

Fixes a Phase 0 bug in passing: settings/nav.controller.js imported the logger
factory rather than calling it, so a DB fault would have thrown a TypeError
inside the catch instead of returning 500.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 19:16:23 -05:00

59 lines
1.7 KiB
JavaScript

require('dotenv').config()
const app = require('./app')
const bootstrap = require('./bootstrap')
const createLogger = require('./utils/logger')
const discordManager = require('./discord/discordManager')
const brand = require('./brand')
const pkg = require('../package.json')
const log = createLogger('server')
const PORT = Number(process.env.PORT) || 4100
const HOST = '0.0.0.0'
async function start() {
log.info(`starting ${brand.name} bot v${pkg.version}`, {
node: process.version,
logFile: createLogger.logFilePath || 'disabled (console only)',
})
const server = app.listen(PORT, HOST, () => {
log.info(`internal API listening on http://${HOST}:${PORT}`)
})
// Pick up the site's effective accent before the first embed can be built.
// Best-effort by design: it never rejects, and a site that is not up yet just
// leaves the bot on its BRAND_ACCENT_COLOR default until the next read.
await brand.refreshAccent()
await bootstrap()
setupShutdown(server)
}
function setupShutdown(server) {
let closing = false
const shutdown = async (signal) => {
if (closing) return
closing = true
log.warn(`${signal} received — shutting down gracefully`)
server.close(() => log.info('internal API closed'))
await discordManager.stop()
await createLogger.close()
process.exit(0)
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('unhandledRejection', (reason) => log.error('unhandledRejection', { reason: String(reason) }))
process.on('uncaughtException', (err) => {
log.error('uncaughtException', err)
process.exit(1)
})
}
start().catch((err) => {
log.error('failed to start bot', err)
process.exit(1)
})