Compare commits
No commits in common. "db6e7d81a4376d0f6d99c0d7fa98fcfe33c33511" and "01e0f27841cb2aa27d7d59f358186638ac7c71df" have entirely different histories.
db6e7d81a4
...
01e0f27841
16
README.md
16
README.md
@ -181,8 +181,6 @@ Runtime files are stored in Electron's `userData` directory, including:
|
|||||||
- `rd_session_state.json`
|
- `rd_session_state.json`
|
||||||
- `rd_history.json`
|
- `rd_history.json`
|
||||||
- `rd_downloader.log`
|
- `rd_downloader.log`
|
||||||
- `audit.log`
|
|
||||||
- `debug_ai_manifest.json`
|
|
||||||
- `session-logs/session_*.txt`
|
- `session-logs/session_*.txt`
|
||||||
- `package-logs/package_*.txt`
|
- `package-logs/package_*.txt`
|
||||||
- `item-logs/item_*.txt`
|
- `item-logs/item_*.txt`
|
||||||
@ -200,24 +198,17 @@ Enable it by creating these files in the same runtime folder that contains `rd_d
|
|||||||
- `debug_host.txt` (optional)
|
- `debug_host.txt` (optional)
|
||||||
Default is `127.0.0.1`. Set `0.0.0.0` only if you really want remote access and protect it with firewall, VPN, or reverse proxy.
|
Default is `127.0.0.1`. Set `0.0.0.0` only if you really want remote access and protect it with firewall, VPN, or reverse proxy.
|
||||||
|
|
||||||
After startup, the app also writes `debug_ai_manifest.json` into the same runtime folder. This file is meant for support tooling and AI agents: it lists all available endpoints, the auth method, the related runtime files, and the one remaining external value the assistant may still need from you for remote access: the server IP or DNS name.
|
|
||||||
|
|
||||||
Available endpoints after restart:
|
Available endpoints after restart:
|
||||||
|
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /meta`
|
- `GET /meta`
|
||||||
- `GET /host/diagnostics`
|
- `GET /host/diagnostics`
|
||||||
- `GET /status`
|
- `GET /status`
|
||||||
- `GET /settings`
|
|
||||||
- `GET /accounts`
|
|
||||||
- `GET /stats`
|
|
||||||
- `GET /history?limit=50&status=completed`
|
|
||||||
- `GET /packages?package=Release&includeItems=1`
|
- `GET /packages?package=Release&includeItems=1`
|
||||||
- `GET /items?status=downloading&package=Release`
|
- `GET /items?status=downloading&package=Release`
|
||||||
- `GET /session?package=Release`
|
- `GET /session?package=Release`
|
||||||
- `GET /log?lines=100&grep=keyword`
|
- `GET /log?lines=100&grep=keyword`
|
||||||
- `GET /logs/main?lines=100&grep=keyword`
|
- `GET /logs/main?lines=100&grep=keyword`
|
||||||
- `GET /logs/audit?lines=100&grep=keyword`
|
|
||||||
- `GET /logs/session?lines=100&grep=keyword`
|
- `GET /logs/session?lines=100&grep=keyword`
|
||||||
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
||||||
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
||||||
@ -232,17 +223,12 @@ Example from PowerShell:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Invoke-RestMethod "http://SERVER:9868/diagnostics?token=YOUR_TOKEN&package=Release"
|
Invoke-RestMethod "http://SERVER:9868/diagnostics?token=YOUR_TOKEN&package=Release"
|
||||||
Invoke-RestMethod "http://SERVER:9868/settings?token=YOUR_TOKEN"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/accounts?token=YOUR_TOKEN"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/stats?token=YOUR_TOKEN"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, package/session/item logs, and host-side Windows crash hints can be inspected remotely.
|
This makes it easy to share one URL plus token during support, so current package status, session state, package/session logs, and host-side Windows crash hints can be inspected remotely.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.70",
|
"version": "1.7.69",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -36,8 +36,6 @@ import { addHistoryEntry, cancelPendingAsyncSaves, clearHistory, createStoragePa
|
|||||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
import { startDebugServer, stopDebugServer } from "./debug-server";
|
import { startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
|
||||||
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
|
||||||
|
|
||||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||||
@ -76,7 +74,6 @@ export class AppController {
|
|||||||
initSessionLog(this.storagePaths.baseDir);
|
initSessionLog(this.storagePaths.baseDir);
|
||||||
initPackageLogs(this.storagePaths.baseDir);
|
initPackageLogs(this.storagePaths.baseDir);
|
||||||
initItemLogs(this.storagePaths.baseDir);
|
initItemLogs(this.storagePaths.baseDir);
|
||||||
initAuditLog(this.storagePaths.baseDir);
|
|
||||||
this.settings = loadSettings(this.storagePaths);
|
this.settings = loadSettings(this.storagePaths);
|
||||||
const session = loadSession(this.storagePaths);
|
const session = loadSession(this.storagePaths);
|
||||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||||
@ -101,10 +98,6 @@ export class AppController {
|
|||||||
});
|
});
|
||||||
logger.info(`App gestartet v${APP_VERSION}`);
|
logger.info(`App gestartet v${APP_VERSION}`);
|
||||||
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
||||||
logAuditEvent("INFO", "App gestartet", {
|
|
||||||
appVersion: APP_VERSION,
|
|
||||||
runtimeDir: this.storagePaths.baseDir
|
|
||||||
});
|
|
||||||
startDebugServer(this.manager, this.storagePaths.baseDir);
|
startDebugServer(this.manager, this.storagePaths.baseDir);
|
||||||
|
|
||||||
if (this.settings.autoResumeOnStart) {
|
if (this.settings.autoResumeOnStart) {
|
||||||
@ -176,14 +169,6 @@ export class AppController {
|
|||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAuditLogPath(): string | null {
|
|
||||||
return getAuditLogPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
|
||||||
logAuditEvent(level, message, fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
||||||
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
||||||
const previousSettings = this.settings;
|
const previousSettings = this.settings;
|
||||||
@ -212,10 +197,6 @@ export class AppController {
|
|||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Einstellungen aktualisiert", {
|
|
||||||
changedKeys: Object.keys(sanitizedPatch),
|
|
||||||
accountChanges: diffAccountSummary(previousSettings, this.settings)
|
|
||||||
});
|
|
||||||
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
||||||
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
||||||
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
||||||
@ -239,7 +220,6 @@ export class AppController {
|
|||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider });
|
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,27 +232,19 @@ export class AppController {
|
|||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
|
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openRealDebridLoginWindow(): Promise<void> {
|
public async openRealDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
|
||||||
await this.realDebridWebFallback.openLoginWindow();
|
await this.realDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAllDebridLoginWindow(): Promise<void> {
|
public async openAllDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
|
||||||
await this.allDebridWebFallback.openLoginWindow();
|
await this.allDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||||
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
return this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||||
this.audit("INFO", "BestDebrid Cookies importiert", {
|
|
||||||
filePath,
|
|
||||||
imported
|
|
||||||
});
|
|
||||||
return imported;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||||
@ -321,17 +293,9 @@ export class AppController {
|
|||||||
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
||||||
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", {
|
|
||||||
hasPackageName: Boolean(payload.packageName)
|
|
||||||
});
|
|
||||||
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
||||||
}
|
}
|
||||||
const result = this.manager.addPackages(parsed);
|
const result = this.manager.addPackages(parsed);
|
||||||
this.audit("INFO", "Links hinzugefügt", {
|
|
||||||
addedPackages: result.addedPackages,
|
|
||||||
addedLinks: result.addedLinks,
|
|
||||||
requestedPackages: parsed.length
|
|
||||||
});
|
|
||||||
return { ...result, invalidCount: 0 };
|
return { ...result, invalidCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,11 +307,6 @@ export class AppController {
|
|||||||
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
||||||
}));
|
}));
|
||||||
const result = this.manager.addPackages(merged);
|
const result = this.manager.addPackages(merged);
|
||||||
this.audit("INFO", "Container importiert", {
|
|
||||||
files: filePaths.length,
|
|
||||||
addedPackages: result.addedPackages,
|
|
||||||
addedLinks: result.addedLinks
|
|
||||||
});
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,73 +319,58 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public clearAll(): void {
|
public clearAll(): void {
|
||||||
this.audit("WARN", "Queue komplett geleert");
|
|
||||||
this.manager.clearAll();
|
this.manager.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.audit("INFO", "Session-Start ausgelöst");
|
|
||||||
await this.manager.start();
|
await this.manager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startPackages(packageIds: string[]): Promise<void> {
|
public async startPackages(packageIds: string[]): Promise<void> {
|
||||||
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
|
||||||
await this.manager.startPackages(packageIds);
|
await this.manager.startPackages(packageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startItems(itemIds: string[]): Promise<void> {
|
public async startItems(itemIds: string[]): Promise<void> {
|
||||||
this.audit("INFO", "Item-Start ausgelöst", { itemIds });
|
|
||||||
await this.manager.startItems(itemIds);
|
await this.manager.startItems(itemIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
this.audit("INFO", "Session-Stopp ausgelöst");
|
|
||||||
this.manager.stop();
|
this.manager.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public togglePause(): boolean {
|
public togglePause(): boolean {
|
||||||
const paused = this.manager.togglePause();
|
return this.manager.togglePause();
|
||||||
this.audit("INFO", "Pause umgeschaltet", { paused });
|
|
||||||
return paused;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public retryExtraction(packageId: string): void {
|
public retryExtraction(packageId: string): void {
|
||||||
this.audit("INFO", "Extraktion manuell wiederholt", { packageId });
|
|
||||||
this.manager.retryExtraction(packageId);
|
this.manager.retryExtraction(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractNow(packageId: string): void {
|
public extractNow(packageId: string): void {
|
||||||
this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId });
|
|
||||||
this.manager.extractNow(packageId);
|
this.manager.extractNow(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetPackage(packageId: string): void {
|
public resetPackage(packageId: string): void {
|
||||||
this.audit("INFO", "Paket zurückgesetzt", { packageId });
|
|
||||||
this.manager.resetPackage(packageId);
|
this.manager.resetPackage(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancelPackage(packageId: string): void {
|
public cancelPackage(packageId: string): void {
|
||||||
this.audit("WARN", "Paket abgebrochen", { packageId });
|
|
||||||
this.manager.cancelPackage(packageId);
|
this.manager.cancelPackage(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public renamePackage(packageId: string, newName: string): void {
|
public renamePackage(packageId: string, newName: string): void {
|
||||||
this.audit("INFO", "Paket umbenannt", { packageId, newName });
|
|
||||||
this.manager.renamePackage(packageId, newName);
|
this.manager.renamePackage(packageId, newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public reorderPackages(packageIds: string[]): void {
|
public reorderPackages(packageIds: string[]): void {
|
||||||
this.audit("INFO", "Paketreihenfolge geändert", { packageIds });
|
|
||||||
this.manager.reorderPackages(packageIds);
|
this.manager.reorderPackages(packageIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeItem(itemId: string): void {
|
public removeItem(itemId: string): void {
|
||||||
this.audit("WARN", "Item entfernt", { itemId });
|
|
||||||
this.manager.removeItem(itemId);
|
this.manager.removeItem(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public togglePackage(packageId: string): void {
|
public togglePackage(packageId: string): void {
|
||||||
this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId });
|
|
||||||
this.manager.togglePackage(packageId);
|
this.manager.togglePackage(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,14 +387,12 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public resetSessionStats(): void {
|
public resetSessionStats(): void {
|
||||||
this.audit("INFO", "Session-Statistik zurückgesetzt");
|
|
||||||
this.manager.resetSessionStats();
|
this.manager.resetSessionStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetDownloadStats(): void {
|
public resetDownloadStats(): void {
|
||||||
this.manager.resetDownloadStats();
|
this.manager.resetDownloadStats();
|
||||||
this.settings = this.manager.getSettings();
|
this.settings = this.manager.getSettings();
|
||||||
this.audit("INFO", "Download-Statistik zurückgesetzt");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportBackup(): Buffer {
|
public exportBackup(): Buffer {
|
||||||
@ -465,11 +407,6 @@ export class AppController {
|
|||||||
session,
|
session,
|
||||||
history
|
history
|
||||||
});
|
});
|
||||||
this.audit("INFO", "Backup exportiert", {
|
|
||||||
historyEntries: history.length,
|
|
||||||
sessionItems: Object.keys(session.items).length,
|
|
||||||
sessionPackages: Object.keys(session.packages).length
|
|
||||||
});
|
|
||||||
return encryptBackup(payload);
|
return encryptBackup(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -538,10 +475,6 @@ export class AppController {
|
|||||||
this.manager.skipShutdownPersist = true;
|
this.manager.skipShutdownPersist = true;
|
||||||
this.manager.blockAllPersistence = true;
|
this.manager.blockAllPersistence = true;
|
||||||
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
||||||
this.audit("WARN", "Backup importiert", {
|
|
||||||
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
|
|
||||||
accountSummary: buildAccountSummary(this.settings)
|
|
||||||
});
|
|
||||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -568,8 +501,6 @@ export class AppController {
|
|||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
this.audit("INFO", "App beendet");
|
|
||||||
shutdownAuditLog();
|
|
||||||
logger.info("App beendet");
|
logger.info("App beendet");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,38 +509,26 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public clearHistory(): void {
|
public clearHistory(): void {
|
||||||
this.audit("WARN", "Verlauf geleert");
|
|
||||||
clearHistory(this.storagePaths);
|
clearHistory(this.storagePaths);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
||||||
this.audit("INFO", "Paket-Priorität geändert", { packageId, priority });
|
|
||||||
this.manager.setPackagePriority(packageId, priority);
|
this.manager.setPackagePriority(packageId, priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
public skipItems(itemIds: string[]): void {
|
public skipItems(itemIds: string[]): void {
|
||||||
this.audit("INFO", "Items übersprungen", { itemIds });
|
|
||||||
this.manager.skipItems(itemIds);
|
this.manager.skipItems(itemIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetItems(itemIds: string[]): void {
|
public resetItems(itemIds: string[]): void {
|
||||||
this.audit("INFO", "Items zurückgesetzt", { itemIds });
|
|
||||||
this.manager.resetItems(itemIds);
|
this.manager.resetItems(itemIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeHistoryEntry(entryId: string): void {
|
public removeHistoryEntry(entryId: string): void {
|
||||||
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
|
|
||||||
removeHistoryEntry(this.storagePaths, entryId);
|
removeHistoryEntry(this.storagePaths, entryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addToHistory(entry: HistoryEntry): void {
|
public addToHistory(entry: HistoryEntry): void {
|
||||||
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
|
|
||||||
id: entry.id,
|
|
||||||
name: entry.name,
|
|
||||||
status: entry.status,
|
|
||||||
provider: entry.provider,
|
|
||||||
fileCount: entry.fileCount
|
|
||||||
});
|
|
||||||
addHistoryEntry(this.storagePaths, entry);
|
addHistoryEntry(this.storagePaths, entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,80 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
type AuditLevel = "INFO" | "WARN" | "ERROR";
|
|
||||||
|
|
||||||
let auditLogPath: string | null = null;
|
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.replace(/\r?\n/g, "\\n");
|
|
||||||
}
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFields(fields?: Record<string, unknown>): string {
|
|
||||||
if (!fields) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const parts = Object.entries(fields)
|
|
||||||
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
|
|
||||||
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
|
|
||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initAuditLog(baseDir: string): void {
|
|
||||||
auditLogPath = path.join(baseDir, "audit.log");
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
|
||||||
if (!fs.existsSync(auditLogPath)) {
|
|
||||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
fs.appendFileSync(auditLogPath, `=== Audit-Log Start: ${new Date().toISOString()} ===\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
auditLogPath = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logAuditEvent(level: AuditLevel, message: string, fields?: Record<string, unknown>): void {
|
|
||||||
if (!auditLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(
|
|
||||||
auditLogPath,
|
|
||||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// ignore write errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAuditLogPath(): string | null {
|
|
||||||
if (!auditLogPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fs.existsSync(auditLogPath) ? auditLogPath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shutdownAuditLog(): void {
|
|
||||||
if (!auditLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(auditLogPath, `=== Audit-Log Ende: ${new Date().toISOString()} ===\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
auditLogPath = null;
|
|
||||||
}
|
|
||||||
@ -2,13 +2,10 @@ import http from "node:http";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { getAuditLogPath } from "./audit-log";
|
|
||||||
import { logger, getLogFilePath } from "./logger";
|
import { logger, getLogFilePath } from "./logger";
|
||||||
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
||||||
import { getSessionLogPath } from "./session-log";
|
import { getSessionLogPath } from "./session-log";
|
||||||
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
||||||
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
|
||||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
|
||||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||||
import type { DownloadManager } from "./download-manager";
|
import type { DownloadManager } from "./download-manager";
|
||||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||||
@ -16,35 +13,6 @@ import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
|||||||
const DEFAULT_PORT = 9868;
|
const DEFAULT_PORT = 9868;
|
||||||
const DEFAULT_HOST = "127.0.0.1";
|
const DEFAULT_HOST = "127.0.0.1";
|
||||||
const MAX_LOG_LINES = 10000;
|
const MAX_LOG_LINES = 10000;
|
||||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
|
||||||
|
|
||||||
type DebugEndpointDescriptor = {
|
|
||||||
method: "GET";
|
|
||||||
path: string;
|
|
||||||
queryExample?: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
|
||||||
{ method: "GET", path: "/health", description: "Basic health, uptime, and memory information." },
|
|
||||||
{ method: "GET", path: "/meta", description: "Lists runtime metadata and all available endpoints." },
|
|
||||||
{ method: "GET", path: "/host/diagnostics", description: "Returns Windows host crash and dump diagnostics." },
|
|
||||||
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
|
|
||||||
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
|
|
||||||
{ method: "GET", path: "/logs/audit", queryExample: "lines=100&grep=keyword", description: "Reads the audit log for support-relevant UI and admin actions." },
|
|
||||||
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
|
||||||
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
|
||||||
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
|
||||||
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
|
||||||
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
|
||||||
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
|
||||||
{ method: "GET", path: "/history", queryExample: "limit=50&status=completed", description: "Returns history entries with optional filters." },
|
|
||||||
{ method: "GET", path: "/status", description: "Returns a live high-level status overview." },
|
|
||||||
{ method: "GET", path: "/packages", queryExample: "package=Release&includeItems=1", description: "Lists packages and optional per-item detail." },
|
|
||||||
{ method: "GET", path: "/items", queryExample: "status=downloading&package=Release", description: "Lists items and supports status/package filters." },
|
|
||||||
{ method: "GET", path: "/session", queryExample: "package=Release", description: "Returns session-wide or package-scoped item state." },
|
|
||||||
{ method: "GET", path: "/diagnostics", queryExample: "package=Release&lines=150", description: "Returns a combined support snapshot with logs, status, settings, accounts, stats, history, and host diagnostics." }
|
|
||||||
];
|
|
||||||
|
|
||||||
let server: http.Server | null = null;
|
let server: http.Server | null = null;
|
||||||
let manager: DownloadManager | null = null;
|
let manager: DownloadManager | null = null;
|
||||||
@ -53,22 +21,6 @@ let bindHost = DEFAULT_HOST;
|
|||||||
let bindPort = DEFAULT_PORT;
|
let bindPort = DEFAULT_PORT;
|
||||||
let runtimeBaseDir = "";
|
let runtimeBaseDir = "";
|
||||||
|
|
||||||
function getStoragePaths() {
|
|
||||||
return createStoragePaths(runtimeBaseDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
function readSupportSettings() {
|
|
||||||
return loadSettings(getStoragePaths());
|
|
||||||
}
|
|
||||||
|
|
||||||
function readSupportHistory() {
|
|
||||||
return loadHistory(getStoragePaths());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAiManifestPath(baseDir: string = runtimeBaseDir): string {
|
|
||||||
return path.join(baseDir, AI_MANIFEST_FILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadToken(baseDir: string): string {
|
function loadToken(baseDir: string): string {
|
||||||
const tokenPath = path.join(baseDir, "debug_token.txt");
|
const tokenPath = path.join(baseDir, "debug_token.txt");
|
||||||
try {
|
try {
|
||||||
@ -161,78 +113,6 @@ function filterLines(lines: string[], grep: string): string[] {
|
|||||||
return lines.filter((line) => line.toLowerCase().includes(pattern));
|
return lines.filter((line) => line.toLowerCase().includes(pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEndpointSummary(endpoint: DebugEndpointDescriptor): string {
|
|
||||||
return `${endpoint.method} ${endpoint.path}${endpoint.queryExample ? `?${endpoint.queryExample}` : ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEndpointSummaries(): string[] {
|
|
||||||
return DEBUG_ENDPOINTS.map((endpoint) => formatEndpointSummary(endpoint));
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|
||||||
const remoteHostHint = bindHost === "0.0.0.0"
|
|
||||||
? "Use the server IP or DNS name for remote access. Ask the user only for that host value if it is unknown."
|
|
||||||
: "If remote access is required and the bind host is local-only, switch debug_host.txt to 0.0.0.0 and reopen the firewall.";
|
|
||||||
|
|
||||||
return {
|
|
||||||
schemaVersion: 1,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
appVersion: APP_VERSION,
|
|
||||||
runtimeBaseDir: baseDir,
|
|
||||||
purpose: "Machine-readable support manifest for AI tools and remote troubleshooting.",
|
|
||||||
quickstart: [
|
|
||||||
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
|
||||||
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
|
||||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
|
||||||
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, or /history."
|
|
||||||
],
|
|
||||||
auth: {
|
|
||||||
required: true,
|
|
||||||
methods: [
|
|
||||||
"Authorization: Bearer <token>",
|
|
||||||
"?token=<token>"
|
|
||||||
],
|
|
||||||
tokenFile: path.join(baseDir, "debug_token.txt")
|
|
||||||
},
|
|
||||||
runtimeFiles: {
|
|
||||||
hostFile: path.join(baseDir, "debug_host.txt"),
|
|
||||||
portFile: path.join(baseDir, "debug_port.txt"),
|
|
||||||
tokenFile: path.join(baseDir, "debug_token.txt"),
|
|
||||||
mainLogFile: getLogFilePath(),
|
|
||||||
auditLogFile: getAuditLogPath(),
|
|
||||||
sessionLogFile: getSessionLogPath(),
|
|
||||||
packageLogDir: path.join(baseDir, "package-logs"),
|
|
||||||
itemLogDir: path.join(baseDir, "item-logs"),
|
|
||||||
settingsFile: path.join(baseDir, "rd_downloader_config.json"),
|
|
||||||
sessionFile: path.join(baseDir, "rd_session_state.json"),
|
|
||||||
historyFile: path.join(baseDir, "rd_history.json")
|
|
||||||
},
|
|
||||||
debugServer: {
|
|
||||||
enabled: Boolean(authToken),
|
|
||||||
host: bindHost,
|
|
||||||
port: bindPort,
|
|
||||||
localBaseUrl: `http://127.0.0.1:${bindPort}`,
|
|
||||||
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
|
||||||
remoteHostHint
|
|
||||||
},
|
|
||||||
askUserFor: [
|
|
||||||
"Server IP or DNS name, if remote access is required and not already known."
|
|
||||||
],
|
|
||||||
endpoints: DEBUG_ENDPOINTS.map((endpoint) => ({
|
|
||||||
...endpoint,
|
|
||||||
summary: formatEndpointSummary(endpoint)
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeAiManifest(baseDir: string): void {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(getAiManifestPath(baseDir), JSON.stringify(buildAiManifest(baseDir), null, 2), "utf8");
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Debug-Server: KI-Support-Datei konnte nicht geschrieben werden: ${String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeItem(item: DownloadItem): Record<string, unknown> {
|
function summarizeItem(item: DownloadItem): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@ -384,14 +264,25 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
host: bindHost,
|
host: bindHost,
|
||||||
port: bindPort
|
port: bindPort
|
||||||
},
|
},
|
||||||
supportFiles: {
|
|
||||||
aiManifest: getAiManifestPath()
|
|
||||||
},
|
|
||||||
logPaths: {
|
logPaths: {
|
||||||
main: getLogFilePath(),
|
main: getLogFilePath(),
|
||||||
session: getSessionLogPath()
|
session: getSessionLogPath()
|
||||||
},
|
},
|
||||||
endpoints: getEndpointSummaries()
|
endpoints: [
|
||||||
|
"GET /health",
|
||||||
|
"GET /meta",
|
||||||
|
"GET /host/diagnostics",
|
||||||
|
"GET /log?lines=100&grep=keyword",
|
||||||
|
"GET /logs/main?lines=100&grep=keyword",
|
||||||
|
"GET /logs/session?lines=100&grep=keyword",
|
||||||
|
"GET /logs/package?package=Release&lines=100&grep=keyword",
|
||||||
|
"GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword",
|
||||||
|
"GET /status",
|
||||||
|
"GET /packages?package=Release&includeItems=1",
|
||||||
|
"GET /items?status=downloading&package=Release",
|
||||||
|
"GET /session?package=Release",
|
||||||
|
"GET /diagnostics?package=Release&lines=150"
|
||||||
|
]
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -409,19 +300,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/logs/audit") {
|
|
||||||
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
|
||||||
const grep = url.searchParams.get("grep") || "";
|
|
||||||
const logPath = getAuditLogPath();
|
|
||||||
const lines = filterLines(readLogTailFromFile(logPath, count), grep);
|
|
||||||
jsonResponse(res, 200, {
|
|
||||||
path: logPath,
|
|
||||||
lines,
|
|
||||||
count: lines.length
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/logs/session") {
|
if (pathname === "/logs/session") {
|
||||||
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
||||||
const grep = url.searchParams.get("grep") || "";
|
const grep = url.searchParams.get("grep") || "";
|
||||||
@ -483,58 +361,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/settings") {
|
|
||||||
const settings = readSupportSettings();
|
|
||||||
jsonResponse(res, 200, buildRedactedSettingsPayload(settings));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/accounts") {
|
|
||||||
const settings = readSupportSettings();
|
|
||||||
jsonResponse(res, 200, buildAccountSummary(settings));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/stats") {
|
|
||||||
if (!manager) {
|
|
||||||
jsonResponse(res, 503, { error: "Manager not initialized" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const snapshot = manager.getSnapshot();
|
|
||||||
const settings = readSupportSettings();
|
|
||||||
jsonResponse(res, 200, {
|
|
||||||
...buildStatsPayload(snapshot),
|
|
||||||
allTime: {
|
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/history") {
|
|
||||||
const entries = readSupportHistory();
|
|
||||||
const limit = normalizeLinesParam(url.searchParams.get("limit"), 50);
|
|
||||||
const statusFilter = String(url.searchParams.get("status") || "").trim().toLowerCase();
|
|
||||||
const grep = String(url.searchParams.get("grep") || "").trim().toLowerCase();
|
|
||||||
let filtered = entries;
|
|
||||||
if (statusFilter) {
|
|
||||||
filtered = filtered.filter((entry) => String(entry.status || "").toLowerCase() === statusFilter);
|
|
||||||
}
|
|
||||||
if (grep) {
|
|
||||||
filtered = filtered.filter((entry) => JSON.stringify(summarizeHistoryEntry(entry)).toLowerCase().includes(grep));
|
|
||||||
}
|
|
||||||
const sliced = filtered
|
|
||||||
.sort((a, b) => Number(b.completedAt || 0) - Number(a.completedAt || 0))
|
|
||||||
.slice(0, limit);
|
|
||||||
jsonResponse(res, 200, {
|
|
||||||
count: sliced.length,
|
|
||||||
total: filtered.length,
|
|
||||||
entries: sliced.map((entry) => summarizeHistoryEntry(entry))
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/status") {
|
if (pathname === "/status") {
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
jsonResponse(res, 503, { error: "Manager not initialized" });
|
jsonResponse(res, 503, { error: "Manager not initialized" });
|
||||||
@ -652,16 +478,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
status: buildStatusPayload(snapshot),
|
status: buildStatusPayload(snapshot),
|
||||||
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
|
||||||
stats: buildStatsPayload(snapshot),
|
|
||||||
accounts: buildAccountSummary(readSupportSettings()),
|
|
||||||
history: {
|
|
||||||
total: readSupportHistory().length,
|
|
||||||
recent: readSupportHistory()
|
|
||||||
.sort((a, b) => Number(b.completedAt || 0) - Number(a.completedAt || 0))
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((entry) => summarizeHistoryEntry(entry))
|
|
||||||
},
|
|
||||||
host: getWindowsHostDiagnostics(),
|
host: getWindowsHostDiagnostics(),
|
||||||
selectedPackage: selectedPackage ? summarizePackage(snapshot, selectedPackage, true) : undefined,
|
selectedPackage: selectedPackage ? summarizePackage(snapshot, selectedPackage, true) : undefined,
|
||||||
logs: {
|
logs: {
|
||||||
@ -669,10 +485,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
path: mainLogPath,
|
path: mainLogPath,
|
||||||
lines: filterLines(readLogTailFromFile(mainLogPath, lineCount), grep)
|
lines: filterLines(readLogTailFromFile(mainLogPath, lineCount), grep)
|
||||||
},
|
},
|
||||||
audit: {
|
|
||||||
path: getAuditLogPath(),
|
|
||||||
lines: filterLines(readLogTailFromFile(getAuditLogPath(), lineCount), grep)
|
|
||||||
},
|
|
||||||
session: {
|
session: {
|
||||||
path: sessionLogPath,
|
path: sessionLogPath,
|
||||||
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
||||||
@ -688,22 +500,35 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
|
|
||||||
jsonResponse(res, 404, {
|
jsonResponse(res, 404, {
|
||||||
error: "Not found",
|
error: "Not found",
|
||||||
endpoints: getEndpointSummaries()
|
endpoints: [
|
||||||
|
"GET /health",
|
||||||
|
"GET /meta",
|
||||||
|
"GET /host/diagnostics",
|
||||||
|
"GET /log?lines=100&grep=keyword",
|
||||||
|
"GET /logs/main?lines=100&grep=keyword",
|
||||||
|
"GET /logs/session?lines=100&grep=keyword",
|
||||||
|
"GET /logs/package?package=Release&lines=100&grep=keyword",
|
||||||
|
"GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword",
|
||||||
|
"GET /status",
|
||||||
|
"GET /packages?package=Release&includeItems=1",
|
||||||
|
"GET /items?status=downloading&package=Bloodline",
|
||||||
|
"GET /session?package=Criminal",
|
||||||
|
"GET /diagnostics?package=Criminal&lines=150"
|
||||||
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startDebugServer(mgr: DownloadManager, baseDir: string): void {
|
export function startDebugServer(mgr: DownloadManager, baseDir: string): void {
|
||||||
runtimeBaseDir = baseDir;
|
runtimeBaseDir = baseDir;
|
||||||
authToken = loadToken(baseDir);
|
authToken = loadToken(baseDir);
|
||||||
bindPort = getPort(baseDir);
|
|
||||||
bindHost = getHost(baseDir);
|
|
||||||
writeAiManifest(baseDir);
|
|
||||||
if (!authToken) {
|
if (!authToken) {
|
||||||
logger.info("Debug-Server: Kein Token in debug_token.txt, Server wird nicht gestartet");
|
logger.info("Debug-Server: Kein Token in debug_token.txt, Server wird nicht gestartet");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
manager = mgr;
|
manager = mgr;
|
||||||
|
bindPort = getPort(baseDir);
|
||||||
|
bindHost = getHost(baseDir);
|
||||||
|
|
||||||
server = http.createServer(handleRequest);
|
server = http.createServer(handleRequest);
|
||||||
server.listen(bindPort, bindHost, () => {
|
server.listen(bindPort, bindHost, () => {
|
||||||
|
|||||||
@ -1,178 +0,0 @@
|
|||||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
|
||||||
import type { AppSettings, HistoryEntry, UiSnapshot } from "../shared/types";
|
|
||||||
|
|
||||||
function hasText(value: unknown): boolean {
|
|
||||||
return String(value || "").trim().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildAccountSummary(settings: AppSettings): Record<string, unknown> {
|
|
||||||
const debridLinkKeyIds = getDebridLinkApiKeyIds(settings.debridLinkApiKeys);
|
|
||||||
const disabledDebridLinkIds = new Set(settings.debridLinkDisabledKeyIds || []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
realDebrid: {
|
|
||||||
configured: hasText(settings.token) || settings.realDebridUseWebLogin,
|
|
||||||
tokenConfigured: hasText(settings.token),
|
|
||||||
webLoginEnabled: settings.realDebridUseWebLogin,
|
|
||||||
rememberToken: settings.rememberToken
|
|
||||||
},
|
|
||||||
megaDebrid: {
|
|
||||||
configured: (hasText(settings.megaLogin) && hasText(settings.megaPassword))
|
|
||||||
|| settings.megaDebridApiEnabled
|
|
||||||
|| settings.megaDebridWebEnabled,
|
|
||||||
loginConfigured: hasText(settings.megaLogin) && hasText(settings.megaPassword),
|
|
||||||
apiEnabled: settings.megaDebridApiEnabled,
|
|
||||||
webEnabled: settings.megaDebridWebEnabled,
|
|
||||||
preferApi: settings.megaDebridPreferApi
|
|
||||||
},
|
|
||||||
bestDebrid: {
|
|
||||||
configured: hasText(settings.bestToken) || settings.bestDebridUseWebLogin,
|
|
||||||
tokenConfigured: hasText(settings.bestToken),
|
|
||||||
webLoginEnabled: settings.bestDebridUseWebLogin
|
|
||||||
},
|
|
||||||
allDebrid: {
|
|
||||||
configured: hasText(settings.allDebridToken) || settings.allDebridUseWebLogin,
|
|
||||||
tokenConfigured: hasText(settings.allDebridToken),
|
|
||||||
webLoginEnabled: settings.allDebridUseWebLogin
|
|
||||||
},
|
|
||||||
ddownload: {
|
|
||||||
configured: hasText(settings.ddownloadLogin) && hasText(settings.ddownloadPassword)
|
|
||||||
},
|
|
||||||
oneFichier: {
|
|
||||||
configured: hasText(settings.oneFichierApiKey)
|
|
||||||
},
|
|
||||||
debridLink: {
|
|
||||||
configured: debridLinkKeyIds.length > 0,
|
|
||||||
keyCount: debridLinkKeyIds.length,
|
|
||||||
enabledKeyCount: debridLinkKeyIds.filter((id) => !disabledDebridLinkIds.has(id)).length,
|
|
||||||
disabledKeyCount: debridLinkKeyIds.filter((id) => disabledDebridLinkIds.has(id)).length
|
|
||||||
},
|
|
||||||
linkSnappy: {
|
|
||||||
configured: hasText(settings.linkSnappyLogin) && hasText(settings.linkSnappyPassword)
|
|
||||||
},
|
|
||||||
disabledProviders: [...(settings.disabledProviders || [])]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function diffAccountSummary(previous: AppSettings, next: AppSettings): Record<string, unknown> {
|
|
||||||
const before = buildAccountSummary(previous);
|
|
||||||
const after = buildAccountSummary(next);
|
|
||||||
const changes: Record<string, unknown> = {};
|
|
||||||
for (const key of Object.keys(after)) {
|
|
||||||
const beforeJson = JSON.stringify(before[key]);
|
|
||||||
const afterJson = JSON.stringify(after[key]);
|
|
||||||
if (beforeJson !== afterJson) {
|
|
||||||
changes[key] = after[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildRedactedSettingsPayload(settings: AppSettings): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
paths: {
|
|
||||||
outputDir: settings.outputDir,
|
|
||||||
extractDir: settings.extractDir,
|
|
||||||
mkvLibraryDir: settings.mkvLibraryDir
|
|
||||||
},
|
|
||||||
providers: {
|
|
||||||
providerOrder: settings.providerOrder,
|
|
||||||
providerPrimary: settings.providerPrimary,
|
|
||||||
providerSecondary: settings.providerSecondary,
|
|
||||||
providerTertiary: settings.providerTertiary,
|
|
||||||
autoProviderFallback: settings.autoProviderFallback,
|
|
||||||
disabledProviders: settings.disabledProviders,
|
|
||||||
hosterRouting: settings.hosterRouting
|
|
||||||
},
|
|
||||||
extraction: {
|
|
||||||
autoExtract: settings.autoExtract,
|
|
||||||
autoExtractWhenStopped: settings.autoExtractWhenStopped,
|
|
||||||
hybridExtract: settings.hybridExtract,
|
|
||||||
createExtractSubfolder: settings.createExtractSubfolder,
|
|
||||||
cleanupMode: settings.cleanupMode,
|
|
||||||
extractConflictMode: settings.extractConflictMode,
|
|
||||||
removeLinkFilesAfterExtract: settings.removeLinkFilesAfterExtract,
|
|
||||||
removeSamplesAfterExtract: settings.removeSamplesAfterExtract,
|
|
||||||
enableIntegrityCheck: settings.enableIntegrityCheck,
|
|
||||||
archivePasswordCount: String(settings.archivePasswordList || "")
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.length,
|
|
||||||
extractCpuPriority: settings.extractCpuPriority,
|
|
||||||
maxParallelExtract: settings.maxParallelExtract
|
|
||||||
},
|
|
||||||
downloads: {
|
|
||||||
maxParallel: settings.maxParallel,
|
|
||||||
retryLimit: settings.retryLimit,
|
|
||||||
autoResumeOnStart: settings.autoResumeOnStart,
|
|
||||||
autoReconnect: settings.autoReconnect,
|
|
||||||
reconnectWaitSeconds: settings.reconnectWaitSeconds,
|
|
||||||
autoSkipExtracted: settings.autoSkipExtracted,
|
|
||||||
completedCleanupPolicy: settings.completedCleanupPolicy
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
packageName: settings.packageName,
|
|
||||||
theme: settings.theme,
|
|
||||||
collapseNewPackages: settings.collapseNewPackages,
|
|
||||||
hideExtractedItems: settings.hideExtractedItems,
|
|
||||||
confirmDeleteSelection: settings.confirmDeleteSelection,
|
|
||||||
clipboardWatch: settings.clipboardWatch,
|
|
||||||
minimizeToTray: settings.minimizeToTray,
|
|
||||||
columnOrder: settings.columnOrder
|
|
||||||
},
|
|
||||||
bandwidth: {
|
|
||||||
speedLimitEnabled: settings.speedLimitEnabled,
|
|
||||||
speedLimitKbps: settings.speedLimitKbps,
|
|
||||||
speedLimitMode: settings.speedLimitMode,
|
|
||||||
bandwidthSchedules: settings.bandwidthSchedules
|
|
||||||
},
|
|
||||||
updates: {
|
|
||||||
updateRepo: settings.updateRepo,
|
|
||||||
autoUpdateCheck: settings.autoUpdateCheck
|
|
||||||
},
|
|
||||||
statistics: {
|
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
|
|
||||||
providerDailyLimitBytes: settings.providerDailyLimitBytes,
|
|
||||||
providerDailyUsageBytes: settings.providerDailyUsageBytes,
|
|
||||||
providerTotalUsageBytes: settings.providerTotalUsageBytes,
|
|
||||||
debridLinkApiKeyDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes,
|
|
||||||
debridLinkApiKeyDailyUsageBytes: settings.debridLinkApiKeyDailyUsageBytes,
|
|
||||||
debridLinkApiKeyTotalUsageBytes: settings.debridLinkApiKeyTotalUsageBytes,
|
|
||||||
providerDailyUsageDay: settings.providerDailyUsageDay
|
|
||||||
},
|
|
||||||
accounts: buildAccountSummary(settings)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildStatsPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
session: snapshot.stats,
|
|
||||||
totals: {
|
|
||||||
totalPackages: Object.keys(snapshot.session.packages).length,
|
|
||||||
totalItems: Object.keys(snapshot.session.items).length,
|
|
||||||
speedText: snapshot.speedText,
|
|
||||||
etaText: snapshot.etaText,
|
|
||||||
canStart: snapshot.canStart,
|
|
||||||
canStop: snapshot.canStop,
|
|
||||||
canPause: snapshot.canPause
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function summarizeHistoryEntry(entry: HistoryEntry): Record<string, unknown> {
|
|
||||||
return {
|
|
||||||
id: entry.id,
|
|
||||||
name: entry.name,
|
|
||||||
status: entry.status,
|
|
||||||
provider: entry.provider,
|
|
||||||
fileCount: entry.fileCount,
|
|
||||||
totalBytes: entry.totalBytes,
|
|
||||||
downloadedBytes: entry.downloadedBytes,
|
|
||||||
durationSeconds: entry.durationSeconds,
|
|
||||||
completedAt: entry.completedAt,
|
|
||||||
outputDir: entry.outputDir,
|
|
||||||
urlCount: Array.isArray(entry.urls) ? entry.urls.length : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
shutdownAuditLog();
|
|
||||||
for (const dir of tempDirs.splice(0)) {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("audit-log", () => {
|
|
||||||
it("writes audit events to the audit log", () => {
|
|
||||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-alog-"));
|
|
||||||
tempDirs.push(baseDir);
|
|
||||||
|
|
||||||
initAuditLog(baseDir);
|
|
||||||
logAuditEvent("INFO", "Settings changed", { changedKeys: ["token", "autoExtract"] });
|
|
||||||
|
|
||||||
const logPath = getAuditLogPath();
|
|
||||||
expect(logPath).not.toBeNull();
|
|
||||||
expect(fs.existsSync(logPath!)).toBe(true);
|
|
||||||
const content = fs.readFileSync(logPath!, "utf8");
|
|
||||||
expect(content).toContain("Audit-Log Start");
|
|
||||||
expect(content).toContain("Settings changed");
|
|
||||||
expect(content).toContain("changedKeys");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -40,14 +40,11 @@ vi.mock("../src/main/windows-host-diagnostics", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
|
||||||
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
||||||
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
||||||
import { configureLogger, getLogFilePath } from "../src/main/logger";
|
import { configureLogger, getLogFilePath } from "../src/main/logger";
|
||||||
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
||||||
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
||||||
import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage";
|
|
||||||
import { getDebridLinkApiKeyIds } from "../src/shared/debrid-link-keys";
|
|
||||||
import type { DownloadManager } from "../src/main/download-manager";
|
import type { DownloadManager } from "../src/main/download-manager";
|
||||||
import type { UiSnapshot } from "../src/shared/types";
|
import type { UiSnapshot } from "../src/shared/types";
|
||||||
|
|
||||||
@ -191,47 +188,13 @@ async function createFixture() {
|
|||||||
const token = "debug-secret";
|
const token = "debug-secret";
|
||||||
const port = await getFreePort();
|
const port = await getFreePort();
|
||||||
const snapshot = buildSnapshot(baseDir);
|
const snapshot = buildSnapshot(baseDir);
|
||||||
const storagePaths = createStoragePaths(baseDir);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(baseDir, "debug_token.txt"), token, "utf8");
|
fs.writeFileSync(path.join(baseDir, "debug_token.txt"), token, "utf8");
|
||||||
fs.writeFileSync(path.join(baseDir, "debug_port.txt"), String(port), "utf8");
|
fs.writeFileSync(path.join(baseDir, "debug_port.txt"), String(port), "utf8");
|
||||||
fs.writeFileSync(path.join(baseDir, "debug_host.txt"), "0.0.0.0", "utf8");
|
fs.writeFileSync(path.join(baseDir, "debug_host.txt"), "0.0.0.0", "utf8");
|
||||||
const debridLinkApiKeys = "key-a\nkey-b";
|
|
||||||
const debridLinkKeyIds = getDebridLinkApiKeyIds(debridLinkApiKeys);
|
|
||||||
|
|
||||||
saveSettings(storagePaths, {
|
|
||||||
...snapshot.settings,
|
|
||||||
token: "rd-secret-token",
|
|
||||||
realDebridUseWebLogin: true,
|
|
||||||
debridLinkApiKeys,
|
|
||||||
debridLinkDisabledKeyIds: debridLinkKeyIds[1] ? [debridLinkKeyIds[1]] : [],
|
|
||||||
totalDownloadedAllTime: 128 * 1024 * 1024,
|
|
||||||
totalCompletedFilesAllTime: 12
|
|
||||||
});
|
|
||||||
saveHistory(storagePaths, [
|
|
||||||
{
|
|
||||||
id: "hist-1",
|
|
||||||
name: "server-package",
|
|
||||||
totalBytes: 123,
|
|
||||||
downloadedBytes: 123,
|
|
||||||
fileCount: 2,
|
|
||||||
provider: "realdebrid",
|
|
||||||
completedAt: Date.now() - 5_000,
|
|
||||||
durationSeconds: 42,
|
|
||||||
status: "completed",
|
|
||||||
outputDir: path.join(baseDir, "downloads", "server-package"),
|
|
||||||
urls: ["https://hoster.example/file-1"]
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
configureLogger(baseDir);
|
configureLogger(baseDir);
|
||||||
fs.writeFileSync(getLogFilePath(), "2026-03-09T00:00:00.000Z [INFO] MAIN-LINE\n", "utf8");
|
fs.writeFileSync(getLogFilePath(), "2026-03-09T00:00:00.000Z [INFO] MAIN-LINE\n", "utf8");
|
||||||
initAuditLog(baseDir);
|
|
||||||
const auditLogPath = getAuditLogPath();
|
|
||||||
if (!auditLogPath) {
|
|
||||||
throw new Error("audit log path missing");
|
|
||||||
}
|
|
||||||
logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" });
|
|
||||||
|
|
||||||
initSessionLog(baseDir);
|
initSessionLog(baseDir);
|
||||||
const sessionLogPath = getSessionLogPath();
|
const sessionLogPath = getSessionLogPath();
|
||||||
@ -276,8 +239,7 @@ async function createFixture() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
token,
|
token
|
||||||
baseDir
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,7 +248,6 @@ afterEach(() => {
|
|||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
shutdownAuditLog();
|
|
||||||
while (tempDirs.length > 0) {
|
while (tempDirs.length > 0) {
|
||||||
const dir = tempDirs.pop();
|
const dir = tempDirs.pop();
|
||||||
if (!dir) {
|
if (!dir) {
|
||||||
@ -314,31 +275,8 @@ describe("debug-server", () => {
|
|||||||
expect(payload.host?.recentKernelPower?.[0]?.id).toBe(41);
|
expect(payload.host?.recentKernelPower?.[0]?.id).toBe(41);
|
||||||
expect(payload.selectedPackage?.name).toBe("server-package");
|
expect(payload.selectedPackage?.name).toBe("server-package");
|
||||||
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
|
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
|
||||||
expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE");
|
|
||||||
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
|
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
|
||||||
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
||||||
expect(payload.accounts?.realDebrid?.configured).toBe(true);
|
|
||||||
expect(payload.history?.total).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("writes a machine-readable AI support manifest into the runtime folder", async () => {
|
|
||||||
const fixture = await createFixture();
|
|
||||||
const manifestPath = path.join(fixture.baseDir, "debug_ai_manifest.json");
|
|
||||||
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
||||||
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, any>;
|
|
||||||
expect(manifest.appVersion).toBeTruthy();
|
|
||||||
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
|
||||||
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
|
||||||
expect(manifest.quickstart?.[1]).toContain("server IP");
|
|
||||||
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
|
||||||
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
|
||||||
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
|
||||||
|
|
||||||
const metaResponse = await fetch(`${fixture.baseUrl}/meta?token=${fixture.token}`);
|
|
||||||
expect(metaResponse.ok).toBe(true);
|
|
||||||
const metaPayload = await metaResponse.json() as Record<string, any>;
|
|
||||||
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves package details and package log by package query", async () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
@ -378,43 +316,6 @@ describe("debug-server", () => {
|
|||||||
expect(payload.assessmentHints?.[0]).toContain("watchdog");
|
expect(payload.assessmentHints?.[0]).toContain("watchdog");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves audit log, settings, accounts, stats, and history", async () => {
|
|
||||||
const fixture = await createFixture();
|
|
||||||
|
|
||||||
const auditResponse = await fetch(`${fixture.baseUrl}/logs/audit?token=${fixture.token}&lines=20`);
|
|
||||||
expect(auditResponse.ok).toBe(true);
|
|
||||||
const auditPayload = await auditResponse.json() as Record<string, any>;
|
|
||||||
expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE");
|
|
||||||
|
|
||||||
const settingsResponse = await fetch(`${fixture.baseUrl}/settings?token=${fixture.token}`);
|
|
||||||
expect(settingsResponse.ok).toBe(true);
|
|
||||||
const settingsPayload = await settingsResponse.json() as Record<string, any>;
|
|
||||||
expect(settingsPayload.accounts?.realDebrid?.configured).toBe(true);
|
|
||||||
expect(settingsPayload.extraction?.archivePasswordCount).toBe(0);
|
|
||||||
expect(JSON.stringify(settingsPayload)).not.toContain("rd-secret-token");
|
|
||||||
expect(JSON.stringify(settingsPayload)).not.toContain("key-a");
|
|
||||||
expect(JSON.stringify(settingsPayload)).not.toContain("key-b");
|
|
||||||
|
|
||||||
const accountsResponse = await fetch(`${fixture.baseUrl}/accounts?token=${fixture.token}`);
|
|
||||||
expect(accountsResponse.ok).toBe(true);
|
|
||||||
const accountsPayload = await accountsResponse.json() as Record<string, any>;
|
|
||||||
expect(accountsPayload.debridLink?.keyCount).toBe(2);
|
|
||||||
expect(accountsPayload.debridLink?.disabledKeyCount).toBe(1);
|
|
||||||
|
|
||||||
const statsResponse = await fetch(`${fixture.baseUrl}/stats?token=${fixture.token}`);
|
|
||||||
expect(statsResponse.ok).toBe(true);
|
|
||||||
const statsPayload = await statsResponse.json() as Record<string, any>;
|
|
||||||
expect(statsPayload.session?.totalDownloaded).toBeGreaterThan(0);
|
|
||||||
expect(statsPayload.allTime?.totalDownloadedAllTime).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const historyResponse = await fetch(`${fixture.baseUrl}/history?token=${fixture.token}&limit=10`);
|
|
||||||
expect(historyResponse.ok).toBe(true);
|
|
||||||
const historyPayload = await historyResponse.json() as Record<string, any>;
|
|
||||||
expect(historyPayload.total).toBe(1);
|
|
||||||
expect(historyPayload.entries?.[0]?.name).toBe("server-package");
|
|
||||||
expect(historyPayload.entries?.[0]?.urlCount).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects unauthenticated requests", async () => {
|
it("rejects unauthenticated requests", async () => {
|
||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
const response = await fetch(`${fixture.baseUrl}/status`);
|
const response = await fetch(`${fixture.baseUrl}/status`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user