Manage the Slack digest in the admin

New settings area: the webhook lives in the database and is only shown
shortened afterwards, the environment variable stays as a fallback. A switch
decides whether internal entries travel, a button sends the digest right
away, and every run is recorded with period, count, projects, result and
who triggered it.
This commit is contained in:
Matthias G
2026-08-03 12:13:57 +02:00
parent 65346d0c09
commit 0fd6da62d1
19 changed files with 2663 additions and 31 deletions
+57
View File
@@ -0,0 +1,57 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { db } from '~/data/db'
import { appSettings } from '~/data/schema'
import { listReportRuns, writeSetting } from '~/data/repositories/settings'
import { readSlackConfig, reportScope, runWeeklyReport } from '~/lib/report-settings'
const origin = 'https://logbuch.nyo.de'
beforeEach(async () => {
await db.delete(appSettings)
delete process.env.SLACK_WEBHOOK_URL
vi.unstubAllGlobals()
})
describe('readSlackConfig', () => {
it('nimmt den hinterlegten Webhook vor der Umgebungsvariable', async () => {
process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/aus/der/umgebung'
await writeSetting('slack.webhook', 'https://hooks.slack.com/services/aus/der/verwaltung')
const config = await readSlackConfig()
expect(config.webhook).toContain('verwaltung')
expect(config.fromEnv).toBe(false)
})
it('fällt auf die Umgebungsvariable zurück', async () => {
process.env.SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/aus/der/umgebung'
const config = await readSlackConfig()
expect(config.fromEnv).toBe(true)
})
it('schickt interne Beiträge mit, solange nichts anderes gesetzt ist', async () => {
expect(reportScope(await readSlackConfig())).toBe('internal')
await writeSetting('slack.includeInternal', 'false')
expect(reportScope(await readSlackConfig())).toBe('customer')
})
})
describe('runWeeklyReport', () => {
it('vermerkt einen Lauf ohne Veröffentlichungen, ohne zu senden', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const outcome = await runWeeklyReport({ origin, trigger: 'test' })
expect(outcome.reason).toBe('nothing_published')
expect(fetchMock).not.toHaveBeenCalled()
const runs = await listReportRuns(1)
expect(runs[0]).toMatchObject({ sent: false, total: 0, trigger: 'test', detail: 'nothing_published' })
})
})
+1 -1
View File
@@ -13,6 +13,6 @@ rmSync(testStorage, { recursive: true, force: true })
process.env.STORAGE_DIR = testStorage
beforeEach(async () => {
await db.execute(sql`truncate table post_read, api_client, post_tag, tag, block, post, issue, media, project, brand, post_type restart identity cascade`)
await db.execute(sql`truncate table post_read, api_client, post_tag, tag, block, post, issue, media, project, brand, post_type, app_setting, report_run restart identity cascade`)
await insertDefaultPostTypes()
})