From 32d9e8498567c838bf94ae7223203c004116fd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Mon, 29 Jun 2026 15:57:36 +0200 Subject: [PATCH] feat: letzten Paperless-Webhook-Aufruf in Settings festhalten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pro Webhook-Aufruf wird ein Status in der settings-Tabelle gepflegt (Tag "last_webhook_call", JSON mit Zeitpunkt, Dokument, action, Ergebnis). Neuer Endpunkt GET /api/webhook/status liefert den letzten Aufruf zum Auslesen (JWT oder API-Key). Persistent über Neustarts, anders als die Container-Logs. Co-Authored-By: Claude Opus 4.8 --- .../src/webhook/webhook.controller.ts | 96 ++++++++++++++++++- .../src/webhook/webhook.module.ts | 4 +- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/paperless-backend/src/webhook/webhook.controller.ts b/paperless-backend/src/webhook/webhook.controller.ts index 0efe857..4630a2e 100644 --- a/paperless-backend/src/webhook/webhook.controller.ts +++ b/paperless-backend/src/webhook/webhook.controller.ts @@ -1,6 +1,7 @@ import { Controller, Post, + Get, Body, Logger, HttpCode, @@ -8,8 +9,11 @@ import { UseGuards, BadRequestException, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { ApiKeyGuard } from '../auth/api-key.guard'; import { PaperlessProcessorService } from '../paperless/paperless-processor.service'; +import { Setting } from '../database/entities/setting.entity'; export interface PaperlessWebhookPayload { doc_url?: string; @@ -18,11 +22,18 @@ export interface PaperlessWebhookPayload { [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') export class WebhookController { private readonly logger = new Logger(WebhookController.name); - constructor(private readonly paperlessProcessor: PaperlessProcessorService) {} + constructor( + private readonly paperlessProcessor: PaperlessProcessorService, + @InjectRepository(Setting) + private readonly settingRepo: Repository, + ) {} @UseGuards(ApiKeyGuard) @Post('paperless') @@ -33,6 +44,11 @@ export class WebhookController { this.logger.warn( `Webhook ohne ermittelbare Dokument-ID: ${JSON.stringify(payload)}`, ); + await this.recordWebhookCall({ + documentId: null, + action: payload.action, + status: 'bad-request', + }); throw new BadRequestException( 'Keine Dokument-ID aus doc_url/document_id ermittelbar', ); @@ -45,10 +61,14 @@ export class WebhookController { try { const result = await this.paperlessProcessor.processDocumentById(documentId); - return { - status: result.processed ? 'processed' : 'skipped', + 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. @@ -56,10 +76,35 @@ export class WebhookController { 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, + * Ergebnis). Über die globalen Guards per JWT oder API-Key zugänglich. + */ + @Get('status') + async getWebhookStatus(): Promise<{ lastCall: unknown }> { + const setting = await this.settingRepo.findOneBy({ + Tag: LAST_WEBHOOK_CALL_TAG, + }); + if (!setting?.Wert) { + return { lastCall: null }; + } + try { + return { lastCall: JSON.parse(setting.Wert) }; + } catch { + return { lastCall: { raw: setting.Wert } }; + } + } + /** * Ermittelt die Dokument-ID aus dem Webhook-Payload. Bevorzugt wird das * optionale Feld `document_id` (für manuelles Testen), andernfalls wird die @@ -79,4 +124,47 @@ export class WebhookController { } 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 { + 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), + ); + } + } } diff --git a/paperless-backend/src/webhook/webhook.module.ts b/paperless-backend/src/webhook/webhook.module.ts index 466eb2e..5a280d7 100644 --- a/paperless-backend/src/webhook/webhook.module.ts +++ b/paperless-backend/src/webhook/webhook.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { WebhookController } from './webhook.controller'; import { PaperlessModule } from '../paperless/paperless.module'; import { AuthModule } from '../auth/auth.module'; +import { Setting } from '../database/entities/setting.entity'; @Module({ - imports: [PaperlessModule, AuthModule], + imports: [TypeOrmModule.forFeature([Setting]), PaperlessModule, AuthModule], controllers: [WebhookController], }) export class WebhookModule {}