Freigabe → main: Mobile UI, Webhook, IMAP, Zahlung-Workflow und weitere Features #5
@@ -0,0 +1,100 @@
|
|||||||
|
// PaperlessProcessorService zieht über die Postprocessing-Kette das ESM-Paket
|
||||||
|
// "webdav" nach, das Jest nicht transformiert. Für diesen Unit-Test ersetzen wir
|
||||||
|
// das Modul durch eine Dummy-Klasse – der Service erhält seine Abhängigkeiten
|
||||||
|
// ohnehin als Mocks injiziert.
|
||||||
|
jest.mock('../paperless/paperless-processor.service', () => ({
|
||||||
|
PaperlessProcessorService: class {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { WebhookQueueService } from './webhook-queue.service';
|
||||||
|
|
||||||
|
describe('WebhookQueueService', () => {
|
||||||
|
let processor: { processDocumentById: jest.Mock };
|
||||||
|
let settingRepo: {
|
||||||
|
findOneBy: jest.Mock;
|
||||||
|
create: jest.Mock;
|
||||||
|
save: jest.Mock;
|
||||||
|
};
|
||||||
|
let service: WebhookQueueService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
processor = {
|
||||||
|
processDocumentById: jest.fn().mockResolvedValue({ processed: true }),
|
||||||
|
};
|
||||||
|
settingRepo = {
|
||||||
|
findOneBy: jest.fn().mockResolvedValue(null),
|
||||||
|
create: jest.fn((x: unknown) => x),
|
||||||
|
save: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
service = new WebhookQueueService(processor as never, settingRepo as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reiht jede ID nur einmal ein (Dedup)', () => {
|
||||||
|
service.enqueue(5);
|
||||||
|
service.enqueue(5);
|
||||||
|
expect(service.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verarbeitet alle eingereihten IDs und leert die Warteschlange', async () => {
|
||||||
|
service.enqueue(1);
|
||||||
|
service.enqueue(2);
|
||||||
|
|
||||||
|
await service.processQueue();
|
||||||
|
|
||||||
|
expect(processor.processDocumentById).toHaveBeenCalledWith(1);
|
||||||
|
expect(processor.processDocumentById).toHaveBeenCalledWith(2);
|
||||||
|
expect(processor.processDocumentById).toHaveBeenCalledTimes(2);
|
||||||
|
expect(service.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('entfernt die ID vor Verarbeitungsbeginn aus der Liste', async () => {
|
||||||
|
let sizeWhileProcessing = -1;
|
||||||
|
processor.processDocumentById.mockImplementation(() => {
|
||||||
|
sizeWhileProcessing = service.size;
|
||||||
|
return Promise.resolve({ processed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
service.enqueue(42);
|
||||||
|
await service.processQueue();
|
||||||
|
|
||||||
|
// Beim Verarbeitungsstart war die einzige ID bereits entfernt.
|
||||||
|
expect(sizeWhileProcessing).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('arbeitet eine während der Verarbeitung erneut eingereihte ID erneut ab', async () => {
|
||||||
|
let firstRun = true;
|
||||||
|
processor.processDocumentById.mockImplementation((id: number) => {
|
||||||
|
// Beim ersten Lauf feuert der Webhook erneut, während verarbeitet wird.
|
||||||
|
if (firstRun && id === 7) {
|
||||||
|
firstRun = false;
|
||||||
|
service.enqueue(7);
|
||||||
|
}
|
||||||
|
return Promise.resolve({ processed: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
service.enqueue(7);
|
||||||
|
await service.processQueue();
|
||||||
|
|
||||||
|
expect(processor.processDocumentById).toHaveBeenCalledTimes(2);
|
||||||
|
expect(service.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('startet keinen zweiten Durchlauf parallel (isProcessing-Guard)', async () => {
|
||||||
|
let resolveFirst: (() => void) | undefined;
|
||||||
|
processor.processDocumentById.mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise<{ processed: boolean }>((res) => {
|
||||||
|
resolveFirst = () => res({ processed: true });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
service.enqueue(1);
|
||||||
|
const firstRun = service.processQueue(); // blockiert in processDocumentById
|
||||||
|
await service.processQueue(); // muss sofort zurückkehren
|
||||||
|
|
||||||
|
expect(processor.processDocumentById).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
resolveFirst?.();
|
||||||
|
await firstRun;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
|
||||||
|
// Tag (Schlüssel) des Settings-Eintrags, der den letzten Webhook-Aufruf festhält.
|
||||||
|
const LAST_WEBHOOK_CALL_TAG = 'last_webhook_call';
|
||||||
|
|
||||||
|
// Prüfintervall der Warteschlange (sehr kurz). Über ENV überschreibbar.
|
||||||
|
const QUEUE_INTERVAL_MS = Number(process.env.WEBHOOK_QUEUE_INTERVAL_MS) || 1000;
|
||||||
|
|
||||||
|
interface WebhookStatusInfo {
|
||||||
|
documentId: number | null;
|
||||||
|
action?: string;
|
||||||
|
status: 'queued' | 'processed' | 'error' | 'bad-request';
|
||||||
|
reason?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entkoppelt den Webhook-Empfang von der Verarbeitung: Eingehende Dokument-IDs
|
||||||
|
* werden in eine deduplizierte Warteschlange gelegt und von einem separaten
|
||||||
|
* Intervall-Prozess **sequenziell** (ohne Überschneidung) abgearbeitet. So führt
|
||||||
|
* mehrfaches schnelles Speichern desselben Dokuments nicht zu parallelen Läufen.
|
||||||
|
*
|
||||||
|
* Verhalten:
|
||||||
|
* - Jede ID kommt nur **einmal** in der Liste vor (Dedup).
|
||||||
|
* - Beim Verarbeitungsstart wird die ID **sofort** aus der Liste entfernt; ein
|
||||||
|
* erneutes Feuern während der Verarbeitung reiht sie wieder ein (ein weiterer
|
||||||
|
* Lauf folgt danach).
|
||||||
|
* - Es läuft immer nur eine Verarbeitung gleichzeitig.
|
||||||
|
*
|
||||||
|
* Hinweis: Die Warteschlange liegt im Speicher; bei einem Neustart gehen noch
|
||||||
|
* nicht verarbeitete IDs verloren (Paperless müsste den Webhook erneut feuern).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WebhookQueueService {
|
||||||
|
private readonly logger = new Logger(WebhookQueueService.name);
|
||||||
|
private readonly pending = new Set<number>();
|
||||||
|
private isProcessing = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly paperlessProcessor: PaperlessProcessorService,
|
||||||
|
@InjectRepository(Setting)
|
||||||
|
private readonly settingRepo: Repository<Setting>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Anzahl der aktuell wartenden Dokument-IDs. */
|
||||||
|
get size(): number {
|
||||||
|
return this.pending.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reiht eine Dokument-ID zur Verarbeitung ein. Ist die ID bereits in der
|
||||||
|
* Warteschlange, wird sie nicht erneut hinzugefügt (Dedup).
|
||||||
|
*/
|
||||||
|
enqueue(documentId: number, action?: string): void {
|
||||||
|
if (this.pending.has(documentId)) {
|
||||||
|
this.logger.log(
|
||||||
|
`Dokument ${documentId} ist bereits in der Warteschlange – nicht erneut hinzugefügt.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.pending.add(documentId);
|
||||||
|
this.logger.log(
|
||||||
|
`Dokument ${documentId} eingereiht (Warteschlange: ${this.pending.size}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
void this.recordStatus({ documentId, action, status: 'queued' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft in kurzen Abständen die Warteschlange und arbeitet vorhandene IDs
|
||||||
|
* nacheinander ab. Ein bereits laufender Durchlauf wird nicht doppelt
|
||||||
|
* gestartet (kein paralleles Verarbeiten).
|
||||||
|
*/
|
||||||
|
@Interval(QUEUE_INTERVAL_MS)
|
||||||
|
async processQueue(): Promise<void> {
|
||||||
|
if (this.isProcessing || this.pending.size === 0) return;
|
||||||
|
this.isProcessing = true;
|
||||||
|
try {
|
||||||
|
// Solange Einträge vorhanden sind, einzeln und sequenziell abarbeiten.
|
||||||
|
while (this.pending.size > 0) {
|
||||||
|
// Nächste ID entnehmen und SOFORT (vor Verarbeitungsstart) entfernen.
|
||||||
|
let documentId: number | undefined;
|
||||||
|
for (const id of this.pending) {
|
||||||
|
documentId = id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (documentId === undefined) break;
|
||||||
|
this.pending.delete(documentId);
|
||||||
|
await this.handleDocument(documentId);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleDocument(documentId: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.paperlessProcessor.processDocumentById(documentId);
|
||||||
|
this.logger.log(`Dokument ${documentId} aus Warteschlange verarbeitet.`);
|
||||||
|
await this.recordStatus({ documentId, status: 'processed' });
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
this.logger.error(
|
||||||
|
`Fehler bei der Verarbeitung von Dokument ${documentId}: ${message}`,
|
||||||
|
);
|
||||||
|
await this.recordStatus({ documentId, status: 'error', message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den letzten festgehaltenen Webhook-Status sowie die aktuelle
|
||||||
|
* Warteschlangen-Größe.
|
||||||
|
*/
|
||||||
|
async getStatus(): Promise<{ lastCall: unknown; queueSize: number }> {
|
||||||
|
const setting = await this.settingRepo.findOneBy({
|
||||||
|
Tag: LAST_WEBHOOK_CALL_TAG,
|
||||||
|
});
|
||||||
|
let lastCall: unknown = null;
|
||||||
|
if (setting?.Wert) {
|
||||||
|
try {
|
||||||
|
lastCall = JSON.parse(setting.Wert);
|
||||||
|
} catch {
|
||||||
|
lastCall = { raw: setting.Wert };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { lastCall, queueSize: this.pending.size };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hält den letzten Webhook-Vorgang in der Settings-Tabelle fest
|
||||||
|
* (Tag "last_webhook_call"). Fehler beim Speichern werden nur geloggt.
|
||||||
|
*/
|
||||||
|
async recordStatus(info: WebhookStatusInfo): Promise<void> {
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify({
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
documentId: info.documentId,
|
||||||
|
action: info.action ?? null,
|
||||||
|
status: info.status,
|
||||||
|
...(info.reason ? { reason: info.reason } : {}),
|
||||||
|
...(info.message ? { message: info.message.slice(0, 100) } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let setting = await this.settingRepo.findOneBy({
|
||||||
|
Tag: LAST_WEBHOOK_CALL_TAG,
|
||||||
|
});
|
||||||
|
if (!setting) {
|
||||||
|
setting = this.settingRepo.create({
|
||||||
|
Typ: 0,
|
||||||
|
Tag: LAST_WEBHOOK_CALL_TAG,
|
||||||
|
Wert: value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setting.Wert = value;
|
||||||
|
}
|
||||||
|
await this.settingRepo.save(setting);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
'Konnte Webhook-Status nicht in den Settings speichern',
|
||||||
|
err instanceof Error ? err.stack : String(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,8 @@ import {
|
|||||||
UseGuards,
|
UseGuards,
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { ApiKeyGuard } from '../auth/api-key.guard';
|
import { ApiKeyGuard } from '../auth/api-key.guard';
|
||||||
import { PaperlessProcessorService } from '../paperless/paperless-processor.service';
|
import { WebhookQueueService } from './webhook-queue.service';
|
||||||
import { Setting } from '../database/entities/setting.entity';
|
|
||||||
|
|
||||||
export interface PaperlessWebhookPayload {
|
export interface PaperlessWebhookPayload {
|
||||||
doc_url?: string;
|
doc_url?: string;
|
||||||
@@ -22,18 +19,11 @@ export interface PaperlessWebhookPayload {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag (Schlüssel) des Settings-Eintrags, der den letzten Webhook-Aufruf festhält.
|
|
||||||
const LAST_WEBHOOK_CALL_TAG = 'last_webhook_call';
|
|
||||||
|
|
||||||
@Controller('api/webhook')
|
@Controller('api/webhook')
|
||||||
export class WebhookController {
|
export class WebhookController {
|
||||||
private readonly logger = new Logger(WebhookController.name);
|
private readonly logger = new Logger(WebhookController.name);
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly webhookQueue: WebhookQueueService) {}
|
||||||
private readonly paperlessProcessor: PaperlessProcessorService,
|
|
||||||
@InjectRepository(Setting)
|
|
||||||
private readonly settingRepo: Repository<Setting>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@UseGuards(ApiKeyGuard)
|
@UseGuards(ApiKeyGuard)
|
||||||
@Post('paperless')
|
@Post('paperless')
|
||||||
@@ -44,7 +34,7 @@ export class WebhookController {
|
|||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
`Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`,
|
||||||
);
|
);
|
||||||
await this.recordWebhookCall({
|
await this.webhookQueue.recordStatus({
|
||||||
documentId: null,
|
documentId: null,
|
||||||
action: payload.action,
|
action: payload.action,
|
||||||
status: 'bad-request',
|
status: 'bad-request',
|
||||||
@@ -55,54 +45,22 @@ export class WebhookController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Webhook: action=${payload.action}, document=${documentId}`,
|
`Webhook: action=${payload.action}, document=${documentId} → eingereiht`,
|
||||||
);
|
);
|
||||||
|
// Sofort einreihen und antworten; die eigentliche Verarbeitung übernimmt
|
||||||
try {
|
// der separate Queue-Prozess (sequenziell, dedupliziert).
|
||||||
const result =
|
this.webhookQueue.enqueue(documentId, payload.action);
|
||||||
await this.paperlessProcessor.processDocumentById(documentId);
|
return { status: 'queued', documentId };
|
||||||
const status = result.processed ? 'processed' : 'skipped';
|
|
||||||
await this.recordWebhookCall({
|
|
||||||
documentId,
|
|
||||||
action: payload.action,
|
|
||||||
status,
|
|
||||||
reason: result.reason,
|
|
||||||
});
|
|
||||||
return { status, reason: result.reason };
|
|
||||||
} catch (err) {
|
|
||||||
// Fehler tolerieren (wie der bisherige Cron) und 200 zurückgeben,
|
|
||||||
// damit Paperless keine Retry-Schleife startet.
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
this.logger.error(
|
|
||||||
`Fehler bei Webhook-Verarbeitung von Dokument ${documentId}: ${message}`,
|
|
||||||
);
|
|
||||||
await this.recordWebhookCall({
|
|
||||||
documentId,
|
|
||||||
action: payload.action,
|
|
||||||
status: 'error',
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
return { status: 'error', message };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert Informationen zum letzten Webhook-Aufruf (Zeitpunkt, Dokument,
|
* Liefert Informationen zum letzten Webhook-Vorgang (Zeitpunkt, Dokument,
|
||||||
* Ergebnis). Über die globalen Guards per JWT oder API-Key zugänglich.
|
* Ergebnis) und die aktuelle Warteschlangen-Größe. Über die globalen Guards
|
||||||
|
* per JWT oder API-Key zugänglich.
|
||||||
*/
|
*/
|
||||||
@Get('status')
|
@Get('status')
|
||||||
async getWebhookStatus(): Promise<{ lastCall: unknown }> {
|
async getWebhookStatus(): Promise<{ lastCall: unknown; queueSize: number }> {
|
||||||
const setting = await this.settingRepo.findOneBy({
|
return this.webhookQueue.getStatus();
|
||||||
Tag: LAST_WEBHOOK_CALL_TAG,
|
|
||||||
});
|
|
||||||
if (!setting?.Wert) {
|
|
||||||
return { lastCall: null };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return { lastCall: JSON.parse(setting.Wert) };
|
|
||||||
} catch {
|
|
||||||
return { lastCall: { raw: setting.Wert } };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,47 +82,4 @@ export class WebhookController {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hält den letzten Webhook-Aufruf in der Settings-Tabelle fest
|
|
||||||
* (Tag "last_webhook_call"). Fehler beim Speichern werden nur geloggt und
|
|
||||||
* beeinflussen die Webhook-Antwort nicht.
|
|
||||||
*/
|
|
||||||
private async recordWebhookCall(info: {
|
|
||||||
documentId: number | null;
|
|
||||||
action?: string;
|
|
||||||
status: string;
|
|
||||||
reason?: string;
|
|
||||||
message?: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
try {
|
|
||||||
const value = JSON.stringify({
|
|
||||||
at: new Date().toISOString(),
|
|
||||||
documentId: info.documentId,
|
|
||||||
action: info.action ?? null,
|
|
||||||
status: info.status,
|
|
||||||
...(info.reason ? { reason: info.reason } : {}),
|
|
||||||
...(info.message ? { message: info.message.slice(0, 100) } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let setting = await this.settingRepo.findOneBy({
|
|
||||||
Tag: LAST_WEBHOOK_CALL_TAG,
|
|
||||||
});
|
|
||||||
if (!setting) {
|
|
||||||
setting = this.settingRepo.create({
|
|
||||||
Typ: 0,
|
|
||||||
Tag: LAST_WEBHOOK_CALL_TAG,
|
|
||||||
Wert: value,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setting.Wert = value;
|
|
||||||
}
|
|
||||||
await this.settingRepo.save(setting);
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(
|
|
||||||
'Konnte letzten Webhook-Aufruf nicht in den Settings speichern',
|
|
||||||
err instanceof Error ? err.stack : String(err),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { WebhookController } from './webhook.controller';
|
import { WebhookController } from './webhook.controller';
|
||||||
|
import { WebhookQueueService } from './webhook-queue.service';
|
||||||
import { PaperlessModule } from '../paperless/paperless.module';
|
import { PaperlessModule } from '../paperless/paperless.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { Setting } from '../database/entities/setting.entity';
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
@@ -8,5 +9,6 @@ import { Setting } from '../database/entities/setting.entity';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Setting]), PaperlessModule, AuthModule],
|
imports: [TypeOrmModule.forFeature([Setting]), PaperlessModule, AuthModule],
|
||||||
controllers: [WebhookController],
|
controllers: [WebhookController],
|
||||||
|
providers: [WebhookQueueService],
|
||||||
})
|
})
|
||||||
export class WebhookModule {}
|
export class WebhookModule {}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ export interface WebhookLastCall {
|
|||||||
at: string; // ISO-Zeitstempel
|
at: string; // ISO-Zeitstempel
|
||||||
documentId: number | null;
|
documentId: number | null;
|
||||||
action: string | null;
|
action: string | null;
|
||||||
status: 'processed' | 'skipped' | 'error' | 'bad-request' | string;
|
status: 'queued' | 'processed' | 'error' | 'bad-request' | string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebhookStatus {
|
export interface WebhookStatus {
|
||||||
lastCall: WebhookLastCall | null;
|
lastCall: WebhookLastCall | null;
|
||||||
|
queueSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const webhookApi = {
|
export const webhookApi = {
|
||||||
|
|||||||
@@ -2774,8 +2774,8 @@ function WebhookStatusTab() {
|
|||||||
|
|
||||||
const renderStatusBadge = (s: string) => {
|
const renderStatusBadge = (s: string) => {
|
||||||
switch (s) {
|
switch (s) {
|
||||||
|
case 'queued': return <Badge status="processing" text="In Warteschlange" />;
|
||||||
case 'processed': return <Badge status="success" text="Verarbeitet" />;
|
case 'processed': return <Badge status="success" text="Verarbeitet" />;
|
||||||
case 'skipped': return <Badge status="warning" text="Übersprungen" />;
|
|
||||||
case 'error': return <Badge status="error" text="Fehler" />;
|
case 'error': return <Badge status="error" text="Fehler" />;
|
||||||
case 'bad-request': return <Badge status="error" text="Ungültige Anfrage" />;
|
case 'bad-request': return <Badge status="error" text="Ungültige Anfrage" />;
|
||||||
default: return <Badge status="default" text={s} />;
|
default: return <Badge status="default" text={s} />;
|
||||||
@@ -2783,6 +2783,7 @@ function WebhookStatusTab() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const lastCall = status?.lastCall ?? null;
|
const lastCall = status?.lastCall ?? null;
|
||||||
|
const queueSize = status?.queueSize ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -2791,13 +2792,19 @@ function WebhookStatusTab() {
|
|||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
Paperless-NGX ruft nach dem Bearbeiten eines Dokuments den Webhook{' '}
|
Paperless-NGX ruft nach dem Bearbeiten eines Dokuments den Webhook{' '}
|
||||||
<Typography.Text code>/api/webhook/paperless</Typography.Text> auf
|
<Typography.Text code>/api/webhook/paperless</Typography.Text> auf
|
||||||
(per API-Key authentifiziert). Hier siehst du den zuletzt
|
(per API-Key authentifiziert). Die ID wird in eine Warteschlange
|
||||||
verarbeiteten Aufruf. Eine vollständige Historie steht in den
|
gelegt und von einem separaten Prozess nacheinander verarbeitet.
|
||||||
Backend-Logs.
|
Hier siehst du den zuletzt festgehaltenen Vorgang. Eine vollständige
|
||||||
|
Historie steht in den Backend-Logs.
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<Button icon={<ReloadOutlined />} loading={loading} onClick={load}>
|
<Space>
|
||||||
Aktualisieren
|
<Button icon={<ReloadOutlined />} loading={loading} onClick={load}>
|
||||||
</Button>
|
Aktualisieren
|
||||||
|
</Button>
|
||||||
|
<Tag color={queueSize > 0 ? 'processing' : 'default'}>
|
||||||
|
Warteschlange: {queueSize}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card size="small" title="Letzter Webhook-Aufruf" loading={loading}>
|
<Card size="small" title="Letzter Webhook-Aufruf" loading={loading}>
|
||||||
|
|||||||
Reference in New Issue
Block a user