feat: add AI Jobs admin page, cache model lists, improve image-gen 502 UX
All checks were successful
Release / Test backend (push) Successful in 40s
Release / Check ui (push) Successful in 1m36s
Release / Docker / caddy (push) Successful in 45s
Release / Docker / backend (push) Successful in 4m27s
Release / Docker / runner (push) Successful in 3m32s
Release / Upload source maps (push) Successful in 44s
Release / Docker / ui (push) Successful in 2m46s
Release / Gitea Release (push) Successful in 58s
All checks were successful
Release / Test backend (push) Successful in 40s
Release / Check ui (push) Successful in 1m36s
Release / Docker / caddy (push) Successful in 45s
Release / Docker / backend (push) Successful in 4m27s
Release / Docker / runner (push) Successful in 3m32s
Release / Upload source maps (push) Successful in 44s
Release / Docker / ui (push) Successful in 2m46s
Release / Gitea Release (push) Successful in 58s
- Add /admin/ai-jobs page with live-polling jobs table, status badges, progress bars, and cancel action - Add listAIJobs() helper in pocketbase.ts; AIJob type - Add POST /api/admin/ai-jobs/[id]/cancel SvelteKit proxy - Add ai-jobs link to admin sidebar nav (all 5 locales) - Cache image-gen and text-gen model lists in Valkey (10 min TTL) via scraper.ts helpers - Cache changelog Gitea API response (5 min TTL) - Improve 502/504 error message on image-gen page with CF AI timeout hint - Show CF AI timeout warning in advanced options when a Cloudflare/FLUX model is selected
This commit is contained in:
@@ -374,6 +374,7 @@ export * from './admin_nav_changelog.js'
|
||||
export * from './admin_nav_image_gen.js'
|
||||
export * from './admin_nav_text_gen.js'
|
||||
export * from './admin_nav_catalogue_tools.js'
|
||||
export * from './admin_nav_ai_jobs.js'
|
||||
export * from './admin_nav_feedback.js'
|
||||
export * from './admin_nav_errors.js'
|
||||
export * from './admin_nav_analytics.js'
|
||||
|
||||
44
ui/src/lib/paraglide/messages/admin_nav_ai_jobs.js
Normal file
44
ui/src/lib/paraglide/messages/admin_nav_ai_jobs.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/* eslint-disable */
|
||||
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
||||
|
||||
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
||||
|
||||
/** @typedef {{}} Admin_Nav_Ai_JobsInputs */
|
||||
|
||||
const en_admin_nav_ai_jobs = /** @type {(inputs: Admin_Nav_Ai_JobsInputs) => LocalizedString} */ () => {
|
||||
return /** @type {LocalizedString} */ (`AI Jobs`)
|
||||
};
|
||||
|
||||
const ru_admin_nav_ai_jobs = /** @type {(inputs: Admin_Nav_Ai_JobsInputs) => LocalizedString} */ () => {
|
||||
return /** @type {LocalizedString} */ (`Задачи ИИ`)
|
||||
};
|
||||
|
||||
const id_admin_nav_ai_jobs = /** @type {(inputs: Admin_Nav_Ai_JobsInputs) => LocalizedString} */ () => {
|
||||
return /** @type {LocalizedString} */ (`Tugas AI`)
|
||||
};
|
||||
|
||||
const pt_admin_nav_ai_jobs = /** @type {(inputs: Admin_Nav_Ai_JobsInputs) => LocalizedString} */ () => {
|
||||
return /** @type {LocalizedString} */ (`Tarefas de IA`)
|
||||
};
|
||||
|
||||
const fr_admin_nav_ai_jobs = /** @type {(inputs: Admin_Nav_Ai_JobsInputs) => LocalizedString} */ () => {
|
||||
return /** @type {LocalizedString} */ (`Tâches IA`)
|
||||
};
|
||||
|
||||
/**
|
||||
* | output |
|
||||
* | --- |
|
||||
* | "AI Jobs" |
|
||||
*
|
||||
* @param {Admin_Nav_Ai_JobsInputs} inputs
|
||||
* @param {{ locale?: "en" | "ru" | "id" | "pt" | "fr" }} options
|
||||
* @returns {LocalizedString}
|
||||
*/
|
||||
export const admin_nav_ai_jobs = /** @type {((inputs?: Admin_Nav_Ai_JobsInputs, options?: { locale?: "en" | "ru" | "id" | "pt" | "fr" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Admin_Nav_Ai_JobsInputs, { locale?: "en" | "ru" | "id" | "pt" | "fr" }, {}>} */ ((inputs = {}, options = {}) => {
|
||||
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
||||
if (locale === "en") return en_admin_nav_ai_jobs(inputs)
|
||||
if (locale === "ru") return ru_admin_nav_ai_jobs(inputs)
|
||||
if (locale === "id") return id_admin_nav_ai_jobs(inputs)
|
||||
if (locale === "pt") return pt_admin_nav_ai_jobs(inputs)
|
||||
return fr_admin_nav_ai_jobs(inputs)
|
||||
});
|
||||
@@ -14,6 +14,23 @@ const PB_PASSWORD = env.POCKETBASE_ADMIN_PASSWORD ?? 'changeme123';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AIJob {
|
||||
id: string;
|
||||
kind: string;
|
||||
slug: string;
|
||||
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
|
||||
from_item: number;
|
||||
to_item: number;
|
||||
items_done: number;
|
||||
items_total: number;
|
||||
model: string;
|
||||
payload: string;
|
||||
error_message?: string;
|
||||
started?: string;
|
||||
finished?: string;
|
||||
heartbeat_at?: string;
|
||||
}
|
||||
|
||||
export interface Book {
|
||||
id: string;
|
||||
slug: string;
|
||||
@@ -2162,3 +2179,13 @@ export async function getUserStats(
|
||||
streak
|
||||
};
|
||||
}
|
||||
|
||||
// ─── AI Jobs ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* List all AI jobs from PocketBase, sorted by started descending.
|
||||
* No caching — admin views always want fresh data.
|
||||
*/
|
||||
export async function listAIJobs(): Promise<AIJob[]> {
|
||||
return listAll<AIJob>('ai_jobs', '', '-started');
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import * as cache from '$lib/server/cache';
|
||||
|
||||
export const BACKEND_URL = env.BACKEND_API_URL ?? 'http://localhost:8080';
|
||||
|
||||
@@ -33,6 +34,50 @@ export async function backendFetch(path: string, init?: RequestInit): Promise<Re
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Cached admin model lists ─────────────────────────────────────────────────
|
||||
|
||||
const MODELS_CACHE_TTL = 10 * 60; // 10 minutes — model lists rarely change
|
||||
|
||||
/**
|
||||
* Fetch image-gen model list from the Go backend with a 10-minute cache.
|
||||
* Returns an empty array on error (callers should surface a warning).
|
||||
*/
|
||||
export async function listImageModels<T>(): Promise<T[]> {
|
||||
const key = 'backend:models:image-gen';
|
||||
const cached = await cache.get<T[]>(key);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const res = await backendFetch('/api/admin/image-gen/models');
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
const models = (data.models ?? []) as T[];
|
||||
await cache.set(key, models, MODELS_CACHE_TTL);
|
||||
return models;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch text-gen model list from the Go backend with a 10-minute cache.
|
||||
* Returns an empty array on error.
|
||||
*/
|
||||
export async function listTextModels<T>(): Promise<T[]> {
|
||||
const key = 'backend:models:text-gen';
|
||||
const cached = await cache.get<T[]>(key);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const res = await backendFetch('/api/admin/text-gen/models');
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
const models = (data.models ?? []) as T[];
|
||||
await cache.set(key, models, MODELS_CACHE_TTL);
|
||||
return models;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Response types ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user