diff --git a/docs/native-hosts.md b/docs/native-hosts.md index 7af07a0..204e1e3 100644 --- a/docs/native-hosts.md +++ b/docs/native-hosts.md @@ -52,8 +52,8 @@ An already available Docker installation (including Docker CE) is reused. If the daemon is unavailable, the installer reports the Docker error and stops before enrollment; inspect `systemctl status docker` and `journalctl -u docker`. -The controller reconciles automatic setup on agent heartbeats and operation -polls. It first pulls `PLUTAINER_IMAGE` (default +The controller reconciles automatic setup on agent heartbeats. Command polls only +deliver work already queued. Setup first pulls `PLUTAINER_IMAGE` (default `ghcr.io/ayymoss/plutainer:latest`), then prepares the latest verified upload for T5 and for T6 when available, one setup operation at a time. Uploading new files or enrolling another host needs no manual propagation. Setup waits for an active, connected, diff --git a/src/lib/server/hosts/preparation.ts b/src/lib/server/hosts/preparation.ts index 8c6da02..313889d 100644 --- a/src/lib/server/hosts/preparation.ts +++ b/src/lib/server/hosts/preparation.ts @@ -173,7 +173,7 @@ function supersedeUndeliveredDownloads(hostId: string): void { } } -/** Agent heartbeats and polls drive durable preparation; no open UI or in-memory worker is needed. */ +/** Heartbeats drive durable preparation; idle checks do not reserve the SQLite writer. */ export function reconcileHostPreparation(hostId: string): HostPreparation { return db().transaction(() => { const host = getHost(hostId); @@ -203,5 +203,5 @@ export function reconcileHostPreparation(hostId: string): HostPreparation { scheduleAvailabilityRefresh(); } return status; - }).immediate(); + })(); } diff --git a/src/routes/api/agent/operations/+server.ts b/src/routes/api/agent/operations/+server.ts index bf7b135..aacb86d 100644 --- a/src/routes/api/agent/operations/+server.ts +++ b/src/routes/api/agent/operations/+server.ts @@ -1,10 +1,8 @@ import { pendingHostOperationRows, waitForHostWork } from '$lib/server/hosts/index.js'; import { authenticateAgent } from '$lib/server/hosts/http.js'; -import { reconcileHostPreparation } from '$lib/server/hosts/preparation.js'; import { hostOperationsResponse } from '$lib/server/hosts/delivery.js'; export async function GET({ request }: { request: Request }) { const host = authenticateAgent(request); - reconcileHostPreparation(host.id); const params = new URL(request.url).searchParams; const lane = params.get('lane') === 'health' ? 'health' : params.get('lane') === 'read' ? 'read' : params.get('lane') === 'write' ? 'write' : 'all'; let operations = pendingHostOperationRows(host.id, lane, false); diff --git a/tests/unit/host-preparation.test.ts b/tests/unit/host-preparation.test.ts index ba26d9f..8703e6b 100644 --- a/tests/unit/host-preparation.test.ts +++ b/tests/unit/host-preparation.test.ts @@ -183,6 +183,14 @@ describe('automatic host preparation', () => { }); expect(operations(host.id)).toEqual([]); + // Poll lanes only deliver queued work. One inventory heartbeat drives setup. + const reconcile = vi.spyOn(preparation, 'reconcileHostPreparation'); + expect((await (await pollOperations({ request: request('operations') })).json()).operations).toEqual([]); + expect(reconcile).not.toHaveBeenCalled(); + expect((await reportHeartbeat({ request: request('heartbeat', { inventory }) })).status).toBe(200); + expect(reconcile).toHaveBeenCalledOnce(); + reconcile.mockRestore(); + const firstPoll = await (await pollOperations({ request: request('operations') })).json(); expect(firstPoll.operations).toHaveLength(1); const runtimeOperation = firstPoll.operations[0];