Easy-to-manage touch screens
무료 오픈소스 터치스크린으로 USB 설정 가이드, 드래그‑드롭 편집기, 로컬 제어를 제공하며 Home Assistant용으로 제작되었습니다
오픈소스 다이빙 로그북, 가스 혼합·다중 다이브·다이브 컴퓨터 가져오기·깊이 프로필·장비 이력·C‑카드 관리
An open-source logbook for scuba divers: gas mixtures, multiple sites per dive, dive-computer import with full depth profiles, gear service history and c-cards.
라이프스타일Python · DockerfileGitHub @opendiving온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Backend for OpenDiving, an open-source dive log. FastAPI + PostgreSQL + Redis. Dives with gas mixes, trips, dive sites, gear service schedules, c-cards, dive-computer import with per-sample profiles, SAC/RMV, passwordless auth.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
The backend component of OpenDiving — an open-source,
self-hostable dive log. FastAPI + PostgreSQL + Redis.
Your dive history should outlive any app. This API keeps every original dive-computer export
alongside the dive it recorded, serves the lot over a clean, documented REST API, and takes the
whole log back out in open formats in a single request — and back in again, so your data is
never more than one curl away in either direction. Run your own copy and the Postgres database
underneath it is yours too.README 전문 보기 ↗The backend component of OpenDiving — an open-source, self-hostable dive log. FastAPI + PostgreSQL + Redis.
Your dive history should outlive any app. This API keeps every original dive-computer export
alongside the dive it recorded, serves the lot over a clean, documented REST API, and takes the
whole log back out in open formats in a single request — and back in again, so your data is
never more than one curl away in either direction. Run your own copy and the Postgres database
underneath it is yours too.
Start at opendiving/opendiving. That is the front door: the pitch, the four-command install, the release the compose file comes from, and every self-hosting guide — installing, configuring, reverse proxies, backup and restore, upgrades and troubleshooting.
This repository is one of the two application containers that install brings up, alongside opendiving-web. It publishes an image and nothing else; what is here is the source, and the notes for working on it.
POST /dive/parse and get the dive back to pre-fill a form; the same reader, the
divejson package, reads both doors, and a file either door keeps gives the same recording. A
file of several dives, or of one dive two computers recorded, goes through logbook import instead.
Attach the file to the dive afterwards and the per-sample profile (depth, temperature, tank
pressure, deco ceiling, the diver's heart rate where the computer measured it, the computer's
own no-decompression clock, time to surface, ppO₂, CNS clock and both gradient factors, dive
events) is extracted server-side and served with ETag caching, its summary stating the maximum and
average heart rate.GET /user/species is the whole life list: every species you have logged, with
how many dives saw it and when. The taxonomy is WoRMS's, whose text content is available under
CC BY and which asks to be cited in full: WoRMS
Editorial Board (2026). World Register of Marine Species. Available from
https://www.marinespecies.org at VLIZ. Accessed 2026-09-11. doi:10.14284/170 — an accessed date a
README cannot keep current, and does not need to, because it stands for no copy of anything:
this app queries the register live rather than holding a snapshot of it, so an instance holds
whatever WoRMS answered on the days its divers went looking.GET /dive-sites/suggest answers from a catalog of real dive sites
bundled in the image, so the site form can offer "SS Thistlegorm" rather than only the town it is
near. A geocoder knows where Dahab is, not where the Blue Hole's north entry is. No account, no
key and no outbound call: the file ships with the app, and picking a suggestion makes a dive site
of your own that you can rename, move and annotate, which keeps the record's OpenStreetMap or
Wikidata entry. A suggestion you already hold names your site..ssrf, a FIT file, a Suunto app export, a
Suunto DM5 XML export, a Shearwater Cloud database or XML export, or a .zip of any of those is
converted on the way in by the divejson package, and each
dive's row lists what its files did not record, what the import worked out, and which of the
dive's values it left out. A file that is one dive is kept on the dive it becomes, and one
recording exported twice - a Suunto's FIT beside its JSON - is one recording with both files.
Restore a backup, migrate between instances, or bring a logbook across from whatever you were
keeping it in.divejson package rather than a change
here, and this app reads whatever the release it pins registers.For working on it, rather than for running it — this compose file builds from ./src, mounts it for
live reload, and publishes the API and Postgres on the host. To run an instance rather than
develop against one, install it from
opendiving/opendiving instead.
git clone https://github.com/opendiving/opendiving-api.git
cd opendiving-api
cp src/.env.example src/.env
openssl rand -hex 32 # put this in SECRET_KEY, and change POSTGRES_PASSWORD
docker compose up
That's the backend: the API on http://localhost:8000 (interactive docs at
/docs), PostgreSQL, Redis, and an arq worker for emails and
scheduled jobs. Pair it with opendiving-web for the
frontend.
An installed instance is configured through the .env beside its compose file —
the configuration reference
covers that one. From source, everything is configured through src/.env, which starts as a copy of
src/.env.example — every setting is listed and commented there. SECRET_KEY
is not optional: it signs every token the API issues, the template's value is published in this
repository, and the app refuses to start on it rather than letting a deployment run on a key
anyone can read. Change POSTGRES_PASSWORD too. The parts worth knowing about:
# Magic-link emails go out over SMTP. Leave SMTP_HOST unset for local development -
# the sign-in link is then only logged, not emailed. Point this at any relay you
# trust (your provider, your host's, your own); don't run your own MTA unless you
# already know why. Resend users: smtp.resend.com, username "resend", password = the
# API key. EMAIL_FROM_ADDRESS is required as soon as SMTP_HOST is set - startup
# fails without it rather than letting an undeliverable address surface hours later.
# SMTP_HOST itself is required on any ENVIRONMENT but local: sign-in is passwordless,
# so a deployed instance with no relay cannot let anybody in at all, and the logged-link
# fallback would put a working sign-in link in whatever collects that instance's logs.
SMTP_HOST="smtp.example.com"
SMTP_PORT=587
SMTP_TLS_MODE="starttls" # starttls (587) | tls (465) | none (a local relay only)
SMTP_USERNAME="..." # both optional: an anonymous relay needs neither
SMTP_PASSWORD="..."
EMAIL_FROM_ADDRESS="[email protected]"
# Used to build magic-link URLs ({FRONTEND_URL}/auth/verify?token=...)
FRONTEND_URL="http://localhost:3000"
# Where the frontend's support form (POST /api/v1/support) delivers to. No default:
# unset, that endpoint answers 503 and the form is simply off, which beats mailing
# your users' support requests to somebody else's inbox.
CONTACT_FORM_EMAIL="[email protected]"
# Optional: Google Sign-In. Both halves of one OAuth client, or neither - an id with no
# secret refuses to start. The id must match the frontend's NEXT_PUBLIC_GOOGLE_CLIENT_ID;
# the secret stays here. Register {FRONTEND_URL}/auth/google/callback as an Authorized
# redirect URI on that client.
GOOGLE_CLIENT_ID="your-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-client-secret"
Token lifetimes, rate limits, and the rest have sensible defaults — they're in src/.env.example
commented out, and src/app/core/config.py is the authoritative list.
With SMTP_HOST unset, nothing is sent and the sign-in link is written to docker compose logs api
— that's the intended local flow and it needs nothing running. To see the mail itself rendered
instead, bring up the opt-in Mailpit profile and point the app at
it:
docker compose --profile mail up # inbox at http://localhost:8025
SMTP_HOST="mailpit"
SMTP_PORT=1025
SMTP_TLS_MODE="none"
Comment SMTP_HOST back out when you're done: left set with no Mailpit running, sends fail against
a dead host and no link is logged either. Either way, an edit to src/.env needs
docker compose up -d --force-recreate api to take effect — a plain restart reuses the
environment the container was created with.
Everything is mounted under /api/v1, and the OpenAPI document is the reference for it — generated
from the routes themselves, so it describes the API as it actually is rather than as a list here
last remembered it. Every instance serves it at /api/v1/openapi.json, production included, with
no token; the interactive page that renders it, /docs, is open on local, behind a superuser on
staging and absent on production.
The families: auth (email link or six-digit code, Google, passkeys, refresh, sign-out, account
restore) and user (profile, avatar, check-in portrait, the check-in details, the check-in link,
email change, dive statistics, gas-use history, the species life list, species suggestions for the
dive form, account deletion); API keys, which a diver creates, lists and revokes for tools of
their own; dives, the bulk of it, with their recordings — attach an export, read one
recording's per-sample profile back, fold two dives into one — alongside trips, dive sites,
the shared species catalog a dive can reference, a geocoding helper for naming a site pinned
on a map, and the map tiles every card and page head is composed from, drawn once for the whole
instance; gear as items, sets, service schedules and service records; certifications with
their card images and the courses that issued them; export in DiveJSON, UDDF, CSV or
full-archive form and import back from either of the first and the last, or from any format the
converter reads; invitations, which exist only where the operator has closed registration
(REGISTRATION_MODE, documented with the rest of the settings in src/.env.example) — a member
sends and revokes their own, makes, resets and removes their invite link, and the routes answer 404
on an open instance; and admin, the operator's own — the queue of people who have asked to be
let in, inviting or removing them in a batch, the daily totals of accounts created, sign-ins and
active accounts, and the species catalog with its photos to hide, pin or re-fetch — which is the one
family gated on is_superuser rather than merely on having a token. All of those want a bearer
token: a browser's access token, or for a tool of your own an API key — created in Settings,
read or write, lasting thirty days, ninety, a year or until revoked, and sent on the same
Authorization: Bearer header. A key is refused on the routes that manage credentials, the sign-in
address or the account itself, and a read key on anything but a safe method;
docs/authentication.md has the rest. The ones that don't are support, the auth routes
themselves, the two health checks — /health says the process is up, /health/ready says Postgres
and Redis answered, and 503s when they didn't — POST /invite-requests, which is how somebody with
no account asks a closed instance for an invitation, , which tells the web app whether
registration is open - whether the project itself operates the instance, whether any join link
exists (), and whether a map renderer draws the map tiles () - before anyone
has signed in, , which says whether one join link is live,
, which says whose invite link one token is, ,
which serves a public Commons image to an tag that has no way to send a token, and
with its and , the check-in page a
diver shared as a link, where the token in the path is the credential, and , the
document itself. is the guard that keeps the half
of that list honest — it compares the app's real route table against its own allowlist and holds the
reason for each — but nothing checks this paragraph, so a new route family belongs here by hand.
A record's create — POST /dive, /trip, /person and the like; /docs lists the header on each
— takes an optional Idempotency-Key header: a repeat under the same key answers the record the
first one made, as it now reads, instead of making another.
A dive draft is a dive not yet logged, saved and finished from either device:
POST /dive-draft, GET /dive-drafts, GET/PATCH/DELETE /dive-draft/{uuid}, and
POST /dive-draft/{uuid}/dive, which logs the dive as POST /dive does and deletes the draft in
the same request.
A typical import flow:
# 1. Parse an export to pre-fill the dive form
curl -X POST http://localhost:8000/api/v1/dive/parse \
-H "Authorization: Bearer $TOKEN" \
-F "file=@Dive_2026-04-17.json"
# 2. Create the dive, then attach the original file to it. The response says which
# *recording* it landed in: a second export of a computer the dive already has
# fills that record, and a different computer becomes a recording of its own.
curl -X POST http://localhost:8000/api/v1/dive/{uuid}/recordings \
-H "Authorization: Bearer $TOKEN" \
-F "file=@Dive_2026-04-17.json" -F "file_token=$FILE_TOKEN"
# 3. One recording's per-sample profile, served with an ETag. A diver on two computers
# has two of them; `GET /dive/{uuid}` lists the recordings and their uuids.
curl http://localhost:8000/api/v1/dive/{uuid}/recording/{rid}/profile -H "Authorization: Bearer $TOKEN"
And a whole logbook, in and out:
# Out: one DiveJSON document, or a zip with the stored files beside it
curl -OJ http://localhost:8000/api/v1/export/divejson -H "Authorization: Bearer $TOKEN"
# Back in, in two phases. The preview writes nothing and reports what it would do;
# the token it returns says which files that report was about. Send `file` once per
# file, in any format the converter reads - `dives.uddf`, `Dive.fit` beside `Dive.json`,
# `garmin-export.zip` - and each dive's row in the report lists what its files did not record.
curl -X POST http://localhost:8000/api/v1/import/logbook/preview \
-H "Authorization: Bearer $TOKEN" -F "[email protected]" -F "[email protected]"
curl -X POST http://localhost:8000/api/v1/import/logbook \
-H "Authorization: Bearer $TOKEN" -F "[email protected]" -F "[email protected]" -F "token=$PREVIEW_TOKEN"
docs/authentication.md — the auth design, with sequence diagrams for every flow.DECISIONS.md — non-obvious choices and gotchas (schema-change workflow, check constraints,
caching strategy…). Read it before your first PR.alembic stamp head — see CONTRIBUTING.md.uv run pytest, or containerised —
docker compose -f docker-compose.yml -f docker-compose.test.yml up --build --abort-on-container-exit api.
docker-compose.test.yml is an overlay and does nothing on its own. Note that the Postgres-backed
tests skip themselves unless the test process can reach the database; see
CONTRIBUTING.md.divejson pin or the extractor version:
docker compose exec api python -m src.scripts.backfill_dive_profiles. Until it runs, every
profile is served as the previous reader left it.docker compose exec api python -m src.scripts.backfill_dive_figures --dry-run,
then without --dry-run. It rewrites each dive's submerged time and average depth that still hold
the whole recording's figures to its time in the water, and leaves a value a diver typed. A
logbook-imported dive moves only for a device named with --device BRAND [MODEL]; the script's
docstring says how to choose them.docker compose exec api python -m src.scripts.backfill_depth_outlines --dry-run, then without
--dry-run. It rewrites each stored outline from its profile's samples and nothing else; until it
runs, the dive list draws the outlines stored before.docker compose exec api python -m src.scripts.backfill_folded_dives --dry-run, then without
. It ends each dive merged before on the later record's CNS, OTU, tank pressure and
exit fix, lists every value it changes, and names each dive whose cylinders and fixes it leaves
for you.| opendiving | The front door — install, docs, and the release |
| opendiving-web | Next.js web app |
| opendiving-ios | SwiftUI app, in development, not in the App Store |
AGPL-3.0. Run it, change it, self-host it freely — but if you offer a modified version as a service, you share your changes. Divers' data stays free.
Two bundled data files are not covered by it, because the AGPL is a software copyleft and says nothing about a vendored database. Both record their own licence inside themselves, where it cannot drift away from the data:
src/app/data/dive_site_catalog.json — the dive-site suggestion catalog, a Derivative Database of
OpenStreetMap under the
Open Database License (ODbL) 1.0. ©
OpenStreetMap contributors, https://osm.org/copyright. It also carries public-domain (CC0)
records from Wikidata, and its country and region names come from Natural Earth (public domain).
Redistributing it — which anyone who redistributes this repository or an image built from it does
— carries ODbL's share-alike and notice conditions with it.src/app/data/marine_areas.geojson — Natural Earth sea polygons, public domain.Both are regenerated by the scripts in scripts/, which is what makes the method of producing them
available alongside the result (ODbL §4.6). See
CONTRIBUTING.md.
GET /configjoin_linksmap_tilesGET /join-channel/{slug}GET /invite-link/{token}GET /species/{uuid}/photo<img>GET /checkin/{token}/portrait/certification/{uuid}/frontGET /openapi.jsontests/test_route_authentication.py--dry-run