Skip to content

feat(alerts): first-run empty-state launcher + migrate alerts lists to Electric collections#192

Merged
Makisuo merged 4 commits into
mainfrom
feat/alerts-empty-state-launcher
Jul 8, 2026
Merged

feat(alerts): first-run empty-state launcher + migrate alerts lists to Electric collections#192
Makisuo merged 4 commits into
mainfrom
feat/alerts-empty-state-launcher

Conversation

@Makisuo

@Makisuo Makisuo commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two changes landed together in the working tree (assembled from two concurrent sessions — reviewer note below).

1. Alerts empty-state redesign (primary)

Turns the alerts overview zero-rules empty state into a first-run starter launcher that fits Maple's operator-terminal identity instead of the generic Empty bell-badge.

  • Signature graphic — a "quiet monitor" SVG: a steady muted signal running below a dashed amber (--primary) threshold with a pulsing leading dot, reusing the existing .infra-ref-line (dashed draw-in) and .infra-pulse idioms. It reads as two true things at once: no incidents, and nothing is watching yet.
  • Starter templates — the five real ALERT_TEMPLATES (High error rate, Slow P95/P99, Low Apdex, Throughput drop) as chart-*-tinted tiles (hues mirror BUILTIN_SIGNAL_OPTIONS on the create form), each a one-click deep link into a pre-filled create form, plus a "Start from scratch" ghost link.
  • Admin-gated — non-admins get the graphic + "Ask an admin…", no CTAs (preserves the existing gate).
  • Template deep link — new template search param on /alerts/create; deriveInitialRuleDraft pre-applies the matching preset and skips the first-touch overlay. Unknown ids fall through to blank + overlay. Reuses applyTemplate, so the overlay, the tiles, and the create_alert_rule MCP tool resolve the same set.

Files: components/alerts/overview/alerts-empty-state.tsx (new), routes/alerts/create.tsx, components/alerts/alert-create-page-content.tsx (+ colocated test), components/alerts/overview/alerts-overview-tab.tsx.

2. Alerts data → TanStack DB + Electric collections (bundled)

Migrates alerts destinations & scrape targets off MapleApiAtomClient.query onto Electric-synced collections: useAlertDestinationsList, alertDestinations / scrapeTargets collections, use-scrape-target-checks, electric-sync shape route updates, settings + scrape-target section wiring, and the 0011_electric_publication_wave1 drizzle migration.

Reviewer note

The empty-state work depends on the Electric-collections migration (my edited alerts-overview-tab.tsx / alert-create-page-content.tsx call the new useAlertDestinationsList), so the two are shipped together and are green as a whole rather than in isolation.

Verification

  • @maple/web typecheck: clean.
  • Alerts + create-page vitest suites pass (incl. new template deep-link test: known id prefills & skips overlay, unknown id falls through to blank).
  • Empty state verified live in the dark-theme dev app: quiet-monitor graphic renders, all five tile hues resolve to their chart-* colors, ?template=low_apdex lands on the Apdex-prefilled form (< 0.8 threshold) with no overlay, and the blank path still opens the overlay.

🤖 Generated with Claude Code


Open in Devin Review

…o Electric collections

Redesign the alerts overview zero-rules empty state into a "starter launcher":
a purpose-built "quiet monitor" SVG (a steady signal held below a dashed amber
threshold, reusing .infra-ref-line / .infra-pulse) replaces the generic
bell-badge, and the five real starter templates surface as chart-*-tinted tiles
that deep-link into a pre-filled create form. Adds a `template` search param to
/alerts/create; deriveInitialRuleDraft pre-applies the matching preset and skips
the first-touch overlay (unknown ids fall through to blank + overlay), reusing
applyTemplate so the overlay, empty-state tiles, and create_alert_rule MCP tool
resolve the same set.

Also bundles the concurrent alerts-data migration to TanStack DB + Electric
collections: useAlertDestinationsList and the alertDestinations/scrapeTargets
collections, the electric-sync shape route updates, the settings + scrape-target
sections wiring, and the 0011_electric_publication_wave1 drizzle migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pullfrog

pullfrog Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Your LLM provider API key was rejected. Rotate the key in your provider dashboard, then update the matching GitHub Actions secret.

Update repo secret → · Model settings → · Setup docs → · Ask in Discord →

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Rerun failed job ➔View workflow run | via Pullfrog | Using Claude Opus𝕏

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Ingest Rust Test + Benchmark Results

Commit: 8f823308c20fdbe9c14e55ed7eefe2776029d93f

Load Benchmark — tinybird mode, median of 3 run(s) vs main

