Appearance
Connector protocol
Everything between the browser, the server and the agent. Keep this page and the connector's src/protocol.ts in step when either side changes.
Localhost API (browser → agent)
Plain HTTP on 127.0.0.1:41780 (printing.local_agent_url). Every response carries CORS headers and Access-Control-Allow-Private-Network: true, which Chrome requires for a public page calling the loopback address.
Until the agent is paired any origin may call it. Once paired, only the paired server's origin, plus the allowedOrigins in the agent's config, is accepted; other origins get 403.
| Method | Path | Body | Answer |
|---|---|---|---|
GET | /status | { name, version, hostname, platform, paired, serverUrl, agentId, agentName, connected } | |
GET | /printers | { printers: [{ name, displayName, isDefault }] } | |
POST | /pair | { serverUrl, token, agentId } | The new status. 409 if already paired, 422 on a bad body, 502 if the server refused the token. |
The browser side is resources/js/Hooks/useLocalPrintAgent.ts.
Pairing sequence
POST {shop}/settings/printers/agents(RegisterPrintAgent) with the name, hostname and platform read from/status. Answers{ agent: PrintAgentData, token, serverUrl }. This is the only time the token is legible; the database keepstoken_hash = sha256(token).POST http://127.0.0.1:41780/pairwith{ serverUrl, token, agentId: agent.uuid }.- The agent immediately calls
POST /print-agent/heartbeatto validate the token. If that fails the pairing is not saved and/pairanswers502with the server's message.
Server API (agent → server)
Prefix /print-agent, registered in routes/printAgent.php under the print-agent middleware group: throttle:print-agent (120/min, keyed by token), AuthenticatePrintAgent, SubstituteBindings. Every request carries Authorization: Bearer <token>; a bad token is 401.
| Method | Path | Body | Answer |
|---|---|---|---|
POST | /heartbeat | { hostname, platform, arch, version, printers: [{ name, display_name, is_default }] } | { agent: { id, name, shop: { id, name } }, websocket: { key, host, port, scheme, channel } } |
POST | /broadcasting/auth | { socket_id, channel_name } | The Pusher auth signature. 403 unless channel_name is the agent's own channel. |
GET | /jobs/pending | { jobs: [PrintJobPayload] } — status sent, sent within the delivery window, oldest first. | |
GET | /jobs/{uuid}/file | The PDF (application/pdf). 404 for another agent's job or a deleted file. | |
POST | /jobs/{uuid}/status | { status: received | printed | failed, error? } | 204. Terminal jobs ignore late reports. |
PrintJobPayload is { id, printer, copies, document, file_name } (PrintJob::toAgentPayload()).
The websocket block is read from the default broadcasting connection. When Reverb is reachable by the shop's computers on a different address than the one the application uses, override it with PRINTING_WEBSOCKET_HOST, PRINTING_WEBSOCKET_PORT and PRINTING_WEBSOCKET_SCHEME.
WebSocket (server → agent)
The agent speaks the Pusher protocol to Reverb (wss://{host}:{port}/app/{key}?protocol=7) and subscribes to private-print-agents.{agent uuid} using the signature from /broadcasting/auth.
| Event | Payload | Meaning |
|---|---|---|
print-job.created | { job: PrintJobPayload } | Fetch and print. Sent by App\Events\PrintJobCreated. |
print-agent.revoked | {} | The shop removed this computer; forget the pairing. Sent by App\Events\PrintAgentRevoked. |
Both events implement ShouldBroadcastNow: they are fired from inside the queued render job or the delete action, and a second queue hop would only add latency.
WebSocket (server → browser)
On the existing private-shop.{id} channel, consumed with useBroadcasting().listenToShopChannel():
| Event | Payload | Who listens |
|---|---|---|
PrintAgentsChanged | {} | The settings page reloads agents. |
PrintJobUpdated | { id, status, error, document, printer } | The settings page reloads recentJobs; the print menu toasts the outcome of jobs it sent. |
Job lifecycle
pending ──render──▶ sent ──agent ack──▶ received ──▶ printed
│ │
└──render failed──▶ failed ◀─────────────┘ (print failed)| Status | Set by | Meaning |
|---|---|---|
pending | CreatePrintJob | Row exists, PDF not rendered yet. |
sent | RenderPrintJob | File on disk, event broadcast, nobody has claimed it. Picked up on reconnect for printing.delivery_window_hours. |
received | agent | Downloading and handing to the spooler. |
printed | agent | Handed to the OS spooler. The file is deleted. |
failed | server or agent | error holds the reason. The file is deleted. |
printed means the operating system accepted the job; a paper jam after that is the printer's to report.
Browser API (browser → server)
| Route | Action | Purpose |
|---|---|---|
GET shop.settings.printers.index | GetPrinterSettings | The settings page. |
GET shop.settings.printers.targets | GetPrintTargets | { agents, settings } for print menus, fetched on first open. |
POST shop.settings.printers.agents.store | RegisterPrintAgent | Pair; answers the token once. Manager or owner. |
PUT shop.settings.printers.agents.update | RenamePrintAgent | Rename. |
DELETE shop.settings.printers.agents.destroy | DeletePrintAgent | Unpair, clear defaults, broadcast print-agent.revoked. |
PUT shop.settings.printers.defaults.update | UpdatePrintSettings | { defaults: { estimate, work_order, invoice: { print_agent_id, printer, copies } | null } }. Validates the agent belongs to the shop and reports that printer. |
POST shop.settings.printers.test | PrintTestPage | Sends resources/views/print/test-page.blade.php to a printer. |
POST shop.workOrders.print | PrintWorkOrder | { print_agent_id?, printer?, copies? }; without a printer, the default for the document type applies, or 422 on printer explains what to configure. |
Outside Inertia pages, enums in JSON responses are plain strings (status: "sent"), not the { __enum, value } envelope. PrintMenu.vue reads them as such.
Configuration
config/printing.php:
| Key | Env | Default | Purpose |
|---|---|---|---|
disk | PRINTING_DISK | local | Private disk holding rendered PDFs until printed. |
path | print-jobs | Directory on that disk. | |
online_threshold_minutes | 10 | An agent is online when its last heartbeat is younger than this. | |
delivery_window_hours | 24 | How long a sent job waits for an offline agent. | |
local_agent_url | PRINTING_LOCAL_AGENT_URL | http://127.0.0.1:41780 | Where the browser looks for the agent. |
websocket.host/port/scheme | PRINTING_WEBSOCKET_* | broadcasting connection | Reverb endpoint as seen from the shop. |
Agent config (~/.vroum-connector/config.json): port, host, pairing { serverUrl, token, agentId }, allowedOrigins.