Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ffb8fd9
feat(audit): move the audit action catalog to domain with readable la…
Makisuo Oct 17, 2026
b11a297
feat(settings): redesign the audit log tab with shareable filters and…
Makisuo Oct 17, 2026
1570545
feat(settings): redesign setup health around one list of findings
Makisuo Oct 17, 2026
ece328e
feat(settings): redesign the data platform tab around connection, sch…
Makisuo Oct 17, 2026
b50cbed
chore(audit): drop em dashes from the moved catalog and audit log com…
Makisuo Oct 17, 2026
be5c65c
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
b25258d
fix(settings): validate audit log actor ids and key pages by struct
Makisuo Oct 17, 2026
f5fe2e6
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
312afcd
fix(settings): keep the saved connection on screen until the refetch …
Makisuo Oct 17, 2026
9f6f85d
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
7defa6e
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
2772fb6
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
fc443d0
fix(settings): release the connection draft when the save trimmed it
Makisuo Oct 17, 2026
2c5cf01
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
e445024
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
eed31d0
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
71ceae0
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
4562ee2
fix(settings): count passed and skipped setup checks apart
Makisuo Oct 17, 2026
00bcf18
fix(settings): key affected chips by id so repeated names stay unique
Makisuo Oct 17, 2026
e0b83e2
fix(settings): describe stored data totals without assuming the manag…
Makisuo Oct 17, 2026
07d19a1
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
6583cbc
fix(settings): commit the connection with the draft the save submitted
Makisuo Oct 17, 2026
66b2c4a
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
64fbfca
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
3370cfe
fix(settings): stamp first data received from the setup audit
Makisuo Oct 17, 2026
a4bf4df
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
e0b00f9
fix(settings): date onboarding rows created by first-data stamping by…
Makisuo Oct 17, 2026
e0b97d8
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
5239c66
fix(settings): track the connection save as in flight until it settles
Makisuo Oct 17, 2026
febac4d
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
503deaf
fix(settings): run config checks when the first-data probe cannot rea…
Makisuo Oct 17, 2026
feb2208
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
fc96ea6
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
28a3d89
Merge branch 'feat/settings-keys-ingestion' into feat/settings-audit-…
Makisuo Oct 17, 2026
8bb9654
fix(settings): send the sampling setup check to the sampling guide
Makisuo Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/api/src/routes/v2/setup-audit.http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { PlanetScaleDiscoveryService } from "@maple/backend/services/integration
import { PlanetScaleOAuthService } from "@maple/backend/services/auth/PlanetScaleOAuthService"
import { RecommendationIssueService } from "@maple/backend/services/errors/RecommendationIssueService"
import { ScrapeTargetsService } from "@maple/backend/services/integrations/ScrapeTargetsService"
import { OnboardingService } from "@maple/backend/services/org/OnboardingService"
import { OrganizationService } from "@maple/backend/services/org/OrganizationService"
import { SetupAuditService } from "@maple/backend/services/org/SetupAuditService"
import { IntegrationInventoryService } from "@maple/backend/services/integrations/IntegrationInventoryService"
import { SignalPresenceService } from "@maple/backend/services/org/SignalPresenceService"
Expand Down Expand Up @@ -138,7 +140,20 @@ const makeHarness = (warehouse: WarehouseQueryServiceApi = warehouseStub()) => {
Layer.provide(planetScaleStubs),
),
Layer.effect(SetupAuditService, SetupAuditService.make).pipe(
Layer.provide(Layer.mergeAll(warehouseLive, IntegrationInventoryService.layer)),
Layer.provide(
Layer.mergeAll(
warehouseLive,
IntegrationInventoryService.layer,
OnboardingService.layer,
// An undatable org: the audit may stamp an existing row but never creates one.
Layer.succeed(OrganizationService, {
retrieve: () => Effect.die("org lookup not exercised"),
}),
Layer.effect(SignalPresenceService, SignalPresenceService.make).pipe(
Layer.provide(warehouseLive),
),
),
),
),
// Sibling group in `AllV2GroupLayersLive`; the stub bundle is deliberately
// unused here, so it needs its own (warehouse-only) layer.
Expand Down
17 changes: 16 additions & 1 deletion apps/api/src/routes/v2/telemetry-signals.http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { IngestAttributeMappingService } from "@maple/backend/services/org/Inges
import { OrgIngestKeysService } from "@maple/backend/services/org/OrgIngestKeysService"
import { RecommendationIssueService } from "@maple/backend/services/errors/RecommendationIssueService"
import { ScrapeTargetsService } from "@maple/backend/services/integrations/ScrapeTargetsService"
import { OnboardingService } from "@maple/backend/services/org/OnboardingService"
import { OrganizationService } from "@maple/backend/services/org/OrganizationService"
import { SetupAuditService } from "@maple/backend/services/org/SetupAuditService"
import { IntegrationInventoryService } from "@maple/backend/services/integrations/IntegrationInventoryService"
import { PlanetScaleDiscoveryService } from "@maple/backend/services/integrations/PlanetScaleDiscoveryService"
Expand Down Expand Up @@ -123,7 +125,20 @@ const makeHarness = (warehouse: WarehouseQueryServiceApi) => {
Layer.provide(planetScaleStubs),
),
Layer.effect(SetupAuditService, SetupAuditService.make).pipe(
Layer.provide(Layer.mergeAll(warehouseLive, IntegrationInventoryService.layer)),
Layer.provide(
Layer.mergeAll(
warehouseLive,
IntegrationInventoryService.layer,
OnboardingService.layer,
// An undatable org: the audit may stamp an existing row but never creates one.
Layer.succeed(OrganizationService, {
retrieve: () => Effect.die("org lookup not exercised"),
}),
Layer.effect(SignalPresenceService, SignalPresenceService.make).pipe(
Layer.provide(warehouseLive),
),
),
),
),
Layer.effect(SignalPresenceService, SignalPresenceService.make).pipe(Layer.provide(warehouseLive)),
).pipe(Layer.provideMerge(Layer.mergeAll(envLive, testDb.layer)))
Expand Down
132 changes: 132 additions & 0 deletions apps/web/src/components/settings/audit-log-date-range.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { useState } from "react"

