feat(admin): runtime-editable crawler & analysis config in the dashboard
Some checks failed
deploy / test-backend (push) Waiting to run
deploy / build-and-push (push) Has been cancelled
deploy / deploy (push) Has been cancelled
deploy / test-frontend (push) Has been cancelled

Move crawler and analysis configuration out of boot-only env vars and into
a DB-backed, admin-editable surface applied live without a restart.

- New `app_settings` table (migration 0026) holds one JSONB row per group;
  env vars seed it on first boot, then the DB is the source of truth.
- `settings.rs` adds serializable Crawler/Analysis DTOs with env-base +
  DB-overlay conversion and field-level validation; env-only/secret fields
  (browser, proxy, TOR, vision API key, PHPSESSID) are never persisted.
- `GET/PUT /api/v1/admin/settings/{crawler,analysis}` (RequireAdmin,
  cookie-only, CSRF-guarded): GET returns the editable DTO + a read-only
  env-managed view + prompt defaults; PUT validates (422 + per-field
  details), persists + audits in one tx, then gracefully respawns the
  affected daemon via a Supervisor.
- AppState swaps the crawler control/resync handles and analysis enable
  gate behind shared runtime cells so reloads take effect with no restart;
  a boot-time spawn failure is logged rather than aborting startup.
- Make the three vision prompts and sampling temperature configurable
  (defaults preserved); cookie_domain is admin-editable too.
- Frontend: tabbed /admin/settings page with grouped fieldsets, prompt
  reset-to-default, env-managed read-only panel, save-&-apply confirm, and
  per-field validation; typed client + ApiError.details.
- Bump version 0.79.1 -> 0.80.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-14 13:32:20 +02:00
parent d3b827421f
commit 2b7a11b480
30 changed files with 2800 additions and 165 deletions

View File

@@ -556,3 +556,110 @@ export type AnalysisEvent =
export function analysisStatusStreamUrl(): string {
return apiUrl('/v1/admin/analysis/status/stream');
}
// ---- runtime settings (crawler + analysis) ---------------------------------
/** Operationally-safe, admin-editable crawler settings. Host/infra and
* session/secret fields are managed via environment and surfaced read-only
* in {@link CrawlerEnvOnly}. */
export type CrawlerSettings = {
daemon_enabled: boolean;
daily_at: string; // "HH:MM"
tz: string; // IANA
idle_timeout_secs: number;
chapter_workers: number;
retention_days: number;
start_url: string | null;
rate_ms: number;
cdn_host: string | null;
cdn_rate_ms: number;
cookie_domain: string | null;
user_agent: string | null;
download_allowlist: string[];
allow_any_host: boolean;
max_image_bytes: number;
manga_limit: number;
job_timeout_secs: number;
metadata_max_consecutive_failures: number;
browser_restart_threshold: number;
};
/** Read-only view of crawler fields managed via environment. */
export type CrawlerEnvOnly = {
browser_mode: string;
browser_args: string[];
proxy: string | null;
tor_control_url: string | null;
tor_credentials_configured: boolean;
session_configured: boolean;
};
export type CrawlerSettingsResponse = {
editable: CrawlerSettings;
env_only: CrawlerEnvOnly;
};
/** Admin-editable analysis settings. Prompts are `null` to mean "use the
* compiled default" (see {@link PromptDefaults}). The vision API key is
* env-only and never returned. */
export type AnalysisSettings = {
enabled: boolean;
workers: number;
endpoint: string;
model: string;
request_timeout_secs: number;
job_timeout_secs: number;
max_tokens: number;
max_pixels: number;
min_slice_height: number;
slice_overlap: number;
tall_aspect_threshold: number;
max_slices: number;
max_image_bytes: number;
response_format: 'json_schema' | 'json_object' | 'none';
frequency_penalty: number;
temperature: number;
system_prompt: string | null;
ocr_prompt: string | null;
grounding_prompt: string | null;
};
export type PromptDefaults = {
system_prompt: string;
ocr_prompt: string;
grounding_prompt: string;
};
export type AnalysisSettingsResponse = {
editable: AnalysisSettings;
env_only: { api_key_configured: boolean };
prompt_defaults: PromptDefaults;
};
export async function getCrawlerSettings(): Promise<CrawlerSettingsResponse> {
return request<CrawlerSettingsResponse>('/v1/admin/settings/crawler');
}
export async function updateCrawlerSettings(
settings: CrawlerSettings
): Promise<CrawlerSettingsResponse> {
return request<CrawlerSettingsResponse>('/v1/admin/settings/crawler', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings)
});
}
export async function getAnalysisSettings(): Promise<AnalysisSettingsResponse> {
return request<AnalysisSettingsResponse>('/v1/admin/settings/analysis');
}
export async function updateAnalysisSettings(
settings: AnalysisSettings
): Promise<AnalysisSettingsResponse> {
return request<AnalysisSettingsResponse>('/v1/admin/settings/analysis', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(settings)
});
}