Add rota backfill (60 days) — separate from the 25-month actuals backfill
runRollingSync only ever syncs wage_scheduled 14 days forward, so the AI insight's 28-day rota-vs-actual comparison had no historical rota to compare against for a freshly-scaffolded app. Adds a dedicated backfill action that pulls past schedules via the same throttled weekly-batch pattern as the existing actuals backfill.
This commit is contained in:
parent
95da5ea237
commit
cabaff8d35
4 changed files with 164 additions and 10 deletions
|
|
@ -613,3 +613,41 @@ export async function runBackfill(onProgress, signal) {
|
|||
await new Promise(r => setTimeout(r, 250))
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill wage_scheduled into the past. Unlike runRollingSync (which only ever syncs
|
||||
// [today, today+14d] for schedules), rota-vs-actual comparisons need historical rota too —
|
||||
// but rota loses relevance much faster than actuals, so this only reaches back 60 days
|
||||
// (comfortably covers the AI insight's 28-day window) rather than runBackfill's 25 months.
|
||||
export async function runScheduledBackfill(onProgress, signal, days = 60) {
|
||||
const today = new Date()
|
||||
const endDate = new Date(today)
|
||||
endDate.setDate(endDate.getDate() - 1)
|
||||
|
||||
const startDate = new Date(today)
|
||||
startDate.setDate(startDate.getDate() - days)
|
||||
|
||||
const totalDays = Math.max(1, Math.ceil((endDate - startDate) / 86_400_000))
|
||||
let processedDays = 0
|
||||
|
||||
let current = new Date(startDate)
|
||||
while (current <= endDate) {
|
||||
if (signal?.aborted) break
|
||||
|
||||
const weekEnd = new Date(current)
|
||||
weekEnd.setDate(weekEnd.getDate() + 6)
|
||||
if (weekEnd > endDate) weekEnd.setTime(endDate.getTime())
|
||||
|
||||
const from = current.toISOString().slice(0, 10)
|
||||
const to = weekEnd.toISOString().slice(0, 10)
|
||||
|
||||
await syncScheduled(from, to)
|
||||
|
||||
const daysInBatch = Math.ceil((weekEnd - current) / 86_400_000) + 1
|
||||
processedDays += daysInBatch
|
||||
|
||||
onProgress?.({ processed: processedDays, total: totalDays, current: from })
|
||||
|
||||
current.setDate(current.getDate() + 7)
|
||||
await new Promise(r => setTimeout(r, 250))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import { requireAuth, requireCap } from '../auth.js'
|
||||
import { getConfig, setConfig } from '../db.js'
|
||||
import { runRollingSync, runBackfill, compareEndpoints } from '../lib/workforce.js'
|
||||
import { runRollingSync, runBackfill, runScheduledBackfill, compareEndpoints } from '../lib/workforce.js'
|
||||
import { fetchAllDepartments } from '../lib/workforce.js'
|
||||
|
||||
let _backfillAbort = null
|
||||
let _rotaBackfillAbort = null
|
||||
|
||||
export async function syncRoutes(fastify) {
|
||||
fastify.addHook('preHandler', requireAuth)
|
||||
|
||||
fastify.get('/api/sync/status', { preHandler: requireCap('view') }, async () => {
|
||||
return {
|
||||
sync_last_at: await getConfig('sync_last_at'),
|
||||
backfill_last_at: await getConfig('backfill_last_at'),
|
||||
backfill_running: _backfillAbort !== null,
|
||||
sync_last_at: await getConfig('sync_last_at'),
|
||||
backfill_last_at: await getConfig('backfill_last_at'),
|
||||
backfill_running: _backfillAbort !== null,
|
||||
rota_backfill_last_at: await getConfig('rota_backfill_last_at'),
|
||||
rota_backfill_running: _rotaBackfillAbort !== null,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -66,6 +69,47 @@ export async function syncRoutes(fastify) {
|
|||
return { ok: true }
|
||||
})
|
||||
|
||||
// Rota backfill — same SSE shape as /api/sync/backfill, but only reaches back 60 days
|
||||
// (via runScheduledBackfill) since rota loses relevance much faster than actuals.
|
||||
fastify.post('/api/sync/backfill/rota', { preHandler: requireCap('sync') }, async (request, reply) => {
|
||||
if (_rotaBackfillAbort) {
|
||||
_rotaBackfillAbort.abort()
|
||||
_rotaBackfillAbort = null
|
||||
}
|
||||
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
})
|
||||
|
||||
const ac = new AbortController()
|
||||
_rotaBackfillAbort = ac
|
||||
request.raw.on('close', () => ac.abort())
|
||||
|
||||
try {
|
||||
await runScheduledBackfill(({ processed, total, current }) => {
|
||||
reply.raw.write(`data: ${JSON.stringify({ processed, total, current })}\n\n`)
|
||||
}, ac.signal)
|
||||
|
||||
await setConfig('rota_backfill_last_at', new Date().toISOString())
|
||||
reply.raw.write(`data: ${JSON.stringify({ done: true })}\n\n`)
|
||||
} catch (err) {
|
||||
reply.raw.write(`data: ${JSON.stringify({ error: err.message })}\n\n`)
|
||||
} finally {
|
||||
_rotaBackfillAbort = null
|
||||
reply.raw.end()
|
||||
}
|
||||
})
|
||||
|
||||
fastify.post('/api/sync/backfill/rota/cancel', { preHandler: requireCap('sync') }, async () => {
|
||||
if (_rotaBackfillAbort) {
|
||||
_rotaBackfillAbort.abort()
|
||||
_rotaBackfillAbort = null
|
||||
}
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
// Compare cost totals from all available Workforce endpoints for a date range.
|
||||
// Useful for diagnosing figure discrepancies between shifts vs timesheets.
|
||||
// Usage: GET /wages/api/sync/compare?from=2025-06-01&to=2025-06-30
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue