feat(zero): /config endpoints + dynamic scheduler hours

- GET/PATCH /api/admin/zero/config — read/write all Zero settings via app_settings
  - scheduler reads GENERATE_HOUR / APPROVE_HOUR dynamically (no restart needed)
  - generateDraft uses PUBLISH_HOUR for scheduled_at (was hardcoded 13)
  - requireAdmin softened — works both with and without users.is_admin column
    (prod has no is_admin; auth is provided by x-internal-secret + web cookie)
This commit is contained in:
Aleksei Pavlov
2026-06-19 11:16:58 +03:00
parent 29788a8f9d
commit 4ffadc6baa
3 changed files with 81 additions and 14 deletions
+10 -8
View File
@@ -19,15 +19,15 @@ const TICK_MS = 60_000;
// Отметка последнего успешного тика по slot'у: { generate: '2026-06-19T13:00', approve: '2026-06-19T07:00' }
const lastRun = {};
const GENERATE_HOUR_MSK = 13;
const APPROVE_HOUR_MSK = 7;
async function generateHourMsk() { return parseInt(await settings.get('ZERO_NOTES_GENERATE_HOUR', '13'), 10); }
async function approveHourMsk() { return parseInt(await settings.get('ZERO_NOTES_APPROVE_HOUR', '7'), 10); }
function slotKey(ymd, hour) {
return `${ymd}T${String(hour).padStart(2, '0')}:00`;
}
async function runGeneration(ymd) {
const key = slotKey(ymd, GENERATE_HOUR_MSK);
const key = slotKey(ymd, await generateHourMsk());
if (lastRun.generate === key) return;
lastRun.generate = key;
@@ -50,7 +50,7 @@ async function runGeneration(ymd) {
}
async function runAutoApprove(ymd) {
const key = slotKey(ymd, APPROVE_HOUR_MSK);
const key = slotKey(ymd, await approveHourMsk());
if (lastRun.approve === key) return;
lastRun.approve = key;
@@ -65,8 +65,9 @@ async function runAutoApprove(ymd) {
async function tick() {
const { hour, ymd } = zeroNotes.nowMsk();
try {
if (hour === GENERATE_HOUR_MSK) await runGeneration(ymd);
if (hour === APPROVE_HOUR_MSK) await runAutoApprove(ymd);
const [genHour, appHour] = [await generateHourMsk(), await approveHourMsk()];
if (hour === genHour) await runGeneration(ymd);
if (hour === appHour) await runAutoApprove(ymd);
// публикация approved-заметок в TG (каждую минуту)
const published = await zeroRunner.publishReady({ limit: 3 });
if (published > 0) console.log(`[zeroNotes/scheduler] published ${published} note(s)`);
@@ -85,8 +86,9 @@ function start() {
intervalRef = setInterval(tick, TICK_MS);
// первый тик через 30 сек после старта (даём engine стабильно подняться)
setTimeout(tick, 30_000);
console.log(`[zeroNotes/scheduler] started, tick every ${TICK_MS / 1000}s, ` +
`generate=${GENERATE_HOUR_MSK}:00 MSK, auto-approve=${APPROVE_HOUR_MSK}:00 MSK`);
Promise.all([generateHourMsk(), approveHourMsk()]).then(([gh, ah]) =>
console.log(`[zeroNotes/scheduler] started, tick every ${TICK_MS/1000}s, generate=${gh}:00 MSK, auto-approve=${ah}:00 MSK (dynamic)`)
).catch(() => {});
}
function stop() {