Files
harbor-net/docs/product/application-state.md
Dmitriy Petrov 198669694c
Some checks failed
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Failing after 1m21s
Handle stale sync state in client UI
2026-07-11 19:31:35 +03:00

4.0 KiB

Harbor application state v1

GET /api/state is the canonical Harbor domain snapshot. Successful POST and DELETE endpoints return the same snapshot as state while retaining their v0 response fields for compatibility.

{
  "apiVersion": 1,
  "revision": 42,
  "generatedAt": "2026-07-11T15:00:00.000Z",
  "mode": "client",
  "subscription": {
    "status": "ready",
    "host": "provider.example/…",
    "fetchedAt": "2026-07-11T14:58:00.000Z",
    "userInfo": {}
  },
  "selection": {
    "desiredServerId": "Amsterdam",
    "appliedServerId": "Amsterdam"
  },
  "connection": {
    "desired": "running",
    "process": "running",
    "startedAt": "2026-07-11T14:59:10.000Z",
    "lastError": null
  },
  "route": {
    "mode": "local-vpn",
    "gatewayAddress": null,
    "lastVerifiedAt": null,
    "reason": "auto"
  },
  "operation": {
    "kind": null,
    "status": "idle",
    "startedAt": null,
    "error": null
  },
  "servers": []
}

The backend owns subscription metadata, servers, desired/applied selection, desired/process connection state, route and current operation. React may keep only unsaved form values, pending selection and visual state. Browser transport freshness is not part of this contract.

Revision rules

revision is persisted in the existing state.json and increases on externally visible transitions, including operation start/completion/failure, import, refresh, forget, apply, start, stop and Gateway Auto changes. generatedAt is response metadata and does not change revision by itself.

A consumer must eventually apply only snapshots whose revision is at least its current revision. The frontend comparison and stale/offline transport envelope are intentionally handled by TASK-002 and TASK-003.

The frontend keeps the accepted snapshot in one reducer and replaces it only when incoming.revision is greater. Equal revisions preserve object identity so background polling does not replay decorative transitions. Mutation responses are applied directly; polling requests started before a mutation are logically invalidated and cannot overwrite its result. A locally pending server choice remains local until a newer snapshot acknowledges it or removes that server.

Browser transport state lives beside, not inside, the domain snapshot. It records boot status, last successful sync time and consecutive failures. Three failed polls mark the retained snapshot stale; the next successful GET or mutation clears that marker. An initial failure shows control-unreachable, incompatible-api or fatal without inventing domain state.

Desired and applied state

selection.desiredServerId records the user's requested server. selection.appliedServerId changes only after its sing-box configuration has been applied. Likewise, connection.desired records intent while connection.process reports the observed runtime. A failed operation can therefore leave desired and applied values different without pretending that the request succeeded.

Server IDs are currently derived from the existing trimmed subscription tag. Stable IDs across cosmetic renames are deferred to TASK-008.

Compatibility and migration

No path, volume or file is renamed. A legacy state.json without revision, appliedTag or connectionDesired is normalized on read: revision starts at 0, the legacy selectedTag is treated as both desired and applied, and connection intent is inferred from the existing config. The next domain write stores the added fields. Existing unknown fields remain untouched.

During the v0 compatibility window, the snapshot also exposes selectedTag, singboxRunning, servers[].tag, gatewayAuto and the other previous GET fields. Mutation responses retain their previous result fields and add state. The canonical subscription object never contains the full subscription URL.

Rollback is code-only: deploy the previous build. The added persisted fields are ignored by the previous implementation, so no data rewrite is needed. Revisions created by the newer build remain harmless integers in state.json.