fix(photo): preserve photo_jobs history in backup/restore; paginate photo history on worker page
This commit is contained in:
@@ -1590,13 +1590,29 @@ function normalizeRestoreData(data) {
|
||||
for (const [k, v] of Object.entries(data.settings || {})) {
|
||||
settings[reqStr(k, 100)] = reqStr(String(v), 10000);
|
||||
}
|
||||
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, student_photos, group_photos, share_links, modules };
|
||||
const PHOTO_JOB_ACTIONS = new Set(['ai', 'enhance', 'restore', 'rollback']);
|
||||
const PHOTO_JOB_STATUSES = new Set(['pending', 'processing', 'done', 'error', 'rejected']);
|
||||
const photo_jobs = (data.photo_jobs || []).map(x => ({
|
||||
id: reqInt(x.id),
|
||||
entry_id: reqInt(x.entry_id),
|
||||
action: (x.action && PHOTO_JOB_ACTIONS.has(x.action)) ? x.action : 'ai',
|
||||
params: (x.params === null || x.params === undefined) ? null : (typeof x.params === 'object' ? JSON.stringify(x.params) : String(x.params)),
|
||||
before_path: optUploadPath(x.before_path, 255),
|
||||
after_path: optUploadPath(x.after_path, 255),
|
||||
status: (x.status && PHOTO_JOB_STATUSES.has(x.status)) ? x.status : 'pending',
|
||||
applied: !!x.applied,
|
||||
attempts: optInt(x.attempts, 0, 2147483647) ?? 0,
|
||||
error: optStr(x.error, 4000),
|
||||
created_at: optTs(x.created_at),
|
||||
finished_at: optTs(x.finished_at),
|
||||
}));
|
||||
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, student_photos, group_photos, share_links, modules, photo_jobs };
|
||||
}
|
||||
|
||||
app.get('/api/backup', requireAdmin, async (req, res) => {
|
||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
||||
try {
|
||||
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl] = await Promise.all([
|
||||
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl, pj] = await Promise.all([
|
||||
pool.query('SELECT * FROM groups ORDER BY id'),
|
||||
pool.query('SELECT * FROM students ORDER BY id'),
|
||||
pool.query('SELECT * FROM entries ORDER BY id'),
|
||||
@@ -1610,10 +1626,11 @@ app.get('/api/backup', requireAdmin, async (req, res) => {
|
||||
pool.query('SELECT * FROM modules ORDER BY id'),
|
||||
pool.query('SELECT * FROM student_photos ORDER BY id'),
|
||||
pool.query('SELECT * FROM share_links ORDER BY id'),
|
||||
pool.query('SELECT * FROM photo_jobs ORDER BY id'),
|
||||
]);
|
||||
const settings = {};
|
||||
st.rows.forEach(r => { settings[r.key] = r.value; });
|
||||
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows };
|
||||
const payload = { version: 1, created_at: new Date().toISOString(), groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows, photo_jobs: pj.rows };
|
||||
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
||||
fs.mkdirSync(path.join(staging, 'uploads'), { recursive: true });
|
||||
const dir = path.join(__dirname, 'uploads');
|
||||
@@ -1771,6 +1788,14 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
[x.id, x.student_name, x.group_id, x.module_id, x.description, x.description_original, x.description_ai, x.ai_status, x.ai_checked_at, x.ai_error, x.photo_path, x.photo_original_path, x.deleted_at, x.created_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.photo_jobs) {
|
||||
const ex = await client.query('SELECT 1 FROM entries WHERE id = $1', [x.entry_id]);
|
||||
if (!ex.rowCount) continue;
|
||||
await client.query(
|
||||
'INSERT INTO photo_jobs (id, entry_id, action, params, before_path, after_path, status, applied, attempts, error, created_at, finished_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)',
|
||||
[x.id, x.entry_id, x.action, x.params, x.before_path, x.after_path, x.status, x.applied, x.attempts, x.error, x.created_at, x.finished_at]
|
||||
);
|
||||
}
|
||||
for (const x of ndata.project_files) {
|
||||
await client.query(
|
||||
'INSERT INTO project_files (id, entry_id, token, path, name, detached_at, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
@@ -1803,7 +1828,7 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
||||
[k, String(v ?? '')]
|
||||
);
|
||||
}
|
||||
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos', 'entry_photos', 'modules', 'student_photos', 'share_links']) {
|
||||
for (const tbl of ['groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos', 'entry_photos', 'modules', 'student_photos', 'share_links', 'photo_jobs']) {
|
||||
const r = await client.query('SELECT COALESCE(MAX(id), 1) AS m FROM ' + tbl);
|
||||
await client.query('SELECT setval(pg_get_serial_sequence($1, $2), $3)', [tbl, 'id', r.rows[0].m]);
|
||||
}
|
||||
@@ -4750,13 +4775,15 @@ app.post('/api/entries/:id/ai/revert', requireAuth, async (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get('/api/photo-jobs/status', requireAdmin, async (_, res) => {
|
||||
app.get('/api/photo-jobs/status', requireAdmin, async (req, res) => {
|
||||
const recentLimit = Math.min(Math.max(optInt(req.query.recent_limit, 1, 200) ?? 15, 1), 200);
|
||||
const recentOffset = Math.max(optInt(req.query.recent_offset, 0, 2147483647) ?? 0, 0);
|
||||
const { rows } = await pool.query(
|
||||
`SELECT status, count(*)::int AS n FROM photo_jobs GROUP BY status`
|
||||
);
|
||||
const counts = { pending: 0, processing: 0, done: 0, error: 0 };
|
||||
rows.forEach(r => { counts[r.status] = r.n; });
|
||||
const [pending, recent, errors] = await Promise.all([
|
||||
const [pending, recentTotal, recent, errors] = await Promise.all([
|
||||
pool.query(
|
||||
`SELECT j.id, j.entry_id, j.action, j.created_at, e.student_name, g.name AS group_name
|
||||
FROM photo_jobs j
|
||||
@@ -4765,6 +4792,12 @@ app.get('/api/photo-jobs/status', requireAdmin, async (_, res) => {
|
||||
WHERE j.status IN ('pending', 'processing')
|
||||
ORDER BY j.id ASC LIMIT 20`
|
||||
),
|
||||
pool.query(
|
||||
`SELECT count(*)::int AS n FROM photo_jobs j
|
||||
JOIN entries e ON e.id = j.entry_id
|
||||
JOIN groups g ON g.id = e.group_id
|
||||
WHERE j.finished_at IS NOT NULL`
|
||||
),
|
||||
pool.query(
|
||||
`SELECT j.id, j.entry_id, j.action, j.status, j.params, j.before_path, j.after_path, j.error, j.created_at, j.finished_at,
|
||||
e.student_name, g.name AS group_name
|
||||
@@ -4772,7 +4805,8 @@ app.get('/api/photo-jobs/status', requireAdmin, async (_, res) => {
|
||||
JOIN entries e ON e.id = j.entry_id
|
||||
JOIN groups g ON g.id = e.group_id
|
||||
WHERE j.finished_at IS NOT NULL
|
||||
ORDER BY j.finished_at DESC LIMIT 30`
|
||||
ORDER BY j.finished_at DESC LIMIT $1 OFFSET $2`,
|
||||
[recentLimit, recentOffset]
|
||||
),
|
||||
pool.query(
|
||||
`SELECT j.id, j.entry_id, j.action, j.error, j.finished_at, e.student_name, g.name AS group_name
|
||||
@@ -4793,6 +4827,7 @@ app.get('/api/photo-jobs/status', requireAdmin, async (_, res) => {
|
||||
counts,
|
||||
pending: pending.rows,
|
||||
recent: recent.rows,
|
||||
recent_total: recentTotal.rows[0].n,
|
||||
errors: errors.rows,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user