Asked for on 2026-11-11 for the dev environment: administrators need to delete printers and "print devices" from the admin screens, and "print device" needs a better name. One more fix rides along: a grouped read of the printing tables was refused for everybody.
1. Delete print stations and printers
Two new actions, on the list selection and on the record screen:
Action
Process
Table
Delete print stations
printingDeleteDevices
printing_device
Delete printers
printingDeleteSelectedPrinters
printing_printer
Steps: confirmation checkbox, then a review that counts what will happen and names every record that will not be deleted with the reason, then the delete, then the result.
What a station delete does (PrintingService.deleteDevice):
Refuses (409, per record) if the station holds a live credential, shows as ONLINE or PAUSED, and has jobs in CLAIMED, RECEIVED or SUBMITTING. Also refuses before anything is revoked if the schema step below was not applied.
Revokes the credential through the existing REVOKE control (unless already revoked).
In one transaction: fills the job name columns where missing; queued jobs become FAILED; sent-but-unreported jobs become OUTCOME_UNKNOWN; each of those gets a printing_event (SERVER_DELETION) naming the actor; writes printing_deletion rows (station and each printer); deletes the control receipts, the printers and the station.
A printer delete (deletePrinter) does the same for one printer and leaves its station in service.
Job history: jobs are never deleted. printing_job gains device_name, computer_name, printer_name, queue_name (written at submission, backfilled for existing rows). device_id, printer_id and printer_row_id keep the identifiers the records had, so retained-acceptance verification and idempotent replay read exactly what they read before; the foreign key from printing_job.device_id to printing_device is dropped. Nulling device_id was rejected because the job API, the retained-print facade and admission accounting all read it.
After a delete: the station's token returns 401; heartbeat, inventory, claim, content, event, control poll, control receipt and management actions return 404; a computer enrolled again is a new station; a queue shared again is a new printer, exposed and not approved; old jobs are attached to neither.
Permission: new printing_delete (PrintingDeletePermission), required together with printing_admin. Remove and Revoke are unchanged.
2. Names
"Print device" is now "Print station" in labels, actions, previews, the home page and process messages. "Printer" is unchanged. All of it comes from PrintingNames. Table, field and process names, HTTP API texts and the agent protocol are unchanged.
Note for PR 37 (feat/station-identity-20261110), which uses "print station" for a station that signs in as its own identity: its labels "Enroll a print station" (printingEnrollStation) and the field label "Print station" (station_name) now collide with the general name and need a qualifier there (for example "Enroll an unattended print station", "Station sign-in name"). PR 37 also hooks identity revocation into PrintingService.deviceAction; deleteDevice calls that method for a live station (revokeBeforeDelete), but a station that was already revoked is deleted without it, so PR 37 should revoke the identity from deleteDevice as well.
3. A grouped read is a read
PrintingReadPermission accepted QueryInput, GetInput, CountInput and the two metadata inputs, and refused everything else with "Use the printing management service to change operational state". QQQ does not tell a custom checker whether the check is for a read or a write, so the input type decides. AggregateInput and ExportInput are now accepted with exactly the permission a query needs. Still refused, by design or left for a decision: InsertInput, UpdateInput, DeleteInput (writes); RunBackendStepInput (QQQ's record-audit process passes it for a READ check, and it is not a read-only type); RecordSearchInput and the help search input (QQQ asks hasTablePermission with them, so the printing tables are left out of global record search and help search rather than failing).
Schema
Additive columns on printing_job, one dropped foreign key, one new table printing_deletion. initialize() does it; the PostgreSQL step for a host that owns its schema is in CHANGELOG.md.
Validation
Maven wrapper verify (Checkstyle and tests): ./mvnw -B -Pui-demo verify on 7ac849d, 217 tests, 0 failures, 0 errors, 1 skipped (PrintingWorkloadTest, skipped on main as well), 0 Checkstyle violations.
Shared contract compatibility considered: no wire, OpenAPI or protocol change; PrintingContractTest and PrintingUiApiContractTest pass unchanged.
New tests in PrintingDeleteTest (8): metadata and names; delete one station; delete several with per-record refusals and late traffic from a deleted station; re-enrollment after delete; delete a printer and rediscovery; permission refused without the grant; schema step (foreign key dropped, names backfilled, refusal when the key is still there); aggregate allowed to a reader and refused without the read permission. The aggregate test was run against the unfixed PrintingReadPermission and failed with the reported refusal.
Not run: PostgreSQL (tests use H2), a real Windows connector, any deployed environment. Physical printing is separate from a SPOOLED response.
## Change
Asked for on 2026-11-11 for the dev environment: administrators need to delete printers and "print devices" from the admin screens, and "print device" needs a better name. One more fix rides along: a grouped read of the printing tables was refused for everybody.
### 1. Delete print stations and printers
Two new actions, on the list selection and on the record screen:
| Action | Process | Table |
|---|---|---|
| Delete print stations | `printingDeleteDevices` | `printing_device` |
| Delete printers | `printingDeleteSelectedPrinters` | `printing_printer` |
Steps: confirmation checkbox, then a review that counts what will happen and names every record that will not be deleted with the reason, then the delete, then the result.
What a station delete does (`PrintingService.deleteDevice`):
1. Refuses (409, per record) if the station holds a live credential, shows as ONLINE or PAUSED, and has jobs in `CLAIMED`, `RECEIVED` or `SUBMITTING`. Also refuses before anything is revoked if the schema step below was not applied.
2. Revokes the credential through the existing REVOKE control (unless already revoked).
3. In one transaction: fills the job name columns where missing; queued jobs become `FAILED`; sent-but-unreported jobs become `OUTCOME_UNKNOWN`; each of those gets a `printing_event` (`SERVER_DELETION`) naming the actor; writes `printing_deletion` rows (station and each printer); deletes the control receipts, the printers and the station.
A printer delete (`deletePrinter`) does the same for one printer and leaves its station in service.
Job history: jobs are never deleted. `printing_job` gains `device_name`, `computer_name`, `printer_name`, `queue_name` (written at submission, backfilled for existing rows). `device_id`, `printer_id` and `printer_row_id` keep the identifiers the records had, so retained-acceptance verification and idempotent replay read exactly what they read before; the foreign key from `printing_job.device_id` to `printing_device` is dropped. Nulling `device_id` was rejected because the job API, the retained-print facade and admission accounting all read it.
After a delete: the station's token returns 401; heartbeat, inventory, claim, content, event, control poll, control receipt and management actions return 404; a computer enrolled again is a new station; a queue shared again is a new printer, exposed and not approved; old jobs are attached to neither.
Permission: new `printing_delete` (`PrintingDeletePermission`), required together with `printing_admin`. Remove and Revoke are unchanged.
### 2. Names
"Print device" is now "Print station" in labels, actions, previews, the home page and process messages. "Printer" is unchanged. All of it comes from `PrintingNames`. Table, field and process names, HTTP API texts and the agent protocol are unchanged.
Note for PR 37 (`feat/station-identity-20261110`), which uses "print station" for a station that signs in as its own identity: its labels "Enroll a print station" (`printingEnrollStation`) and the field label "Print station" (`station_name`) now collide with the general name and need a qualifier there (for example "Enroll an unattended print station", "Station sign-in name"). PR 37 also hooks identity revocation into `PrintingService.deviceAction`; `deleteDevice` calls that method for a live station (`revokeBeforeDelete`), but a station that was already revoked is deleted without it, so PR 37 should revoke the identity from `deleteDevice` as well.
### 3. A grouped read is a read
`PrintingReadPermission` accepted `QueryInput`, `GetInput`, `CountInput` and the two metadata inputs, and refused everything else with "Use the printing management service to change operational state". QQQ does not tell a custom checker whether the check is for a read or a write, so the input type decides. `AggregateInput` and `ExportInput` are now accepted with exactly the permission a query needs. Still refused, by design or left for a decision: `InsertInput`, `UpdateInput`, `DeleteInput` (writes); `RunBackendStepInput` (QQQ's record-audit process passes it for a READ check, and it is not a read-only type); `RecordSearchInput` and the help search input (QQQ asks `hasTablePermission` with them, so the printing tables are left out of global record search and help search rather than failing).
### Schema
Additive columns on `printing_job`, one dropped foreign key, one new table `printing_deletion`. `initialize()` does it; the PostgreSQL step for a host that owns its schema is in `CHANGELOG.md`.
## Validation
- [x] Maven wrapper verify (Checkstyle and tests): `./mvnw -B -Pui-demo verify` on 7ac849d, 217 tests, 0 failures, 0 errors, 1 skipped (`PrintingWorkloadTest`, skipped on `main` as well), 0 Checkstyle violations.
- [x] Shared contract compatibility considered: no wire, OpenAPI or protocol change; `PrintingContractTest` and `PrintingUiApiContractTest` pass unchanged.
- [x] Documentation/changelog updated: `CHANGELOG.md`, `README.md`, `docs/UI-WALKTHROUGH.md`, `docs/STORAGE-AND-RETENTION.md`.
New tests in `PrintingDeleteTest` (8): metadata and names; delete one station; delete several with per-record refusals and late traffic from a deleted station; re-enrollment after delete; delete a printer and rediscovery; permission refused without the grant; schema step (foreign key dropped, names backfilled, refusal when the key is still there); aggregate allowed to a reader and refused without the read permission. The aggregate test was run against the unfixed `PrintingReadPermission` and failed with the reported refusal.
Not run: PostgreSQL (tests use H2), a real Windows connector, any deployed environment. Physical printing is separate from a SPOOLED response.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Delete print stations (printingDeleteDevices) and Delete printers
(printingDeleteSelectedPrinters), on a selection and on the record screen,
behind a new permission, printing_delete, required in addition to
printing_admin. A station is revoked through the existing REVOKE control,
then its queued jobs are canceled, jobs sent without a reported result are
closed as OUTCOME_UNKNOWN, and its control receipts, printers and row are
deleted in one transaction. A station or printer that is connected and
printing is refused by name; the rest of the selection is still deleted.
Jobs are never deleted. printing_job gains device_name, computer_name,
printer_name and queue_name (the names at submission), keeps device_id,
printer_id and printer_row_id as the identifiers the records had, and loses
its foreign key to printing_device. printing_deletion records who deleted
what, when and what went with it (table Deletion history). A computer
enrolled again is a new station; a queue shared again is a new printer
awaiting approval.
Labels: "Print device" becomes "Print station" everywhere an administrator
reads it, from one place (PrintingNames). Table, field and process names,
the HTTP API texts and the agent protocol are unchanged.
PrintingReadPermission refused an aggregate (and an export) as a write, for
everybody. Both are reads and now need exactly the permission a query needs.
Schema step for hosts that own the schema is in CHANGELOG.md.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Change
Asked for on 2026-11-11 for the dev environment: administrators need to delete printers and "print devices" from the admin screens, and "print device" needs a better name. One more fix rides along: a grouped read of the printing tables was refused for everybody.
1. Delete print stations and printers
Two new actions, on the list selection and on the record screen:
printingDeleteDevicesprinting_deviceprintingDeleteSelectedPrintersprinting_printerSteps: confirmation checkbox, then a review that counts what will happen and names every record that will not be deleted with the reason, then the delete, then the result.
What a station delete does (
PrintingService.deleteDevice):CLAIMED,RECEIVEDorSUBMITTING. Also refuses before anything is revoked if the schema step below was not applied.FAILED; sent-but-unreported jobs becomeOUTCOME_UNKNOWN; each of those gets aprinting_event(SERVER_DELETION) naming the actor; writesprinting_deletionrows (station and each printer); deletes the control receipts, the printers and the station.A printer delete (
deletePrinter) does the same for one printer and leaves its station in service.Job history: jobs are never deleted.
printing_jobgainsdevice_name,computer_name,printer_name,queue_name(written at submission, backfilled for existing rows).device_id,printer_idandprinter_row_idkeep the identifiers the records had, so retained-acceptance verification and idempotent replay read exactly what they read before; the foreign key fromprinting_job.device_idtoprinting_deviceis dropped. Nullingdevice_idwas rejected because the job API, the retained-print facade and admission accounting all read it.After a delete: the station's token returns 401; heartbeat, inventory, claim, content, event, control poll, control receipt and management actions return 404; a computer enrolled again is a new station; a queue shared again is a new printer, exposed and not approved; old jobs are attached to neither.
Permission: new
printing_delete(PrintingDeletePermission), required together withprinting_admin. Remove and Revoke are unchanged.2. Names
"Print device" is now "Print station" in labels, actions, previews, the home page and process messages. "Printer" is unchanged. All of it comes from
PrintingNames. Table, field and process names, HTTP API texts and the agent protocol are unchanged.Note for PR 37 (
feat/station-identity-20261110), which uses "print station" for a station that signs in as its own identity: its labels "Enroll a print station" (printingEnrollStation) and the field label "Print station" (station_name) now collide with the general name and need a qualifier there (for example "Enroll an unattended print station", "Station sign-in name"). PR 37 also hooks identity revocation intoPrintingService.deviceAction;deleteDevicecalls that method for a live station (revokeBeforeDelete), but a station that was already revoked is deleted without it, so PR 37 should revoke the identity fromdeleteDeviceas well.3. A grouped read is a read
PrintingReadPermissionacceptedQueryInput,GetInput,CountInputand the two metadata inputs, and refused everything else with "Use the printing management service to change operational state". QQQ does not tell a custom checker whether the check is for a read or a write, so the input type decides.AggregateInputandExportInputare now accepted with exactly the permission a query needs. Still refused, by design or left for a decision:InsertInput,UpdateInput,DeleteInput(writes);RunBackendStepInput(QQQ's record-audit process passes it for a READ check, and it is not a read-only type);RecordSearchInputand the help search input (QQQ askshasTablePermissionwith them, so the printing tables are left out of global record search and help search rather than failing).Schema
Additive columns on
printing_job, one dropped foreign key, one new tableprinting_deletion.initialize()does it; the PostgreSQL step for a host that owns its schema is inCHANGELOG.md.Validation
./mvnw -B -Pui-demo verifyon7ac849d, 217 tests, 0 failures, 0 errors, 1 skipped (PrintingWorkloadTest, skipped onmainas well), 0 Checkstyle violations.PrintingContractTestandPrintingUiApiContractTestpass unchanged.CHANGELOG.md,README.md,docs/UI-WALKTHROUGH.md,docs/STORAGE-AND-RETENTION.md.New tests in
PrintingDeleteTest(8): metadata and names; delete one station; delete several with per-record refusals and late traffic from a deleted station; re-enrollment after delete; delete a printer and rediscovery; permission refused without the grant; schema step (foreign key dropped, names backfilled, refusal when the key is still there); aggregate allowed to a reader and refused without the read permission. The aggregate test was run against the unfixedPrintingReadPermissionand failed with the reported refusal.Not run: PostgreSQL (tests use H2), a real Windows connector, any deployed environment. Physical printing is separate from a SPOOLED response.
🤖 Generated with Claude Code