import { Button } from "@maple/ui/components/ui/button"
import { Popover, PopoverContent, PopoverTrigger } from "@maple/ui/components/ui/popover"
import { cn } from "@maple/ui/lib/utils"

import { ClockIcon } from "@/components/icons"
import { CustomRangePicker } from "@/components/time-range-picker/custom-range-picker"
import { useTimezonePreference } from "@/hooks/use-timezone-preference"
import { formatTimestampInTimezone, normalizeTimestampInput } from "@/lib/timezone-format"

import { AUDIT_RANGE_PRESETS, type AuditDateRange } from "./audit-log-filters"

const ALL_TIME: AuditDateRange = { kind: "all" }

function useRangeLabel(range: AuditDateRange): string {
const { effectiveTimezone } = useTimezonePreference()
if (range.kind === "all") return "All time"
if (range.kind === "preset") {
return AUDIT_RANGE_PRESETS.find((preset) => preset.value === range.preset)?.label ?? "All time"
}
const format = (value: string) => formatTimestampInTimezone(value, { timeZone: effectiveTimezone })
if (range.since !== undefined && range.until !== undefined) {
return `${format(range.since)} to ${format(range.until)}`
}
if (range.since !== undefined) return `Since ${format(range.since)}`
if (range.until !== undefined) return `Until ${format(range.until)}`
return "All time"
}

/** The audit log's window: everything, a rolling preset, or a fixed range. */
export function AuditLogDateRange({
value,
onChange,
}: {
value: AuditDateRange
onChange: (range: AuditDateRange) => void
}) {
const [open, setOpen] = useState(false)
const [custom, setCustom] = useState(false)
const label = useRangeLabel(value)

function select(range: AuditDateRange) {
onChange(range)
setOpen(false)
setCustom(false)
}

const options: ReadonlyArray<{ key: string; label: string; range: AuditDateRange }> = [
{ key: "all", label: "All time", range: ALL_TIME },
...AUDIT_RANGE_PRESETS.map((preset) => ({
key: preset.value,
label: preset.label,
range: { kind: "preset", preset: preset.value } satisfies AuditDateRange,
})),
]
const activeKey = value.kind === "all" ? "all" : value.kind === "preset" ? value.preset : null

return (
<Popover
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) setCustom(false)
}}
>
<PopoverTrigger
render={
<Button
variant="outline"
size="sm"
className="gap-1.5"
aria-label={`Date range: ${label}`}
>
<ClockIcon />
<span>{label}</span>
</Button>
}
/>
<PopoverContent
align="start"
className={custom ? "w-auto max-w-[calc(100vw-1.5rem)] p-4" : "w-48 p-0"}
>
{custom ? (
<CustomRangePicker
startTime={value.kind === "custom" ? value.since : undefined}
endTime={value.kind === "custom" ? value.until : undefined}
onApply={(range) =>
select({
kind: "custom",
since: normalizeTimestampInput(range.startTime),
until: normalizeTimestampInput(range.endTime),
})
}
onCancel={() => setCustom(false)}
/>
) : (
<div className="flex flex-col py-1.5">
{options.map((option) => (
<button
key={option.key}
type="button"
onClick={() => select(option.range)}
className={cn(
"flex h-7 items-center px-3 text-left text-xs transition-colors hover:bg-muted/50",
option.key === activeKey
? "font-medium text-foreground"
: "text-foreground/80",
)}
>
{option.label}
</button>
))}
<div className="mx-3 my-1 h-px bg-border/60" />
<button
type="button"
onClick={() => setCustom(true)}
className={cn(
"flex h-7 items-center px-3 text-left text-xs transition-colors hover:bg-muted/50 hover:text-foreground",
value.kind === "custom"
? "font-medium text-foreground"
: "text-muted-foreground",
)}
>
Custom range…
</button>
</div>
)}
</PopoverContent>
</Popover>
)
}
113 changes: 113 additions & 0 deletions apps/web/src/components/settings/audit-log-filters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, it } from "vitest"