Metric main (median) PR (median) Delta
Requests/sec 1941.39 1947.57 +0.3% better
Rows/sec 19413.94 19475.70 +0.3% better
p50 latency 31.72 ms 31.80 ms +0.2% worse
p95 latency 37.96 ms 37.83 ms -0.3% better
p99 latency 41.34 ms 39.46 ms -4.6% better
Export catch-up 0.026 s 0.026 s -0.1% better
Max RSS 100.61 MiB 104.29 MiB +3.7% worse
Failures 0 0 same

Same code path on both sides (same LOAD_TEST_INGEST_MODE), so the delta column is meaningful. Numbers come from ubuntu-latest, which is noisy — treat single-digit-percent deltas as noise.

PR load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 24,
    "duration_seconds": 1.052165615,
    "export_catchup_seconds": 0.026205813,
    "request_rps": 1900.8414374005179,
    "row_rps": 19008.41437400518,
    "p50_ms": 32.542,
    "p95_ms": 37.827,
    "p99_ms": 40.121,
    "max_rss_mb": 105.2265625,
    "max_cpu_percent": 67.8,
    "avg_cpu_percent": 50.333333333333336
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 25,
    "duration_seconds": 1.02692088,
    "export_catchup_seconds": 0.026295137,
    "request_rps": 1947.5697095573712,
    "row_rps": 19475.697095573712,
    "p50_ms": 31.798,
    "p95_ms": 38.423,
    "p99_ms": 39.46,
    "max_rss_mb": 104.28515625,
    "max_cpu_percent": 67.8,
    "avg_cpu_percent": 56.0
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 26,
    "duration_seconds": 0.965432056,
    "export_catchup_seconds": 0.026203633,
    "request_rps": 2071.611344962426,
    "row_rps": 20716.113449624256,
    "p50_ms": 29.527,
    "p95_ms": 37.278,
    "p99_ms": 38.558,
    "max_rss_mb": 98.3203125,
    "max_cpu_percent": 71.4,
    "avg_cpu_percent": 52.35
  }
]
main load benchmark JSON (per-iteration)
[
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 24,
    "duration_seconds": 1.083577624,
    "export_catchup_seconds": 0.026574747,
    "request_rps": 1845.7376340211324,
    "row_rps": 18457.376340211325,
    "p50_ms": 33.036,
    "p95_ms": 42.968,
    "p99_ms": 45.334,
    "max_rss_mb": 101.6796875,
    "max_cpu_percent": 64.8,
    "avg_cpu_percent": 48.53333333333334
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 26,
    "duration_seconds": 1.030187432,
    "export_catchup_seconds": 0.025617597,
    "request_rps": 1941.3942918301882,
    "row_rps": 19413.94291830188,
    "p50_ms": 31.724,
    "p95_ms": 37.956,
    "p99_ms": 41.344,
    "max_rss_mb": 100.60546875,
    "max_cpu_percent": 69.6,
    "avg_cpu_percent": 51.56666666666666
  },
  {
    "ingest_mode": "tinybird",
    "requests": 2000,
    "successes": 2000,
    "failures": 0,
    "rows_sent": 20000,
    "rows_exported": 20000,
    "imports": 25,
    "duration_seconds": 0.958913149,
    "export_catchup_seconds": 0.026225924,
    "request_rps": 2085.6946242584063,
    "row_rps": 20856.94624258406,
    "p50_ms": 29.285,
    "p95_ms": 35.813,
    "p99_ms": 38.655,
    "max_rss_mb": 99.4765625,
    "max_cpu_percent": 73.2,
    "avg_cpu_percent": 44.900000000000006
  }
]

WAL-acked microbench (cargo bench --bench ingest_bench)

   Compiling maple-ingest v0.1.0 (/home/runner/work/maple/maple/apps/ingest)
    Finished `bench` profile [optimized] target(s) in 40.87s
     Running benches/ingest_bench.rs (target/release/deps/ingest_bench-581d2100de893627)
Gnuplot not found, using plotters backend
test ingest_accept/logs_10_rows_wal_ack ... bench:      512991 ns/iter (+/- 29101)
test ingest_accept/traces_10_spans_wal_ack ... bench:      545127 ns/iter (+/- 38495)

cargo test

