From 01566d2fe05281b9bb93b81f1f2e70b8f380230d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 10 Nov 2026 02:24:44 +0100 Subject: [PATCH 1/3] chore(repo): remove the dead self-host Docker path, fix CI path filters and repo scripts Docker - docker-compose.yml keeps only postgres and electric (what `bun db:up` starts). The api, web, ingest, alerting, scraper and otel-collector services could not start: the API is a Worker with no `start` script, apps/alerting has no Dockerfile, and the ingest build context was wrong. - Delete apps/api/Dockerfile, apps/web/Dockerfile and apps/web/nginx.conf, and the root `docker:build` / `docker:up` scripts. Drop the README "Docker (Local)" section. Docs - README and CONTRIBUTING no longer describe an embedded PGlite path for local entrypoints (nothing implements it). PR previews are described as Neon branches. - CONTRIBUTING: chat and triage run in apps/ai; the model overrides are MAPLE_TRIAGE_MODEL_OPENROUTER and MAPLE_TRIAGE_MODEL_WORKERS_AI. - apps/ios/README.md: five tabs, and the push code lives in packages/backend. - apps/landing/README.md replaces the Astro starter template. CI - `ts` filter: patterns are OR-ed, so the negated '!apps/ingest/**' and '!apps/ios/**' matched every file outside those paths and the filter was always true. Use the positive extglob 'apps/!(ingest|ios)/**' instead, and list the root files the TypeScript jobs read (knip.json, bunfig.toml, patches/**, the generated ingest insert mappings). - `cli` filter: the workspace closure of apps/cli and apps/local-ui is 21 packages and 6 libs, so list packages/** and lib/** whole, as `workers` does. - shellcheck lints by the same globs as the `shell` filter instead of a hand-kept list. Scripts - install.sh stages the bundle beside the targets and renames it into place: `cp` over a running binary fails with "Text file busy" on Linux. - uninstall.sh names everything it leaves under ~/.maple. - build-local-binary.sh runs from the repo root, so it works from any directory. - Delete package-charts.sh and bench-cold-queries.sh (no references). - generate-ios-openapi.ts drops getSpan and getLog, which no Swift code calls; the spec is regenerated. Dependencies - apps/landing drops the unused direct @astrojs/markdown-satteri dependency and generate-og.mjs imports playwright from its own workspace. - packages/email lists effect and @maple/strings as runtime dependencies. --- .github/workflows/ci.yml | 49 ++- CONTRIBUTING.md | 36 +-- README.md | 28 +- apps/api/Dockerfile | 30 -- .../MapleAPI/Sources/MapleAPI/openapi.json | 296 ------------------ apps/ios/README.md | 16 +- apps/landing/DESIGN.md | 2 +- apps/landing/README.md | 47 +-- apps/landing/package.json | 1 - apps/landing/scripts/generate-og.mjs | 5 +- apps/web/Dockerfile | 36 --- apps/web/nginx.conf | 62 ---- bun.lock | 7 +- docker-compose.yml | 95 ------ package.json | 2 - packages/email/package.json | 6 +- packages/email/src/template.ts | 6 +- scripts/bench-cold-queries.sh | 54 ---- scripts/build-local-binary.sh | 8 +- scripts/generate-ios-openapi.ts | 2 - scripts/install.sh | 12 +- scripts/package-charts.sh | 29 -- scripts/publish-maple-otel-chart.sh | 2 +- scripts/uninstall.sh | 10 +- 24 files changed, 93 insertions(+), 748 deletions(-) delete mode 100644 apps/api/Dockerfile delete mode 100644 apps/web/Dockerfile delete mode 100644 apps/web/nginx.conf delete mode 100644 scripts/bench-cold-queries.sh delete mode 100755 scripts/package-charts.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d08f0b72fa..733ad61183 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,39 +55,39 @@ jobs: - '.github/workflows/ci.yml' - '.github/actions/**' - 'scripts/ci/**' + # Patterns are OR-ed, so a negated one ('!apps/ingest/**') would match + # every file outside that path and make this always true. The extglob + # is a positive pattern: every app except ingest (Rust) and ios (Swift, + # ios.yml's). It excludes by prefix, so an app named ingest* or ios* + # needs its own line. ts: - *base - 'alchemy.run.ts' + - 'knip.json' + - 'bunfig.toml' + - 'patches/**' - 'test/**' - - 'apps/**' + - 'apps/!(ingest|ios)/**' - 'examples/**' - 'packages/**' - 'lib/**' - 'scripts/**' - 'skills/**' - - '!apps/ingest/**' # Read by @maple/domain's tests (turbo.json test inputs). - 'apps/ingest/src/ai_session/**' - 'apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json' - # Swift is ios.yml's; ios:openapi:check below still covers the contract. - - '!apps/ios/**' - # The workspace closure of apps/cli + apps/local-ui. Update it when - # their package.json workspace deps change. + # Generated; clickhouse:schema:check verifies it. + - 'apps/ingest/src/clickhouse_insert_mappings.rs' + # What apps/cli + apps/local-ui are built from. Their workspace closure + # is 21 of packages/ and 6 of lib/, so both are listed whole, as in `workers`. cli: - *base - 'apps/cli/**' - - 'packages/local-server/**' - 'apps/local-ui/**' - # local-ui renders apps/web's source modules and the shared kit. + # local-ui renders apps/web's source modules. - 'apps/web/src/**' - - 'packages/views/**' - - 'packages/effect-sdk/**' - - 'packages/domain/**' - - 'packages/query-engine/**' - - 'packages/infra/**' - - 'packages/ui/**' - - 'packages/browser/**' - - 'packages/browser-session/**' + - 'packages/**' + - 'lib/**' - 'scripts/build-local-binary.sh' - 'scripts/gen-ui-embed.ts' # The archive probes test the CLI's own on-disk format, which only @@ -183,21 +183,8 @@ jobs: # shellcheck is preinstalled; a runner of its own would be ~90% setup. - name: shellcheck if: ${{ steps.filter.outputs.shell == 'true' }} - run: >- - shellcheck scripts/install.sh scripts/uninstall.sh scripts/build-local-binary.sh - apps/cli/test/native-checkpoint-smoke.sh - apps/cli/test/native-checkpoint-refresh-probe.sh - apps/cli/test/native-local-store-migration.sh - apps/cli/test/native-archive-smoke.sh - apps/cli/test/native-archive-crash-recovery-probe.sh - apps/cli/test/native-archive-adversarial-probe.sh - apps/cli/test/native-archive-gc-probe.sh - apps/cli/test/native-archive-merge-probe.sh - apps/cli/test/native-retention-probe.sh - apps/cli/test/native-remote-auth-smoke.sh - apps/cli/docker/entrypoint.sh - apps/cli/docker/build-image.sh - apps/cli/docker/image-smoke.sh + # The same globs as the `shell` filter, so a new script is linted without a list edit. + run: shellcheck scripts/*.sh apps/cli/test/*.sh apps/cli/docker/*.sh # The installers claim POSIX /bin/sh; check them against it. - name: sh -n diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67e406bdc9..dfeddf5752 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,8 +87,8 @@ CLICKHOUSE_USER=maple CLICKHOUSE_PASSWORD=maple CLICKHOUSE_DATABASE=default -# App database (`alchemy dev` binds MAPLE_DB to the docker Postgres on 5499) -# MAPLE_DB_URL is intentionally blank for the Workers; see db:migrate:local below. +# App database: nothing to set. `alchemy dev` binds MAPLE_DB to the docker Postgres on 5499; +# see db:migrate:local below. # Auth — self-hosted mode (no Clerk account required) MAPLE_AUTH_MODE=self_hosted @@ -269,18 +269,18 @@ Default health port: `3475` Loads `../../.env.local` via `bun --env-file`. -### 5. AI chat and triage (in `apps/api`) +### 5. AI chat and triage (`apps/ai`) -`/chat` and `/investigations/*` are served by `apps/api` itself: the `ChatSession` Durable -Object owns each transcript and the agent turn runs in-process on `@opencode-ai/ai` against the -Workers AI `AI` binding. There is no second worker to start — but the chat routes only work -when `apps/api` is running **under `alchemy dev`** (`bun dev api`), because a plain -`bun` process has neither the Durable Object namespace nor the `AI` binding. +`/chat` and `/investigations/*` are forwarded by `apps/api` to the `apps/ai` Worker over a service +binding: the `ChatSession` Durable Object there owns each transcript and runs the agent turn. +Run both under `alchemy dev` (`bun dev api ai`), because a plain `bun` process has no Durable +Object namespace. -The `AI` binding needs a Cloudflare account with Workers AI, through an alchemy profile -(`bunx alchemy login`). Confirm the default model id in `apps/ai/src/platform/Llm.ts` is still in -the catalog — `cd apps/api && bunx wrangler ai models list`; a retired id returns `410`. -Override it with `MAPLE_TRIAGE_MODEL`. +The agent runs on OpenRouter by default and needs `OPENROUTER_API_KEY`. Set +`MAPLE_LLM_PROVIDER=workers-ai` to run it on the Workers AI `AI` binding instead, which needs a +Cloudflare account with Workers AI through an alchemy profile (`bunx alchemy login`). The default +model ids live in `apps/ai/src/platform/Llm.ts`. Override them per provider with +`MAPLE_TRIAGE_MODEL_OPENROUTER` or `MAPLE_TRIAGE_MODEL_WORKERS_AI`. > **`MAPLE_ORG_ID_OVERRIDE` breaks the investigation chat.** It pins every API request to > one org, but the browser addresses the session as `:`. With the @@ -294,11 +294,11 @@ sets the opt-in for you; set it yourself when you run the API outside the dev st When an autonomous investigation fails to start, the `investigations.error` column names it: -| `error` value | Cause | -| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| `agent_unavailable: …` | `apps/api` isn't running under `alchemy dev`, so there is no `CHAT_SESSION` binding | -| `start_failed: a turn is already running for this investigation` | The session is busy; retry | -| `diagnosis_timeout: …` | The turn ran but never called `submit_diagnosis` within 15 minutes | +| `error` value | Cause | +| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `agent_unavailable: …` | No `ChatSession` binding: `apps/ai` isn't running under `alchemy dev` with the API | +| `start_failed: a turn is already running for this investigation` | The session is busy; retry | +| `diagnosis_timeout: …` | The turn ran but never called `submit_diagnosis` within 15 minutes | ### All at once @@ -331,7 +331,7 @@ Health checks: ## Tests and quality ```bash -bun test +bun run test # Vitest via turbo, not `bun test` bun typecheck bun run format # oxfmt + oxlint --fix ``` diff --git a/README.md b/README.md index 1d8aa9f20e..38b2d93ad0 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ claude mcp add --transport http maple https://api.maple.dev/mcp | Path | What lives there | | --------------------------- | ------------------------------------------------------------------------ | -| `apps/web` | Dashboard (Foldkit, Vite) | +| `apps/web` | Dashboard (Foldkit, Vite) | | `apps/api` | Effect HTTP API, auth and OAuth | | `apps/ai` | MCP server, chat agent and investigations | | `apps/ingest` | OTLP ingest gateway: key auth, org enrichment, forwarding | @@ -121,21 +121,6 @@ bun run build bun run test ``` -## Docker (Local) - -Run the local multi-service stack (API + web + ingest + otel collector): - -```bash -docker compose -f docker-compose.yml up --build -``` - -Services: - -- API: `http://localhost:3472` -- Web: `http://localhost:3471` -- Ingest: `http://localhost:3474` -- OTEL collector: `4317` (gRPC), `4318` (HTTP), `13133` (health/extensions) - ## Cloudflare Deploy (Alchemy) Deployments run on **Alchemy v2** (Effect-based): the root `alchemy.run.ts` exports a @@ -153,8 +138,8 @@ Stage grammar is `prd` / `pr-` / dev names, resolved via `resolveHyperdriveRefId`, `resolveMapleProfile`). prd binds the dashboard-managed Hyperdrive by config ID (`resolveHyperdriveRefId`) — origin credentials never touch a deploy. `MAPLE_PG_URL` is only needed for dev stages, whose Hyperdrive alchemy -manages itself. PR previews bind **no database at all** (their profile's `database` is `"none"`): -DB-backed routes 500, everything else in the preview works. +manages itself. PR previews get their own Neon branch, migrated by the deploy and deleted on +teardown (`docs/pr-previews.md`). Run locally: @@ -239,14 +224,13 @@ For ingest + key auth, set these at minimum in your root `.env` when running the - `INGEST_REPLAY_MAX_SESSION_BYTES` (optional; ceiling on the decompressed rrweb payload one replay session may record, default 1 GiB, `0` disables) -## Persistence (PostgreSQL / PGlite) +## Persistence (PostgreSQL) Maple persists application state in PostgreSQL: - Production uses PlanetScale Postgres through Cloudflare Hyperdrive. -- Wrangler development uses the Docker Postgres started by `bun db:up`. -- Non-Worker local entrypoints use embedded PGlite under `apps/api/.data/pglite`; set - `MAPLE_DB_URL=memory://` for an ephemeral database. +- Local development (`bun dev`) uses the Docker Postgres started by `bun db:up`, on port 5499. +- Tests use embedded PGlite (`createTestDb()`); they need no Docker Postgres. Migration commands: diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile deleted file mode 100644 index 9d47c4be27..0000000000 --- a/apps/api/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -# Build context: monorepo root (.) -FROM oven/bun:1.4.4 AS base -WORKDIR /app - -FROM base AS pruner -COPY . . -RUN bunx turbo prune @maple/api --docker --out-dir /app/out - -FROM base AS installer -COPY --from=pruner /app/out/json/ ./ -COPY --from=pruner /app/out/bun.lock ./bun.lock -RUN apt-get update && \ - apt-get install -y --no-install-recommends python3 make g++ && \ - rm -rf /var/lib/apt/lists/* -RUN bun install -COPY --from=pruner /app/out/full/ ./ -RUN bunx turbo typecheck - -FROM oven/bun:1.4.4-slim AS runner -WORKDIR /app - -COPY --from=installer --chown=bun:bun /app /app -USER bun - -ENV NODE_ENV=production -ENV PORT=3472 -EXPOSE 3472 - -WORKDIR /app/apps/api -CMD ["bun", "run", "start"] diff --git a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json index 605d88b983..be8e2efde7 100644 --- a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json +++ b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json @@ -11647,150 +11647,6 @@ ] } }, - "/v2/logs/{id}": { - "get": { - "description": "Returns a log by its opaque `log_…` ID. Requires `logs:read`.", - "operationId": "getLog", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "$ref": "#/components/schemas/LogId" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Log" - } - } - }, - "description": "An OpenTelemetry log record." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/LogIdInvalidError failure. HTTP 400. | The @maple/http/errors/WarehouseInvalidSqlError failure. HTTP 400. | The @maple/http/v2/InvalidRequestError failure. HTTP 400." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/InvalidCredentialsError failure. HTTP 401. | The @maple/http/v2/InvalidDpopProofError failure. HTTP 401. | The @maple/http/errors/UnauthorizedError failure. HTTP 401." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/InsufficientScopeError failure. HTTP 403. | The @maple/http/v2/OrganizationAccessDeniedError failure. HTTP 403. | The @maple/http/v2/NoActiveOrganizationError failure. HTTP 403." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/LogNotFoundError failure. HTTP 404." - }, - "429": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseQuotaExceededError failure. HTTP 429. | The @maple/http/v2/RateLimitError failure. HTTP 429.", - "headers": { - "Retry-After": { - "description": "Seconds to wait or an HTTP-date indicating when the request may be retried.", - "example": 60, - "schema": { - "oneOf": [ - { - "minimum": 1, - "type": "integer" - }, - { - "type": "string" - } - ] - } - } - } - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseMalformedQueryError failure. HTTP 500. | The @maple/http/errors/WarehouseScopeError failure. HTTP 500. | The @maple/http/errors/OrgClickHouseSettingsEncryptionError failure. HTTP 500. | The @maple/http/v2/ResponseSchemaError failure. HTTP 500. | The @maple/http/v2/UnexpectedError failure. HTTP 500." - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseQueryError failure. HTTP 502. | The @maple/http/errors/WarehouseAuthError failure. HTTP 502. | The @maple/http/errors/WarehouseConfigError failure. HTTP 502. | The @maple/http/errors/WarehouseClientError failure. HTTP 502. | The @maple/http/errors/WarehouseSchemaDriftError failure. HTTP 502. | The @maple/http/errors/WarehouseResultDecodeError failure. HTTP 502. | The @maple/http/errors/OrgClickHouseSettingsStoredConfigInvalidError failure. HTTP 502." - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseUpstreamError failure. HTTP 503. | The @maple/http/errors/ApiKeyLookupPersistenceError failure. HTTP 503. | The @maple/http/errors/AuthorizationUnavailableError failure. HTTP 503." - }, - "504": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/WorkerUnavailableError failure. HTTP 504." - } - }, - "security": [ - { - "bearer": [] - } - ], - "summary": "Retrieve a log", - "tags": [ - "Logs" - ] - } - }, "/v2/mobile_devices": { "get": { "description": "Returns the devices the current user has registered in the current organization. Requires the `mobile_devices:read` scope.", @@ -13692,158 +13548,6 @@ ] } }, - "/v2/traces/{trace_id}/spans/{span_id}": { - "get": { - "description": "Returns one span with complete attribute maps. Requires `traces:read`.", - "operationId": "getSpan", - "parameters": [ - { - "in": "path", - "name": "trace_id", - "required": true, - "schema": { - "$ref": "#/components/schemas/_maple_TraceId" - } - }, - { - "in": "path", - "name": "span_id", - "required": true, - "schema": { - "$ref": "#/components/schemas/_maple_SpanId" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Span" - } - } - }, - "description": "An OpenTelemetry span." - }, - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseInvalidSqlError failure. HTTP 400. | The @maple/http/v2/InvalidRequestError failure. HTTP 400." - }, - "401": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/InvalidCredentialsError failure. HTTP 401. | The @maple/http/v2/InvalidDpopProofError failure. HTTP 401. | The @maple/http/errors/UnauthorizedError failure. HTTP 401." - }, - "403": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/InsufficientScopeError failure. HTTP 403. | The @maple/http/v2/OrganizationAccessDeniedError failure. HTTP 403. | The @maple/http/v2/NoActiveOrganizationError failure. HTTP 403." - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/SpanNotFoundError failure. HTTP 404." - }, - "429": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseQuotaExceededError failure. HTTP 429. | The @maple/http/v2/RateLimitError failure. HTTP 429.", - "headers": { - "Retry-After": { - "description": "Seconds to wait or an HTTP-date indicating when the request may be retried.", - "example": 60, - "schema": { - "oneOf": [ - { - "minimum": 1, - "type": "integer" - }, - { - "type": "string" - } - ] - } - } - } - }, - "500": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseMalformedQueryError failure. HTTP 500. | The @maple/http/errors/WarehouseScopeError failure. HTTP 500. | The @maple/http/errors/OrgClickHouseSettingsEncryptionError failure. HTTP 500. | The @maple/http/v2/ResponseSchemaError failure. HTTP 500. | The @maple/http/v2/UnexpectedError failure. HTTP 500." - }, - "502": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseQueryError failure. HTTP 502. | The @maple/http/errors/WarehouseAuthError failure. HTTP 502. | The @maple/http/errors/WarehouseConfigError failure. HTTP 502. | The @maple/http/errors/WarehouseClientError failure. HTTP 502. | The @maple/http/errors/WarehouseSchemaDriftError failure. HTTP 502. | The @maple/http/errors/WarehouseResultDecodeError failure. HTTP 502. | The @maple/http/errors/OrgClickHouseSettingsStoredConfigInvalidError failure. HTTP 502." - }, - "503": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/errors/WarehouseUpstreamError failure. HTTP 503. | The @maple/http/errors/ApiKeyLookupPersistenceError failure. HTTP 503. | The @maple/http/errors/AuthorizationUnavailableError failure. HTTP 503." - }, - "504": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MapleErrorEnvelope" - } - } - }, - "description": "The @maple/http/v2/WorkerUnavailableError failure. HTTP 504." - } - }, - "security": [ - { - "bearer": [] - } - ], - "summary": "Retrieve a span", - "tags": [ - "Traces" - ] - } - }, "/v2/widget_credentials/{installation_id}": { "delete": { "description": "Retires the installation's credential for this organization; the app calls this on sign-out and when the user leaves the organization. Idempotent — an installation with nothing to revoke is already in the requested state. Requires the `widget_credentials:write` scope.", diff --git a/apps/ios/README.md b/apps/ios/README.md index 99eb0fe9c8..e20856cd25 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -1,20 +1,22 @@ # Maple for iOS -A native SwiftUI client for Maple, reading the **v2 public API**. Three tabs, -in the order the questions get asked — see [`PRODUCT.md`](PRODUCT.md): +A native SwiftUI client for Maple, reading the **v2 public API**. Four tabs +and a search tab, in the order the questions get asked (see [`PRODUCT.md`](PRODUCT.md)): -- **Home** — is anything wrong right now? Status headline, open alerts with +- **Overview**: is anything wrong right now? Status headline, open alerts with the rule's own last hour, services needing attention, what's new in 24h. -- **Services** — the list, and a detail that leads with a health verdict and +- **Services**: the list, and a detail that leads with a health verdict and its reason, one full-width scrubbable chart per golden signal (throughput, errors, latency percentiles), window aggregates, scoped alerts, the busiest operations re-rankable by failures / p95 / volume, and issues. It paints from one request, `GET /v2/services/{name}/overview`, which composes the summary, an all-signal timeseries and the operations server-side; alerts and issues arrive in a second pass. -- **Alerts** — the triage hub: incidents (with a "why" detail: what the rule +- **Triage**: the triage hub, with incidents (with a "why" detail: what the rule saw, what changed on the service, likely cause, timeline), error issues, anomalies. +- **Explore**: traces, logs and read-only dashboards under one tab. +- **Search**: services, issues and trace ids; a 32-hex query opens that trace. ## Getting started @@ -48,7 +50,7 @@ Config/ xcconfigs; Secrets.xcconfig is gitignored Maple/App entry point, auth gate, build-time config, Route Maple/Auth token provider, session state machine, org picker Maple/DesignSystem tokens, type scale, service colours, shared primitives -Maple/Features Home, Services, Alerts (incidents/anomalies), Issues +Maple/Features Home, Services, Alerts (incidents/anomalies), Issues, Explore, Search Maple/Components LoadableView, formatting, Sparkline, alert formatting Maple/Fixtures FixtureAPI — the app without Clerk or a network Maple/Widgets publishes the widget's snapshot; background refresh @@ -71,7 +73,7 @@ incident is opened (or from the bell on Home), receives the APNs token via tab root. Registration is per organization; sign-out unregisters. A tapped notification lands on the incident (`AppNavigation.openIncident`). -The server side is `apps/api/src/services/push` + `platform/Apns.ts`; it sends +The server side is `packages/backend/src/services/push` + `packages/backend/src/platform/Apns.ts`; it sends only when `APNS_TEAM_ID` / `APNS_KEY_ID` / `APNS_PRIVATE_KEY` are set on the alerting worker. `aps-environment` in the entitlements is `development` and Xcode flips it for archives; the app reads whichever landed in the embedded diff --git a/apps/landing/DESIGN.md b/apps/landing/DESIGN.md index de856e1f81..3b5a0a3e9a 100644 --- a/apps/landing/DESIGN.md +++ b/apps/landing/DESIGN.md @@ -54,4 +54,4 @@ The illustration occupies the right background and fades behind the copy; on sma The 1200 × 630 OG cards use the same shared dark tokens, local Geist fonts and tree mark. The general card reuses `maple-rome-balanced.webp`; docs uses the Roman archive from `features/log-management.webp`. Large left-aligned text sits over a charcoal fade, with the architecture filling the right side. Keep the graphics recognizable at social-preview size. -Run `bun run --cwd apps/landing generate:og` to regenerate both public PNGs and the application's matching `og-image.png`. The script uses the workspace's existing `apps/web` Playwright dependency (install its Chromium browser with `apps/web/node_modules/.bin/playwright install chromium` if needed). It renders local HTML with embedded assets, so generation needs no running app or network. Existing metadata paths continue to point to these files. +Run `bun run --cwd apps/landing generate:og` to regenerate both public PNGs and the application's matching `og-image.png`. The script uses this workspace's Playwright devDependency (install its Chromium browser with `apps/landing/node_modules/.bin/playwright install chromium` if needed). It renders local HTML with embedded assets, so generation needs no running app or network. Existing metadata paths continue to point to these files. diff --git a/apps/landing/README.md b/apps/landing/README.md index 7cf5d7cc41..06cdc75d86 100644 --- a/apps/landing/README.md +++ b/apps/landing/README.md @@ -1,43 +1,6 @@ -# Astro Starter Kit: Minimal +# @maple/landing -```sh -bun create astro@latest -- --template minimal -``` - -> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! - -## 🚀 Project Structure - -Inside of your Astro project, you'll see the following folders and files: - -```text -/ -├── public/ -├── src/ -│ └── pages/ -│ └── index.astro -└── package.json -``` - -Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. - -There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - -Any static assets, like images, can be placed in the `public/` directory. - -## 🧞 Commands - -All commands are run from the root of the project, from a terminal: - -| Command | Action | -| :-------------------- | :----------------------------------------------- | -| `bun install` | Installs dependencies | -| `bun dev` | Starts local dev server at `localhost:4321` | -| `bun build` | Build your production site to `./dist/` | -| `bun preview` | Preview your build locally, before deploying | -| `bun astro ...` | Run CLI commands like `astro add`, `astro check` | -| `bun astro -- --help` | Get help using the Astro CLI | - -## 👀 Want to learn more? - -Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). +The Maple marketing site and documentation (maple.dev), built with Astro and deployed as a static-asset Worker. +Run it with `bun dev landing` (in the stack, `https://landing.localhost`) or `bun --filter=@maple/landing dev` (alone, `http://127.0.0.1:3391`, override with `PORT`). +Docs content lives in `src/content/docs/`; the blog, changelog, customers and roadmap collections sit beside it in `src/content/`. +Design rules for the site are in `DESIGN.md`. diff --git a/apps/landing/package.json b/apps/landing/package.json index 19c9be8aaa..ad6c20475e 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -17,7 +17,6 @@ "dependencies": { "@alchemy.run/cloudflare": "catalog:alchemy", "@astrojs/markdown-remark": "^7.3.6", - "@astrojs/markdown-satteri": "^0.4.7", "@astrojs/mdx": "^8.1.3", "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.4", diff --git a/apps/landing/scripts/generate-og.mjs b/apps/landing/scripts/generate-og.mjs index 38765c6e62..eb31a15b86 100644 --- a/apps/landing/scripts/generate-og.mjs +++ b/apps/landing/scripts/generate-og.mjs @@ -1,8 +1,7 @@ import { readFile, writeFile } from "node:fs/promises" -import { createRequire } from "node:module" +// A devDependency of this workspace, so the landing build never loads it. +import { chromium } from "playwright" -// Reuse the workspace's browser tooling; no browser dependency in the landing build. -const { chromium } = createRequire(new URL("../../web/package.json", import.meta.url))("playwright") const landing = new URL("../", import.meta.url) const dataUrl = async (path, mime) => `data:${mime};base64,${(await readFile(new URL(path, landing))).toString("base64")}` diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile deleted file mode 100644 index 9a8f3d4810..0000000000 --- a/apps/web/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -# Build context: monorepo root (.) -FROM oven/bun:1.4.4 AS base -WORKDIR /app - -FROM base AS pruner -COPY . . -RUN bunx turbo prune @maple/web --docker --out-dir /app/out - -FROM base AS build -COPY --from=pruner /app/out/json/ ./ -COPY --from=pruner /app/out/bun.lock ./bun.lock -RUN apt-get update && \ - apt-get install -y --no-install-recommends python3 make g++ && \ - rm -rf /var/lib/apt/lists/* -RUN bun install -COPY --from=pruner /app/out/full/ ./ - -# Build workspace dependencies that publish from dist/ -RUN bun run --cwd packages/effect-sdk build - -# Build-time env vars (baked into the SPA bundle) -ARG VITE_API_BASE_URL -ARG VITE_MAPLE_AUTH_MODE=self_hosted -ARG VITE_ELECTRIC_SYNC_URL -ENV VITE_API_BASE_URL=$VITE_API_BASE_URL -ENV VITE_MAPLE_AUTH_MODE=$VITE_MAPLE_AUTH_MODE -ENV VITE_ELECTRIC_SYNC_URL=$VITE_ELECTRIC_SYNC_URL - -RUN bun run --cwd apps/web build - -FROM nginx:1.27-alpine AS serve -COPY --from=build /app/apps/web/dist /usr/share/nginx/html -COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf deleted file mode 100644 index 8a8db07d2a..0000000000 --- a/apps/web/nginx.conf +++ /dev/null @@ -1,62 +0,0 @@ -# Frame and referrer policy, keyed on the request path. -# -# Mirrors src/handler.ts, which does the same job on the Cloudflare path — keep -# the two in step. The app refuses framing everywhere; /share/ is the one -# exception, because embedding a public chart is a feature. Which share links -# may actually be framed is the API's call (`embeddable`), enforced by the page. -# This layer only knows the path. -# -# A `map` rather than per-location `add_header`: the SPA fallback is an internal -# redirect to /index.html, and nginx applies add_header from the location the -# request *ends* in, so a directive on `location /share/` would be dropped for -# exactly the routes it exists to cover. The maps key on `$request_uri`, the -# path as the client sent it: `$uri` is already `/index.html` after the -# fallback. An empty value makes nginx skip the header entirely. -map $request_uri $maple_csp { - default "object-src 'none'; base-uri 'self'; frame-ancestors 'none'"; - "~^/share(/|[?]|$)" "object-src 'none'; base-uri 'self'"; -} - -map $request_uri $maple_xfo { - default "DENY"; - "~^/share(/|[?]|$)" ""; -} - -# The share token is in the URL, so no-referrer is what stops it reaching a -# third party in a Referer header. Everything else sends only the origin cross-site. -map $request_uri $maple_referrer_policy { - default "strict-origin-when-cross-origin"; - "~^/share(/|[?]|$)" "no-referrer"; -} - -server { - listen 80; - - add_header Content-Security-Policy $maple_csp always; - add_header X-Frame-Options $maple_xfo always; - add_header Referrer-Policy $maple_referrer_policy always; - add_header X-Content-Type-Options nosniff always; - server_name _; - root /usr/share/nginx/html; - index index.html; - - # SPA fallback — serve index.html for all non-file routes - location / { - try_files $uri $uri/ /index.html; - } - - # Cache static assets. Its own add_header stops inheritance from server, so repeat them. - location /assets/ { - expires 1y; - add_header Cache-Control "public, immutable"; - add_header Content-Security-Policy $maple_csp always; - add_header X-Frame-Options $maple_xfo always; - add_header Referrer-Policy $maple_referrer_policy always; - add_header X-Content-Type-Options nosniff always; - } - - # Gzip - gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; - gzip_min_length 256; -} diff --git a/bun.lock b/bun.lock index b093e6bd9c..5006b9d3e7 100644 --- a/bun.lock +++ b/bun.lock @@ -194,7 +194,6 @@ "dependencies": { "@alchemy.run/cloudflare": "catalog:alchemy", "@astrojs/markdown-remark": "^7.3.6", - "@astrojs/markdown-satteri": "^0.4.7", "@astrojs/mdx": "^8.1.3", "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.4", @@ -819,10 +818,12 @@ }, "packages/email": { "name": "@maple/email", - "devDependencies": { + "dependencies": { "@maple/strings": "workspace:*", - "@types/bun": "^1.4.4", "effect": "catalog:effect", + }, + "devDependencies": { + "@types/bun": "^1.4.4", "foldkit": "0.170.0", "tailwindcss": "^4.3.5", "typescript": "catalog:tooling", diff --git a/docker-compose.yml b/docker-compose.yml index 376d9f4a0a..78f4e5c9ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,100 +40,5 @@ services: postgres: condition: service_healthy - api: - build: - context: . - dockerfile: apps/api/Dockerfile - ports: - - "3472:3472" - env_file: .env - environment: - PORT: 3472 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3472/health"] - interval: 10s - timeout: 5s - retries: 3 - - web: - build: - context: . - dockerfile: apps/web/Dockerfile - args: - VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:3472} - VITE_MAPLE_AUTH_MODE: ${MAPLE_AUTH_MODE:-self_hosted} - ports: - - "3471:80" - - ingest: - build: - context: . - dockerfile: apps/ingest/Dockerfile - ports: - - "3474:3474" - env_file: .env - environment: - INGEST_PORT: 3474 - INGEST_FORWARD_OTLP_ENDPOINT: http://otel-collector:4318 - # Required: the org the gateway files its OWN telemetry under. Local - # stacks point it at the default org so self-telemetry is readable in - # the local UI rather than orphaned under an id nothing owns. - MAPLE_INTERNAL_ORG_ID: ${MAPLE_INTERNAL_ORG_ID:-${MAPLE_DEFAULT_ORG_ID:-default}} - INGEST_FORWARD_TIMEOUT_MS: 10000 - INGEST_MAX_REQUEST_BODY_BYTES: 20971520 - INGEST_REQUIRE_TLS: "false" - # Self-hosted ClickHouse usually sits on an internal name (e.g. clickhouse:8123). - INGEST_CLICKHOUSE_ALLOW_PRIVATE_ENDPOINTS: "true" - depends_on: - otel-collector: - condition: service_started - - alerting: - build: - context: . - dockerfile: apps/alerting/Dockerfile - env_file: .env - environment: - MAPLE_APP_BASE_URL: ${MAPLE_APP_BASE_URL:-http://web} - depends_on: - api: - condition: service_healthy - ingest: - condition: service_started - - scraper: - build: - context: . - dockerfile: apps/scraper/Dockerfile - ports: - - "3475:3475" - env_file: .env - environment: - PORT: 3475 - MAPLE_API_URL: http://api:3472 - MAPLE_INGEST_URL: http://ingest:3474 - SD_INTERNAL_TOKEN: ${SD_INTERNAL_TOKEN:-maple-sd-dev-token} - depends_on: - api: - condition: service_healthy - ingest: - condition: service_started - - otel-collector: - build: - context: . - dockerfile: otel/Dockerfile - ports: - - "4317:4317" - - "4318:4318" - - "13133:13133" - env_file: .env - volumes: - - otel-queue-data:/var/lib/otelcol/file_storage - depends_on: - api: - condition: service_healthy - volumes: - otel-queue-data: postgres-data: diff --git a/package.json b/package.json index 6a50bcf920..c83e5adbda 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "seed:demo:env": "bun run ./scripts/seed-demo/make-env.ts", "seed:demo:code-review": "bun run ./scripts/seed-demo/code-review.ts", "prepare": "effect-tsgo unpatch --no-typescript --oxlint --tsrs && effect-tsgo patch --no-typescript --oxlint --tsrs", - "docker:build": "COMPOSE_PARALLEL_LIMIT=1 docker compose build", - "docker:up": "COMPOSE_PARALLEL_LIMIT=1 docker compose up --build", "db:up": "docker compose up -d postgres electric", "ch:up": "docker compose -f docker-compose.development.yml up -d clickhouse", "ch:down": "docker compose -f docker-compose.development.yml stop clickhouse", diff --git a/packages/email/package.json b/packages/email/package.json index 99131948ce..0e611b47d4 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -17,10 +17,12 @@ "test": "vitest run", "typecheck": "tsrs --noEmit" }, - "devDependencies": { + "dependencies": { "@maple/strings": "workspace:*", + "effect": "catalog:effect" + }, + "devDependencies": { "@types/bun": "^1.4.4", - "effect": "catalog:effect", "foldkit": "0.170.0", "tailwindcss": "^4.3.5", "typescript": "catalog:tooling", diff --git a/packages/email/src/template.ts b/packages/email/src/template.ts index 9cdc7b0353..6175df79a0 100644 --- a/packages/email/src/template.ts +++ b/packages/email/src/template.ts @@ -4,9 +4,9 @@ import { replaceWith } from "@maple/strings" * The whole email runtime: HTML escaping plus a token/slot splicer for the * templates compiled into `src/generated/`. * - * Zero dependencies on purpose — this runs inside the API Worker, where - * pulling react + react-email in just to produce a string was the problem this - * package exists to solve. + * Only `effect` and `@maple/strings` at runtime, on purpose: this runs inside + * the API Worker, where pulling react + react-email in just to produce a string + * was the problem this package exists to solve. */ /** Escapes into a form that is safe in both text nodes and quoted attributes. */ diff --git a/scripts/bench-cold-queries.sh b/scripts/bench-cold-queries.sh deleted file mode 100644 index 1ac8026632..0000000000 --- a/scripts/bench-cold-queries.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Ad-hoc benchmark of old vs new cold-load query shapes against the deployed -# Tinybird warehouse. Read-only. Reports min/median elapsed + deterministic -# rows_read / bytes_read. Run: bash scripts/bench-cold-queries.sh -set -u -TB_HOST=$(jq -r '.host' .tinyb); TB_TOKEN=$(jq -r '.token' .tinyb) - -bench() { - local label="$1" runs="$2" sql="$3"; local times=() rr="" br="" err="" - for _ in $(seq 1 "$runs"); do - local resp; resp=$(curl -s --get "$TB_HOST/v0/sql" \ - --data-urlencode "q=${sql} SETTINGS use_query_cache=0 FORMAT JSON" \ - -H "Authorization: Bearer $TB_TOKEN" 2>/dev/null) - err=$(printf '%s' "$resp" | jq -r '.error // empty' 2>/dev/null) - if [ -n "$err" ]; then echo " $label: ERROR ${err:0:140}"; return; fi - times+=("$(printf '%s' "$resp" | jq -r '.statistics.elapsed')") - rr=$(printf '%s' "$resp" | jq -r '.statistics.rows_read') - br=$(printf '%s' "$resp" | jq -r '.statistics.bytes_read') - done - local mn md - mn=$(printf '%s\n' "${times[@]}" | sort -n | head -1) - md=$(printf '%s\n' "${times[@]}" | sort -n | awk '{a[NR]=$1} END{print (NR%2)?a[(NR+1)/2]:(a[NR/2]+a[NR/2+1])/2}') - printf " %-24s min=%6.0fms med=%6.0fms rows=%-9s MB=%.0f\n" \ - "$label" "$(echo "$mn*1000"|bc -l)" "$(echo "$md*1000"|bc -l)" "$rr" "$(echo "$br/1048576"|bc -l)" -} - -ORG=org_3Ei9QGrsqzBb83qKR4EzhgBQ1uh; S='2026-06-06 18:00:00'; E='2026-06-07 18:00:00' -W="OrgId='$ORG' AND Timestamp>='$S' AND Timestamp<='$E'" - -echo "### servicesFacets (24h) — UNION vs arrayJoin vs GROUPING SETS" -bench "OLD union(4)" 4 "SELECT DeploymentEnv AS name,count() c,'environment' f FROM service_overview_spans WHERE $W AND DeploymentEnv!='' GROUP BY name ORDER BY c DESC LIMIT 50 -UNION ALL SELECT ServiceNamespace,count(),'namespace' FROM service_overview_spans WHERE $W AND ServiceNamespace!='' GROUP BY ServiceNamespace ORDER BY count() DESC LIMIT 50 -UNION ALL SELECT CommitSha,count(),'commit_sha' FROM service_overview_spans WHERE $W AND CommitSha!='' GROUP BY CommitSha ORDER BY count() DESC LIMIT 50 -UNION ALL SELECT ServiceName,count(),'service' FROM service_overview_spans WHERE $W AND ServiceName!='' GROUP BY ServiceName ORDER BY count() DESC LIMIT 50" -bench "NEW arrayJoin" 4 "SELECT facet.1 f,facet.2 name,count() c FROM service_overview_spans ARRAY JOIN arrayFilter(x->tupleElement(x,2)!='',[('environment',DeploymentEnv),('namespace',ServiceNamespace),('commit_sha',CommitSha),('service',ServiceName)]) AS facet WHERE $W GROUP BY f,name ORDER BY c DESC LIMIT 50 BY f" -bench "ALT grouping-sets" 4 "SELECT multiIf(GROUPING(DeploymentEnv)=0,'environment',GROUPING(ServiceNamespace)=0,'namespace',GROUPING(CommitSha)=0,'commit_sha','service') AS f, multiIf(GROUPING(DeploymentEnv)=0,DeploymentEnv,GROUPING(ServiceNamespace)=0,ServiceNamespace,GROUPING(CommitSha)=0,CommitSha,ServiceName) AS name, count() c FROM service_overview_spans WHERE $W GROUP BY GROUPING SETS ((DeploymentEnv),(ServiceNamespace),(CommitSha),(ServiceName)) HAVING name!='' ORDER BY c DESC LIMIT 50 BY f" - -echo "### tracesFacets (6h) — UNION(7) vs arrayJoin vs grouping-sets" -TS='2026-06-07 12:00:00'; TE='2026-06-07 18:00:00'; TW="OrgId='$ORG' AND Timestamp>='$TS' AND Timestamp<='$TE'" -bench "OLD union(7)" 4 "SELECT ServiceName AS name,count() c,'service' f FROM trace_list_mv WHERE $TW GROUP BY name ORDER BY c DESC LIMIT 50 -UNION ALL SELECT SpanName,count(),'spanName' FROM trace_list_mv WHERE $TW AND SpanName!='' GROUP BY SpanName ORDER BY count() DESC LIMIT 20 -UNION ALL SELECT HttpMethod,count(),'httpMethod' FROM trace_list_mv WHERE $TW AND HttpMethod!='' GROUP BY HttpMethod ORDER BY count() DESC LIMIT 20 -UNION ALL SELECT HttpStatusCode,count(),'httpStatus' FROM trace_list_mv WHERE $TW AND HttpStatusCode!='' GROUP BY HttpStatusCode ORDER BY count() DESC LIMIT 20 -UNION ALL SELECT DeploymentEnv,count(),'deploymentEnv' FROM trace_list_mv WHERE $TW AND DeploymentEnv!='' GROUP BY DeploymentEnv ORDER BY count() DESC LIMIT 20 -UNION ALL SELECT ServiceNamespace,count(),'serviceNamespace' FROM trace_list_mv WHERE $TW AND ServiceNamespace!='' GROUP BY ServiceNamespace ORDER BY count() DESC LIMIT 20 -UNION ALL SELECT 'error',count(),'errorCount' FROM trace_list_mv WHERE $TW AND HasError=1" -bench "NEW arrayJoin" 4 "SELECT facet.1 f,facet.2 name,count() c FROM (SELECT ServiceName,SpanName,HttpMethod,HttpStatusCode,DeploymentEnv,ServiceNamespace,HasError FROM trace_list_mv WHERE $TW) t ARRAY JOIN arrayFilter(x->tupleElement(x,2)!='',[('service',ServiceName),('spanName',SpanName),('httpMethod',HttpMethod),('httpStatus',HttpStatusCode),('deploymentEnv',DeploymentEnv),('serviceNamespace',ServiceNamespace),('errorCount',if(HasError=1,'error',''))]) AS facet GROUP BY f,name ORDER BY c DESC LIMIT 50 BY f" - -echo "### serviceUsage — OLD (2 separate) vs NEW (1 combined)" -US='2026-06-07 12:00:00'; UE='2026-06-07 18:00:00'; PS='2026-06-07 06:00:00'; PE='2026-06-07 12:00:00' -SU_SUM="SELECT ServiceName n, sum(LogCount)+sum(TraceCount)+sum(SumMetricCount) c FROM service_usage WHERE OrgId='$ORG' AND Hour>=toStartOfHour(toDateTime('%s')) AND Hour<=toStartOfHour(toDateTime('%s')) GROUP BY n ORDER BY c DESC" -bench "OLD current" 4 "$(printf "$SU_SUM" "$US" "$UE")" -bench "OLD previous" 4 "$(printf "$SU_SUM" "$PS" "$PE")" -bench "NEW combined" 4 "SELECT ServiceName n, sumIf(LogCount,Hour>=toStartOfHour(toDateTime('$US')) AND Hour<=toStartOfHour(toDateTime('$UE')))+sumIf(TraceCount,Hour>=toStartOfHour(toDateTime('$US')) AND Hour<=toStartOfHour(toDateTime('$UE')))+sumIf(SumMetricCount,Hour>=toStartOfHour(toDateTime('$US')) AND Hour<=toStartOfHour(toDateTime('$UE'))) cur, sumIf(LogCount,Hour>=toStartOfHour(toDateTime('$PS')) AND Hour<=toStartOfHour(toDateTime('$PE'))) prev FROM service_usage WHERE OrgId='$ORG' AND Hour>=toStartOfHour(toDateTime('$PS')) AND Hour<=toStartOfHour(toDateTime('$UE')) GROUP BY n ORDER BY cur DESC" diff --git a/scripts/build-local-binary.sh b/scripts/build-local-binary.sh index e85bdf08d2..6ce923dffd 100755 --- a/scripts/build-local-binary.sh +++ b/scripts/build-local-binary.sh @@ -52,6 +52,10 @@ else fi mkdir -p "$OUT_DIR" +# The `bun run` steps below resolve root scripts and workspaces from the working +# directory. Pin OUT_DIR first so a relative one still means the caller's. +OUT_DIR="$(cd "$OUT_DIR" && pwd)" +cd "$REPO_ROOT" # Several workspace packages are published from gitignored `dist/` directories. # A fresh checkout / CI only runs `bun install`, so build the repository's @@ -68,11 +72,11 @@ trap restore_stub EXIT bun run "$REPO_ROOT/scripts/gen-ui-embed.ts" echo "==> Compiling maple binary (bun build --compile) — version $MAPLE_BUILD_VERSION" -( cd "$REPO_ROOT" && bun build apps/cli/src/bin.ts --compile \ +bun build apps/cli/src/bin.ts --compile \ --define "__MAPLE_VERSION__=\"$MAPLE_BUILD_VERSION\"" \ --define "__CHDB_VERSION__=\"$LIBCHDB_VERSION\"" \ ${COMPILE_TARGET[@]+"${COMPILE_TARGET[@]}"} \ - --outfile "$OUT_DIR/maple" ) + --outfile "$OUT_DIR/maple" echo "==> Downloading libchdb $LIBCHDB_VERSION for $MAPLE_TARGET" case "$MAPLE_TARGET" in diff --git a/scripts/generate-ios-openapi.ts b/scripts/generate-ios-openapi.ts index ff2d67ddb4..7cf6261deb 100644 --- a/scripts/generate-ios-openapi.ts +++ b/scripts/generate-ios-openapi.ts @@ -60,10 +60,8 @@ const IOS_OPERATIONS: ReadonlyArray = [ // Traces and logs "searchTraces", "getTrace", - "getSpan", "searchLogs", "queryLogTimeseries", - "getLog", // Dashboards "listDashboards", "getDashboard", diff --git a/scripts/install.sh b/scripts/install.sh index 2daeb428b6..ee911fa868 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -211,8 +211,16 @@ tar -xzf "$tmp/bundle.tar.gz" -C "$tmp" [ -d "$tmp/$name" ] || die "unexpected archive layout (no $name/ directory)" mkdir -p "$INSTALL_DIR" -cp "$tmp/$name/maple" "$tmp/$name/libchdb.so" "$INSTALL_DIR/" -chmod +x "$INSTALL_DIR/maple" +# Stage beside the targets, then rename: `cp` over a running `maple` fails with +# "Text file busy" on Linux, while a rename only swaps the directory entry. +staged_maple="$INSTALL_DIR/.maple.new.$$" +staged_lib="$INSTALL_DIR/.libchdb.so.new.$$" +trap 'rm -rf "$tmp" "$staged_maple" "$staged_lib"' EXIT INT TERM +cp "$tmp/$name/maple" "$staged_maple" +cp "$tmp/$name/libchdb.so" "$staged_lib" +chmod +x "$staged_maple" +mv -f "$staged_maple" "$INSTALL_DIR/maple" +mv -f "$staged_lib" "$INSTALL_DIR/libchdb.so" # macOS: clear the Gatekeeper quarantine flag set on downloaded files. if [ "$os" = "Darwin" ] && command -v xattr >/dev/null 2>&1; then diff --git a/scripts/package-charts.sh b/scripts/package-charts.sh deleted file mode 100755 index e8cdd35a33..0000000000 --- a/scripts/package-charts.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Package every Helm chart under deploy/ into dist/charts/. -# -# Used by both the local dev path (`./scripts/package-charts.sh`) and the -# CI publish workflows (publish-k8s-infra-chart.yml, publish-maple-otel- -# chart.yml). Each chart is linted before packaging — failure aborts the -# whole script. -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -OUT_DIR="${1:-$ROOT/dist/charts}" - -CHARTS=( - "deploy/k8s-infra" - "deploy/maple-otel" -) - -mkdir -p "$OUT_DIR" - -for chart in "${CHARTS[@]}"; do - echo "==> Linting $chart" - helm lint "$ROOT/$chart" - echo "==> Packaging $chart" - helm package "$ROOT/$chart" --destination "$OUT_DIR" -done - -echo -echo "Packaged charts:" -find "$OUT_DIR" -maxdepth 1 -name '*.tgz' -print diff --git a/scripts/publish-maple-otel-chart.sh b/scripts/publish-maple-otel-chart.sh index 3272d29ddd..0ecb9d142e 100755 --- a/scripts/publish-maple-otel-chart.sh +++ b/scripts/publish-maple-otel-chart.sh @@ -24,7 +24,7 @@ echo "$HELM_REGISTRY_PASSWORD" | helm registry login "$REGISTRY" \ --username "$HELM_REGISTRY_USERNAME" \ --password-stdin -for chart in "$PACKAGE_DIR"/$CHART_NAME-*.tgz; do +for chart in "$PACKAGE_DIR/$CHART_NAME"-*.tgz; do helm push "$chart" "oci://$REGISTRY/$OWNER_LC/charts" done diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 2c6c9e3c83..ff787ae92d 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -4,9 +4,10 @@ # curl -fsSL https://maple.dev/cli/uninstall | sh # # Removes the `maple` PATH symlink and the `~/.maple/bin` bundle (maple + -# libchdb.so). Your data dir (`~/.maple/data`) and config (`~/.maple/config.json`, -# holding any remote token) are KEPT unless you opt in — the data dir prompts -# (or set MAPLE_REMOVE_DATA=1 for non-interactive removal). +# libchdb.so). Your data dir (`~/.maple/data`) is KEPT unless you opt in: it prompts +# (or set MAPLE_REMOVE_DATA=1 for non-interactive removal). Everything else under +# `~/.maple` always stays: config.json (holding any remote token), ingest-rules.json, +# archive/, scratch/, cache/ and the log. # # Honors the same overrides as the installer so a custom install can be undone: # MAPLE_INSTALL_DIR bundle directory (default: ~/.maple/bin) @@ -70,4 +71,5 @@ if [ -d "$DATA_DIR" ]; then fi say "" -say "Maple uninstalled. ~/.maple/config.json (if present) was left in place." +say "Maple uninstalled. Anything else under ~/.maple was left in place:" +say "config.json, ingest-rules.json, archive/, scratch/, cache/ and the log." From 6168670642ae09582da59bef61397c47444f7e96 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 10 Nov 2026 04:51:50 +0100 Subject: [PATCH 2/3] ci: keep every app's alchemy entrypoint in the ts filter The positive app pattern drops apps/ingest whole, but its alchemy.run.ts is typechecked by the alchemy program, and its package.json is a workspace manifest the install reads. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 733ad61183..1b9031813d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,6 +78,9 @@ jobs: - 'apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json' # Generated; clickhouse:schema:check verifies it. - 'apps/ingest/src/clickhouse_insert_mappings.rs' + # Typechecked by `tsrs -p tsconfig.alchemy.json`; ingest's is the one the extglob drops. + - 'apps/*/alchemy.run.ts' + - 'apps/ingest/package.json' # What apps/cli + apps/local-ui are built from. Their workspace closure # is 21 of packages/ and 6 of lib/, so both are listed whole, as in `workers`. cli: From df9d128c198df53b1b2560752e6584d7d7a32736 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Tue, 10 Nov 2026 06:02:25 +0100 Subject: [PATCH 3/3] test(domain): drop getSpan and getLog from the iOS operation allowlist The generator's allowlist lost them in this PR because no Swift file calls them; the domain test keeps its own copy of the list and still expected them. --- packages/domain/src/http/v2/openapi-ios.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/domain/src/http/v2/openapi-ios.test.ts b/packages/domain/src/http/v2/openapi-ios.test.ts index f0be804c38..478a379e42 100644 --- a/packages/domain/src/http/v2/openapi-ios.test.ts +++ b/packages/domain/src/http/v2/openapi-ios.test.ts @@ -49,10 +49,8 @@ const IOS_OPERATIONS = [ "setErrorIssueSeverity", "searchTraces", "getTrace", - "getSpan", "searchLogs", "queryLogTimeseries", - "getLog", "listDashboards", "getDashboard", "queryDashboardWidgetData",