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.
This commit is contained in:
Matthias G
2026-08-03 13:38:17 +02:00
parent 5ef9cd704e
commit 328db743d8
6 changed files with 56 additions and 15 deletions
+6 -3
View File
@@ -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": {
+6 -3
View File
@@ -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": {
+16 -7
View File
@@ -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
<AdminSubmit pendingLabel={t('saving')}>{t('save')}</AdminSubmit>
</form>
<form action={reportAction} className="flex flex-col gap-3">
<div className="flex flex-col gap-3">
<AdminFormMessage state={report} />
<AdminFormMessage state={reset} />
<span className="flex flex-wrap items-center gap-4">
<AdminSubmit quiet pendingLabel={t('sending')} icon={<TbSend className="size-4" />}>
{t('sendNow')}
</AdminSubmit>
<form action={reportAction}>
<AdminSubmit quiet pendingLabel={t('sending')} icon={<TbSend className="size-4" />}>
{t('sendNow')}
</AdminSubmit>
</form>
<form action={resetAction}>
<AdminSubmit quiet pendingLabel={t('resetting')} icon={<TbRotate2 className="size-4" />}>
{t('resetWindow')}
</AdminSubmit>
</form>
<span className={hintClass}>{t('hints.sendNow')}</span>
</span>
</form>
</div>
</div>
)
}
+10 -1
View File
@@ -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<AdminFormState> {
const viewer = await requireAdmin()
const result = await performResetWindow(viewer)
revalidatePath(adminSettingsPath)
return result
}
+15
View File
@@ -187,3 +187,18 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData):
return { status: 'ok', message: 'messageDeleted' }
}
export async function performResetWindow(viewer: Viewer): Promise<AdminFormState> {
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' }
}
+3 -1
View File
@@ -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<SlackResult> {
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') }
}