유대선
프로젝트로
·기술 회고·3

Stop the app from lying: dormant flags hide all mock, plus live cloud→local sync and a mobile cockpit fix

The founder caught the specialized screens (Marketing/Customer/founder-ops) presenting hardcoded sample data as if it were his real pipeline. We didn't delete anything — we routed every mock surface through a single dormant feature-flag registry (hide, don't destroy), backed up the seed rows, kept the real action-SDK engine untouched, made cloud→local sync re-render without a refresh, and fixed the phone cockpit's garbled render.

What the founder saw

Opening 알리다 (Marketing) showed "30 content rows / 10 leads / 27 approvals" — a dense pipeline he never created. Same for 고객 (Customer), and a whole "Invoice reissue" workflow screen wired to invoice-system · payment-processor · crm. His question was blunt: "이런 화면도 다 사기인거냐?" (is this all fake?).

It was. The data came from hardcoded constants:

Verified by grep: executeAction (the real executor) is called 2 times in the whole app, while MOCK_CONNECTORS is referenced 18 times — almost entirely to display fake capabilities. The screens looked operational but did nothing.

Real vs fake — the line we drew

The mock data is garbage to surface, but the action-sdk engine around it is not: 32 functions for an untrusted-content prompt-injection envelope, an approval-gate / risk-permission model, provider-neutral execution plans + receipts, and manifest validation. Crucially, verificationEvidence (which the Mission Control merge gate actually uses) imports from the same package. Deleting it would have broken a real, load-bearing system.

So the decision (the founder's: "숨기는 애들은 미사용 플래그 걸어"): hide, don't delete; flag what's hidden so future-me never wonders if it's in use.

The dormant registry

apps/desktop/src/featureFlags.ts is the single source of truth for what's parked:

export const DORMANT = {
  marketingMockActions:  { enabled: false, since: "2026-06-20", why: "…가짜 — 채널 연결 0" },
  customerMockWorkflows: { enabled: false, since: "2026-06-20", why: "founder_workflow_mock 기반" },
  founderOpsMockScreens: { enabled: false, since: "2026-06-20", why: "mock 데모 화면" },
  seedSampleLedgers:     { enabled: false, since: "2026-06-20", why: "박힌 가짜 기록(백업됨)" },
} as const satisfies Record<string, DormantFeature>;

Every render path for those surfaces is now isEnabled(...) && (...). Every parked constant/block carries an @dormant featureFlags:<key> comment, so grep -rn "@dormant" lists everything dormant at a glance. Flip a flag to true to revive. The seed rows were copied verbatim to docs/archive/removed-fake-seed-data-2026-06-20.md before anything changed — nothing was physically deleted.

The render tests now assert the inverse of what they used to: the mock sections must NOT appear by default (expect(html).not.toContain("Campaign actions"), etc.). 330/330 vitest green = the engine and merge gate are intact.

Two more fixes in the same pass

cloud → local was data-only. CloudSync pulls remote checklist/widget changes into localStorage every 5s, but ZonePage read its checkbox state once at mount, so a cloud edit didn't show until you navigated away and back. sync.ts now counts rows it actually applied and dispatches a dalkkak:remote-data event only when something changed; ZonePage re-reads on that event. No manual refresh.

The phone cockpit rendered as a field of dots and "??". Two causes: (1) the same tmux session attached on phone and Mac at different sizes makes tmux fill the dead window region with ·; (2) the phone's terminal font has no emoji glyph (👀 → "??"), worsened by no unicode-width addon. Fix in apps/terminal-cockpit/static/index.html: load the unicode11 addon (term.unicode.activeVersion = "11"), add "Apple Color Emoji" to the font stack, and on session-switch run tmux set -g window-size latest + setw -g aggressive-resize on before attach so the window follows whichever client is active. The HTML is include_str!-embedded, so the cockpit binary was rebuilt.

Pattern

When a surface looks like it works, separate the engine (keep — it may be real and load-bearing) from the data/connectors it operates on (the fake part). Don't delete in anger; route everything questionable through one flag registry with a written reason and a grep-able tag, back up the raw data, and let the tests assert the mock is gone. "Reversible and labeled" beats "deleted and forgotten."