@@ -305,16 +305,21 @@ retries.
305305 a delivery at or below the recorded revision is dropped and still answers 204. The ledger and the
306306 values advance in one transaction, so a revision is never recorded without the values it
307307 admitted.
308+ - ` PUT .../block ` applies a revision-fenced block state, the same write as dashboard-api's
309+ ` PUT /v1/management/projects/{projectID}/block ` (see Dashboard API below): the
310+ ` projection.project_blocks ` revision and ` teams.is_blocked ` / ` blocked_reason ` commit together,
311+ and every accepted delivery, a dropped one included, evicts the team's auth cache.
308312- ` DELETE /unstable/management/projects/{projectID} ` soft-deletes the project, described below.
309313
310- Both revision fences are the target's for the same reason: the caller can only fence what it
314+ The revision fences are the target's for the same reason: the caller can only fence what it
311315sends, so two deliveries in flight arrive in whichever order the network gives them, and the older
312- one has to be refused where it lands. The project, membership and limits writes live in
316+ one has to be refused where it lands. The project, membership, limits and block writes live in
313317` internal/management ` with their post-commit cache evictions rather than in the handlers.
314318
315- A limits delivery may carry ` decided_at ` , when the caller decided that revision; the ledger row
316- stores it beside the revision. Each delivery the ledger accepts records the seconds since then in
317- ` dashboard-api.management.apply_lag ` with ` projection ` ` project_limits ` ; one without ` decided_at `
319+ A limits or block delivery may carry ` decided_at ` , when the caller decided that revision; the
320+ ledger row stores it beside the revision. Each delivery the ledger accepts records the seconds
321+ since then in ` dashboard-api.management.apply_lag ` with ` projection ` ` project_limits ` or
322+ ` project_blocks ` ; one without ` decided_at `
318323counts in ` dashboard-api.management.origin_unknown ` instead, never as zero lag. Dropped, refused
319324and failed deliveries record nothing. The names keep the dashboard-api prefix because dashboard-api
320325records its block deliveries under the same two metrics, so one alert and one dashboard read both.
@@ -341,12 +346,31 @@ deletes memberships, keys and envs created since (a sandbox that paused before t
341346it leaves a snapshot); and enqueues the teardown again, which is a no-op while one is still open.
342347
343348Afterwards the API's deleted-project middleware, running after authentication, answers 404 to every
344- non-GET route under ` /unstable/management/ ` that names ` {projectID} ` : the upsert, member, limits
345- and settings writes. The DELETE is exempt so a retry still reaches it. dashboard-api guards its own
349+ non-GET route under ` /unstable/management/ ` that names ` {projectID} ` : the upsert, member, limits,
350+ block and settings writes. The DELETE is exempt so a retry still reaches it. dashboard-api guards its own
346351management and admin writes the same way. Shared auth refuses a membership or API key still left
347352for the project with 403, and the API leaves the project out of a user's team list. Volumes, builds
348353and stored artifacts are not reclaimed.
349354
355+ #### Team bans (` /unstable/admin/teams ` )
356+
357+ ` PUT /unstable/admin/teams/{teamID}/ban ` sets ` teams.is_banned ` , evicts the team's auth cache so
358+ its API keys stop authenticating, then kills its running sandboxes and cancels its builds through
359+ the same orchestrator and template-manager calls as ` POST /admin/teams/{teamID}/sandboxes/kill `
360+ and ` .../builds/cancel ` . The ban commits first: a kill or cancel step that fails is reported in
361+ the 200 response and does not undo it. The work is detached from the request, so a caller that
362+ disconnects does not stop it. Each team's kill and cancel run side by side, and all of a request's
363+ kills and cancellations share a 25-second budget; a step the budget cuts short is reported as
364+ failed with what it counted so far. A build delete already
365+ under way keeps its own 30 seconds, so a request ends within about 55 seconds. A failed cache
366+ eviction answers 500, so the caller's retry repeats the ban and its containment; banning a banned
367+ team contains it again. ` DELETE .../ban ` clears the flag and evicts the cache.
368+ ` POST /unstable/admin/teams/bans ` takes up to 100 teams. It stores every ban first, five teams at
369+ a time, then contains the banned teams, five at a time under the one budget, and sorts each team
370+ into banned, not found or failed; one team's failure does not stop the others. A deleted project
371+ answers 404 to the single-team routes and counts as not found in the bulk route. The logic lives
372+ in ` internal/team ` .
373+
350374### Orchestrator (` packages/orchestrator ` )
351375
352376A single Go binary running on every sandbox node (as root). ` ORCHESTRATOR_SERVICES ` selects its
@@ -610,7 +634,8 @@ state. The `projection.project_blocks` revision and `public.teams.is_blocked` /
610634` blocked_reason ` commit together. Older or duplicate revisions leave stored state
611635unchanged. Every accepted delivery invalidates the team auth cache; eviction
612636failures return an error so retries can finish invalidation after commit. The API
613- uses this state when admitting billable work. It is the only route that writes the
637+ uses this state when admitting billable work. It and the API's copy at
638+ ` /unstable/management/projects/{projectID}/block ` are the only routes that write the
614639team's block state.
615640
616641Block deliveries may carry ` decided_at ` , when the caller decided that
0 commit comments