Replaces the "Geplant: Audit-Log" placeholder on /admin with a working
viewer that unions paliad.project_events + caldav_sync_log + reminder_log
into a single keyset-paginated timeline.
- AuditService.ListEntries (internal/services/audit_service.go) does one
UNION ALL across the three sources, projecting each into a unified
AuditEntry shape and ordering by (timestamp, id) DESC. Cursor is
(BeforeTS, BeforeID) — matches the project-event Verlauf pattern. ILIKE
search escapes %/_ so "100%" doesn't act as a wildcard.
- GET /api/audit-log (internal/handlers/audit.go) accepts
source/from/to/q/before_ts/before_id/limit, validates the cursor halves
are paired, and returns { entries, next_cursor }. Both API and the
GET /admin/audit-log SPA shell are wrapped in auth.RequireAdminFunc, so
non-admins get 403 (API) / 302 (browser) via the same gate /admin/team
uses.
- Frontend (admin-audit-log.tsx + client/admin-audit-log.ts) renders the
table with source dropdown, range presets (24h / 7d / 30d / custom /
all), free-text search (debounced 250ms), and "Weitere laden" cursor
pagination. project_events rows reuse translateEvent (t-paliad-067 PR-1)
for DE/EN narrative parity with the dashboard activity feed; caldav and
reminder rows have their own per-event-type i18n keys.
- /admin landing card moved from PLANNED to AVAILABLE; sidebar admin
group gains a third entry.
35 lines
798 B
Go
35 lines
798 B
Go
package handlers
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseAuditTimestamp_RFC3339(t *testing.T) {
|
|
got, err := parseAuditTimestamp("2026-04-29T14:35:06Z")
|
|
if err != nil {
|
|
t.Fatalf("parse RFC3339: %v", err)
|
|
}
|
|
want := time.Date(2026, 4, 29, 14, 35, 6, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Errorf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAuditTimestamp_DateOnly(t *testing.T) {
|
|
got, err := parseAuditTimestamp("2026-04-29")
|
|
if err != nil {
|
|
t.Fatalf("parse date-only: %v", err)
|
|
}
|
|
want := time.Date(2026, 4, 29, 0, 0, 0, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Errorf("got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseAuditTimestamp_Invalid(t *testing.T) {
|
|
if _, err := parseAuditTimestamp("not-a-time"); err == nil {
|
|
t.Error("expected error for malformed timestamp")
|
|
}
|
|
}
|