Report what Slack actually answers

The lookup swallowed Slack's error and the interface guessed at a cause.
It now shows the raw reason, widens the search window to six hours and
falls back to matching the message prefix.
This commit is contained in:
Matthias G
2026-08-03 13:03:32 +02:00
parent 7a10754e70
commit bb5028ee55
5 changed files with 45 additions and 19 deletions
+2 -1
View File
@@ -299,7 +299,8 @@
"runNotSent": "Bei diesem Lauf ging keine Nachricht raus.", "runNotSent": "Bei diesem Lauf ging keine Nachricht raus.",
"runNoChannel": "Es ist kein Kanal hinterlegt.", "runNoChannel": "Es ist kein Kanal hinterlegt.",
"runNotFound": "Im Kanal war keine passende Nachricht zu finden. Braucht der Bot noch das Recht channels:history?", "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." "channelRequired": "Zum Bot-Token gehört ein Kanal, sonst weiß Logbuch nicht, wohin.",
"slackSaid": "Slack sagt: {detail}"
}, },
"errors": { "errors": {
"nameRequired": "Trag einen Namen ein.", "nameRequired": "Trag einen Namen ein.",
+2 -1
View File
@@ -299,7 +299,8 @@
"runNotSent": "That run sent no message.", "runNotSent": "That run sent no message.",
"runNoChannel": "No channel is stored.", "runNoChannel": "No channel is stored.",
"runNotFound": "No matching message found in the channel. Does the bot still need channels:history?", "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." "channelRequired": "A bot token needs a channel, otherwise Logbuch has no target.",
"slackSaid": "Slack says: {detail}"
}, },
"errors": { "errors": {
"nameRequired": "Enter a name.", "nameRequired": "Enter a name.",
+5 -1
View File
@@ -30,7 +30,11 @@ export function MessageDeleteButton({ runId, hint }: MessageDeleteButtonProps) {
</AdminSubmit> </AdminSubmit>
{hint ? <span className="font-mono text-micro text-ink-3">{hint}</span> : null} {hint ? <span className="font-mono text-micro text-ink-3">{hint}</span> : null}
{state.status === 'error' && state.message ? ( {state.status === 'error' && state.message ? (
<span role="alert" className={errorClass}>{messages(state.message)}</span> <span role="alert" className={errorClass}>
{state.message === 'slackSaid'
? messages('slackSaid', { detail: state.values?.detail ?? '' })
: messages(state.message)}
</span>
) : null} ) : null}
</form> </form>
) )
+10 -4
View File
@@ -132,21 +132,27 @@ export async function performDeleteMessage(viewer: Viewer, formData: FormData):
return invalid('runNoChannel') return invalid('runNoChannel')
} }
const messageTs = run.messageTs ?? await findMessageTs({ let messageTs = run.messageTs
if (!messageTs) {
const lookup = await findMessageTs({
token, token,
channel, channel,
text: reportText({ from: run.fromAt, to: run.toAt, total: run.total }), text: reportText({ from: run.fromAt, to: run.toAt, total: run.total }),
around: run.createdAt, around: run.createdAt,
}) })
if (!messageTs) { if (!lookup.ok) {
return invalid('runNotFound') return invalid('slackSaid', { detail: lookup.detail })
}
messageTs = lookup.messageTs
} }
const result = await deleteFromSlack(token, channel, messageTs) const result = await deleteFromSlack(token, channel, messageTs)
if (!result.ok) { if (!result.ok) {
return invalid('runDeleteFailed') return invalid('slackSaid', { detail: result.detail })
} }
await markReportRunDeleted(run.id) await markReportRunDeleted(run.id)
+22 -8
View File
@@ -79,14 +79,18 @@ export async function deleteFromSlack(token: string, channel: string, messageTs:
return { ok: true, sent: { channel, messageTs } } return { ok: true, sent: { channel, messageTs } }
} }
export type LookupResult =
| { ok: true, messageTs: string }
| { ok: false, detail: string }
export async function findMessageTs(args: { export async function findMessageTs(args: {
token: string token: string
channel: string channel: string
text: string text: string
around: Date around: Date
window?: number window?: number
}): Promise<string | undefined> { }): Promise<LookupResult> {
const window = args.window ?? 30 * 60 * 1000 const window = args.window ?? 6 * 60 * 60 * 1000
const oldest = (args.around.getTime() - window) / 1000 const oldest = (args.around.getTime() - window) / 1000
const latest = (args.around.getTime() + window) / 1000 const latest = (args.around.getTime() + window) / 1000
@@ -95,15 +99,25 @@ export async function findMessageTs(args: {
oldest: String(oldest), oldest: String(oldest),
latest: String(latest), latest: String(latest),
inclusive: true, inclusive: true,
limit: 100, limit: 200,
}) })
if (!ok || !Array.isArray(data.messages)) { if (!ok) {
return undefined return { ok: false, detail: String(data.error ?? 'unknown_error') }
} }
const found = (data.messages as { text?: string, ts?: string }[]) const messages = Array.isArray(data.messages) ? data.messages as { text?: string, ts?: string }[] : []
.find(message => typeof message.ts === 'string' && message.text === args.text) const exact = messages.find(message => typeof message.ts === 'string' && message.text === args.text)
return found?.ts if (exact?.ts) {
return { ok: true, messageTs: exact.ts }
}
const loose = messages.find(message => typeof message.ts === 'string' && (message.text ?? '').startsWith('Logbuch,'))
if (loose?.ts) {
return { ok: true, messageTs: loose.ts }
}
return { ok: false, detail: `no_match_in_${messages.length}_messages` }
} }