Operational runbook for the fk-dev GCP dev-mirror VM: how to connect, how to deploy a branch by bind-mount (no image rebuild), how the two NÚSZ fixes (FKITDEV-8747 + FKITDEV-8959) now ride one branch, and the proven standalone recipe for FKITDEV-8959 test case TC-8959-02 (key-inaccessible image deletion). All facts verified on fk-dev on 2026-07-03.

For Agents

  • Box: fk-dev = GCP dev-mirror VM on tailnet taild4189d.ts.net (100.91.108.61). It replaces the on-prem ssh Facekom / lederera-447-fk-hardver box, which is decommissioned (offline since ~2026-06-27) — never use that alias again. Host summary: dev-build-host.
  • Connect: command ssh ops@fk-dev.taild4189d.ts.net (user ops, Tailscale SSH, no keypair). Gotcha: a kaku wrapper shadows ssh/scp in Claude’s non-interactive shell → always use command ssh / command scp.
  • Operator UI: https://oss-fk-dev.taild4189d.ts.net (vuer_oss, :10081 inside the container).
  • NÚSZ branch: customization/nusz tip d426cc6ae1 carries both fixes — 8747 (5099b8ad8b, PR #7929) and 8959 (519d3b933e, PR #8010, merged). Both are vuer_oss-only.
  • Deployed customization/nusz → fk-dev vuer_oss on 2026-07-03. Restore the box’s original commit bd8923d69f (InstaCash line) when NÚSZ testing is done.
  • TC-8959-02 result: PASS — key-inaccessible image is blanked + archived + encryptionId=null (not thrown). See Result — PASS.
  • Provisioning + architecture of fk-dev: tailscale-gcp-dev-box-migration.

fk-dev — connection & operational reality

fk-dev is the deployed, in-use realization of the GCP dev-box mirror designed in tailscale-gcp-dev-box-migration. As of 2026-07-03 the 8b per-service Tailscale-sidecar overlay is live (that note ended at “push gated on user go”; the box is now running it).

PropertyValue
Host / tailnet IPfk-dev.taild4189d.ts.net / 100.91.108.61
Tailnettaild4189d.ts.net
SSHcommand ssh ops@fk-dev.taild4189d.ts.net — user ops, Tailscale SSH (no keypair); Tailscale is already up on the Mac
Operator UIhttps://oss-fk-dev.taild4189d.ts.net (vuer_oss Express :10081 inside)
Stackvuer_docker compose, per-service Tailscale sidecars (oss-fk-dev, css-fk-dev, esign-*-fk-dev, portal-fk-dev), app source bind-mounted at /workspace/<repo>, supervisord inside each container
Postgrescontainer postgresql

Operational gotchas

  • kaku shadows ssh. In Claude’s non-interactive shell a kaku wrapper intercepts plain ssh/scp → use command ssh / command scp. (Same class of gotcha as the on-prem box — see dev-box-esign-container-startup-failures-2026-06-01.)
  • Postgres peer-auth blocks psql -U postgres on the postgresql container. Don’t fight it — reach the DB through the app’s own Sequelize connection instead (the verification harness below does exactly this).
  • nginx_proxy is crash-looping (pre-existing). It doesn’t matter: the per-service Tailscale sidecars are the access path and bypass nginx_proxy entirely.

Deploy recipe — bind-mount branch swap (no image rebuild)

Because app source is bind-mounted at /workspace/<repo> and each container runs supervisord, deploying a branch is a git checkout + in-container rebuild + restart — no image build, no compose recreate.

Git-forward gotcha — the box has no GitHub key

The box’s git remote is git@github.com:TechTeamer/vuer_oss (SSH), but fk-dev has no GitHub deploy key. Forward your own agent: ssh-add ~/.ssh/id_ed25519 on the Mac (key id_ed25519, no passphrase), then connect with command ssh -A so git fetch on the box authenticates as you.

# From the Mac: load key + connect with agent forwarding
ssh-add ~/.ssh/id_ed25519
command ssh -A ops@fk-dev.taild4189d.ts.net
 
# On the box, in the bind-mounted source tree:
cd /workspace/vuer_oss
git fetch origin customization/nusz        # forwarded key authenticates
git checkout customization/nusz            # tip d426cc6ae1
 
# Rebuild + restart INSIDE the container (node_modules/dist are bind-mounted):
docker exec vuer_oss sh -c 'cd /workspace/vuer_oss && yarn install && yarn build'
docker exec vuer_oss supervisorctl restart all

Verify the deploy:

  • docker exec vuer_oss supervisorctl status all → every program RUNNING
  • vuer_oss log shows Web server is listening on 10081
  • operator UI returns HTTP 302: https://oss-fk-dev.taild4189d.ts.net

Restore after testing

The box’s original vuer_oss commit was bd8923d69f (the InstaCash line). customization/nusz was checked out over it for NÚSZ testing on 2026-07-03 — check bd8923d69f back out (same recipe) when NÚSZ testing is done so fk-dev returns to its prior baseline.

NÚSZ branch consolidation — both fixes on customization/nusz

The two NÚSZ tickets in flight both landed on one branch, both vuer_oss-only:

TicketCommitPRState
FKITDEV-8747 daily-stat “eltérés”5099b8ad8b#7929merged
FKITDEV-8959 image deletion519d3b933e#8010merged

customization/nusz tip = d426cc6ae1 contains both. Deploying that one branch to fk-dev exercises both fixes together (their shared test cases live at FKITDEV-8959-8747-test-cases.md).

Stale remote-tracking ref — the 8959 branch was auto-deleted on merge

The old feature branch fix/FKITDEV-8959-nusz-image-deletion was auto-deleted from origin when PR #8010 merged, but the local origin/fix/FKITDEV-8959-nusz-image-deletion ref was still present and stale (facekom clones use narrowed fetch refspecs with no prune — same failure mode as FKITDEV-8959’s parent work and the SSH-topology notes). Always git ls-remote origin <branch> to confirm a branch actually exists before assuming a local origin/... ref is real. The fix now lives only on customization/nusz (via the merge).

FKITDEV-8959 — TC-8959-02 verification (key-inaccessible image deletion)

Proven on fk-dev, 2026-07-03. TC-8959-02 (see the test-case file) is the scenario the old code died on: an old image whose customer encryption key is offline / inaccessible at cron time. The fixed cron must blank + archive the image instead of throwing The key options property is required… and looping forever.

Code path under test (customization/nusz)

The cron RemoveAttachmentDataCronJob is a thin wrapper over CustomRemoveOldDataCronService.removeAttachmentData():

RemoveAttachmentDataCronJob.run()
  → CustomRemoveOldDataCronService.removeAttachmentData()
      → getOldImageAttachments()      # query: type LIKE 'image/%'
                                       #        AND isArchived = false
                                       #        AND createdAt < cutoff
                                       # batched, excludes the `file` blob
      → removeOldAttachments()         # the KEY GUARD:
          const encryption = encryptionId ? getEncryption() : null
          if (encryption?.key) encryptBuffer(empty, { key })   // normal path
          else { file = Buffer.from(''); encryptionId = null; skippedNoKey++ }  // key-offline path
          isArchived = true
          save()

Key dependency chain (why a standalone harness needs the full crypto stack)

encryption.key is not a column — it is a Sequelize getter (server/db/model/encryption.js) that resolves the actual key lazily:

So a standalone harness must init serviceContainer.service.cryptos.{media,data,attachment}, CryptoService, and customerKeyStorage. Stubbing customerKeyStorage.getKey() => null faithfully models “customer key offline / inaccessible” — the exact real-world bug trigger (a customer-managed key not loaded at the midnight cron). This is the same encryption-model machinery documented under sms-verification-code-dev-testing (cryptos.data = DataCryptoService, per-row key).

Test harness pattern (worked)

A Node script placed in /workspace/vuer_oss/bin/ (so ../server/... requires resolve), delivered and run inside the container:

  1. require('../server/bootstrap/process-settings')() — load config.
  2. Override serviceContainer.logger with a console Proxy (so any logger.x() call just prints).
  3. setupServices — instantiate cryptos.{media,data,attachment} + CryptoService + a customerKeyStorage whose getKey() is stubbed to return null.
  4. setupDb../server/db/sequelize authenticate + load ../server/db/models.
  5. Seed via raw sequelize.query INSERTs (bypasses model hooks):
    • encryptions row with key = NULL
    • attachments row: type = 'image/png', old createdAt, isArchived = false, encryptionId = <enc>
  6. Call the real getOldImageAttachments() + removeOldAttachments(), scoped by a synthetic date window around the seeded row.
  7. SELECT the row before and after.

Delivery: command scp the script to /tmp on the box → docker cp into the container → docker exec … node <script>rm.

Table shapes (fk-dev NÚSZ DB, ids start at 1 — empty DB)

  • attachments — NOT NULL: name, size, type, createdAt, updatedAt. file is BYTEA nullable; encryptionId FK nullable; there is no required roomId.
  • encryptionskey is VARCHAR(44) nullable (holds the wrapped key; NULL models “no key”).

Result — PASS

BEFORE : { isArchived: false, encryptionId: 3, file_bytes: 9 }
TALLY  : { processed: 1, archived: 1, skippedNoKey: 1, errored: 0 }
AFTER  : { isArchived: true,  encryptionId: null, file_bytes: 0 }

The key-guard blanks the file (9→0 bytes), nulls encryptionId, and archives the row instead of the old throw — exactly the TC-8959-02 expected behaviour. Matches the devel e2e result recorded in the test-case file (image blanked + archived + encryptionId → null).

Open question — retention period (7 vs 28)

fk-dev config: attachments.active = true, expiryDays = 7 (dev.json) / 28 (docker.json). The parent report ASSNUSZ-117 says 7 days; the deployed default is 28. The fix deletes at whatever is configured (it does not change the period). Still OPEN — confirm the agreed retention with NÚSZ before sign-off; if 7 is required it is a one-line config change.