Metic 3.0.0: unified TypeScript core + Bun server (#532) - #561
Open
hessius wants to merge 152 commits into
Open
Conversation
Approved brainstorm output: collapse the dual Python/TS runtimes onto a single shared packages/core handler consumed by both apps/frontend (native + browser) and a compiled single-binary Bun apps/server on distroless. Drops Python, nginx, s6, mosquitto, MCP, and MQTT/HA; keeps Tailscale UI via LocalAPI-over-socket. Seamless upgrade for existing server users (frozen /api/*, /api/ws/live, and /data contracts; flat-JSON persistence behind a Storage interface). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7-phase plan (monorepo foundation, core extraction, browser platform+host, node platform+Bun server, tailscale/health/startup, distroless container, cutover+rollout). Scheduled after 2.7.0. Full TDD step detail for Phase 1; task-level detail for Phases 2-7 (re-baselined per phase at implementation). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Establishes the architectural keystone for the 3.0.0 unified TS core: a single Web-standard request handler handle(req, platform) plus the Platform interface that is the sole seam between portable logic and each host runtime (browser direct mode, Bun proxy mode). Includes an in-memory mock Platform and a contract-test harness that locks the frozen /api/* contract host-independently. Refs #532 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
First Phase 2 extraction slice, proving the extract-and-rewire mechanism end-to-end: the pure Espresso-Compass logic now lives canonically in @metic/core (with its test as a core contract test), and the frontend consumes it via a file: workspace dependency. Verified green: core tests, frontend vitest, and the full vite build all resolve the linked package. Refs #532, #528 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves the portable, framework-free logic into @metic/core as its canonical home, each with its test as a core contract test (253 tests total): analysisSchema, shotFacts, analysisLint, uuid, decentConverter, tags, profileAnalysis, profileRecommendation. Frontend paths become thin re-export shims (via the @metic/core file: dependency) so all existing importers keep working unchanged. This is the substance of former issue #528 done as extraction toward the unified core. Verified green: 253 core contract tests + typecheck; 1118 frontend tests (migrated suites now live in core); full vite build resolves the linked package. Refs #532, #528 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ic/core The web-builder stage flattened apps/web into /build and never copied packages/core, so the frontend's file:../../packages/core dependency could not resolve under bun install. Copy packages/core into the build context and build from /build/apps/web, keeping the two-levels-up relative path intact (also fixes the VERSION path read by vite.config). Verified with a local web-builder build. Refs #532 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…o @metic/core Moves the framework-free AI pieces into @metic/core (with their tests as core contract tests): AIServiceError/AIErrorCode (aiErrors), the analysis domain knowledge + fact-sheet builder (analysisKnowledge), and the shared prompt builders (prompts). Frontend paths remain thin re-export shims. The i18n- and SDK-coupled provider/retry/model-resolver layers stay in the frontend for the later provider abstraction (Phase 2.6). Verified green: 298 core contract tests + typecheck; 1073 frontend tests; full vite build resolves the linked package; lint clean. Refs #532, #528 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds apps/bun-server, the 3.0.0 single-binary Bun server that consumes @metic/core. Verified end-to-end against a live Meticulous machine. - platform/node.ts: filesystem-backed Node implementation of the core Platform interface (JSON repos over the existing /data shapes, env secrets, TTL aiCache, blob store, timer scheduler). - machineProxy.ts: transparent reverse proxy for /api/v1/* to the machine (streams body, mirrors status/headers, strips hop-by-hop). - static.ts: SPA static serving with immutable asset caching + index fallback and path-traversal protection. - telemetryHub.ts: single upstream Socket.IO connection fanned out to browser WebSocket clients at /api/ws/live, emitting the exact flat snapshot shape (+ _ts, _heartbeat) the frontend already consumes; mapping mirrors direct-mode useMachineTelemetry for runtime parity. - server.ts / main.ts: Bun.serve wiring (WS upgrade, /api/v1 proxy, /api/* -> handle(), static) and serve|healthcheck subcommands. Additive only: Python apps/server and DirectModeInterceptor remain the live runtimes until the Phase 7 cutover. 19 tests pass; typecheck clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make the package's own `bun run typecheck` gate clean: explicit generic on aiCache.get and unknown-cast fetch mocks. No runtime behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the shot-annotations CRUD route family into @metic/core against the
Platform storage seam, matching Python (shot_annotations_service.py) and
native (directModeStorage.ts) behavior exactly.
- handleAnnotationRoutes dispatcher: GET/PATCH/DELETE single annotation and
GET summaries, keyed by `${date}/${filename}`.
- PATCH parity: rating-only merge keeps existing text; clearing both deletes.
- validateRating: integer 1-5, else 422; invalid JSON -> 400.
- Node Platform: add fsKeyedMapRepo backing annotations with a single
shot_annotations.json object map (Python data-volume compatible, handles
slash/colon-containing ids that the dir-based fsRepo would collide).
- 14 core contract tests + 1 node-platform repo test. Live-verified end-to-end
through the Bun server against machine 192.168.50.168.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the Dial-In Guide session routes into @metic/core against the Platform
storage seam, matching Python (dialin_service.py + dialin.py) and native
(directModeStorage.ts + DirectModeInterceptor.ts dispatch) exactly.
- handleDialInRoutes: create/get/list(+status filter)/delete sessions, add
iterations (auto-incrementing, active-only), update iteration
recommendations, rule-based /recommend, and /complete. Served under both
/api/dialin/... and the bare /dialin/... alias.
- Validation mirrors the native runtime (coffee roast/process enums, taste
x/y in [-1,1], recommendation string lists) and returns FastAPI-style
{ detail } errors with matching 400/404 status.
- Rule-based recommendations reproduce the exact taste-coordinate guidance and
strings shared by both runtimes; the Python AI path layers on later with the
provider abstraction (native /recommend is already rules-only).
- Node Platform: back dialInSessions with fsKeyedMapRepo -> dialin_sessions.json
(id-keyed map matching Python's on-disk layout for Phase 7 migration), same
approach as annotations.
- 20 core contract tests. Live-verified end-to-end through the Bun server
against machine 192.168.50.168.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the pour-over preferences routes into @metic/core against the Platform
storage seam, matching Python (pour_over_preferences.py + pour_over.py) and
native (directModeStorage.ts + DirectModeInterceptor.ts dispatch).
- handlePourOverRoutes: GET returns stored per-mode preferences (defaults when
unset), PUT normalizes, persists, and echoes the normalized result.
- Validation mirrors the native runtime's strict typing (wrong types throw)
and its ratio-mode defaults of dose 18g / ratio 15, a deliberate divergence
from the Python nulls that this unification adopts since the native/browser
runtime is what the frontend relies on. Unknown keys are dropped.
- Error shapes match native: 400 { detail: "Invalid preferences" } for bad
input or invalid JSON, 500 for load/save failures.
- Node Platform: rename the singleton file to pour_over_preferences.json to
match Python's on-disk layout for Phase 7 migration.
- 6 core contract tests. Live-verified end-to-end through the Bun server.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a PlatformAI seam (isConfigured/generateText/optional generateImage) to the core Platform interface, mirroring the frontend AIProvider so the browser Platform can delegate to the active provider in Phase 3. Wire a Gemini-backed PlatformAI into the Node platform via @google/genai, reading GEMINI_API_KEY/GEMINI_MODEL from the environment. Port the dial-in recommendation prompt builder to core and make the dial-in /recommend route AI-first: build the prompt, call the provider, strip markdown fences, parse JSON, cap at 6 recommendations (source "ai"); on any error or when unconfigured, fall back to the rule-based path (source "rules"). This matches the Python oracle and gives the native runtime AI dial-in recommendations once wired in Phase 3. Add scriptedAI/throwingAI/unconfiguredAI mock helpers and contract tests for the AI path plus a prompt-builder unit test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extend the Platform's machine seam with a fetch(path, init?) method so core routes can read machine state (shot history, profiles) uniformly across hosts. The Node platform joins relative paths onto the resolved base URL and fetches server-side; the browser platform will fetch over the LAN in Phase 3. Add scriptedMachine/unwiredMachine mock helpers for contract tests and cover the Node URL-join + unconfigured-rejection behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the shot-analysis building blocks from the native DirectModeInterceptor into @metic/core so both runtimes share one implementation: - computeRichLocalAnalysis (structured local shot analysis) + its helpers - parseRecommendationsJson / isActionableRecommendation / isRecommendationPatchable / termMatches (RECOMMENDATIONS_JSON extraction) - buildAnalyzeLlmPrompt and the PROFILING_KNOWLEDGE constant it needs The ports are byte-identical (prompts) or logic-identical (analysis) to the native source, reusing the already-ported buildShotFacts. Add contract tests for the analysis output shape and the recommendation parsing/patchability rules. These modules back the shot-analysis routes ported next. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the /api/shots/analyze, /api/shots/analyze-llm and /api/shots/analyze-recommendations routes to @metic/core, served by handle() for both runtimes (also under the bare /shots/... alias): - analyze: fetch the shot from the machine and return the structured local analysis (computeRichLocalAnalysis). - analyze-llm: build the analysis prompt over the shot data, run it through the AI provider seam with the existing validate/retry/repair loop, and cache the result for recommendation extraction. - analyze-recommendations: parse the RECOMMENDATIONS_JSON block (from the posted text or the cache) and flag patchability against the profile's variables fetched from the machine. Reuses the ported shot-analysis logic, prompt builder and recommendation parser. Adds 9 contract tests driven by the scriptedMachine/scriptedAI mocks. Verified live through the Bun server against a real machine and Gemini: structured analysis, a full AI analysis, and cached recommendation extraction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add /api/profiles/recommend and /api/profiles/find-similar to @metic/core (also under the bare /profiles/... alias), reusing the shared profileRecommendation scorer. Profiles are read from the machine with ?full=true so stages are included for structural scoring in a single request, matching the server's canonical behavior and improving the native runtime (which previously scored off stage-less list data). Adds 5 contract tests asserting parity with the shared scorer. Verified live through the Bun server against a real machine catalogue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add POST /api/profile/{name}/apply-recommendations to @metic/core (also under
the bare /profile/{name}/... alias). It patches selected recommendations onto a
machine profile and saves it back:
- global temperature / final_weight,
- adjustable profile variables (info-only / non-adjustable are skipped),
- stage exit_triggers and limits,
- a fuzzy fallback that recovers model-invented positional ids
(e.g. "pressure_2") by type + current_value + stage.
Client-only metadata (change_id, in_history, has_description) is stripped
before saving, matching the native runtime. The server's defensive bounds
guards (temperature <= 100 C, final_weight > 0) are kept so invalid values are
skipped rather than written to the machine. Adds 12 contract tests.
Also fixes a latent DOM-lib typing issue in routes/profiles.ts (toNumber took
FormDataEntryValue, which is unavailable under the bun-server tsconfig) and
exports the MockMachine test type. Verified live through the Bun server against
a real machine (idempotent apply round-trips find -> get -> patch -> save).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add POST /api/profile/{id}/regenerate-description to @metic/core (also under
the bare /profile/{id}/... alias). It regenerates a profile's human-readable
description, preferring an AI write-up and falling back to a deterministic
static summary:
- resolve the profile from the machine (identifier as profile id, then name),
- when AI is configured, build the barista prompt (with the shared tags
prompt), generate, strip the Tags line, resolve any leftover variable
placeholders to concrete values, cache the description and parsed tags,
- otherwise (or on AI failure / a "generated without AI" signal) build and
cache the static description.
The two parity oracles resolve the profile from runtime-specific caches
(server: profile-generation history; native: machine shot history + in-memory
profile caches) that have no host-independent equivalent, so this port resolves
through the machine client, the one path available on every host. The browser
Platform will map the new description / ai-tags storage repos onto its existing
native caches in Phase 3.
Adds two new Platform storage repos (descriptions, aiTags) wired in the Node
platform (profile_descriptions.json, profile_ai_tags.json) and the mock. Ports
buildStaticProfileDescription + resolveDescriptionPlaceholders into
logic/profileDescription.ts (reusing the existing core tags helpers). Adds 13
contract tests. Verified live through the Bun server against a real machine +
real Gemini (AI description by id and by name, placeholder-free, tags stripped,
cached to disk, 404 path).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…he core package Ports the flagship profile-generation route to @metic/core against the Platform seam: builds the full profiling prompt (+optional image), runs the AI generate/validate/retry loop, converts the Gemini JSON to OEPF, and writes the new profile to the machine, caching the analysis text as its description. Adds profileValidator, oepf, and the prompt/validate/retry helpers, all ported byte/logic-identical from the native oracle. Fixes a latent parity bug in the OEPF converter (both runtimes): object-form dynamics whose points were already [t, v] arrays skipped $ref/number resolution, so arithmetic expression strings (e.g. "10000 * ($ratio / 2.5)") survived and the machine rejected the save with "is not referencing a variable but is a string". Now array-form points are resolved like the other two dynamics branches. Live-verified end-to-end against machine 192.168.50.168 + real Gemini: profile generated, saved to the machine, then cleaned up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ystone) Adds apps/web/src/services/platform/browserPlatform.ts: the direct-mode counterpart to the Bun server's Node platform. It implements the @metic/core Platform interface with browser primitives so the SAME handle(request, platform) can run entirely client-side, the foundation for replacing the bespoke per-route logic in DirectModeInterceptor. - Storage: IndexedDB-backed repos that persist documents VERBATIM under the settings key-value store, faithfully mirroring the Node fsKeyedMapRepo / fsSingletonRepo semantics (so the shared contract tests hold on both hosts). Blob store maps to the profile-image IDB store (Blob <-> Uint8Array). - Machine: fetch joins core's path onto getDefaultMachineUrl(); takes an injectable original fetch to avoid re-entrancy with the fetch interceptor. - AI: delegates to the frontend's active provider (identical generateText signature); image output converted Blob -> bytes. No scheduler (direct mode cannot run deferred actuation, so schedule routes 501, matching the legacy interceptor). Includes an integration test that runs @metic/core handle() against the real IndexedDB-backed platform (fake-indexeddb + fake machine fetch + scripted provider) across annotations, dial-in, pour-over, machine-fetch, and AI seams. 7 tests. Not yet wired into window.fetch: the live cutover and DirectModeInterceptor deletion are gated on on-device parity verification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ore package Adds the system/meta route family to @metic/core so the Bun server (proxy mode) serves GET/POST /api/settings, GET /api/network-ip, and GET /api/version that the frontend calls at startup. Previously only the native DirectModeInterceptor implemented these; the server returned 404, leaving settings and version unavailable in proxy mode. - packages/core/src/routes/system.ts: settings payload mirrors the stable frontend contract (geminiApiKeyConfigured/meticulousIp/authorName/ geminiModel/mqttEnabled); the raw AI key is never returned. Configured state uses platform.ai.isConfigured() (the canonical cross-host signal). - platform.ts: add optional Platform.appVersion for GET /api/version. - node platform: resolveAppVersion() (APP_VERSION env, else VERSION file); browser platform: appVersion from the __APP_VERSION__ build define. - 9 contract tests; live-verified against the Bun server + machine 192.168.50.168 (settings round-trip, version, network-ip) and via a headless browser load of the built proxy-mode frontend (startup 404s cleared). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rlay Adds the /api/history family to @metic/core using native semantics (user-approved): the machine shot history from /api/v1/history filtered by local soft-delete tombstones, with per-shot notes and AI descriptions overlaid from the Platform storage. Includes list, single detail with profile_json, notes GET/PATCH round-trip, and DELETE tombstone. Fixes a latent default-parameter bug where an absent limit query param resolved to 0 (Number(null)) instead of the intended fallback of 50. Live-verified through the Bun server against machine 192.168.50.168: 20 shots normalized, notes round-trip, tombstone delete 20 to 19. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the GET read family to @metic/core with native semantics, all
served through the shared visible-history loader so local soft-deletes
apply consistently:
- /api/last-shot
- /api/shots/dates
- /api/shots/recent
- /api/shots/recent/by-profile
- /api/shots/by-profile/{name}
- /api/shots/data/{date}/{filename}
Extracts the machine-history store access (loadVisibleHistory, overlay,
notes, descriptions) into logic/historyStore so the history and shots
families share one implementation, and adds hasRecentShotAnnotation to
machineHistory. Annotation flags reuse the ported annotations summaries.
Live-verified through the Bun server against machine 192.168.50.168:
20 recent shots, 4 profile groups, paged by-profile, and 338-point
telemetry converted for /api/shots/data.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the machine actuation and status family to @metic/core with native semantics (mirrors DirectModeInterceptor + commands.py/system.py): - POST /api/machine/command/start | stop | load-profile - POST /api/machine/preheat - GET /api/machine/status/health (watcher /status on port 3000) - GET /api/machine/system-info - GET /api/machine/status (synthetic idle) - GET /api/machine/detect (501) - POST /api/machine/schedule-shot (501, no scheduler) Ports the pure watcher-response transform + size/uptime parsers into logic/watcher, reached through the machine seam which already passes absolute URLs through on both platforms. Live-verified through the Bun server against machine 192.168.50.168: status/health returns 7 live services + system metrics, status idle, detect 501; system-info degrades to null per key exactly as native does against this firmware. Actuation paths covered by contract tests to avoid dispensing on the live machine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the machine profile CRUD + actuation family (run-profile,
run-with-overrides, import, import-all, convert-decent, import-from-url,
order, load, rename, delete, machine/profiles list, profile/{id}/json,
orphaned, target-curves, edit, profile/{name} GET, sync/status, sync)
into @metic/core against the Platform seam, mirroring the native
DirectModeInterceptor oracle. Adds logic/targetCurves.ts and
logic/profileList.ts. 34 contract tests; live-verified against machine
192.168.50.168 (36 profiles, stages, target-curves, rename + edit
round-trips).
Fix a latent parity bug in BOTH runtimes: /target-curves resolved the
profile from the machine profile list, which omits stages, so it
returned an empty curve set for saved (non-override) profiles. Both
runtimes now fetch the full profile by id when the list entry lacks
stages, matching the include_stages fallback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port modelResolver (dynamic Gemini model discovery + ranking) into @metic/core as the single source of truth; the native services/ai/modelResolver becomes a thin re-export shim. Add optional PlatformAI.listModels()/currentModel() seam, implemented by the Node platform (Gemini SDK models.list + core ranking) and the browser platform (active provider). Add system routes /api/available-models (live discovery, static fallback, text-only filtering), /api/changelog (GitHub releases), /api/update-method, /api/tailscale-status, and 501 stubs for check-updates/restart/beta-channel/feedback. 8 new contract tests (445 core total). Live-verified against machine 192.168.50.168 + real Gemini: 18 text models (no image/tts/embedding leakage), 5 real releases, stubs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the bundled pour-over recipe library into
packages/core/src/data/recipes.ts as the single source of truth and add
routes/recipes.ts serving GET /api/recipes (sorted by name) and
GET /api/recipes/{slug} (404 on unknown). The native DirectModeInterceptor
now imports POUR_OVER_RECIPES from core instead of carrying its own inline
copy (removes ~11KB of duplicated data). Un-ignore packages/core/src/data
so the source asset is tracked. 3 contract tests (448 core total); native
interceptor suite still green (55). Live-verified against the Bun server:
9 recipes sorted, single-slug lookup, 404 path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The built config.json ships a dev serverUrl (http://localhost:8000) that
points at a dead host in production. The legacy nginx deployment rewrote
it to an empty string so the SPA talks to its own origin over relative
URLs. The Bun server now does the same: GET /config.json returns
{"serverUrl":""} regardless of the build artifact. Adds a booted-server
test and live-verified through the Bun server.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… v2) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tings store Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The controller previously ended the activity with nil content, leaving the last mid-extraction frame on screen and never producing the terminal summary (final weight/time/avg temp). Track the last extraction weight/elapsed and build a .done ContentState via ShotContentBuilder.summary on stop(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native direct-mode connection used the raw stored machine URL and never applied the existing :8080 -> :80 port fallback (or rediscovery) once past onboarding. When firmware moves the API from :8080 to :80, the app was stuck retrying the dead port forever (TCP RST / ECONNREFUSED) even though the machine was reachable — the official app followed the mDNS-advertised port and worked. - useResolvedMachineUrl: on mount, probe the stored URL's port candidates and adopt+persist the reachable one (cheap, bounded — no mDNS on launch). - MachineServiceProvider: if the adapter never connects within 8s, run full discovery once and adopt the machine's current address (DHCP IP-change recovery), persisting it so the adapter rebuilds against the healed URL. - discovery: add resolveAndHealMachineUrl (port fallback -> rediscovery), tested. Mirrors the server runtime's lazy self-heal (platform/node.ts) for parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rename the settings section 'Home-screen widgets' to 'Widgets' across all 6 locales. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Favourites persist a fully-resolved absolute profile-image URL that pins the machine address+port at favourite time (e.g. http://IP:8080/...). When the machine's API port changes (firmware :8080 -> :80) or its IP is reassigned, that URL becomes unreachable: the native cacheImage download fails, no file is written to the App Group, and widget tiles silently fall back to a monogram. Re-adding the widget can't fix it because the app never re-resolved the URL. - Add rehostMachineImageUrl(): rewrites the origin of machine profile-image URLs (path /api/v1/profile/image/) onto the current base, leaving data URIs and external CDN URLs untouched. - useWidgetSync: re-host every favourite image before pushing to the bridge, and re-push favourites on MACHINE_URL_CHANGED so cached images refresh the moment the machine URL self-heals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Four defects surfaced on-device: 1. Triggered only when entering the Live View. useLiveActivitySync was mounted inside LiveShotView, so it ran only while that screen was open. Hoist it to the app root (alongside useShotTelemetryRecorder) so the activity starts the moment a shot is detected/started, regardless of the current view. 2. Heating temperature showed 0°. The machine emits heating thermocouples on the Socket.IO 'sensors' event, but ShotStreamer only recognised 'temperatures' (which never fires), so chamber/head stayed nil -> 0°. Accept 'sensors' (keep 'temperatures' for older firmware) and, since heating has no 'status' frames carrying temps, push a Live Activity update on each temp frame (merged into the last known frame). 3. Chamber/head were swapped vs. the in-app live view. Match the app mapping: t_bar_up = boiler / 'Brew Chamber', t_bar_down = 'Brew Head'. 4. Expanded Dynamic Island overflowed (long profile name in the .leading region spilled past the camera) and showed almost no content. Keep leading/trailing compact (icon + glanceable) and render the full ShotLockScreenView in the .bottom region. Swift + web tests updated; MeticWidgetsTests 43 pass, web suite 1227 pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The expanded Dynamic Island clipped a few pixels on every edge: the phase icon (top-left), the glanceable degree value (top-right) and the heating progress bars (both ends) sat flush against the rounded corners. Inset the leading / trailing region content and add horizontal padding to the bottom region so nothing touches the corners. The lock-screen presentation was already padded and unaffected. Also replace the bare orange header dot with the shared 'Metic.' wordmark so the activity is recognisably branded (the lone dot was ambiguous). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Live Activity froze mid-heating and never recovered because ShotStreamer finished its stream permanently on the first socket drop (a transient WiFi blip or a missed Engine.IO heartbeat killed it for good), and every update passed staleDate: nil so the frozen values looked live. - ShotStreamer now reconnects with exponential backoff (0.5s -> 8s), resetting on a successful handshake, capped by a max-duration guard so it can't spin forever. Both the design spec (reconnect/backoff, line 126/168) called for this; it was missing from the implementation. - Every activity.update now carries a staleDate (~12s) so when iOS suspends the backgrounded app and the local socket stops, the system dims the activity instead of presenting stale values as current. - start() ends any activity orphaned by a prior app termination so a new shot can't stack a duplicate. Note: continuous background updates while the app is suspended still require an APNs push relay, which native mode intentionally lacks (accepted constraint). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…trings When iOS suspends the backgrounded app the local socket can't run, so the activity goes stale. It now shows a localised 'Open Metic to resume live updates' hint (driven by ActivityViewContext.isStale) so the user knows the frozen values aren't live and how to refresh them — the app can't self-reconnect while suspended (no code runs; only APNs push, which native mode lacks, could). Also fixes an i18n gap: every widget label (Brew Chamber/Head, Ready, Start, tiles, summary, Done) was hardcoded English. They're now passed from the web layer via a ShotLocalizedStrings bundle (single source of truth = react-i18next), localised across all 6 locales, with English defaults as a safety fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update all user-facing and contributor docs to reflect the 3.0.0 single distroless Bun binary architecture and the removal of the legacy Python/ FastAPI + s6-overlay + nginx + MQTT/bridge/MCP stack. - README/API: drop s6-svc, /api/logs, /api/bridge, /docs, missing addons.ps1; correct the WebSocket + machine-command docs (Socket.IO, not MQTT) - HOME_ASSISTANT: convert to a clean "removed in 3.0.0" + migration doc - UPDATING/IOS_SHORTCUTS: fix stale s6/8000//docs references - docs/local-shot-analysis: point at shared @metic/core TS logic + vitest - AGENTS/CLAUDE/GEMINI + copilot-instructions + CONVENTIONS + skills: Bun + @metic/core stack; correct the dual-runtime guardrail (logic now shared in @metic/core, DirectModeInterceptor removed) - apps/web + android READMEs: fix stale proxy/DirectModeInterceptor claims Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clean up development-only files from the published GitHub repo. Local copies of the design docs are retained via .gitignore; they are simply no longer tracked/published. - Untrack docs/superpowers/ plans & specs (shipped-feature design history) - Untrack the accidentally committed .copilot/ session state - Remove the transient .release-notes-beta.md - gitignore docs/superpowers/, .copilot/, and .release-notes-beta.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Metic 3.0.0 — unified TypeScript core + Bun server
Epic #532. This is the architectural cutover that ends the dual-runtime split: all analysis / profile / curve / recommendation / dial-in / machine-API logic now lives once in a shared
@metic/coreTypeScript package, consumed by both the browser/native app and a new Bun server. The Python backend is deleted.Highlights
@metic/core(packages/core): single source of truth for the request handler (handle()/tryHandle()), a host-agnosticPlatformseam (storage, blob store,machine.fetch, AI provider), and every/api/*route family: profiles CRUD, shot history/analysis, dial-in, annotations, pour-over, recommendations, profile generation, image-proxy, system meta, settings. Backed by a contract-test parity suite.apps/bun-server,@metic/server): proxy-mode server + NodePlatform(fs-JSON repos, env secrets, TTL AI cache, SQLite backend, timer scheduler). Transparent/api/v1machine proxy, static SPA,handle()for/api/*, Socket.IO→WS telemetry hub, Tailscale routes over the LocalAPI unix socket.Platform(IndexedDB/Capacitor) implements the same seam; the legacy ~3700-lineDirectModeInterceptoris deleted. Native iOS parity (image generation + profile-creation progress) confirmed on-device.feat!):apps/servergone; unified distroless single-binary image with a CI size gate.Notes
VERSIONstays3.0.0-beta.1(beta channel; latest stays 2.x).Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com