import {
auditLogSearchFrom,
hasAuditLogFilters,
parseAuditLogSearch,
pickAuditLogSearch,
resolveAuditDateRange,
} from "./audit-log-filters"

describe("parseAuditLogSearch", () => {
it("reads every filter from the search params", () => {
const filters = parseAuditLogSearch({
range: "7d",
actor: "user_abc",
actor_type: "api_key",
outcome: "denied",
})
expect(filters).toEqual({
range: { kind: "preset", preset: "7d" },
actorId: "user_abc",
actorType: "api_key",
outcome: "denied",
})
expect(hasAuditLogFilters(filters)).toBe(true)
})

it("drops hand-edited values the API would reject instead of failing the page", () => {
const filters = parseAuditLogSearch({
range: "forever",
since: "yesterday",
until: "2026-10-01 12:00:00",
actor: "",
actor_type: "robot",
outcome: "maybe",
})
expect(filters.range).toEqual({ kind: "all" })
expect(filters.actorId).toBeUndefined()
expect(filters.actorType).toBeUndefined()
expect(filters.outcome).toBeUndefined()
expect(hasAuditLogFilters(filters)).toBe(false)
})

it("keeps user, API key and agent actor ids", () => {
for (const actor of ["user_2fj3K9dLqWm8xYbT", "key_aXwpxqBkqtYwtBtmsGbR41", "actor_4CzLmR1pTx"]) {
expect(parseAuditLogSearch({ actor }).actorId).toBe(actor)
}
})

it("drops actor ids the app never writes", () => {
for (const actor of [
"user_abc|2026-01-01T00:00:00Z",
"dash_abc",
"user_",
"bob",
"key_abc def",
"user_abc/../x",
]) {
expect(parseAuditLogSearch({ actor }).actorId).toBeUndefined()
}
})

it("treats a since/until pair as a fixed range", () => {
expect(
parseAuditLogSearch({ since: "2026-10-01T00:00:00.000Z", until: "2026-10-02T00:00:00.000Z" })
.range,
).toEqual({ kind: "custom", since: "2026-10-01T00:00:00.000Z", until: "2026-10-02T00:00:00.000Z" })
})
})

describe("auditLogSearchFrom", () => {
it("clears the params of filters that were unset", () => {
const search = auditLogSearchFrom({ range: { kind: "preset", preset: "24h" } })
expect(search).toEqual({
range: "24h",
since: undefined,
until: undefined,
actor: undefined,
actor_type: undefined,
outcome: undefined,
})
})

it("round-trips through the search params", () => {
const search = {
since: "2026-10-01T00:00:00.000Z",
until: "2026-10-02T00:00:00.000Z",
actor: "key_123",
actor_type: "api_key",
outcome: "allowed",
}
expect(pickAuditLogSearch(search)).toEqual({ range: undefined, ...search })
})
})

describe("resolveAuditDateRange", () => {
const now = Date.parse("2026-10-17T12:00:00.000Z")

it("resolves a rolling preset against the given moment", () => {
expect(resolveAuditDateRange({ kind: "preset", preset: "24h" }, now)).toEqual({
since: "2026-10-16T12:00:00.000Z",
until: undefined,
})
})

it("passes a fixed range through and leaves all time unbounded", () => {
expect(resolveAuditDateRange({ kind: "custom", since: "2026-10-01T00:00:00.000Z" }, now)).toEqual({
since: "2026-10-01T00:00:00.000Z",
until: undefined,
})
expect(resolveAuditDateRange({ kind: "all" }, now)).toEqual({ since: undefined, until: undefined })
})
})
Loading
Loading