From 328db743d83fe9b6f04b44273f41da36eff58d6a Mon Sep 17 00:00:00 2001 From: Matthias G Date: Mon, 3 Aug 2026 13:38:17 +0200 Subject: [PATCH] Give the digest window a way back Deleting a message that Slack no longer has counts as done, so the window rewinds instead of staying stuck. A reset button clears the mark by hand, and the empty-run notice now says since the last dispatch instead of seven days. --- messages/de.json | 9 ++++++--- messages/en.json | 9 ++++++--- src/components/admin/SlackForm.tsx | 23 ++++++++++++++++------- src/lib/admin-actions.ts | 11 ++++++++++- src/lib/admin-settings.ts | 15 +++++++++++++++ src/lib/slack.ts | 4 +++- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/messages/de.json b/messages/de.json index 1c5adfc..f8bf6c3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -287,7 +287,7 @@ "typeInUse": "Diese Art wird noch benutzt. Nimm stattdessen den Haken bei Aktiv heraus.", "slackSaved": "Die Slack-Einstellungen sind gespeichert.", "reportSent": "Der Bericht ist raus.", - "reportEmpty": "In den letzten sieben Tagen wurde nichts veröffentlicht, deshalb ging keine Nachricht raus.", + "reportEmpty": "Seit dem letzten Versand ist nichts dazugekommen, deshalb ging keine Nachricht raus.", "reportNoWebhook": "Es ist kein Webhook hinterlegt.", "reportRejected": "Slack hat die Nachricht abgelehnt.", "messageDeleted": "Die Nachricht ist in Slack gelöscht.", @@ -300,7 +300,8 @@ "runNoChannel": "Es ist kein Kanal hinterlegt.", "runNotFound": "Im Kanal war keine passende Nachricht zu finden. Braucht der Bot noch das Recht channels:history?", "channelRequired": "Zum Bot-Token gehört ein Kanal, sonst weiß Logbuch nicht, wohin.", - "slackSaid": "Slack sagt: {detail}" + "slackSaid": "Slack sagt: {detail}", + "windowReset": "Der Zeitraum ist zurückgesetzt. Der nächste Bericht umfasst wieder die letzten sieben Tage." }, "errors": { "nameRequired": "Trag einen Namen ein.", @@ -775,7 +776,9 @@ "pathNone": "Noch kein Weg hinterlegt, es geht nichts raus.", "messageNoToken": "Ohne Bot-Token nicht löschbar", "lastSent": "Zuletzt verschickt am {when}. Der nächste Bericht beginnt dort und enthält nur, was seitdem erschienen ist.", - "neverSent": "Noch nichts verschickt. Der erste Bericht umfasst die letzten sieben Tage." + "neverSent": "Noch nichts verschickt. Der erste Bericht umfasst die letzten sieben Tage.", + "resetWindow": "Zeitraum zurücksetzen", + "resetting": "Wird zurückgesetzt" } }, "filter": { diff --git a/messages/en.json b/messages/en.json index 5048748..7e8a064 100644 --- a/messages/en.json +++ b/messages/en.json @@ -287,7 +287,7 @@ "typeInUse": "This type is still in use. Clear the active checkbox instead.", "slackSaved": "The Slack settings are saved.", "reportSent": "The digest is out.", - "reportEmpty": "Nothing was published in the last seven days, so no message went out.", + "reportEmpty": "Nothing appeared since the last digest, so no message went out.", "reportNoWebhook": "No webhook is stored.", "reportRejected": "Slack rejected the message.", "messageDeleted": "The message is deleted in Slack.", @@ -300,7 +300,8 @@ "runNoChannel": "No channel is stored.", "runNotFound": "No matching message found in the channel. Does the bot still need channels:history?", "channelRequired": "A bot token needs a channel, otherwise Logbuch has no target.", - "slackSaid": "Slack says: {detail}" + "slackSaid": "Slack says: {detail}", + "windowReset": "The window is reset. The next digest covers the last seven days again." }, "errors": { "nameRequired": "Enter a name.", @@ -775,7 +776,9 @@ "pathNone": "No route stored yet, nothing goes out.", "messageNoToken": "Not deletable without a bot token", "lastSent": "Last sent on {when}. The next digest starts there and only carries what appeared since.", - "neverSent": "Nothing sent yet. The first digest covers the last seven days." + "neverSent": "Nothing sent yet. The first digest covers the last seven days.", + "resetWindow": "Reset the window", + "resetting": "Resetting" } }, "filter": { diff --git a/src/components/admin/SlackForm.tsx b/src/components/admin/SlackForm.tsx index 93ead0d..5aa369f 100644 --- a/src/components/admin/SlackForm.tsx +++ b/src/components/admin/SlackForm.tsx @@ -2,12 +2,12 @@ import { useActionState, useState } from 'react' import { useTranslations } from 'next-intl' -import { TbSend } from 'react-icons/tb' +import { TbRotate2, TbSend } from 'react-icons/tb' import { AdminField } from './AdminField' import { AdminFormMessage } from './AdminFormMessage' import { AdminSubmit } from './AdminSubmit' import { checkboxClass, hintClass, inputClass, labelClass } from './styles' -import { saveSlackSettings, sendWeeklyReportNow } from '~/lib/admin-actions' +import { resetReportWindow, saveSlackSettings, sendWeeklyReportNow } from '~/lib/admin-actions' import { emptyAdminFormState } from '~/lib/admin-forms' export type SlackFormProps = { @@ -22,6 +22,7 @@ export function SlackForm({ hasWebhook, hasBot, channel, fromEnv, includeInterna const t = useTranslations('admin.settings') const [state, formAction] = useActionState(saveSlackSettings, emptyAdminFormState) const [report, reportAction] = useActionState(sendWeeklyReportNow, emptyAdminFormState) + const [reset, resetAction] = useActionState(resetReportWindow, emptyAdminFormState) const [replace, setReplace] = useState(!hasWebhook) const [replaceToken, setReplaceToken] = useState(!hasBot) const path = hasBot ? 'pathBot' : hasWebhook ? 'pathWebhook' : 'pathNone' @@ -132,15 +133,23 @@ export function SlackForm({ hasWebhook, hasBot, channel, fromEnv, includeInterna {t('save')} -
+
+ - }> - {t('sendNow')} - + + }> + {t('sendNow')} + + +
+ }> + {t('resetWindow')} + +
{t('hints.sendNow')}
- +
) } diff --git a/src/lib/admin-actions.ts b/src/lib/admin-actions.ts index 4f53877..22b5d24 100644 --- a/src/lib/admin-actions.ts +++ b/src/lib/admin-actions.ts @@ -6,7 +6,7 @@ import { performSaveBrand } from './admin-brands' import { performCreateClient, performRevokeClient } from './admin-clients' import { performDeletePostType, performSavePostType } from './admin-post-types' import { performSaveProject } from './admin-projects' -import { performDeleteMessage, performSaveSlack, performSendReport } from './admin-settings' +import { performDeleteMessage, performResetWindow, performSaveSlack, performSendReport } from './admin-settings' import { performGrantRole, performRevokeRole, performSetUserAdmin } from './admin-users' import type { AdminFormState } from './admin-forms' import { @@ -168,3 +168,12 @@ export async function deleteSlackMessage(_state: AdminFormState, formData: FormD return result } + +export async function resetReportWindow(_state: AdminFormState): Promise { + const viewer = await requireAdmin() + const result = await performResetWindow(viewer) + + revalidatePath(adminSettingsPath) + + return result +} diff --git a/src/lib/admin-settings.ts b/src/lib/admin-settings.ts index 1af926d..ba61eb3 100644 --- a/src/lib/admin-settings.ts +++ b/src/lib/admin-settings.ts @@ -187,3 +187,18 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData): return { status: 'ok', message: 'messageDeleted' } } + +export async function performResetWindow(viewer: Viewer): Promise { + await clearSetting('slack.lastSentAt') + + await recordAudit({ + actorId: viewer.id, + actorLabel: viewer.email, + action: 'settings.reportReset', + entity: 'setting', + entityId: 'slack', + data: {}, + }) + + return { status: 'ok', message: 'windowReset' } +} diff --git a/src/lib/slack.ts b/src/lib/slack.ts index e18efc7..6150cfd 100644 --- a/src/lib/slack.ts +++ b/src/lib/slack.ts @@ -69,10 +69,12 @@ export async function sendToSlack(message: SlackMessage, target: SlackTarget): P return { ok: true, sent: { channel: null, messageTs: null } } } +const goneErrors = ['message_not_found', 'already_deleted'] + export async function deleteFromSlack(token: string, channel: string, messageTs: string): Promise { const { ok, data } = await callApi(token, 'chat.delete', { channel, ts: messageTs }) - if (!ok) { + if (!ok && !goneErrors.includes(String(data.error))) { return { ok: false, detail: String(data.error ?? 'unknown_error') } }