Skip to content

feat(import): make SaaS→self-host import work + dup guard + regression test#31

Merged
samir1498 merged 2 commits into
mainfrom
feat/import-from-saas
May 18, 2026
Merged

feat(import): make SaaS→self-host import work + dup guard + regression test#31
samir1498 merged 2 commits into
mainfrom
feat/import-from-saas

Conversation

@samir1498
Copy link
Copy Markdown
Collaborator

Why

The documented two-step adapt-cli-export.tsPOST /api/import was broken in v1.7.1 and prior — the adapter emitted snake_case (url_monitors, status_code, timeout_ms) while /api/import reads camelCase (urlMonitors, statusCode, timeoutMs), so SaaS→self-host onboarding imported zero rows, silently. Scripts ship in the Docker image and this is the documented workflow, so it's been broken on :latest.

What

  • adapt-cli-export.ts rewritten as an importable adaptSaaSExport() emitting exactly what /api/import consumes; thin CLI kept. SaaS-only resources (suites/heartbeats/alert channels/status pages/incidents) are reported as skipped, not silently dropped.
  • scripts/import-from-saas.ts — the one-shot wrapper: obs export (or --from <file>) → in-process adapt → authed POST. --dry-run (no key/DB).
  • Idempotency: there's no unique-name constraint, so re-import would silently duplicate (the old plan's "skips on conflict" assumption was wrong). Added a pre-flight name-collision check that refuses (exit 1) unless --allow-duplicates. Non-atomic by design — catches the realistic re-run footgun; documented.
  • scripts/import-from-saas-test.ts — pure contract regression guard (no DB/server): asserts the adapter output is exactly /api/import's shape and the snake_case keys never reappear. Wired into the integration suite + bun run test:import. Anti-vacuous: regressing to snake_case fails it.
  • docs/import-from-saas.md + README pointer.

Verified

End-to-end against a live stack: clean import creates rows (was zero), re-run refuses with exit 1 (no duplicate), --allow-duplicates overrides, --dry-run needs no key. Contract test passes; negative control proves it catches the exact regression. Pre-push suite green incl. the new stage.

Scope: minimal working importer (HTTP + API checks). Full parity (QA suites / channels / status pages / db monitors → import) is a deliberate follow-up. Distinct from backup/restore (that's instance→instance DR, not SaaS migration). Patch → 1.7.2 (the shipped, documented workflow was broken on :latest).

🤖 Generated with Claude Code

samir1498 and others added 2 commits May 18, 2026 18:16
The documented two-step (`adapt-cli-export.ts` → `POST /api/import`) was
broken in v1.7.1 and earlier: the adapter emitted snake_case
(`url_monitors`, `status_code`, `timeout_ms`) but `/api/import` reads
camelCase (`urlMonitors`, `statusCode`, `timeoutMs`), so it imported
**zero rows, silently**. SaaS→self-host onboarding has never functioned.

- adapt-cli-export.ts: rewritten as an importable `adaptSaaSExport()`
  emitting exactly what `/api/import` consumes; thin CLI kept. Reports
  SaaS resources with no self-host mapping (suites/heartbeats/alert
  channels/status pages/incidents) as skipped, not silently dropped.
- scripts/import-from-saas.ts: one-shot wrapper — `obs export` (or
  `--from <file>`) → in-process adapt → authed POST. `--dry-run`.
- Idempotency: there's no unique-name constraint, so re-import would
  silently DUPLICATE (not skip, as the old plan assumed). Added a
  pre-flight name-collision check that refuses (exit 1) unless
  `--allow-duplicates`. Non-atomic by design — catches the realistic
  re-run footgun, documented.
- docs/import-from-saas.md + README pointer.

Verified end-to-end against a live stack: clean import creates rows
(was zero), re-run refuses with exit 1 (no duplicate), --allow-duplicates
overrides, --dry-run needs no key. Patch → 1.7.2 (scripts ship in the
image; the documented workflow was broken on :latest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/import-from-saas-test.ts — pure, no DB/server. Asserts
adaptSaaSExport() output is exactly the camelCase shape /api/import
consumes, and that the snake_case keys which caused the silent
zero-import regression never reappear. Wired into run-integration.sh
(pre-push + CI) and `bun run test:import`.

Anti-vacuous: regressing the adapter back to snake_case fails the
`camelCase top-level keys` and `no snake_case keys leak` checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@samir1498 samir1498 merged commit 5a481e2 into main May 18, 2026
5 checks passed
@samir1498 samir1498 deleted the feat/import-from-saas branch May 18, 2026 16:22
samir1498 added a commit that referenced this pull request May 26, 2026
…onsistency (#88)

Three small UX fixes batched together.

1. Settings → API keys: clicking "I've copied it" used to be a visual
   no-op. The reveal-panel host's innerHTML was populated when oneTimeKey
   was set but never cleared on the next render. Add an explicit
   `if (!oneTimeKey && host) host.innerHTML = '';` so the panel goes
   away when dismissed.

2. Import JSON dialog: drop the `www.` from the observeone.com/cli link.
   Aligns with README (which already uses bare apex) so every operator-
   facing URL in the repo points at the same canonical host.

3. Heartbeat detail page back-link reads "← back" instead of
   "← All monitors", matching the other 7 monitor types' detail pages.

Test: tests/ui/api-key-dismiss.e2e.spec.ts creates a key, asserts the
reveal panel is visible, clicks dismiss, asserts the cleartext element
is hidden afterward.

Closes friction #13, #31, #32.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant