diff --git a/public/js/worker.js b/public/js/worker.js
index 7b84283..fb4c3df 100644
--- a/public/js/worker.js
+++ b/public/js/worker.js
@@ -1,5 +1,6 @@
const AUTO_REFRESH_MS = 5000;
const RECENT_PAGE_SIZE = 15;
+const PHOTO_PAGE_SIZE = 15;
const STATUS_LABELS = {
pending: 'В очереди',
processing: 'Проверяется',
@@ -23,6 +24,8 @@ let busy = false;
let photoBusy = false;
let recentRows = [];
let recentPage = 1;
+let photoPage = 1;
+let photoTotal = 0;
const PHOTO_ACTION_LABELS = {
ai: 'ИИ-обработка',
@@ -236,7 +239,10 @@ function renderPhoto() {
renderPhotoPending();
const tbody = document.querySelector('#photoTable tbody');
const empty = document.getElementById('photoEmpty');
- if (!rows.length) { tbody.innerHTML = ''; empty.style.display = 'block'; return; }
+ const pager = document.getElementById('photoRecentPager');
+ const maxPage = Math.max(1, Math.ceil(photoTotal / PHOTO_PAGE_SIZE));
+ if (photoPage > maxPage) photoPage = maxPage;
+ if (!photoTotal) { tbody.innerHTML = ''; empty.style.display = 'block'; renderPager(pager, photoPage, maxPage, p => { photoPage = p; loadPhoto(); }); return; }
empty.style.display = 'none';
tbody.innerHTML = rows.map(r => {
const before = r.before_path ? `
` : '—';
@@ -257,6 +263,7 @@ function renderPhoto() {
`;
}).join('');
+ renderPager(pager, photoPage, maxPage, p => { photoPage = p; loadPhoto(); });
renderIcons();
}
@@ -304,9 +311,10 @@ async function loadPhoto() {
if (photoBusy) return;
photoBusy = true;
try {
- const res = await fetch(`${API}/api/photo-jobs/status`, { headers: hdr() });
+ const res = await fetch(`${API}/api/photo-jobs/status?recent_limit=${PHOTO_PAGE_SIZE}&recent_offset=${(photoPage - 1) * PHOTO_PAGE_SIZE}`, { headers: hdr() });
if (!res.ok) return;
photoState = await res.json();
+ photoTotal = photoState.recent_total || 0;
renderPhoto();
} catch {
} finally {
diff --git a/public/worker.html b/public/worker.html
index 20dcda4..1dfdc30 100644
--- a/public/worker.html
+++ b/public/worker.html
@@ -125,6 +125,7 @@
Обработок фотографий пока нет.
+
diff --git a/server.js b/server.js
index 701c253..697f9d1 100644
--- a/server.js
+++ b/server.js
@@ -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,
});
});