final dev

This commit is contained in:
2026-03-11 13:45:24 +00:00
parent 6f6d4f1e58
commit 0f0cfffa7a
2 changed files with 267 additions and 4 deletions

View File

@@ -3463,16 +3463,31 @@ class PipelineService extends EventEmitter {
pushCandidate(path.join(baseDir, variantFolderName)); pushCandidate(path.join(baseDir, variantFolderName));
} }
} }
const existingDirectories = [];
for (const candidate of candidates) { for (const candidate of candidates) {
try { try {
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) { if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
existingDirectories.push(candidate);
}
} catch (_error) {
// ignore fs errors
}
}
if (existingDirectories.length === 0) {
return null;
}
for (const candidate of existingDirectories) {
try {
if (hasBluRayBackupStructure(candidate) || findPreferredRawInput(candidate)) {
return candidate; return candidate;
} }
} catch (_error) { } catch (_error) {
// ignore fs errors // ignore fs errors
} }
} }
return null;
return existingDirectories[0];
} }
async migrateRawFolderNamingOnStartup(db) { async migrateRawFolderNamingOnStartup(db) {
@@ -9113,10 +9128,23 @@ class PipelineService extends EventEmitter {
|| findPreferredRawInput(resolvedReviewRawPath) || findPreferredRawInput(resolvedReviewRawPath)
); );
if (!hasRawInput) { if (!hasRawInput) {
let hasAnyRawEntries = false;
try {
hasAnyRawEntries = fs.readdirSync(resolvedReviewRawPath).length > 0;
} catch (_error) {
hasAnyRawEntries = false;
}
if (!hasAnyRawEntries) {
const error = new Error('Review-Neustart nicht möglich: keine Mediendateien im RAW-Pfad gefunden. Disc muss zuerst gerippt werden.'); const error = new Error('Review-Neustart nicht möglich: keine Mediendateien im RAW-Pfad gefunden. Disc muss zuerst gerippt werden.');
error.statusCode = 400; error.statusCode = 400;
throw error; throw error;
} }
await historyService.appendLog(
jobId,
'SYSTEM',
`Review-Neustart: keine direkten Mediendateien erkannt, versuche Analyse trotzdem mit RAW-Pfad ${resolvedReviewRawPath}.`
);
}
const currentStatus = String(sourceJob.status || '').trim().toUpperCase(); const currentStatus = String(sourceJob.status || '').trim().toUpperCase();
if (['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(currentStatus)) { if (['ANALYZING', 'RIPPING', 'MEDIAINFO_CHECK', 'ENCODING'].includes(currentStatus)) {

View File

@@ -0,0 +1,235 @@
# Runtime Activities API
Ripster verfolgt alle laufenden und kürzlich abgeschlossenen Aktivitäten (Skripte, Skript-Ketten, Cron-Jobs, interne Tasks) in Echtzeit über den `RuntimeActivityService`.
---
## Übersicht
Aktivitäten entstehen, wenn Ripster intern Aktionen ausführt z. B. beim Start eines Cron-Jobs, beim Ausführen einer Skript-Kette oder beim Durchlaufen von Pipeline-Schritten. Sie sind **nicht persistent** (kein DB-Speicher) und werden nur im Arbeitsspeicher gehalten.
- **Aktive Aktivitäten** (`active`): Laufen gerade.
- **Letzte Aktivitäten** (`recent`): Abgeschlossen, max. 120 Einträge.
Änderungen werden über WebSocket (`RUNTIME_ACTIVITY_CHANGED`) in Echtzeit gesendet.
---
## Aktivitäts-Objekt
```json
{
"id": 7,
"type": "chain",
"name": "Post-Encode Aufräumen",
"status": "running",
"source": "cron",
"message": "Schritt 2 von 3",
"currentStep": "cleanup.sh",
"currentStepType": "script",
"currentScriptName": "cleanup.sh",
"stepIndex": 2,
"stepTotal": 3,
"parentActivityId": null,
"jobId": 42,
"cronJobId": 3,
"chainId": 5,
"scriptId": null,
"canCancel": true,
"canNextStep": false,
"outcome": "running",
"errorMessage": null,
"output": null,
"stdout": null,
"stderr": null,
"stdoutTruncated": false,
"stderrTruncated": false,
"startedAt": "2026-03-10T10:00:00.000Z",
"finishedAt": null,
"durationMs": null,
"exitCode": null,
"success": null
}
```
### Felder
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | `number` | Eindeutige ID (Laufzähler, nicht persistent) |
| `type` | `string` | Art der Aktivität: `script` \| `chain` \| `cron` \| `task` |
| `name` | `string \| null` | Anzeigename der Aktivität |
| `status` | `string` | Aktueller Status: `running` \| `success` \| `error` |
| `source` | `string \| null` | Auslöser (z. B. `cron`, `pipeline`, `manual`) |
| `message` | `string \| null` | Kurztext zum aktuellen Zustand |
| `currentStep` | `string \| null` | Name des aktuell ausgeführten Schritts |
| `currentStepType` | `string \| null` | Typ des Schritts (z. B. `script`, `wait`) |
| `currentScriptName` | `string \| null` | Name des Skripts im aktuellen Schritt |
| `stepIndex` | `number \| null` | Aktueller Schritt (1-basiert) |
| `stepTotal` | `number \| null` | Gesamtanzahl Schritte |
| `parentActivityId` | `number \| null` | ID der übergeordneten Aktivität |
| `jobId` | `number \| null` | Verknüpfte Job-ID |
| `cronJobId` | `number \| null` | Verknüpfte Cron-Job-ID |
| `chainId` | `number \| null` | Verknüpfte Skript-Ketten-ID |
| `scriptId` | `number \| null` | Verknüpfte Skript-ID |
| `canCancel` | `boolean` | Abbrechen über API möglich |
| `canNextStep` | `boolean` | Nächster Schritt über API auslösbar |
| `outcome` | `string \| null` | Abschluss-Ergebnis: `success` \| `error` \| `cancelled` \| `skipped` \| `running` |
| `errorMessage` | `string \| null` | Fehlermeldung (max. 2.000 Zeichen) |
| `output` | `string \| null` | Allgemeine Ausgabe (max. 12.000 Zeichen) |
| `stdout` | `string \| null` | Standardausgabe des Prozesses (max. 12.000 Zeichen) |
| `stderr` | `string \| null` | Fehlerausgabe des Prozesses (max. 12.000 Zeichen) |
| `stdoutTruncated` | `boolean` | `true`, wenn `stdout` gekürzt wurde |
| `stderrTruncated` | `boolean` | `true`, wenn `stderr` gekürzt wurde |
| `startedAt` | `string` | ISO-8601-Zeitstempel des Starts |
| `finishedAt` | `string \| null` | ISO-8601-Zeitstempel des Endes |
| `durationMs` | `number \| null` | Laufzeit in Millisekunden |
| `exitCode` | `number \| null` | Exit-Code des Prozesses |
| `success` | `boolean \| null` | Erfolgsstatus (`null` bei laufender Aktivität) |
---
## Snapshot-Objekt
Alle Aktivitäts-Endpunkte geben einen Snapshot zurück:
```json
{
"active": [ /* laufende Aktivitäten, nach startedAt absteigend */ ],
"recent": [ /* abgeschlossene Aktivitäten, nach finishedAt absteigend, max. 120 */ ],
"updatedAt": "2026-03-10T10:05:00.000Z"
}
```
---
## Endpunkte
### GET `/api/activities`
Aktuellen Aktivitäts-Snapshot abrufen.
**Antwort:**
```json
{
"active": [],
"recent": [
{
"id": 5,
"type": "script",
"name": "notify.sh",
"status": "success",
"outcome": "success",
"startedAt": "2026-03-10T09:58:00.000Z",
"finishedAt": "2026-03-10T09:58:02.000Z",
"durationMs": 2100,
"exitCode": 0,
"success": true,
"canCancel": false,
"canNextStep": false
}
],
"updatedAt": "2026-03-10T10:05:00.000Z"
}
```
---
### POST `/api/activities/:id/cancel`
Aktive Aktivität abbrechen (nur wenn `canCancel: true`).
**Parameter:**
| Name | In | Typ | Beschreibung |
|------|----|-----|--------------|
| `id` | path | `number` | Aktivitäts-ID |
| `reason` | body | `string` | Optionaler Abbruchgrund |
**Request Body:**
```json
{ "reason": "Manueller Abbruch durch Benutzer" }
```
**Antwort (Erfolg):**
```json
{
"ok": true,
"action": null,
"snapshot": { "active": [], "recent": [], "updatedAt": "..." }
}
```
**Fehlercodes:**
| HTTP | Bedeutung |
|------|-----------|
| `404` | Aktivität nicht gefunden oder bereits abgeschlossen |
| `409` | Abbrechen wird von dieser Aktivität nicht unterstützt |
---
### POST `/api/activities/:id/next-step`
Nächsten Schritt einer Aktivität auslösen (nur wenn `canNextStep: true`).
**Parameter:**
| Name | In | Typ | Beschreibung |
|------|----|-----|--------------|
| `id` | path | `number` | Aktivitäts-ID |
**Antwort (Erfolg):**
```json
{
"ok": true,
"action": null,
"snapshot": { "active": [], "recent": [], "updatedAt": "..." }
}
```
**Fehlercodes:**
| HTTP | Bedeutung |
|------|-----------|
| `404` | Aktivität nicht gefunden |
| `409` | Nächster Schritt wird von dieser Aktivität nicht unterstützt |
---
### POST `/api/activities/clear-recent`
Alle abgeschlossenen Aktivitäten aus `recent` löschen.
**Antwort:**
```json
{
"ok": true,
"removed": 14,
"snapshot": { "active": [], "recent": [], "updatedAt": "..." }
}
```
---
## Grenzwerte
| Wert | Limit |
|------|-------|
| Maximale `recent`-Einträge | 120 |
| Maximale Länge `stdout` / `stderr` / `output` | 12.000 Zeichen |
| Maximale Länge `errorMessage` / `message` | 2.000 Zeichen |
| Maximale Länge `outcome` | 40 Zeichen |
Gekürzte Ausgaben erhalten den Suffix ` ...[gekürzt]` (bei Inline-Text) bzw. `\n...[gekürzt]` (bei mehrzeiligem Output).
---
## Echtzeit-Updates
Änderungen werden automatisch als [`RUNTIME_ACTIVITY_CHANGED`](websocket.md#runtime_activity_changed) WebSocket-Event gesendet. Die Frontend-Komponente braucht `GET /api/activities` nur beim initialen Laden aufzurufen.