* ✨ Enrich backend audit events with team and entity ids
Backend audit events now carry the data a consumer needs to rebuild
state without joining production tables:
- prepare-rpc-event adds :team-id when the event carries a project or
file id; commands that already loaded the team set it themselves.
- add-profile-to-team! emits add-team-member on every real insert.
- create-project and create-file emit the new row id.
- clone-template, import-binfile and duplicate-project emit one event
per created entity.
- both binfile versions return {:file-ids [...]} from import-files!.
Closes#12219
AI-assisted-by: deepseek-v4.1-flash
* ✨ Emit delete and hard-delete audit events with entity data
The event stream must let Nexus rebuild local state without joining
production tables. This completes the enrichment with the delete paths:
- delete-team keeps returning nil but now carries the deleted row
(id, name, timestamps) in the audit props, like delete-project.
- create-team adds created-at to its audit props.
- delete-profile carries id, email, fullname and deleted-at; its
props used to be empty because every param is namespaced.
- objects-gc emits hard-delete-team/project/file per purged batch,
with the deleted ids, only when the audit log is on.
- delete-team-member needs no change: the delete is physical and the
params already carry team-id and member-id.
The nitrate push-audit-events tests now drop the membership events
emitted by their own profile setup before counting.
Closes#12219
AI-assisted-by: muse-spark-1.3-free
* ✨ Announce cascade deletes and missing ids in audit events
The :delete-object worker task marked teams, projects and files without
leaving any event, so an event-sourced projection kept them alive for
weeks. It now emits one cascade-deleted event per execution with every
affected id, in the same transaction, only with :audit-log on. Rows
purged by objects-gc and snapshot cascades stay silent on purpose.
Also: create-team-with-invitations carries the new team id, and
update-team-member-role names the demoted member on owner transfers.
Closes#12219
AI-assisted-by: muse-spark-1.3-free
* 🐛 Identify the SREPL profile delete in audit events
The SREPL delete-profile! emitted no profile-id and empty props, so the
row could not be attributed by any consumer. It now carries the profile
id in both fields. delete-team! named the wrong cause in its context.
Closes#12219
AI-assisted-by: muse-spark-1.3-free
* 🐛 Identify the SREPL profile restore in audit events
The SREPL restore-profile! emitted no profile-id, so the consumer
keyed on the zero profile instead of the restored one. It now carries
the profile id like a normal profile event, and no consumer special
case is needed.
Closes#12219
AI-assisted-by: muse-spark-1.3-free
* 🐛 Stop attributing SREPL events to the subject profile
profile-id names the actor, and SREPL calls have none: setting the
subject id blamed the user for an admin action and would have leaked
those rows into PostHog as user activity. The subject id stays in
props, where the consumer resolves it, and the event profile stays
zero.
Closes#12219
AI-assisted-by: muse-spark-1.3-free
* ✨ Expose file analysis data for the nexus pipeline
The nexus file-security pipeline needs file content and scheduling
state from the backend. This adds a read-only management method,
`get-file-for-analysis`, returning the semantic projection of one
file (pages, frames, texts, links, interactions; ids as strings;
capped with a truncated flag), plus identity, revn, timestamps and
share state. No prior projection code existed, so the method ships
its own pure `file->projection`.
Audit events now carry what the `:local` consumer schedules from:
revn and modified-at on update-file, name and timestamps on every
create-file emitter, deleted-at on delete-file, and new
create/delete-share-link events with file and link ids.
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Rename deletion audit events to action names
Event names are actions, not past tense: `cascade-deleted` becomes
`delete-object-with-cascade` with the same shape and transaction.
The three `hard-delete-*` events merge into `hard-delete-objects`:
one event per purged chunk carrying only that chunk's entity key
(`:team-ids`, `:project-ids` or `:file-ids`), because each GC
section purges in its own chunk transactions and a single event
could never share them all. Every submit stays inside its delete
transaction, `triggered-by` stays annotated, and silent paths stay
silent.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Read worker parallelism knobs with plain config keys
The runner wiring looked up `:app.main/worker-*-parallelism`
(namespaced) while the schema and the environment provide plain
keys, so `PENPOT_WORKER_DEFAULT_PARALLELISM` and
`PENPOT_WORKER_WEBHOOK_PARALLELISM` never applied and workers
always ran with the fallback. Read the keys the config holds.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix binfile audit test with a real file row
`submit-file-created-event` reads the light columns back from the
row, so the mocked importer must return an existing file id: a
phantom id emits no event and the test saw zero `create-file`
events. Create the file first and use its id. The audit-log memory
now tells both bulk-event helpers apart.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix audit event fidelity from review findings
The review of the audit enrichment found data-fidelity gaps; fix
them while the branch is fresh.
Membership events now belong to the actor: the flows where the
owner adds somebody else pass an explicit actor id, the role is
derived from the flags (the table has no role column), and the
event carries the request context like its siblings.
Also: team-id resolution skips read events, duplicate-project
emits the project name and timestamps, delete-profile is
idempotent again, the GC tests assert physical purge, and
get-file-for-analysis leaves an audit trace.
AI-assisted-by: muse-spark-1.3-contributor
* ♻️ Nest cascade ids under :cascade in delete event
The cascade event carried every affected id flat next to the
object identity; nest them under `:cascade` so the payload reads
as header plus affected set. The event is an action like its
siblings, not a command. Drop the `:audit-log` flag re-check:
`submit` already fans out per flag.
AI-assisted-by: muse-spark-1.3-contributor
* 📎 Update copyright name on file header
The company name changed; a single header in this branch still
carried the old one. Align it with the current form.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix nil event-id on webhook trigger audit rows
process-event generated the row id inside append-audit-entry, so the
:process-webhook-event task never received it and the trigger row
stored a nil :event-id. Assign the id up-front and share it between
the stored row and the worker params; the telemetry shadow row keeps
its own id to avoid a PK collision. Add regression tests covering
the handoff and the end-to-end trigger row.
AI-assisted-by: muse-spark-1.3-contributor
objects-gc deleted every kind of object inside one transaction per proc,
looping until empty, so a single row that a foreign key still references
aborted the whole run: the task died on its first file every night, after
the earlier steps had already spent around twenty minutes. The per-file
savepoint only saved the files proc and left the same shape everywhere
else.
Now every proc streams its pending keys with db/plan inside a read-only
transaction of its own and deletes them chunk by chunk, each chunk in a
transaction of its own with FOR UPDATE SKIP LOCKED. A chunk that fails is
reported with l/err and skipped, so the run reaches the end and the next
run retries the skipped rows.
AI-assisted-by: muse-spark-1.3-contributor