phase 9 notification toggles (per-alert, per-category, master; default-disabled)

This commit is contained in:
2026-04-18 23:51:59 +00:00
parent 39a5482516
commit 8a45b59b28
12 changed files with 520 additions and 33 deletions

View File

@@ -6,6 +6,12 @@ const { applyConfigUpdates, readAllConfig } = require('../services/configPersist
const { logSystem } = require('../services/debugLog');
const { REGISTRY: NOTIFICATION_REGISTRY } = require('../services/notificationRegistry');
const { ALLOWED_CONFIG_KEYS } = require('../services/configSchema');
const {
getAllState: getNotificationState,
setKeyEnabled,
setCategoryEnabled,
setMasterEnabled
} = require('../services/notificationEnabled');
const router = express.Router();
@@ -161,6 +167,62 @@ router.get('/notifications/alerts', (req, res) => {
res.json(NOTIFICATION_REGISTRY);
});
// GET /notifications/state — Phase 9: master flag + per-key enable map
router.get('/notifications/state', (req, res) => {
res.json(getNotificationState());
});
// POST /notifications/toggle — Phase 9: mutate one of {master, category, key}
//
// Body shapes (exactly one of these must be used):
// { master: true, enabled: <bool> }
// { category: <str>, enabled: <bool> }
// { key: <str>, enabled: <bool> }
//
// Mutates CONFIG in memory via notificationEnabled, then persists through
// applyConfigUpdates so the value passes schema validation and ends up in .env.
router.post('/notifications/toggle', express.json(), async (req, res) => {
const body = req.body;
if (!body || typeof body !== 'object' || Array.isArray(body)) {
return res.status(400).json({ error: 'Invalid body' });
}
if (typeof body.enabled !== 'boolean') {
return res.status(400).json({ error: '`enabled` must be boolean' });
}
const hasMaster = Object.prototype.hasOwnProperty.call(body, 'master');
const hasCategory = Object.prototype.hasOwnProperty.call(body, 'category');
const hasKey = Object.prototype.hasOwnProperty.call(body, 'key');
const specifiedCount = Number(hasMaster) + Number(hasCategory) + Number(hasKey);
if (specifiedCount !== 1) {
return res.status(400).json({ error: 'Specify exactly one of: master, category, key' });
}
let updates;
if (hasMaster) {
setMasterEnabled(body.enabled);
updates = { NOTIFICATIONS_MASTER_ENABLED: body.enabled };
} else if (hasCategory) {
if (typeof body.category !== 'string' || !Object.prototype.hasOwnProperty.call(NOTIFICATION_REGISTRY, body.category)) {
return res.status(400).json({ error: 'Unknown category' });
}
const newJson = setCategoryEnabled(body.category, body.enabled);
updates = { NOTIFICATION_ENABLED_JSON: newJson };
} else {
if (typeof body.key !== 'string' || !body.key) {
return res.status(400).json({ error: '`key` must be a non-empty string' });
}
const newJson = setKeyEnabled(body.key, body.enabled);
updates = { NOTIFICATION_ENABLED_JSON: newJson };
}
const result = applyConfigUpdates(updates);
if (result.errors.length > 0) {
return res.status(500).json({ error: 'Persistence failed', details: result.errors });
}
res.json({ state: getNotificationState() });
});
// Expose the allowlist for the Phase 8 schema smoke test. Attached to the
// router function object; doesn't show up as a route.
router._allowedKeys = Array.from(ALLOWED_CONFIG_KEYS);