* docs(readme): reconcile root README as the single source of truth
/README.md (renders on GitHub) and work/README.md (packaged by
work/pyproject.toml's readme = "README.md") had cross-drifted: each
had two of four correct lines. Root had the correct `cd meshai/work`
path and work/-prefixed curl URLs; work/README.md had the correct
gemini-3.1-flash-lite model (google retired the gemini-2.x lite tier
on this project's API key).
Pull the one missing fix (model name) into root/README.md. Verified
via diff that these were the only 4 lines (8 diff lines) that ever
differed between the two files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(work): eliminate work/README.md as a second source of truth
The hand-maintained duplicate is what caused the cross-drift fixed in
the previous commit. Replace work/README.md with a symlink to
../README.md so there is exactly one file to edit.
This required unhooking work/'s packaging from a physical README.md:
- setuptools' pyproject reader hard-rejects readme paths outside the
project dir (`_assert_local` in setuptools/config/expand.py) — so
`readme = "../README.md"` is not an option, tested and confirmed.
- The symlink resolves fine for local packaging (pip install -e .,
python -m build --sdist/--wheel all tested passing, PKG-INFO
correctly carries the root content through the symlink).
- It does NOT resolve for the Docker image build: work/Dockerfile's
`COPY README.md .` and both work/docker-compose.yml (context: .)
and .github/workflows/docker-publish.yml (context: work) pin the
build context to work/, which does not contain the symlink's
target. Confirmed with an isolated repro: Docker COPY on a symlink
whose target is outside the build context fails with "too many
links". Repointing the build context at the repo root would touch
every COPY path in the Dockerfile plus CI — out of scope here and
not worth it for a README.
Chose the minimal fix instead: drop `readme = "README.md"` from
work/pyproject.toml (meshai isn't published to PyPI — no publish
workflow exists, only GHCR image publishing — so there's no
long_description to lose in practice) and drop the now-unnecessary
`COPY README.md .` from work/Dockerfile. Confirmed a dangling
same-named symlink left in the build context, never COPYed, does not
break context transfer.
Tested end-to-end: a full `docker build -f work/Dockerfile work`
against the real Dockerfile succeeded (frontend build, apt deps, pip
install -e ., fastembed model fetch), and the resulting image imports
meshai and reports correct `pip show` metadata with no README
involved.
Note for docs/onboarding-via-gui (PR #147) and
chore/untrack-dashboard-static: both edited work/README.md, the file
GitHub never rendered. That target is gone; their Quick-start content
needs to be re-applied to root README.md when those branches rebase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(dashboard): untrack committed frontend build output
work/meshai/dashboard/static/ held committed vite build artifacts
(index.html + hashed assets/*.js|css + copied public/ images) that were
last built 2026-07-07 (PR #87), 13 frontend commits ago. Docker builds
the frontend fresh and overwrites these on every image build, but the
pip install -e . path (documented in README Quick start) ships this
stale directory verbatim via [tool.setuptools.packages.find] include.
The root .gitignore already had a rule for this
("meshai/dashboard/static/") but it silently stopped matching when
2e1fb325 moved the source tree into work/ -- gitignore patterns
containing a "/" are anchored to the .gitignore's own directory, so the
unprefixed pattern only ever matched a repo-root meshai/dashboard/static/
that hasn't existed since that move. Fixed by prefixing with work/,
matching the existing work/data/secrets/.env convention elsewhere in
the file.
Every file under static/ is generated: assets/* and index.html come
from `vite build` (outDir: ../meshai/dashboard/static, emptyOutDir:
true, confirmed in dashboard-frontend/vite.config.ts), and the two
PNGs are vite's copy of dashboard-frontend/public/*.png (byte-identical
to the source). None of it is hand-authored, so the whole directory is
untracked rather than partially.
Blobs remain in git history; files remain on disk, just untracked.
* docs(readme): document the required frontend build for pip installs
work/meshai/dashboard/static/ is no longer committed (previous commit),
so the pip install -e . Quick start path now needs an explicit frontend
build step or the dashboard UI silently doesn't exist. Add it, with a
note clarifying the bot/API still work without it -- only the web UI is
affected. Docker is unaffected (already builds the frontend in its own
stage).
* fix(dashboard): log clearly when the built frontend is missing
Previously, if meshai/dashboard/static/ (or its index.html) was absent,
create_app() silently skipped mounting /assets and registering the
root/catch-all routes -- no log, no error. Any request to "/" would
just 404 with FastAPI's generic "Not Found", giving no clue why.
This was latent before (Docker always builds the frontend, and a git
checkout with the artifacts committed always had the directory), but
now that static/ is untracked, a fresh pip install without the frontend
build step will hit this path as its first real-world trigger. Add a
warning log naming the missing path and the exact build command, and
make clear the bot/API are unaffected -- only the dashboard UI is
absent.
* docs(readme): move frontend-build note to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the frontend-build documentation added in
56eba0f7 from work/README.md to root README.md so it isn't silently
dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
* fix(entrypoint): drop three phantom history keys from the seeded default
The config written on first boot -- what EVERY fresh Docker install starts
from -- seeded three keys that do not exist on HistoryConfig and are
silently discarded on load:
auto_cleanup: true
cleanup_interval_hours: 24
max_age_days: 30
HistoryConfig has only `database`, `max_messages_per_user`, and
`conversation_timeout`.
To be precise about the impact: history cleanup DOES work -- cleanup_expired()
is wired at main.py:237 and _prune_history() honours max_messages_per_user.
What never existed is the time-based retention model these keys describe (a
30-day age cutoff on a 24h interval). An operator setting max_age_days: 90
expecting 90-day retention was silently ignored.
Not implemented here -- whether time-based retention should exist is a
product decision, not a cleanup. This only stops the default config
promising it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: point onboarding at the dashboard, not the legacy example config
The documented setup path was `cp config.example.yaml config.yaml`, but that
file is ~40% incomplete: of the 20 top-level Config fields it covers 15,
omitting coverage (the universal bbox), danger_zones, generic_sources,
meshcore_context, commands, and the current notification model
(toggles/destinations/region_routes). Its own notifications header still says
the schema "will be replaced in v0.3 by the 8-toggle model" -- which shipped
long ago. Anyone following the project's own instructions landed on a
degraded surface with no signal a richer config existed.
It is also read by NOTHING at runtime: docker-entrypoint.sh sets
MESHAI_CONFIG=/data/config.yaml and writes its own inline default on first
boot. The Dockerfile still COPYs config.example.yaml into the image, so it is
kept and now labelled reference-only rather than a starting point.
- README: Docker quick-start seeds itself; configure via the dashboard. The
pip path still uses config.example.yaml (nothing seeds one there) but now
carries an honest note that it is a minimal bootstrap, not a reference.
Adds an Advanced section for the split /data/config/ layout and the
migrate_config_v03 path into it.
- docker-compose.yml: the comment claimed config lives at /data/config.yaml
as though that were the only layout; corrected to describe both, and note
secrets live in /data/secrets/.env.
- Dockerfile: document why config.example.yaml is still shipped.
Not done deliberately: config.example.yaml is NOT expanded to cover all 20
sections. A second hand-maintained schema is what caused this drift; the
dashboard is the authoritative surface. The legacy single-file loader is
untouched and still fully supported.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(readme): move dashboard-first onboarding rewrite to root README
work/README.md is about to become a symlink to root README.md on
docs/single-readme. Retarget the Docker/dashboard-first quick start
and config.example.yaml honesty note added in be867d23 from
work/README.md to root README.md, adapted to the root file's own
Quick start structure (cd meshai/work, /work/-prefixed curl URLs), so
none of it is silently dropped when that symlink lands.
---------
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 2e1fb325 ("refactor: move source tree into work/, multi-stage Docker
build") COPIED the source tree into work/ but never removed the top-level
originals, so main has been tracking two full copies of the app ever since.
Only work/ is live:
- CI (.github/workflows/docker-publish.yml, the sole workflow) builds with
`context: work` / `file: work/Dockerfile`. It never references the
top-level meshai/, Dockerfile, tests/, or pyproject.toml.
- Production (CT 108) runs work/docker-compose.yml, whose build context is
work/ and whose Dockerfile does `COPY meshai/ ./meshai/` relative to it.
- work/ is self-contained: its own Dockerfile, docker-compose.yml,
pyproject.toml, requirements.txt, config.example.yaml, dashboard-frontend/
and tests/.
The top-level copy carried ZERO unique content. work/ is strictly newer
everywhere (migrations v19-v28 vs top-level's v18; whole subsystems the top
level never had: coverage, generic_http, satpass, wzdx, notifications/gating,
notifications/formatters, transport/). Every file that existed ONLY at the top
level is legacy code that work/ deliberately removed after the fork:
- meshai/cli/configurator.py removed from work/ in 5de683f7 (#45, legacy TUI configurator)
- meshai/commands/subscribe.py removed from work/ in 9e6a3715 / 04604624 (subscription backend)
- meshai/subscriptions.py removed from work/ in 9e6a3715 / 04604624 (subscription backend)
- meshai/notifications/scheduled/fire_digest.py
removed from work/ in b0b0697b (#107, fire digest feature)
- meshai/notifications/pipeline/severity_router.py
never existed in work/; superseded by notifications/gating + formatters
- tests/test_fire_digest_recency.py tests the removed fire digest
- tests/test_meshcore_per_family_routing.py
tests the top-level-only meshcore_channel impl, superseded in work/ by transport/
- dashboard-frontend/src/pages/Alerts.tsx
removed from work/ in 9e6a3715 (subscription backend)
- dashboard-frontend/src/pages/Environment.tsx.bak
stray backup file, never in any build
The duplication is an ACTIVE HAZARD, not just dead weight: a fix applied to the
top-level meshai/ passes review, gets merged, and then silently does nothing,
because neither CI nor prod ever reads that tree. This already happened --
ff3ded8c (#9, "independent per-family MeshCore channel") landed its config.py /
connector.py / channels.py / dispatcher.py changes in the DEAD top-level copy
while work/ got only the transport-layer half. (Nothing was lost there: work/
carries a full, later-generation implementation of the same feature wired
through config.py, connector.py, transport/base.py, transport/composite_transport.py,
transport/meshcore_transport.py and the dashboard routes.) Deleting the copy
makes that class of mistake impossible.
Removed (280 files): meshai/, tests/, dashboard-frontend/, config/, Dockerfile,
docker-compose.yml, docker-entrypoint.sh, config.example.yaml, pyproject.toml,
requirements.txt.
Also fixes README Quick start, which pointed at the now-deleted root paths
(main/docker-compose.yml, main/config.example.yaml -> main/work/...) and still
advertised `meshai --config`, the interactive TUI removed from work/ in 5de683f7.
Verification: work/ test suite unchanged before and after --
20 failed, 2240 passed, 72 skipped (identical failure list; all pre-existing).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Native adapters vs. Central" subsection: each hazard feed chooses its
data path with feed_source (native = direct public-API fetch, the default and
original path; central = subscribe to Central's pre-aggregated NATS JetStream
firehose). Documents the environmental.central config, mutual exclusivity,
the satpass (central-only) / ducting (native-only) exceptions, runtime
auto-reconnect behavior, and the startup-needs-Central caveat.
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrite the README to reflect the current project: dual-transport
(Meshtastic base + MeshCore auto-on via meshcore_host), the web dashboard
(with live screenshots), per-mesh routing, the conversational bot with
per-mesh scoped context + three privacy lanes, environmental/hazard
broadcasts, mesh-health scoring, and the RAG knowledge base. Removes the
retired subscription backend/commands, updates the LLM model + architecture,
and adds live dashboard screenshots under docs/images/.
For transparency, documents that the project was vibecoded (built with LLM
coding assistants).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add MeshMonitor Integration section with features and configuration
- Add credit line in Features list
- Link to github.com/Yeraze/meshmonitor
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Features:
- Multi-backend LLM support (OpenAI, Anthropic, Google)
- Rolling summary memory for token optimization (~70-80% reduction)
- Per-user conversation history with SQLite persistence
- Bang commands (!help, !ping, !reset, !status, !weather)
- Meshtastic integration via serial or TCP
- Message chunking for mesh network constraints (150 char limit)
- Rate limiting to prevent network congestion
- Rich TUI configurator
- Docker support
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>