feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen
- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg) - Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist - Feature-Status auf In Review gesetzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+393
@@ -0,0 +1,393 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
|
||||
function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("archivmail_token");
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("archivmail_token");
|
||||
window.location.href = "/";
|
||||
}
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text();
|
||||
throw new Error(body || `Request failed: ${res.status}`);
|
||||
}
|
||||
|
||||
if (res.status === 204) return {} as T;
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface MeResponse {
|
||||
username: string;
|
||||
role: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface SMTPStatus {
|
||||
running: boolean;
|
||||
enabled: boolean;
|
||||
bind: string;
|
||||
domain: string;
|
||||
tls: boolean;
|
||||
max_size_mb: number;
|
||||
allowed_ips: string[];
|
||||
received: number;
|
||||
rejected: number;
|
||||
last_mail_at?: string;
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SearchHit {
|
||||
id: string;
|
||||
score: number;
|
||||
from?: string;
|
||||
to?: string;
|
||||
subject?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
export interface SearchResponse {
|
||||
total: number;
|
||||
hits: SearchHit[];
|
||||
}
|
||||
|
||||
export interface MailAttachment {
|
||||
index: number;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface MailDetail {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
cc?: string;
|
||||
subject: string;
|
||||
date: string;
|
||||
size: number;
|
||||
body_html?: string;
|
||||
body_plain?: string;
|
||||
raw_headers: string;
|
||||
attachments: MailAttachment[];
|
||||
}
|
||||
|
||||
export interface AuditEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
event_type: string;
|
||||
username: string;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface AuditResponse {
|
||||
total: number;
|
||||
entries: AuditEntry[];
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// API functions
|
||||
|
||||
export async function login(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<LoginResponse> {
|
||||
return request<LoginResponse>("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getMe(): Promise<MeResponse> {
|
||||
return request<MeResponse>("/api/auth/me");
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
await request<void>("/api/auth/logout", { method: "POST" });
|
||||
}
|
||||
|
||||
export async function searchEmails(params: {
|
||||
q?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<SearchResponse> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params.q) sp.set("q", params.q);
|
||||
if (params.from) sp.set("from", params.from);
|
||||
if (params.to) sp.set("to", params.to);
|
||||
if (params.date_from) sp.set("date_from", params.date_from);
|
||||
if (params.date_to) sp.set("date_to", params.date_to);
|
||||
if (params.page) sp.set("page", String(params.page));
|
||||
if (params.page_size) sp.set("page_size", String(params.page_size));
|
||||
return request<SearchResponse>(`/api/search?${sp.toString()}`);
|
||||
}
|
||||
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
return request<User[]>("/api/users");
|
||||
}
|
||||
|
||||
export async function createUser(data: CreateUserRequest): Promise<User> {
|
||||
return request<User>("/api/users", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export interface StorageStats {
|
||||
total_mails: number;
|
||||
total_bytes: number;
|
||||
}
|
||||
|
||||
export async function getStorageStats(): Promise<StorageStats> {
|
||||
return request<StorageStats>("/api/admin/storage/stats");
|
||||
}
|
||||
|
||||
export async function getSMTPStatus(): Promise<SMTPStatus> {
|
||||
return request<SMTPStatus>("/api/admin/smtp/status");
|
||||
}
|
||||
|
||||
export async function getHealth(): Promise<HealthResponse> {
|
||||
return request<HealthResponse>("/api/health");
|
||||
}
|
||||
|
||||
export async function getMail(id: string): Promise<MailDetail> {
|
||||
return request<MailDetail>(`/api/mails/${id}`);
|
||||
}
|
||||
|
||||
export async function downloadMailAttachment(
|
||||
id: string,
|
||||
index: number
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/mails/${id}/attachments/${index}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
const match = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
const filename = match ? match[1].replace(/['"]/g, "") : `anhang-${index}`;
|
||||
return { blob: await res.blob(), filename };
|
||||
}
|
||||
|
||||
export async function downloadMailRaw(
|
||||
id: string
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/mails/${id}/raw`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||
return { blob: await res.blob(), filename: `${id}.eml` };
|
||||
}
|
||||
|
||||
export interface ServiceStatus {
|
||||
name: string;
|
||||
display_name: string;
|
||||
active: string; // active | inactive | failed | unknown
|
||||
sub: string; // running | dead | exited | ...
|
||||
enabled: string; // enabled | disabled | static | unknown
|
||||
description: string;
|
||||
external_blocked?: boolean; // only present for archivmail
|
||||
}
|
||||
|
||||
export async function getServices(): Promise<ServiceStatus[]> {
|
||||
return request<ServiceStatus[]>("/api/admin/services");
|
||||
}
|
||||
|
||||
export async function serviceAction(
|
||||
name: string,
|
||||
action: "start" | "stop" | "restart" | "enable" | "disable" | "block_external" | "allow_external"
|
||||
): Promise<ServiceStatus> {
|
||||
return request<ServiceStatus>(`/api/admin/services/${encodeURIComponent(name)}/action`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ action }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuditLog(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
username?: string;
|
||||
event_type?: string;
|
||||
}): Promise<AuditResponse> {
|
||||
const sp = new URLSearchParams();
|
||||
if (params.page) sp.set("page", String(params.page));
|
||||
if (params.page_size) sp.set("page_size", String(params.page_size));
|
||||
if (params.username) sp.set("username", params.username);
|
||||
if (params.event_type) sp.set("event_type", params.event_type);
|
||||
return request<AuditResponse>(`/api/audit?${sp.toString()}`);
|
||||
}
|
||||
|
||||
// ── IMAP ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ImapFolder {
|
||||
name: string;
|
||||
excluded: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ImapAccount {
|
||||
id: number;
|
||||
owner: string;
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
tls: string;
|
||||
username: string;
|
||||
excluded_folders: string[];
|
||||
status: string;
|
||||
error_msg: string;
|
||||
last_import_at?: string;
|
||||
last_import_count: number;
|
||||
progress_current: number;
|
||||
progress_total: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ImapTestResult {
|
||||
ok: boolean;
|
||||
folders?: ImapFolder[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function getImapAccounts(): Promise<ImapAccount[]> {
|
||||
return request<ImapAccount[]>("/api/imap");
|
||||
}
|
||||
|
||||
export async function createImapAccount(data: {
|
||||
name: string;
|
||||
host: string;
|
||||
port: number;
|
||||
tls: string;
|
||||
username: string;
|
||||
password: string;
|
||||
excluded_folders: string[];
|
||||
}): Promise<ImapAccount> {
|
||||
return request<ImapAccount>("/api/imap", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteImapAccount(id: number): Promise<void> {
|
||||
await request<void>(`/api/imap/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function testImapConnection(data: {
|
||||
host: string;
|
||||
port: number;
|
||||
tls: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<ImapTestResult> {
|
||||
return request<ImapTestResult>("/api/imap/test", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function startImapImport(id: number): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}/import`, { method: "POST" });
|
||||
}
|
||||
|
||||
export async function getImapProgress(id: number): Promise<ImapAccount> {
|
||||
return request<ImapAccount>(`/api/imap/${id}/progress`);
|
||||
}
|
||||
|
||||
// ── System Stats ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface SystemStatsCPU {
|
||||
load1: number;
|
||||
load5: number;
|
||||
load15: number;
|
||||
num_cpu: number;
|
||||
}
|
||||
|
||||
export interface SystemStatsRAM {
|
||||
total_bytes: number;
|
||||
used_bytes: number;
|
||||
free_bytes: number;
|
||||
used_pct: number;
|
||||
}
|
||||
|
||||
export interface SystemStatsDisk {
|
||||
mount: string;
|
||||
total_bytes: number;
|
||||
used_bytes: number;
|
||||
free_bytes: number;
|
||||
used_pct: number;
|
||||
fstype: string;
|
||||
}
|
||||
|
||||
export interface SystemStatsMailInfo {
|
||||
id: string;
|
||||
date: string;
|
||||
from: string;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
cpu: SystemStatsCPU;
|
||||
ram: SystemStatsRAM;
|
||||
disks: SystemStatsDisk[];
|
||||
archive: {
|
||||
first_mail: SystemStatsMailInfo | null;
|
||||
last_mail: SystemStatsMailInfo | null;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSystemStats(): Promise<SystemStats> {
|
||||
return request<SystemStats>("/api/admin/system/stats");
|
||||
}
|
||||
Reference in New Issue
Block a user