feat(admin): runtime-editable crawler & analysis config in the dashboard
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:
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user