final dev
This commit is contained in:
@@ -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,9 +9128,22 @@ class PipelineService extends EventEmitter {
|
|||||||
|| findPreferredRawInput(resolvedReviewRawPath)
|
|| findPreferredRawInput(resolvedReviewRawPath)
|
||||||
);
|
);
|
||||||
if (!hasRawInput) {
|
if (!hasRawInput) {
|
||||||
const error = new Error('Review-Neustart nicht möglich: keine Mediendateien im RAW-Pfad gefunden. Disc muss zuerst gerippt werden.');
|
let hasAnyRawEntries = false;
|
||||||
error.statusCode = 400;
|
try {
|
||||||
throw error;
|
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.');
|
||||||
|
error.statusCode = 400;
|
||||||
|
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();
|
||||||
|
|||||||
235
docs/api/runtime-activities.md
Normal file
235
docs/api/runtime-activities.md
Normal 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.
|
||||||
Reference in New Issue
Block a user