Task Breakdown: Qontak Launchpad — Audit Logs for Permission Changes
Decisions recorded
| Item | Decision |
|---|---|
| Pagination shape | meta.total (with COUNT query) |
crs_edit.go team notification | Already done — no task needed |
| Slicing mode | Horizontal |
| Blocked tasks | Include all |
Effort Summary
| Phase / Area | FE days | BE days | QA days | Total |
|---|---|---|---|---|
| Phase 1 — UI (mocked) | 3 | — | 0.5 | 3.5 |
| Phase 2 — API integration | 0.5 | 5 | 1 | 6.5 |
| Grand total | 3.5 | 5 | 1.5 | 10 |
Confidence: medium→high. Both recon unknowns resolved: (1)
canCreateUsersconfirmed atuseManagePermission.ts:53as the correct permission gate. (2) Nav items are API-driven viaqontak-unified-componentTopNav — FE change isspaLinksonly; actual menu entry requires platform team config coordination (not in estimate).
Phase 1 — UI (APIs mocked)
Task 1.1: [FE] Audit Logs page — permission guard, nav tab, filter bar, data table (AL-S01)
A user with
usman_users_managecan navigate to the Audit Logs tab and see a filterable, paginated table of their company's permission-change events.
Status: ⚠️ Partially blocked — real GET /iag/v1/audit-logs endpoint does not exist yet; composable uses a mocked stub. Real API call wired in Task 2.5.
Design reference: n/a — no Figma link in RFC. UI contract specified in RFC §2.A.
What to build
New "Audit Logs" section under User Management: page shell, route guard (redirect to /unauthorized if no usman_users_manage), spaLinks addition for the new route, filter bar (object type, action, date range, actor SSO ID), data table with pagination. All API responses are mocked in this task.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | app/features/user_management/composables/useAuditLogs.ts | Pinia store (useAuditLogsStore) — filter state, mocked fetch, pagination state |
| create | app/features/user_management/composables/useAuditLogs.spec.ts | Vitest tests: filter changes, pagination, loading/error states |
| create | app/features/user_management/audit_logs/index/views/AuditLogFilters.vue | Filter bar: object type dropdown, action dropdown, date range pickers, actor SSO ID input |
| create | app/features/user_management/audit_logs/index/views/AuditLogTable.vue | Data table: Date/Time, Actor, Object Type, Action, Target; skeleton rows; empty state; MpPagination |
| create | app/pages/user_management/audit-logs/index.vue | Thin page shell — imports views, definePageMeta({ layout: "default" }) |
| extend | app/middleware/permission.global.ts | Guard: path /user_management/audit-logs + !canCreateUsers → navigateTo('/unauthorized') |
| extend | app/layouts/default.vue:215 | Add "/user_management/audit-logs" to spaLinks array |
| extend | app/common/components/layouts/QontakOneSidebar.vue | Same spaLinks addition for QontakOne layout |
| ⚠ config | qontak-unified-component TopNav menu API | Actual "Audit Logs" nav item is server-driven — coordinate with platform team for menu config entry (outside this repo) |
Permission property confirmed (
useManagePermission.ts:53) —usersPermissions.canManageUsersdoes not exist. Correct property isusersPermissions.canCreateUsers; it gates onusman_users_manage.Nav is API-driven —
TopNavfromqontak-unified-componentfetches menu items from server config. The FE change is onlyspaLinks. Actual menu entry requires platform team coordination.
Implementation Steps
-
Explore — Open
app/features/user_management/composables/useRoles.tsanduseRoles.spec.ts. Note Pinia store structure, howuseClientis called, and the vitest mock pattern (vi.hoisted,vi.stubGlobal("useRuntimeConfig", ...)). New store mirrors this exactly. -
Write failing tests (red) — Create
app/features/user_management/composables/useAuditLogs.spec.ts. Cover: initial state (empty filters, page 1),fetchAuditLogs()loading transitions, filter change resets page to 1, error state. Runpnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts— confirm failure. -
Scaffold store —
defineStore("usman-audit-logs-store", ...)with refs:filters,data,loading,error,pagination(page, perPage, total). -
Wire mocked fetch — Stub response with hardcoded array. Use
useClient<AuditLogListResponse>pattern. Add// TODO Task 2.5: replace with real endpoint. -
Build
AuditLogFilters.vue— Pixel3MpFlex,MpSelect,MpDatepicker,MpInput,MpButton. Filter changes callfetchAuditLogs(). -
Build
AuditLogTable.vue— Pixel3 table. Skeleton rows on loading, "No audit logs found" on empty,MpPaginationconsumingpagination.total. -
Page shell — Import both views.
onMounted(() => useAuditLogsStore().fetchAuditLogs()). -
Route guard —
permission.global.ts: if path starts with/user_management/audit-logsand!managePermissionStore.usersPermissions.canCreateUsers→navigateTo('/unauthorized'). -
spaLinks — Add
"/user_management/audit-logs"tospaLinksinapp/layouts/default.vue:215andapp/common/components/layouts/QontakOneSidebar.vue. Coordinate with platform team for TopNav menu config entry. -
Go green + gate —
pnpm testpasses →pnpm lint && pnpm build.
Acceptance Criteria
-
/user_management/audit-logsloads for user withusman_users_manage; redirects to/unauthorizedwithout it - "Audit Logs" nav item visible only when
canCreateUsersis true (pending platform team menu config) - Filter bar: object type, action, date range, actor SSO ID — all controls render
- Changing any filter resets page to 1 and re-fetches
- Table: Date/Time, Actor, Object Type, Action, Target columns
- Loading → skeleton rows; empty → "No audit logs found"
- Pagination functional; page changes trigger re-fetch
-
pnpm testpasses;pnpm buildsucceeds - (Pending Task 2.5) Real API call replaces mock stub
Test Strategy
Vitest tests on useAuditLogsStore. Stub useClient via vi.hoisted + vi.stubGlobal following useRoles.spec.ts pattern. Assert: loading transitions, mock response maps to data, filter changes pass correct query params to useClient.
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | 3 |
| Backend | — |
| QA | 0.5 |
| Total | 3.5 |
Assumptions: Pixel3 MpTable/MpSelect/MpDatepicker work with Nuxt 4. Reuses
useClient+ Pinia pattern fromuseRoles.ts.spaLinksfiles confirmed. Platform team nav config coordination not included in estimate.
Run to verify
pnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts && pnpm lint && pnpm build
Depends on
- None (mocked API; fully independent of BE work)
Phase 2 — API Integration
Task 2.1: [BE] Write-side audit gap fixes — crs_edit bug + UpdatePermissionLevel + BackfillPermission (AL-S02, AL-S03, AL-S04)
Permission level updates and backfill operations produce audit log entries; role edits emit
update(notcreate) as the action type.
Status: ✅ Actionable
Design reference: n/a — BE only
What to build
Three targeted changes in the roles service layer: fix ActionAuditCreate → ActionAuditUpdate at crs_edit.go:247; add CreateAuditLog after the primary DB write in UpdatePermissionLevel; add CreateAuditLog after the primary DB write in BackfillPermission.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| fix | internal/app/service/roles/crs_edit.go:247 | consts.ActionAuditCreate → consts.ActionAuditUpdate |
| extend | internal/app/service/roles/update_permission_level.go | Add s.auditLogger.CreateAuditLog(...) after primary DB write; swallow audit error with Error log |
| extend | internal/app/service/roles/backfill_permission.go | Same audit call pattern after primary mutation |
| extend | internal/app/service/roles/crs_edit_test.go | Update assertion: action_audit == "update" |
| extend/create | internal/app/service/roles/update_permission_level_test.go | Add TestUpdatePermissionLevel_AuditsOnSuccess + TestUpdatePermissionLevel_NoAuditOnFailure |
| extend/create | internal/app/service/roles/backfill_permission_test.go | Add TestBackfillPermission_AuditsOnSuccess + TestBackfillPermission_NoAuditOnFailure |
Implementation Steps
-
Explore — Open
internal/app/service/roles/crs_edit.go:240–260andcrs_edit_test.go. Note hows.auditLogger.CreateAuditLogis called (arg order:ctx, repo, objectAudit, actionAudit, actorSsoID, objectID, data) and how the existing test captures the mock call. Openroles/main.goto confirms.auditLoggerwired in constructor. -
Write failing tests (red) — Change
crs_edit_test.goassertion toActionAuditUpdate(will fail now). AddTestUpdatePermissionLevel_AuditsOnSuccessusingMockAuditLogger; assert called once withconsts.ObjectAuditRoleHasFeaturePermissions+consts.ActionAuditUpdate. AddNoAuditOnFailurevariant: mock primary DB failure; assertCreateAuditLogNOT called. Mirror for backfill. Rungo test -race ./internal/app/service/roles/...— confirm failures. -
Fix
crs_edit.go:247—consts.ActionAuditCreate→consts.ActionAuditUpdate. -
Extend
update_permission_level.go— After confirmed successful primary DB write:if auditErr := s.auditLogger.CreateAuditLog(ctx, s.repo,consts.ObjectAuditRoleHasFeaturePermissions, consts.ActionAuditUpdate,req.ActorSsoID, req.RoleID, req); auditErr != nil {slog.ErrorContext(ctx, "audit log failed", slog.Any("error", auditErr))}Audit failure must NOT propagate to caller.
-
Extend
backfill_permission.go— Same pattern. Inspect file to confirm whichObjectAuditconstant applies. -
Go green —
go test -race ./internal/app/service/roles/... -
Quality gate —
go fmt ./... && go vet ./... && make lint && make sec
Acceptance Criteria
-
crs_edit_test.gopasses withaction_audit == "update"assertion -
TestUpdatePermissionLevel_AuditsOnSuccess:CreateAuditLogcalled once after success -
TestUpdatePermissionLevel_NoAuditOnFailure:CreateAuditLogNOT called when primary mutation fails - Both tests mirrored for
BackfillPermission - Audit failure does NOT propagate to caller (Error log only)
-
go test -race ./internal/app/service/roles/...passes -
make lint && make secclean
Test Strategy
Use mockery-generated MockAuditLogger. Success tests: assert CreateAuditLog called once with correct constants. Failure tests: configure primary DB mock to fail; assert CreateAuditLog NOT called.
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1 |
| QA | 0.5 |
| Total | 1.5 |
Assumptions:
MockAuditLoggeralready generated by mockery.s.auditLoggerwired inroles/main.goconstructor (confirmed by recon).
Run to verify
go test -race ./internal/app/service/roles/... && make lint
Depends on
- None (existing service layer; fully independent)
Task 2.2: [BE] DB layer — migration, enriched SQL query, sqlc generate, IStore extension (AL-S01)
The repository exposes
GetUsmanAuditLogsEnriched(SELECT with LEFT JOINs) andGetUsmanAuditLogsEnrichedCount(COUNT formeta.total), both company-scoped via non-nullable$1.
Status: ✅ Actionable
Design reference: n/a — BE only
What to build
Four sequential steps: create the created_at index migration, add both new queries to the SQL file, run sqlc generate, sync generated output to internal/app/repository/.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | db/migration/<ts>_add_audit_logs_created_at_index.up.sql | CREATE INDEX IF NOT EXISTS idx_usman_audit_logs_created_at ON usman_audit_logs (created_at DESC) |
| create | db/migration/<ts>_add_audit_logs_created_at_index.down.sql | DROP INDEX IF EXISTS idx_usman_audit_logs_created_at |
| extend | db/query/usman_audit_log.sql | Append GetUsmanAuditLogsEnriched :many (RFC §2.3 SQL) + GetUsmanAuditLogsEnrichedCount :one (same WHERE, SELECT COUNT(*), params $1–$7 only) |
| regenerate | db/sqlc/ | sqlc generate — do not edit manually |
| extend | internal/app/repository/querier.go | Add both new method signatures to Querier interface |
| sync | internal/app/repository/usman_audit_log.sql.go | Copy generated impl from db/sqlc/ (strip SQLC headers) |
Implementation Steps
-
Explore — Read
db/query/usman_audit_log.sqlfor query style. Readinternal/app/repository/querier.gofor interface declaration pattern. Check newest migration filename for timestamp format. -
Create migration files — Use timestamp matching existing naming convention.
-
Add enriched queries — Append both queries to
db/query/usman_audit_log.sql.$1(company_id) is non-nullable in both. -
Run sqlc generate:
sqlc generateConfirm
GetUsmanAuditLogsEnriched,GetUsmanAuditLogsEnrichedParams,GetUsmanAuditLogsEnrichedRow,GetUsmanAuditLogsEnrichedCountare generated indb/sqlc/. -
Sync to repository — Copy from
db/sqlc/usman_audit_log.sql.gointointernal/app/repository/usman_audit_log.sql.go(remove SQLC headers). Add both method signatures tointernal/app/repository/querier.go. -
Verify —
go build ./...compiles.
Acceptance Criteria
- Migration up/down files exist with correct SQL
- Both new queries in
db/query/usman_audit_log.sql;$1is non-nullable company_id -
sqlc generatesucceeds -
GetUsmanAuditLogsEnriched+GetUsmanAuditLogsEnrichedCountonQuerierinterface -
go build ./...passes
Test Strategy
No unit test at this layer — sqlc-generated code is trusted as correct. Build pass is the gate.
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1.5 |
| QA | 0 |
| Total | 1.5 |
Assumptions:
sqlcinstalled andsqlc.yamlconfigured. Migration timestamp format from most recent file indb/migration/.
Run to verify
sqlc generate && go build ./...
Depends on
- None (DB layer is standalone)
Task 2.3: [BE] Service layer — request/response types + AuditLogService.GetAuditLogs (AL-S01)
AuditLogService.GetAuditLogsreturns company-scoped, paginated audit entries usingcompany_idfrom context — never from request params.
Status: ✅ Actionable (depends on Task 2.2 for repository types)
Design reference: n/a — BE only
What to build
Two new type files, extension of audit_log.go with GetAuditLogs method + IAuditLogService interface, and unit tests asserting company scoping.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | internal/pkg/request/audit_log_request.go | AuditLogListRequest with 8 query params + validator tags; embeds http.PaginationRequest |
| create | internal/pkg/response/audit_log_response.go | AuditLogEntryResponse (nullable fields as pointers) + AuditLogListResponse{Data, Meta{Page,PerPage,Total}} |
| extend | internal/app/service/audit_log.go | Add IAuditLogService interface + GetAuditLogs(ctx, req) — reads company_id from CurrentUser context; calls both enriched repo methods |
| create | internal/app/service/audit_log_test.go | Tests: CompanyScopedFromContext (primary), FiltersPassedToRepo, RepoError |
Implementation Steps
-
Explore — Open
internal/pkg/request/user_request.goandinternal/pkg/response/user_response.gofor field naming, JSON tags, and validator tag patterns. Openinternal/pkg/http/pagination.goforPaginationRequestandGetLimitOffset(). -
Write failing tests (red) — Create
internal/app/service/audit_log_test.go:// TestGetAuditLogs_CompanyScopedFromContext — primary safety assertionctx := context.WithValue(context.Background(), ctxKey.User, CurrentUser{User: repository.FindUserBySsoIdRow{CompanyID: testUUID},})_, _ = svc.GetAuditLogs(ctx, AuditLogListRequest{})mockRepo.AssertCalled(t, "GetUsmanAuditLogsEnriched",mock.Anything,mock.MatchedBy(func(arg GetUsmanAuditLogsEnrichedParams) bool {return arg.CompanyID == testUUID}),)Run
go test -race ./internal/app/service/...— confirm failure. -
Create
audit_log_request.go:type AuditLogListRequest struct {ObjectAudit string `schema:"object_audit" validate:"omitempty,oneof=user company_role teams role user_role role_has_feature_permissions role_has_data_permissions"`ObjectID *uuid.UUID `schema:"object_id" validate:"omitempty,uuid"`ActionAudit string `schema:"action_audit" validate:"omitempty,oneof=create update destroy transfer_data"`ActorSsoID string `schema:"actor_sso_id"`From *time.Time `schema:"from"`To *time.Time `schema:"to"`http.PaginationRequest} -
Create
audit_log_response.go—AuditLogEntryResponsewith all fields from RFC §2.4 (nullable as pointers).AuditLogListResponse{Data []AuditLogEntryResponse, Meta struct{Page, PerPage, Total int}}. -
Extend
audit_log.go— AddIAuditLogServiceinterface. ImplementGetAuditLogs: readcurrentUserfrom context → extractCompanyID→ callGetUsmanAuditLogsEnriched+GetUsmanAuditLogsEnrichedCount→ map to response. -
Go green —
go test -race ./internal/app/service/... -
Quality gate —
go fmt ./... && go vet ./... && make lint
Acceptance Criteria
-
TestGetAuditLogs_CompanyScopedFromContext: repo called withcompany_idfrom context, never from request -
TestGetAuditLogs_FiltersPassedToRepo: optional filters correctly forwarded -
TestGetAuditLogs_RepoError: error propagated -
meta.totalpopulated viaGetUsmanAuditLogsEnrichedCount - No PII in
slog.InfoContextcalls -
go test -race ./internal/app/service/...passes
Test Strategy
Mock repository.IStore via mockery. Primary assertion: TestGetAuditLogs_CompanyScopedFromContext verifies company_id always originates from context, not the request struct.
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1 |
| QA | 0 |
| Total | 1 |
Assumptions:
middleware.GetCurrentUser(ctx)helper exists (verify insso_auth.go; fallback:ctx.Value(ctxKey.User)).IAuditLogServiceinterface defined here — Task 2.4 depends on it.
Run to verify
go test -race ./internal/app/service/... && make lint
Depends on
- [Task 2.2] —
GetUsmanAuditLogsEnrichedRowtype must exist before service compiles
Task 2.4: [BE] AuditLogHandler + permission mapping + route wiring (AL-S01)
GET /iag/v1/audit-logswith a validusman_users_manageJWT returns 200 paginated company-scoped audit logs; 401 without JWT; 403 without the permission.
Status: ✅ Actionable
Design reference: n/a — BE only
What to build
New handler file with List method + Swagger annotation, one-line permission mapping addition, route registration under /iag/v1, Swagger regeneration.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| create | internal/server/audit_log_handler.go | AuditLogHandler struct + List method; Swagger annotation @Router /iag/v1/audit-logs [get] |
| create | internal/server/audit_log_handler_test.go | Handler tests: 200, 400 (invalid UUID), 401, 403 |
| extend | internal/pkg/middleware/permission_check.go | Add "GET /audit-logs": "usman_users_manage" to staticPermissionMappings |
| extend | internal/server/rest_router.go | r.Get("/audit-logs", auditLogHandler.List) inside /iag/v1 authenticated group |
| regenerate | docs/swagger.json, docs/swagger.yaml | make init after Swagger annotation added |
Implementation Steps
-
Explore — Open
internal/app/handler/role_handler.gofor handler struct pattern. Openinternal/server/rest_router.go:77–105for/iag/v1group structure. Open an existing handler for Swagger annotation format. -
Write failing tests (red) — Create
internal/server/audit_log_handler_test.go. Table:- 200: inject
CurrentUserin context; mock service returns test data; assert JSON shape - 400:
object_id=not-a-uuid; assert 400 +GetAuditLogsNOT called - 401: no
CurrentUserin context - 403: permission denied
Run
go test -race ./internal/server/...— confirm failures.
- 200: inject
-
Create
audit_log_handler.go:type AuditLogHandler struct {auditLogService service.IAuditLogServicebinder binder.IBinder}List: bind + validateAuditLogListRequest→ callGetAuditLogs→ return response. -
Add Swagger annotation — all 8 query params documented;
@Security BearerAuth;@Router /iag/v1/audit-logs [get]. -
Add permission mapping:
// internal/pkg/middleware/permission_check.go — staticPermissionMappings:"GET /audit-logs": "usman_users_manage", -
Register route —
r.Get("/audit-logs", auditLogHandler.List)inside/iag/v1group. -
Regenerate Swagger —
make init -
Go green + gate —
go test -race ./internal/server/...→make prepare
Acceptance Criteria
- Authorised token → 200 JSON with
dataarray andmeta.total -
object_id=invalid-uuid→ 400INVALID_PARAM;GetAuditLogsnot called - No JWT → 401
- Valid JWT, no
usman_users_manage→ 403 -
slog.InfoContextlogs onlyobject_audit+page(no PII) - Swagger annotation present;
make initsucceeds -
make preparepasses (build + test + lint + sec)
Test Strategy
httptest.NewRecorder() + httptest.NewRequest() table tests. Mock IAuditLogService via mockery. 200 case: inject CurrentUser in context, assert JSON shape. 400 case: assert GetAuditLogs never called (AssertNotCalled).
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | — |
| Backend | 1.5 |
| QA | 0.5 |
| Total | 2 |
Assumptions:
IBinderinjection same as existing handlers.IAuditLogServiceinterface from Task 2.3.
Run to verify
go test -race ./internal/server/... && make prepare
Depends on
- [Task 2.3] —
IAuditLogServiceinterface +GetAuditLogsmethod must exist
Task 2.5: [FE] Wire real GET /iag/v1/audit-logs (AL-S01)
The Audit Logs page fetches live data with correct handling for 403 mid-session and 5xx failures.
Status: ✅ Actionable (depends on Task 2.4 endpoint being deployed)
Design reference: n/a — wiring task
What to build
Replace the mock stub in useAuditLogsStore with the real useClient call. Update spec to assert correct URL, query params, and error paths.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | app/features/user_management/composables/useAuditLogs.ts | Replace mock stub with real useClient('/iag/v1/audit-logs', ...) + 403 mid-session handler |
| extend | app/features/user_management/composables/useAuditLogs.spec.ts | Assert correct URL + params; add 403 redirect test; add 5xx error test |
Implementation Steps
-
Explore — Open
app/features/user_management/composables/useRoles.tsto confirm exactuseClientcall pattern. -
Replace mock — Find
// TODO Task 2.5comment. Replace with:const { data, error } = await useClient<AuditLogListResponse>('/iag/v1/audit-logs',{ method: 'GET', query: toRaw(filters.value) }// company_id NOT included — backend derives from JWT)if (error.value?.statusCode === 403) {return navigateTo('/unauthorized')} -
Update tests — Assert
useClientSpycalled with'/iag/v1/audit-logs'and nocompany_idkey. Add: 403 spy → assertnavigateTo('/unauthorized'); 5xx spy → asserterrorref truthy. -
Go green + gate —
pnpm test -- ...useAuditLogs.spec.ts→pnpm lint && pnpm build
Acceptance Criteria
-
useClientcalled with'/iag/v1/audit-logs'; nocompany_idin query params - 403 mid-session →
navigateTo('/unauthorized') - 5xx → error state set;
toast.notifycalled -
pnpm testpasses;pnpm buildsucceeds
Test Strategy
Update useAuditLogs.spec.ts with vi.hoisted mock pattern. Assert URL and param shape. Add 403/5xx error path tests.
Effort Estimate
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0 |
| Total | 0.5 |
Assumptions:
useClienthandles token injection.navigateTois Nuxt auto-imported.
Run to verify
pnpm test -- app/features/user_management/composables/useAuditLogs.spec.ts && pnpm lint && pnpm build
Depends on
- [Task 2.4] —
GET /iag/v1/audit-logsendpoint deployed and reachable
Ordering Rationale
- Task 1.1 — start immediately. FE work is fully independent; build entire UI in parallel with BE.
- Tasks 2.1 and 2.2 — also Day 1, in parallel. Write-side audit fixes (2.1) and DB layer (2.2) share no files; two BE devs start simultaneously.
- Task 2.3 after 2.2 — service needs
GetUsmanAuditLogsEnrichedRowtype from sqlc. - Task 2.4 after 2.3 — handler needs
IAuditLogServiceinterface. - Task 2.5 last — FE wiring requires the endpoint deployed.
- Critical path: 2.2 → 2.3 → 2.4 → 2.5. SQL generation (2.2) is the longest sequential chain — unblock it first.
Skipped Stories
No stories blocked or excluded. All four are actionable:
| Story | Tasks |
|---|---|
AL-S01: User with usman_users_manage views company audit log | 1.1, 2.2, 2.3, 2.4, 2.5 |
AL-S02: UpdatePermissionLevel writes audit entry | 2.1 |
AL-S03: BackfillPermission writes audit entry | 2.1 |
AL-S04: Role edits emit update action (not create) | 2.1 |