BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2,或任何藍牙控制器搭配 MyWhoosh、Rouvy、Zwift 等。可於 iOS、Android、Windows 與 macOS 免費試用。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Self-hosted dive log that imports from DiveMate and Garmin, exports to UDDF, CSV, and DiveMate, and exposes a read-only MCP endpoint with a built-in OAuth server
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗[!TIP] Try it in ten seconds. The read-only demo runs entirely from seven anonymized sample dives with complete profiles — no account, no database, nothing to install.
[!TIP] Planning your next dive? DiveCalc.app offers free scuba calculators and practical guides for MOD, SAC/RMV, gas planning, Nitrox, unit conversions, and safer dive preparation.
Divetracx is a self-hosted home for your dives, sites, buddies, equipment, cylinders, gases, profiles, pictures, and certifications.
Dive computers, phone apps, and exchange formats each keep their own copy of your dives, with their own schemas and their own gaps. Divetracx pulls them together into one canonical logbook: run a full historical import once, then let incremental syncs pick up new dives — or skip the import entirely and log dives straight from the UI. Either way the data lives on your own PostgreSQL and storage, and you can export it again in the format of your choice.
.ddb backups (with photos and
certification cards) and Garmin dive computers via the official FIT SDK..ddb, Subsurface .ssrf,
UDDF 3.2.3, CSV, or versioned Divetracx JSON at any time.Three import sources are supported today. All are run from Settings → Integrations and from the command line; DiveMate and Garmin can also run on a schedule. Every run lands in Import history, which reports discovered, new, changed, unchanged, and failed source records.
| Source | Full import | Incremental sync | What comes across | How it connects |
|---|---|---|---|---|
DiveMate .ddb | ✅ | ✅ | Dives, profiles, tanks, sites, buddies, equipment, equipment sets, certifications with card images, shops, dive types, diver profile, pictures | Reads DiveMate.ddb plus its Media and Cards files from a Google Drive folder via a service account |
| Garmin dive computers | ✅ | ✅ | Dives, profiles, tanks, gases; gear and certifications from the Garmin Dive app | Divetracx signs in with your account (MFA supported), pulls FIT activities from Garmin Connect, and reads gear from the Garmin Dive service |
Subsurface .ssrf / .xml | — | ✅ | Dives, profiles, cylinders, gas changes, sites with GPS, buddies and dive guides, weights, tags | Upload a logbook file from the browser or the CLI |
[!NOTE] Not yet supported: importing UDDF, CSV, or Divetracx JSON. Those three formats are export-only for now. If your dives live elsewhere, log them by hand, or export them from Subsurface — which reads most dive computers and logbook formats — and upload that file.
Full and incremental mean different things:
Each integration card has a What to sync list where you can switch off the kinds of data you don't want brought across — say, certifications from DiveMate while you keep its dives. Everything is on by default. A switched-off kind is left out of later imports but nothing already imported is removed, and dives keep pointing at sites, people, and gear that came across earlier. Some kinds are tied together and the list says so: profiles and tanks belong to dives, gear sets are made of gear, and a Garmin activity or Subsurface dive is the thing its profile and people are read from, so those dives are always on. Switching an entity back on imports its records as new.
Configure DIVEMATE_GOOGLE_DRIVE_FOLDER_ID and GOOGLE_APPLICATION_CREDENTIALS
for a service account with read access to the Drive folder holding your
backup. The importer uses SQLite row IDs and content hashes for idempotent
incremental detection. Images are copied to configured storage with immutable
originals and generated WebP thumbnails.
DiveMate buddy IDs import as buddies. Its legacy free-text Divemaster value
imports as a linked person with the Divemaster role. Divetracx exports
Divemaster, Instructor, and Guide assignments back into that field with role
labels, so those richer assignments survive a Divetracx → DiveMate → Divetracx
round trip.
bun run sync:divemate # incremental, from the CLI
bun run import:incremental --integration=divemate # same thing, generic entry point
Garmin Connect runs directly in Divetracx’s server process. Connect your Garmin account once from Settings → Integrations; MFA is supported. Passwords and verification codes are used only for the login, while the resulting OAuth tokens are retained in Divetracx’s server-only database for later imports.
Dives are listed through the Garmin Dive service (the backend of the Garmin Dive app), so dives you logged by hand in the app come across too, and each dive’s original FIT file is downloaded for everything the computer recorded: the depth, temperature, and ceiling profile with remaining no-deco limit, time to surface, CNS and nitrogen loading per sample; gases with their planned role (bottom, deco, travel); the decompression model and gradient factors; CNS and OTU totals; fresh or salt water; every alert and gas switch as a timeline of computer events; the recording device with serial and firmware; and — when the watch recorded it — heart rate. Wrist heart rate underwater is off by default on Descent computers; enable it in the watch’s dive settings if you want it in your profiles.
A dive read from more than one source shows all of them under Recorded by on its page, with what each contributed. No source is treated as primary: each one fills in whatever the dive did not have yet.
Activities are reconciled against existing log entries by start time within 45 minutes, so a computer-recorded profile attaches to the dive you already logged instead of creating a duplicate.
Gear and certifications you keep in the Garmin Dive app come across too. They live in a separate, undocumented Garmin service (the one the Dive app talks to), which Divetracx reaches by exchanging the stored Connect token for a Dive-scoped one — no second login. Gear brings brand, model, serial number, purchase date, price and shop, weight, retirement, and the next service date; Garmin stores certifications as a kind of gear, so those arrive with name and date only. Both are matched to existing gear and certifications by name before anything new is created, so a DiveMate import of the same item is enriched, not duplicated. Because this API is unofficial, a failure there is recorded in the run's diagnostics and never blocks the dive import. Switch either off under What to sync to skip the Dive service entirely.
The Probe Dive API button under the Garmin account exercises those endpoints read-only and shows what each returned, which is useful when Garmin changes something.
bun run import:incremental --integration=garmin
Divetracx reads the native Subsurface XML in all three layouts it has used over
the years — the original <dives> files, <divelog version='2'> with inline
<location> elements, and the current <divelog version='3'> with a
<divesites> table — so .ssrf files saved by any Subsurface release import.
Because Subsurface has no stable dive identifier, each dive is keyed by its
start date and time; uploading a newer export of the same logbook updates those
dives in place and never deletes anything. Sites are matched to existing
Divetracx sites by name or coordinates before new ones are created, buddies and
dive guides become people, cylinders become tanks, gas-change events become the
active tank on each profile sample, and boat/shore/deco tags map onto the
entry type and decompression flag. A tag that matches one of your dive types
assigns it. The file itself is not stored; only the parsed records and a
fingerprint of the upload are kept for change detection.
bun run import:subsurface --file=backups/logbook.ssrf
Everything you put in, you can take out again. Settings → Export offers
one-click downloads, and the same files are served from /api/export/* for
scripts and backups.
| Format | Endpoint | Best for | What it contains |
|---|---|---|---|
DiveMate backup .ddb | /api/export/divemate | Opening your whole logbook in DiveMate again | A DiveMate-compatible SQLite database rebuilt from canonical data, including Garmin-imported and hand-logged dives |
Divetracx backup .json | /api/export/json | Complete, lossless backups | Every table in the database as versioned JSON (divetracx-backup, currently version 17) |
Dive spreadsheet .csv | /api/export/csv | Spreadsheets and data analysis | One joined row per dive: site, buddies, equipment, tanks, conditions, notes, and the full profile as inline samples |
Universal dive log .uddf | /api/export/uddf | Other logbook software | UDDF 3.2.3 with diver, sites, and dives including depth profiles |
Subsurface logbook .ssrf | /api/export/subsurface | Opening the logbook in Subsurface, or a Subsurface ⇄ Divetracx round trip | Native Subsurface XML (format 3) with sites and GPS, cylinders, weights, buddies and dive team, tags, gas-change events, and full profiles with per-tank pressure and deco ceiling |
The DiveMate export uses your configured .ddb only as a schema template and
rebuilds the supported tables from canonical data. Besides downloading it, you
can publish it back to Google Drive with Export to Drive on the
Integrations page. That replaces DiveMate.ddb in your Drive folder, asks for
confirmation, and is never triggered automatically; Drive keeps the previous
revision. The same export is available from the CLI:
bun run export:divemate --output=backups/DiveMate.ddb
[!WARNING] Exports contain personal data — locations, health-related notes, contact details, certification numbers. Treat the files accordingly.
You do not need a dive computer or a DiveMate history to use Divetracx. The UI is a complete logbook editor, and hand-logged records live alongside imported ones in the same canonical model.
Dive computers end a dive when you surface, so a short surface interval can leave one dive logged as two. Open either entry, choose Merge dives, and pick the ones that belong with it. The earliest entry is the one that is kept — that is where the dive began, so it keeps its dive number — and the rest are appended to it at their real clock times and deleted. The tanks you kept breathing are continued rather than duplicated, buddies, gear, photos, and notes move across, and the duration, depths, and temperatures are recomputed over the whole dive.
The profile chart draws each recorded segment separately, with the surface interval between them shaded and left empty — no sample is invented to bridge the gap.
A merged dive is an ordinary dive and keeps syncing. It holds the provenance of every entry that went into it, so later imports update it as they always would and never recreate the entries you merged in. Merging itself is not reversible.
Because provenance is stored separately from the logbook, re-running an import never overwrites a field you edited yourself.
Divetracx exposes a scoped
Model Context Protocol endpoint so your own
AI tools can search and maintain dives, sites, buddies, gear, and your diver
profile. Every tool can be switched on or off under Settings → AI access.
Each connection then receives explicit read, write, and optional delete
scopes on the consent screen. Existing clients, their active scopes, immediate
revocation, and the last 100 authorization and tool events are managed in the
same page.
It ships its own OAuth 2.1 authorization server — dynamic client registration, mandatory S256 PKCE, refresh-token rotation with replay detection, hashed token storage, immediate revocation — and uses your existing Hodor owner session for consent, so no external identity provider is needed.
codex mcp add divetracx --url https://dives.example.com/api/mcp
codex mcp login divetracx
Tool results can contain private health, location, contact, and certification
data. Write tools use the same validation and canonical mutation services as
the web UI; delete tools are separately scoped and marked destructive. The
endpoint is always part of Divetracx at /api/mcp and can be paused without
deleting clients from Settings → AI access.
Routing details are in docs/deployment.md.
A modern, boring-where-it-counts TypeScript stack with full-stack type safety from the database row to the rendered field:
| Layer | Choice |
|---|---|
| Runtime and package manager | Bun 1.4 |
| Full-stack framework | TanStack Start with server functions |
| Routing | TanStack Router, file-based and fully typed |
| UI | React 19, shadcn/ui, Base UI, Tailwind CSS v4 |
| Icons and maps | Lucide, MapLibre GL |
| Database | PostgreSQL 18 with Drizzle ORM and committed migrations; ephemeral PGlite for the demo |
| Media storage | Local filesystem or any S3-compatible bucket |
| Dive computer data | Garmin's official FIT JavaScript SDK |
| Validation | Zod at every boundary, including environment variables |
| Tooling | Vite, Biome, Knip, bun test |
| Deployment | Docker Compose or Helm, fronted by the Hodor auth proxy |
Server-only concerns — database access, storage providers, Google Drive and Garmin transport, and secrets — never cross into browser code, and configuration is validated lazily on first use, reporting the affected variable names.
Docker Compose starts PostgreSQL 18, applies committed migrations, starts Divetracx, and exposes it through Hodor.
cp .env.example .env
# Replace HODOR_PASSWORD and HODOR_SECRET in .env.
# Generate a signing secret with: openssl rand -hex 32
docker compose up -d postgres
docker compose run --rm app bun run scripts/migrate.ts
docker compose up -d
Open http://localhost:3000, sign in through Hodor, and either start logging dives or head to Settings → Integrations to connect DiveMate or Garmin.
Divetracx uses Bun 1.4.0. With PostgreSQL available at the DATABASE_URL from
.env:
bun install --frozen-lockfile
cp .env.example .env
docker compose up -d postgres
bun run db:migrate
bun run dev
The unauthenticated development server listens on http://localhost:3000. It applies pending migrations before startup and watches for newly generated migrations.
| Command | Purpose |
|---|---|
bun run dev | Apply migrations and start the watched development server |
bun run check | Check source formatting and lint |
bun run typecheck | Run TypeScript without emitting files |
bun run test | Run tests; environment-gated integration suites are skipped |
bun run test:integration:imports | Run the PostgreSQL-backed import integration tests |
bun run test:integration:divemate-export | Run the DiveMate writeback integration tests |
bun run lint:deadcode | Find unused code and dependencies with Knip |
bun run build | Build production assets |
bun run build:demo | Build the database-free Vercel demo |
bun run check:helm | Lint and template the Helm chart |
bun run verify | Run the complete local quality gate |
bun run db:generate | Generate a Drizzle migration after a schema change |
bun run db:migrate | Apply committed migrations |
bun run db:studio | Open Drizzle Studio |
bun run db:seed | Seed the same fictional dataset as the public demo |
bun run sync:divemate | Run an incremental DiveMate import |
bun run import:incremental --integration=garmin | Run an incremental Garmin import |
bun run import:subsurface --file=backups/logbook.ssrf | Import a Subsurface logbook file |
bun run export:divemate --output=backups/DiveMate.ddb | Rebuild a DiveMate backup from canonical data |
bun run media:refresh-thumbnails | Regenerate WebP thumbnails for stored pictures |
| Guide | What is inside |
|---|---|
| Development and testing | Workflow, tests, tooling, CLI import and export |
| Configuration reference | Every environment variable |
| Deployment | Docker, Compose, Helm, MCP routing, security boundary |
| Read-only demo mode | How the PGlite demo is built |
| Design system | Layout contract and UI conventions |
| Helm chart reference | Chart values, Secrets, CronJobs |
src/routes/ TanStack Router file routes and API endpoints
src/components/ Shared application UI and shadcn/ui primitives
src/modules/ Domain modules: dives, sites, gear, export, media, profile, …
src/modules/mcp/ Scoped MCP tools, owner policy, and built-in OAuth 2.1 server
src/modules/integrations/ Generic import service, runs, and provenance
src/modules/divemate/ DiveMate .ddb reader, mapper, and writer
src/modules/garmin/ Garmin Connect client, FIT mapping, and import flow
src/db/ Drizzle schema and database connection
drizzle/ Committed migrations and metadata
scripts/ CLI entry points: migrate, seed, sync, and export
charts/ Helm chart
[!WARNING]
src/routeTree.gen.tsis generated. Runbun run generate-routesinstead of editing it manually.