test telemetry::tests::metrics_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::metrics_summary_data_points_are_dropped ... ok
test telemetry::tests::migrate_legacy_shard_relocates_frames_into_lanes ... ok
test telemetry::tests::pipeline_can_start_for_clickhouse_only_without_tinybird_credentials ... ok
test telemetry::tests::clickhouse_export_drops_passworded_non_https_endpoint_without_sending ... ok
test telemetry::tests::pipeline_e2e_exports_gzip_ndjson_to_fake_tinybird ... ok
test telemetry::tests::pipeline_e2e_exports_traces_to_fake_tinybird ... ok
test telemetry::tests::sampling_keeps_errors_even_when_ratio_low ... ok
test telemetry::tests::scraper_contract::scraper_otlp_json_decodes_with_gateway_serde_and_encodes_to_rows ... ok
test telemetry::tests::signal_tag_round_trips_all_variants ... ok
test telemetry::tests::pipeline_e2e_exports_metrics_to_fake_tinybird ... ok
test telemetry::tests::telemetry_signal_as_str_is_canonical_lowercase ... ok
test telemetry::tests::timestamp_has_nano_precision ... ok
test telemetry::tests::timestamps_match_clickhouse_datetime64_nine_format ... ok
test telemetry::tests::trace_encoder_matches_tinybird_row_shape ... ok
test telemetry::tests::traces_emit_exactly_the_jsonpaths_declared_in_datasources_ts ... ok
test telemetry::tests::wal_partial_drain_advances_cursor_without_truncating ... ok
test telemetry::tests::wal_round_trips_frame ... ok
test telemetry::tests::wal_truncates_after_full_drain_allowing_further_appends ... ok
test telemetry::tests::pipeline_exports_ready_org_to_clickhouse_without_tinybird_calls ... ok
test telemetry::tests::slow_clickhouse_lane_does_not_block_cosharded_tinybird_org ... ok
test telemetry::tests::clickhouse_breaker_sheds_after_threshold_failures ... ok

test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.78s

     Running unittests src/bin/load_test.rs (target/debug/deps/load_test-661a0aa1eb3f6d6d)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/maple_ingest-c33bf80c577edb95)

running 38 tests
test autumn::tests::flat_hardcap_with_remaining_allows ... ok
test autumn::tests::allowed_only_no_balance_field ... ok
test autumn::tests::flat_hardcap_depleted_blocks ... ok
test autumn::tests::flat_overage_allows ... ok
test autumn::tests::flat_sub_one_gb_remaining_still_allows ... ok
test autumn::tests::flat_unlimited_allows ... ok
test autumn::tests::nested_balance_object_with_remaining_allows ... ok
test autumn::tests::nested_overage_allows ... ok
test autumn::tests::null_balance_no_subscription_blocks ... ok
test autumn::tests::unrecognized_shape_returns_none ... ok
test autumn::tests::nested_balance_object_depleted_blocks ... ok
test tests::api_error_kind_maps_status_to_stable_label ... ok
test tests::clickhouse_destination_is_terminal_in_dual_mode ... ok
test tests::api_error_from_pipeline_maps_variants_to_status ... ok
test tests::clickhouse_destination_uses_native_pipeline_even_in_forward_mode ... ok
test tests::clickhouse_target_resolver_decrypts_current_schema_password ... ok
test tests::clickhouse_target_resolver_rejects_password_over_http ... ok
test tests::clickhouse_target_resolver_requires_current_schema ... ok
test tests::cloudflare_ndjson_payload_parses_multiple_records ... ok
test tests::cloudflare_log_record_maps_body_severity_and_attributes ... ok
test tests::cloudflare_validation_payload_is_detected ... ok
test tests::cloudflare_timestamps_support_rfc3339_unix_and_unix_nano ... ok
test tests::decrypt_aes256_gcm_matches_node_crypto_fixture ... ok
test tests::enrichment_overwrites_tenant_fields ... ok
test tests::extract_ingest_key_returns_sentinel_literal_unchanged ... ok
test tests::hash_is_deterministic ... ok
test tests::rejection_span_status_is_error_only_for_5xx ... ok
test tests::resolve_ingest_key_keeps_stale_schema_on_managed_native_path ... ok
test tests::resolve_connector_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_none_when_hash_missing ... ok
test tests::resolve_ingest_key_refreshes_routing_before_auth_cache_expires ... ok
test tests::resolve_ingest_key_returns_self_managed_false_when_no_settings_row ... ok
test tests::resolve_ingest_key_returns_self_managed_true_when_active_settings_row ... ok
test tests::sentinel_token_matches_only_exact_literal ... ok
test tests::tinybird_destination_keeps_forward_mode_on_forward_path ... ok
test autumn::tests::fails_open_on_transport_error ... ok
test tests::resolve_ingest_key_serves_last_known_routing_when_refresh_fails ... ok
test tests::forward_mode_switches_ready_org_to_clickhouse_without_forwarding_again ... ok

test result: ok. 38 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.23s

   Doc-tests maple_ingest

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

devin-ai-integration[bot]

This comment was marked as resolved.

…policy

useAlertDestinationsList() resolves off the live-synced collection, so its
Result is only ever `initial` (loading) or `success` — never a failure. The
`.onError` branch in the escalation policy section was therefore unreachable
dead code, and its "Retry" button was wired to the hook's no-op refresh. Remove
the dead error/retry branch; loading shows the skeleton and success renders the
form. Addresses Devin review finding on escalation-policy-section.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Makisuo Makisuo merged commit 785ae3f into main Jul 8, 2026
8 checks passed
@Makisuo Makisuo deleted the feat/alerts-empty-state-launcher branch July 8, 2026 22:17
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