Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
브라우저만으로 무료 베다 별자리 운세 제공 계정·서버 필요 없고 최초 로드 후 오프라인 오픈소스 엔진 정직한 해석
Free Vedic (sidereal) astrology computed entirely in your browser. No accounts, no server, offline after first load. Open-source engine, honest readings.
라이프스타일TypeScript · PythonGitHub @gainratio최근 접속 확인 실패운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Your Vedic (traditional Indian) astrology chart, free and computed in your own browser — no account, no data harvesting.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Your Vedic (traditional Indian) astrology chart, free and computed in your own browser — no account, no data harvesting.
Real output of the example below — the home screen make demo opens at http://localhost:4173 (the same app runs at almamesh.com).README 전문 보기 ↗Your Vedic (traditional Indian) astrology chart, free and computed in your own browser — no account, no data harvesting.
Live demo · Docs · Quickstart
Technical docs: Architecture · Getting started for developers · In-browser engine · Security policy
Real output of the example below — the home screen make demo opens at http://localhost:4173 (the same app runs at almamesh.com).
main; changes since v0.4.0 are listed under Unreleased. See CHANGELOG.Fastest: open almamesh.com — nothing to install, no sign-up.
To run your own copy (needs uv and Bun):
git clone https://github.com/gainratio/almamesh.git && cd almamesh && make demo
The first run takes longer than a minute: it installs packages, downloads the in-browser Python engine and the planet-position tables once, signs a local copy of them with a throwaway key, and builds the app. Then:
cd backend && uv sync --extra dev
uv run almamesh-chart "1990-01-15T12:00:00+00:00" 40.7128 -74.0060 # New York, 15 Jan 1990, 12:00 UTC
Excerpt of the real output. lagna is the rising sign; a nakshatra is one of
the 27 star groups the Moon passes through, split into four pada quarters:
{
"ayanamsa_value": 23.7174,
"lagna": {
"longitude": 264.9083218673543,
"sign": "Sagittarius",
"sign_degrees": 24.90832186735429,
"sign_lord": "jupiter",
"nakshatra": "Purva Ashadha",
"nakshatra_pada": 4,
The full JSON (about 47 KB) also lists every planet, the houses, and the time
periods. It runs with no network and no account. Everything above depends only
on the three arguments. The current_maha, current_antar, and
current_pratyantar fields do not: this CLI has no reference-date option, so
they show the periods running on the day you run it.
More runnable examples: examples/.
The first time you generate a chart, your browser downloads a signed engine bundle (the real Python chart engine, its planet-position tables, and the packages it needs) and checks its signature and every file's hash before using it. The engine then runs inside your browser tab, in a background worker, using Pyodide (Python compiled to WebAssembly). Your birth details go straight from the form to that engine; the finished chart is saved in a database inside your browser. The only lookup that leaves the device during chart creation is the city name, and only while you search for your birthplace.
flowchart LR
A["You enter birth date, time, place"] --> B["Chart engine running in your browser tab"]
B --> C["Your chart, saved only in this browser"]
D["Signed engine download from almamesh.com, about 38 MB, then cached"] -.->|signature and hashes checked before use| B
A -.->|city name only| E["Open-Meteo place lookup"]
classDef blue fill:#e8f4f8,stroke:#7aa7b8,color:#171717
classDef green fill:#e8f8e8,stroke:#7ab87a,color:#171717
classDef orange fill:#f8f0e8,stroke:#b8987a,color:#171717
classDef purple fill:#f0e8f8,stroke:#9a7ab8,color:#171717
class A blue
class B,C green
class D orange
class E purple
Explore the interactive architecture map →
(Archify, generated from docs/architecture/runtime.architecture.json).
Deep dive: docs/ARCHITECTURE.md.
Most astrology apps ask you to trust a server with personal birth data. AlmaMesh does not have a chart-data server. The browser downloads a signed engine once, verifies it, and computes charts locally in a Web Worker.
The engine reads no clock when the app calls it. A chart is a pure function of four recorded inputs — birth instant, latitude, longitude, and the reference instant that selects the current Vimshottari period. The app stores that fourth input with the chart, so the same inputs produce the same bytes on CPython and in the browser. (The terminal CLI above is the exception: it passes no reference instant, so the engine falls back to today's date for the "current period" fields.)
Browser (the product) ─ installable PWA, offline after first load
│
├─ frontend/apps/web React + Vite + Tailwind UI
│ └─ birthplace search Open-Meteo first, bundled city fallback
│
├─ frontend/packages/browser the in-browser engine
│ ├─ edge-proc bundle sync ──▶ verifies ed25519 + sha256
│ │ SQLite on OPFS; in memory + a notice if OPFS is refused
│ └─ Pyodide Web Worker ────▶ boots the UNCHANGED almamesh wheel, computes the chart
│ │ emits SiderealChart (TS mirror of the Python SiderealContext)
│ ▼
├─ frontend/packages/store pure adapters (reshape only, no astrology):
│ ├─ SiderealChart -> ChartData (the UI contract)
│ ├─ buildChartGeometry(SiderealChart) (N/S kundli geometry)
│ ├─ buildEnergyFrame(SiderealChart, t) (3D force-field frame)
│ ├─ profiles + members (named, password-less people; typed relationships)
│ ├─ portable SQLite state canonical user data in one OPFS database;
│ │ portable choices + AI key included; legacy storage
│ │ is verified, migrated once, and then deleted
│ └─ mesh (MeshEdgeContext per pair → the /mesh edge view)
├─ frontend/packages/llm optional interpretation + chat, NO AI by default;
│ opt-in, BYO OpenAI-compatible endpoint (one-click
│ OpenRouter preset or a local Ollama); save runs a
│ connectivity test; chart prompts PII-redacted, fail-closed
│ local_only; mesh narration is role-anonymized
│ (no names leave the device)
├─ frontend/packages/shared-types UI-facing TypeScript contracts
├─ frontend/packages/constants single design-token source
├─ frontend/packages/memory local semantic chat memory:
│ ├─ MiniLM Web Worker self-hosted, on-device embeddings
│ └─ SQLite vector Worker sqlite-vector exact search; OPFS persistence
External Lego: @gainratio/browser signed-bundle sync plus SQLite/vector browser substrate
Build-time (Python, no server)
│
└─ backend/src/almamesh
├─ calculations.py sidereal astronomy (Skyfield + DE421; Lahiri default,
│ True-Chitra ayanamsa + True-node selectable)
├─ dasha/ yogas/ Vimshottari dasha + yoga detection
├─ transits/ strength/ predictive: Gochara/Sade Sati, Ashtakavarga + Shadbala, vargas
├─ mesh/ relationship engine: Ashtakoota Guna Milan + Mangal (cited
│ classical tables), chart overlay, daśā synchrony, significators
│ → a frozen, read-only MeshEdgeContext per pair
└─ edge/
├─ chart_runtime.py deterministic on-device chart runtime (also runs under Pyodide)
├─ bundle.py signed bundle publisher + consumer
├─ cli.py almamesh-chart (offline chart, no browser)
└─ publish_cli.py almamesh-bundle (keygen + sign + publish the bundle)
The Python entrypoint the browser calls (calculate_sidereal_context(..., reference_date=...)) is the same one the CLI calls. The fixed reference_date
pins the "current" dasha, which is what makes a chart reproducible byte-for-byte
across CPython and Pyodide.
See frontend/README.md for the monorepo layout and the
full set of dev/build/test commands.
.almamesh file—including the
optional API key—that you can restore in another browser. For example, export
in Chrome, move the file to an iPhone, and import it in Safari to recreate the
same durable AlmaMesh state. You enter the transfer password once during
import; the installed live OPFS database then opens normally without a
password prompt. The decrypted payload is standard SQLite; rebuildable caches
and signed engine assets are left out —
spec.| Option | Better choice when | Where AlmaMesh differs |
|---|---|---|
| Paid astrology sites and apps | You want a human-written or curated reading, or an astrologer to talk to | Free, no account or email, and no AlmaMesh server ever holds your birth data; with the optional AI off, nothing about your chart leaves your device |
| Free online chart calculators | You only need a quick one-off chart and don't mind the site seeing your birth data | The calculation runs in your tab; charts are saved locally and work offline |
| Desktop astrology software (for example, Jagannatha Hora on Windows) | You are a practitioner who needs far more techniques and settings than AlmaMesh offers | Runs in any browser on any OS; the engine is open source and checked against an independent astronomy reference |
| Do nothing / ask a family astrologer | You trust a person more than software | AlmaMesh shows its math and states what it cannot prove |
The chart engine is zero-egress: chart computation stays on your device, and it is deterministic in the sense above — same four recorded inputs, same bytes. The complete browser network inventory is:
| Trigger | Destination | Data sent | Explicitly not sent |
|---|---|---|---|
| First load, signed engine sync, PWA update | almamesh.com | Asset URLs, normal HTTP request metadata | Names, birth data, charts, life events |
| Birthplace search while online | Open-Meteo geocoding | City text you type plus ordinary HTTPS/request metadata visible to the provider | Name, birth date/time, chart; an offline city-list fallback is bundled |
| Optional interpretation or chat | Your configured OpenAI-compatible endpoint | PII-redacted chart facts (planet positions and month-precision period dates, which can reveal the birth date and approximate time) and, for chat, the question you type | Profile name and the birth date field itself |
| Optional life-event organization | Your configured OpenAI-compatible endpoint | The narrative you submit, as written, after the disclosure | Birth details and chart data |
| OpenRouter model list or credit check in Settings | OpenRouter | API request plus the configured key; the credit response concerns that provider account | Chart, birth data, chat, life events |
| Opening feedback when Turnstile is configured | Cloudflare Turnstile | Normal anti-bot request metadata and a challenge token | Chart and birth data |
| Sending feedback | Same-origin /api/feedback | Page identifier, thumbs sentiment, optional message as written, Turnstile token | Chart and birth data unless the user puts them in the optional message |
local_only fail-closes if a configured AI endpoint is not local. Production
diagnostics emit allowlisted codes only—never raw prompts, narratives, provider
errors, city text, chart data, or profile names.
The browser enforces that table. This inventory is not a promise you have to
take on trust: the deployed Content-Security-Policy (see
frontend/apps/web/public/_headers) restricts connect-src to a closed
allowlist—this origin, openrouter.ai, geocoding-api.open-meteo.com, and
loopback (localhost / 127.0.0.1, any port, for a local model). Any other
destination is refused by the browser itself, so code that tried to send your
data somewhere else could not, even if it wanted to. A consequence worth knowing:
a custom AI endpoint on a non-loopback address—a LAN machine like
http://192.168.1.10:11434, or a third-party proxy—is blocked by that policy;
use a loopback endpoint or OpenRouter.
latest
pointer is checked against the ed25519 release key (public.key, served from
the same origin), and the manifest and every content-addressed chunk against
their SHA-256 hashes. Production bundles carry a signed, increasing sequence
number, so an older genuinely-signed bundle cannot be replayed over a newer one.local_only refuses a non-local AI endpoint. The engine refuses out-of-range
coordinates instead of silently computing a wrong chart. The browser refuses
network destinations outside the allowlist above.almamesh.com origin (the verification key is delivered from that
same origin); what the AI provider you opt into, or Open-Meteo, does with what
you send it. Data inside the browser is not encrypted by AlmaMesh — anyone with
access to your browser profile can read it (every current export is
password-encrypted).make demo signs a local
bundle with a throwaway key), run the accuracy and parity checks in
What this proves, and compare
deployed bundles against the live https://almamesh.com/public.key — see
Publish a signed bundle for why a local
key never matches the production key.See SECURITY.md for reporting a vulnerability.
Planet positions agree with the committed astropy + JPL Horizons oracle to about 0.05
arcsec; the natal regression gate is looser (18 arcsec for planets, 180 arcsec for the
ascendant) and the transit gate enforces 1 arcsec. You can check this yourself. It runs fully
offline: the engine uses the vendored backend/de421.bsp, and the independent
astropy + JPL Horizons oracle values are committed as fixtures (no live download
or API call at test time).
# Engine longitudes vs an independent astropy oracle (with a committed JPL
# Horizons cross-check), agreeing to sub-arcsecond — natal + transits:
cd backend && uv run pytest tests/validation/test_ground_truth.py tests/test_transit_reference.py -q
Yes, and you can watch it being checked. verify-browser-parity.mjs boots the
app in headless Chromium from a served origin, drives the real Pyodide Web
Worker, and asserts every fixture in the committed CPython golden comes back
byte-for-byte identical. It runs on every PR and every push to main.
cd frontend && bun install && bash apps/web/scripts/setup-dev-assets.sh && cd apps/web
bunx playwright install chromium # one time
VITE_API_URL= VITE_EXIT_GATE_HOOKS=1 ./node_modules/.bin/vite build --outDir dist-verify
VITE_API_URL= ./node_modules/.bin/vite preview --outDir dist-verify --port 4199 --strictPort &
node scripts/verify-browser-parity.mjs http://localhost:4199 \
--reference-date=2025-01-01T00:00:00+00:00
The reference date is an argument, not a constant baked into the gate. That
matters: the date pins the "current" dasha, and a gate that hardcodes the one
value it is pinning cannot tell you whether the pin ever reached the engine. So
the gate also runs a sensitivity control — the same chart at a different
reference date must produce different bytes. If it doesn't, the gate fails,
because a parity result that ignores its own inputs proves nothing. Pass a
different --reference-date yourself and watch the comparison go red.
The PDF report is reproducible: two exports of one chart are the same file,
asserted by SHA-256 in e2e/report-pdf.e2e.spec.ts.
What this does not prove.
backend/tests/fixtures/chart_golden_de421.json, and it fails if the golden
gains a fixture the gate does not compute. The transit, predictive, and mesh
goldens are enforced CPython-side by the backend test suite; their
Pyodide-in-a-browser parity is not browser-gated yet. Saying so here rather
than letting the badge imply more than it checks.TL;DR: one clone and the locked package installs build everything. The
Python side resolves edge-proc from
PyPI. The browser consumes the public
@gainratio/browser Lego
from npm at its latest release (^0.4.2, locked in frontend/bun.lock); no copied sync/storage implementation remains here. See
docs/edgeproc-browser.md for provenance and the
consumer boundary.
No sibling checkout, private access, or token is required: git clone, then
uv sync + bun install, then run. CI uses the same frozen locks.
Hosted: nothing to install — use almamesh.com, and install it from the browser as an app if you want it offline.
From source: requires Bun (version pinned in
frontend/.bun-version), uv, and Python 3.13.
Everything else resolves from public sources through committed locks and exact pins.
git clone https://github.com/gainratio/almamesh.git && cd almamesh
# One command, from the repo root. Installs deps, builds the dev assets, then
# builds and opens the app at http://localhost:4173.
uv run poe demo # same as `make demo`
The first run fetches the Pyodide dist and the DE421 ephemeris once (network required); after that the app is fully offline. Use
uv run poe demo-freshto force-rebuild the signed dev bundle.
# 1. Install workspace deps
cd frontend
bun install
# 2. One-time: build the dev assets the in-browser engine needs.
# This fetches a self-hosted Pyodide dist and signs a dev edge-proc bundle
# (DE421 + wheels + meta) into apps/web/public/ — all gitignored.
# The script lives at frontend/apps/web/scripts/setup-dev-assets.sh.
cd apps/web
./scripts/setup-dev-assets.sh
# 3. Build and preview. IMPORTANT: the engine's module Workers only resolve in a
# production build, NOT `vite dev` — so build first, then preview.
bun run build
bun run preview # prints a local URL, e.g. http://localhost:4173
Open the previewed URL, enter a birth date/time/place (birthplace search tries Open-Meteo first and falls back to the bundled city list), and generate a chart. After the location is resolved, chart calculation and rendering stay on-device; the app keeps working with the network disabled.
Dev-server caveat:
bun run dev(vite dev) is fine for editing UI, but the dev server's ESM module Workers fail to resolve thepyodideimport in worker scope, so the engine only runs in a real build (vite build+vite preview). The browser parity gate above drives exactly that build.
In the browser: onboard with a name, birth date/time, and city; the dashboard then shows the chart, periods, and timing. Profiles, people you add to your mesh, AI settings, language, and backup/restore live in Settings.
Terminal chart (almamesh-chart): the same engine as an offline CLI — no
browser, no server.
cd backend
uv sync --extra dev
uv run almamesh-chart "1990-01-15T12:00:00+00:00" 40.7128 -74.0060
It prints the full sidereal chart as JSON — ascendant, the nine grahas with
sign/nakshatra/pada, whole-sign houses, and the dasha hierarchy — with no
network and no account. (examples/run_chart.sh wraps the same call.)
The engine's data and wheels are delivered to browsers as a signed, content-addressed bundle. A device verifies its ed25519 signature against the same-origin release key and fails closed on any mismatch. The service worker keeps a release-matched offline copy, while online loads revalidate the key so a key and its newly signed bundle can rotate together. Compute always stays local; the network is delivery-only.
cd backend
uv run almamesh-bundle keygen ./keys # raw ed25519 keypair (0o600 private key); prints its key_id
uv run almamesh-bundle bundle ./origin ./keys/private.key --version v1
./origin is a static directory any web server or CDN can serve; public.key is
the release verification key delivered from that same origin. (setup-dev-assets.sh
runs this for you to produce the local dev bundle.)
One identity note for cold readers: the signing key is per-environment and never
committed. A local build uses a throwaway dev key that setup-dev-assets.sh
generates into frontend/apps/web/public/public.key (git-ignored), while the
production deploy injects the separate prod key from CI secrets — so the live
https://almamesh.com/public.key will not match your local copy, by design.
Verify live bundles against the live /public.key.
Nothing needs configuring to draw a chart. The knobs that exist:
local_only mode; language;
profiles and people; backup and restore. One OPFS SQLite database is
authoritative for durable user state, including your optional AI key;
localStorage and IndexedDB are not ongoing user-data mirrors. Older values are
read only for a verified one-time migration and are then deleted. A saved
interpretation that cannot be read is set aside, not dropped, in a
quarantine table inside that same SQLite file for up to 30 days. It is never
in backups. Deleting a profile removes the set-aside rows holding that
profile's readings; a row that matches no profile stays until "start fresh",
"Reset & reload", clearing site data, or the 30-day expiry. A backup
carries the key only inside the password-encrypted .almamesh transport file.
The live OPFS database is not password-encrypted, so normal launches never ask
for the transfer password. AlmaMesh has no account passwords, and the backup
passphrase is never stored or exported.VITE_ env vars for the web app): VITE_BUNDLE_BASE_URL
(overrides where the signed bundle is synced from; default is this origin),
VITE_TURNSTILE_SITE_KEY (enables the feedback anti-bot check), and the
VITE_LLM_* defaults for the optional AI provider. VITE_EXIT_GATE_HOOKS=1
is for test builds only.PYODIDE_VERSION, PYODIDE_DIST (use a local Pyodide dist
instead of downloading), and DEV_BUNDLE_SEQUENCE for
frontend/apps/web/scripts/setup-dev-assets.sh.backend, pydantic-settings, reads .env): EPHEMERIS_FILE
(default de421.bsp, the ephemeris the publisher ships).Signing keys are never committed: private.key files are git-ignored, and the
production key comes from CI secrets. Don't bake a real VITE_LLM_API_KEY into a
build you publish — build-time values end up in the shipped JavaScript.
Shipped (in v0.4.0 or listed under Unreleased in the CHANGELOG):
| Capability | What | State |
|---|---|---|
| Engine | Deterministic sidereal chart + dasha + yogas (Python); Lahiri ayanamsa and mean node | ✅ shipped, tested |
| Engine validation | External golden-reference check: astropy (independent code path) + committed JPL Horizons cross-check, measured agreement ~0.05 arcsec (gate tolerance 18 arcsec natal / 1 arcsec transit); license-clean (no Swiss Ephemeris) | ✅ shipped, tested |
| Bundle publisher | Signed, content-addressed bundle publish/sync | ✅ shipped, tested |
| Offline CLI | almamesh-chart, almamesh-bundle | ✅ shipped, tested |
| In-browser engine | The Python wheel in Pyodide/WASM, off the UI thread | ✅ shipped (byte-parity gated in CI, in a real browser) |
| N/S Indian charts | Degree-accurate SVG kundli off a pure geometry adapter | ✅ shipped |
| 3D force-field | three.js hero, planets at real ecliptic longitude | ✅ shipped |
| D9 Navamsa | Engine computes the Navamsa; renders in both kundli styles + the print report | ✅ shipped |
| Divisional charts (D1–D60) | Full Shodasavarga set; D9 also rendered as a kundli, the rest as tables | ✅ shipped |
| Predictive layer ("Sky & Timing") | Transits/Gochara + Sade Sati, dasha depth (antar/pratyantar), Ashtakavarga + Shadbala, per-life-domain forecasts; /predictive route (incl. a Periods explorer + Road Ahead) + report sections VIII–XI | ✅ shipped |
| The mesh (relational astrology) | Per-pair relationship read of two whole charts: Ashtakoota Guna Milan + Mangal screening (cited classical tables, partner edges only), chart overlay, daśā synchrony, significators; role-anonymized AI narration, read-only by construction; /mesh constellation + /mesh/:memberId edge view | ✅ shipped |
| Members | People you add to your mesh, with typed relationships (spouse/partner/family/friend/…), each owning a full chart; persisted with a versioned migration; managed in Settings → People | ✅ shipped |
| AI interpretation + chat |
The old SaaS backend (FastAPI, Postgres, Redis, Supabase auth) has been removed. AlmaMesh has no account or chart-data API; only the optional same-origin feedback function stores the disclosed anonymous feedback record.
Planned (not shipped):
almamesh-chart CLI.Versioning: v0.4.0 is the application release tag. Backend and frontend workspace package versions describe independently versioned implementation layers and need not equal the app tag.
make gate
That runs backend uv run poe gate (ruff, mypy, xenon, pytest with coverage)
and frontend bun run gate (typecheck, lint, unit tests, build) — the same two
commands CI runs. The required native Dagger checks also generate the signed
browser assets and drive real onboarding, parity, offline, privacy/reset,
Chromium/WebKit, and report-PDF journeys; run them locally with dagger check.
The backend gate asks Git which docs are public, inside Dagger, from your
checkout's .git directory. A linked git worktree has a .git file instead,
so there export the paths first; Dagger then uses that ignored manifest.
# in a linked worktree only, first:
python3 scripts/public_docs_manifest.py --repo-root . --output .almamesh-public-docs.json
dagger check
New here? docs/GETTING_STARTED.md takes you from a fresh clone to a green local build and your first change.
See CONTRIBUTING.md.
make demo to a rendered chart, in one page.MIT — see LICENSE.
| Off by default (pure calculation); opt-in BYO OpenAI-compatible endpoint (one-click OpenRouter preset or a local Ollama). Natal reading and current timeline are separate, explicit actions with independent progress and storage; mount/reload/day rollover spend no tokens. Prompts are PII-redacted and life-event prose is disclosed separately; fail-closed |
| ✅ shipped |
| PDF export | Report available after a chart exists (cover + D1/D9 + daśā + deterministic predictive sections VIII–XI + Birth Time Authority §XII); stable natal AI prose is optional. Date-sensitive AI timeline prose stays on the dashboard, where its generated/as-of date remains visible. Byte-reproducible: two exports of one chart are the same file, asserted by SHA-256 in e2e/report-pdf.e2e.spec.ts | ✅ shipped |
| Birth-time rectification | Per-profile rectified time + confidence in Settings; recomputes the chart | ✅ shipped |
| Named profiles | Multiple password-less people per device, each owning its charts; rename + delete (chart cascade) | ✅ shipped |
| Birthplace search | Online-primary Open-Meteo lookup with a bundled offline fallback; the city query and ordinary network metadata leave the device, never chart/profile data | ✅ shipped |
| Internationalization | English / Spanish / Portuguese; react-i18next, offline bundled catalogs (zero-egress), persisted language + <html lang> sync, AI answers in-language; en authoritative, es/pt machine-translated | ✅ shipped |
| PWA delivery | Service worker + offline reboot + provenance footer | ✅ shipped |