Ctrl K
ComunidadGuías…

README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# OpenDiving Web

**A dive log built to outlive every vendor. Your dives, your data — vendor exports in, open formats
out, everything in one click. Yours to self-host.**

OpenDiving is an open-source logbook for scuba divers — recreational and technical: log dives with
multi-tank gas mixtures (nitrox and trimix), import dives straight from your dive computer's export
file — full depth/temperature/tank-pressure profile, heart rate, deco ceiling, the readouts the
computer itself computed and dive events included — group them into trips, and keep your gear
service history and c-cards in one place.

Cloud dive logs come and go — Movescount, Deepblu, Diveboard — and when they go, years of dive
history go with them. OpenDiving is built on a different premise: the app is AGPL-licensed, the data
sits in a plain Postgres database, and every dive-computer file you upload stays with the recording
it came from, downloadable at any time. Self-hosting isn't a feature here; it's the guarantee behind
the rest — anyone can run this software, one click hands the whole log back in open formats, and it
reads a logbook back in from DiveJSON, UDDF, Subsurface, FIT, a Suunto app or DM5 export, or a
Shearwater Cloud database or XML export, so no shutdown, acquisition, or paywall can ever take your
logbook with it.

**This repository is the web app — one component of the stack.** The project itself, and everything
about running it, lives at **[opendiving/opendiving](https://github.com/opendiving/opendiving)**:
the install, the configuration reference, the operator guides and the release that ties the
components together. Start there if you want to run OpenDiving rather than work on this half of it.
It is also where the instance this project runs itself is named, for anyone who would rather not run
one.

![Home](docs/screenshots/home.png)

## Features

- **Dive logging** — times, depths, duration, the kind of dive, water and air temperature,
  visibility, water type, altitude, current, waves, weather, how you got in and the boat you dived
  from, weight, notes, your own rating out of five, and any number of gas mixtures (O₂/He, start/end
  pressures) per dive. File dives and dive sites under tags of your own words, then filter the log
  by a tag or by the kind of dive, or read it best-rated first. A dive can span multiple dive sites
  (drift dives happen), in order. Switch off the fields you never fill in and save the arrangement
  as a named set — the choice follows your account, not the device.
- **Technical diving** — trimix and nitrox mixes get derived gas names and per-mix **MOD** at your
  ppO₂ limit, plus END/EAD; the profile chart shades the **deco ceiling**, plots the computer's own
  **NDL, TTS, ppO₂, CNS and gradient factors** and marks dive events; each recording says the mode
  it ran in, the salinity it was set to and the **decompression model** behind those numbers, and
  keeps its own **CNS/OTU** oxygen exposure and surface pressure from the import; per-cylinder ppO₂
  limits and gas roles are kept too.
- **Dive-computer import** — upload one dive's file in any format **logbook import** reads (below)
  and the form pre-fills itself, keeping the file's own UTC offset where it records one and falling
  back to your current timezone where it does not; a file holding several dives, or one file
  carrying two computers' records of a dive, goes through logbook import instead. Every file you
  upload stays with the **recording** it came from and can be re-downloaded any time — a dive logged
  off two computers has two recordings, and one computer's JSON beside its FIT is two files of one
  recording, each filling what the other left blank. The per-sample **dive profile** is extracted
  and charted per recording, with a switcher when there is more than one: depth, temperature, tank
  pressure and the deco ceiling on the depth plot; on a panel under it the **heart rate** the wrist
  sensor or strap measured, then the six readouts the computer itself computed — **no-deco time,
  time to surface, ppO₂, CNS and both gradient factors**; and the device's own event markers along
  the axis. The dive page states the heart rate's maximum and average. A computer that chopped one
  dive into two logs them as two dives, and **Merge** folds them back into one.
- **Air consumption** — SAC and RMV are derived automatically, including a **per-tank breakdown**
  across recorded gas switches on multi-tank dives, with a consumption trend chart on the Home page.
- **Trips** — group dives into a liveaboard or a holiday week, a part at a time: each part carries
  its own place and its own dates, and the trip spans them all.
- **Dive sites** — one map and one search over your own sites and OpenDiving Commons, a shared pool
  of named sites: the sites nearest the map's centre until you type, then the sites and the places
  that match, each place with how many sites are near it. Your own sites record the other names a
  site goes by, the depths it is dived at, its water, its altitude and how you get in, and your own
  tags. Use a Commons site as it stands: every diver's dives name the same record, with your notes
  and tags on top and yours alone. Where the Commons is edited, propose a site of yours or a change
  to a record, for a reviewer to accept. A site's page sums up your dives there — how many, the last
  and the deepest, the species you saw and your average rating — beside every dive you've logged at
  it, and a dive logged there starts from its water, altitude and entry. Under _Your sites_, sort
  the list by name, by dives or by the last dive, and filter it by a tag. The dive form picks a site
  from the same map, opened on the dive's sites, its GPS position or its trip.
- **Gear tracking** — your equipment with per-item dive counts and dive time, groupable into gear
  sets you can attach to a dive in one click; a cylinder records its volume, working pressure and
  material once, and a dive's tank links it. Plus **service schedules** (annual service, visual
  inspection, hydro test…) by months, dives or hours of diving, with due-soon reminders under the
  notifications bell and by email.
- **Marine life** — record what you saw against a real species catalog, resolved live against the
  World Register of Marine Species and Wikidata so a name you half-remember still finds the animal,
  with how many you counted or a band — few, many, abundant — and a note on each. Your **life list**
  collects every species you have ever logged, with a photograph fetched from Wikimedia Commons and
  served from the instance you are on — your browser never talks to Wikimedia, and each species has
  a page carrying its credit, its classification and the dives you saw it on. The taxonomy is
  WoRMS's, whose text content is available under
  [CC BY](https://creativecommons.org/licenses/by/4.0/) 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_ — a date that stands
  for no copy of anything, because **this app queries the register live rather than holding a
  snapshot of it**.
- **Certifications** — keep photos of your c-cards on hand at the dive shop without digging out the
  plastic, and an email when a card or one of your insurance policies comes within 90 days of
  expiring, and again when it does.
- **Check-in** — the details a dive shop asks for at the desk, kept once: a portrait cropped like a
  passport photo, date of birth, phone, the email you choose to give out, which need not be the one
  you sign in with, your emergency contacts in the order to call them, and your insurance policies.
  One page gathers them with your c-cards, dive count and last dive, and prints — your browser's
  save-as-PDF is the copy for a desk with no signal — or shares as a link and a QR code the desk
  opens on its own phone, for a day or until you revoke it.
- **Courses** — the training itself, with the agency, instructor and shop: link the dives you did on
  it and the cards it issued, so a course is one record instead of a shape you have to remember.
- **Contacts** — the dive centers, schools, shops, clubs and places you stayed, each kept once with
  its phone, email, website, address and place on the map, and picked from a list for a dive, a
  course, a card, a gear service or a trip part. A dive logged on a course takes its dive center
  from it, and a trip names its dive centers from its dives.
- **People** — your buddies, guides, instructors, fellow students and whoever came along, each kept
  once and picked for a dive, a trip or a course with the role they had that time; a course's and a
  card's instructor is one of them. Type a name that is new and it becomes a person. Link one to
  their account on the same instance by its username and you see that account's current `@username`
  beside the name, nothing else of theirs, and they are not told. UDDF and Subsurface buddies arrive
  as people when you import.
- **Full export** — one click to take _everything_ out in open formats: a **DiveJSON** document
  holding the whole logbook, a **UDDF** one other programs import, a **CSV** for a spreadsheet, or a
  complete **archive** carrying all three alongside every dive-computer file you uploaded, every
  c-card image you stored, and your profile picture and portrait. [DiveJSON](https://divejson.org)
  is the open dive-log interchange format this project maintains, and this app is its reference
  implementation. A data-ownership log without an exit door is a contradiction.
- **Logbook import** — and a door that only opens outwards is half a promise, so a whole logbook
  reads straight back in. One **Import** page takes any number of files at once, in any mix: the
  **DiveJSON** document and the **archive**, and — through the
  [DiveJSON converter](https://github.com/divejson/divejson-py) — **UDDF**, a **Subsurface** `.ssrf`
  save file, **FIT**, a **Suunto app** export, a **Suunto DM5** `.xml`, and **Shearwater Cloud**'s
  whole-database `.db` and one-dive `.xml` exports, or a `.zip` of any of them, which is how a
  watch's account export arrives. Drop a folder and the app sorts it: a computer's two exports of
  one dive become one dive with both files, and a converted file that is one dive is kept on the
  dive it becomes, as it is when uploaded to a dive; a logbook of several dives, or a DiveJSON
  document, arrives without its file. Move a logbook between instances, restore one from a backup,
  or bring years of history out of something else. The files go up once and the server holds them,
  so an import started on one device can be finished on another. The review is dive cards of what is
  new and what changes, each opening the dive or what it was and what it becomes, with any file that
  could not be read listed first; nothing is written until you import. Each import stays as history,
  with a log of what its files did not record, what the import worked out, and which of the dive's
  values it left out. Records you already have are matched rather than duplicated, and a dive you
  deleted stays deleted. The archive additionally restores the dive-computer files and c-card scans,
  which the bare document names by digest but does not carry. The check-in details a logbook
  carries, and an archive's portrait, are shown beside yours in the review, and only what you choose
  there is saved.
- **Year in review** — each January, an email with the year before in figures: dives and time
  underwater, the deepest and longest, dive sites, and species with the ones you saw for the first
  time.
- **Passwordless sign-in** — email magic links, a six-digit code, passkeys, or Google. No passwords
  stored, ever.
- **Dark mode & responsive** — works on the boat, in the dive shop, and on your desk.
- **Installs like an app** — add it to a phone's home screen from the browser's install or share
  menu (on iOS 16.4 and later, Safari's Share → Add to Home Screen), and it opens in its own window
  on your Home page.

|                                                                                                        |                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ![A dive, with the profile charted from its dive-computer recording](docs/screenshots/dive-detail.png) | ![A gear item with its service schedule and history](docs/screenshots/gear-item.png)<br>![A dive site on the map, with every dive logged there beside it](docs/screenshots/dive-site.png) |

## Planned

Roadmap items, roughly in priority order — contributions welcome:

- **More importers** — UDDF, Subsurface, FIT, Suunto's own two — the app's JSON and DM5's XML — and
  Shearwater Cloud's two — its whole-database export and its one-dive XML — already import and fill
  the dive form, both through the DiveJSON converter (a Shearwater Cloud database of several dives
  goes through the Import page, the form taking one dive), and a new format is a reader there, for
  both, rather than a change here. The iOS app downloads from Shearwater and Suunto Ocean computers
  over Bluetooth, through [libdivecomputer](https://www.libdivecomputer.org/), and the logbook
  import takes what it brings, keeping each dive's original download beside it.
- **Statistics** — depth/time records, dives per year, sites map.
- **Sharing** — public link to a dive or trip.
- **iOS companion app** — in development, not yet installable
  ([opendiving-ios](https://github.com/opendiving/opendiving-ios)).

## How it compares

Honest answers to "why not X":

- **[Subsurface](https://subsurface-divelog.org/)** — the open-source reference, with unmatched
  dive-computer support and a full deco planner. It's desktop-first with no web app or self-hostable
  server; OpenDiving is the server-shaped complement — a modern web UI, API-first, reachable from
  any browser, and self-hostable on a box of your own. Use Subsurface to download from cables and
  import its save file or its UDDF export here, so both can hold the same log.
- **[Submersion](https://submersion.app/) / [Bubbletrail](https://bubbletrail.app/)** — excellent
  newer open-source _apps_: local-first, on-device databases, Bluetooth downloads. OpenDiving is the
  server-shaped alternative: one instance behind every browser and every family member, with an API
  — and yours to run on the household server if that is where you want it. Its own iOS app downloads
  over Bluetooth too, and hands the dives to that server rather than keeping them.
- **Vendor clouds (Shearwater, Garmin, Suunto, Oceanic+)** — where dives are born, not where they
  should live. OpenDiving imports their exports — any number at once, each file of one dive kept on
  the recording it came from — so switching computers never splits your history, and a dive logged
  off two of them keeps both computers' records side by side.

## Self-hosting

One compose file brings up the whole stack — this app, the API and its worker, Postgres, Redis, and
a Caddy that provisions TLS for your domain. That file is not in this repository and neither are the
instructions for it: an install is a product-level thing, so it is driven from
**[opendiving/opendiving](https://github.com/opendiving/opendiving)** — the four commands, the six
values in the `.env` that matter, and the upgrade are all on that page.

What this repository contributes to it is one image. `ghcr.io/opendiving/opendiving-web` is prebuilt
for amd64 and arm64, so a Raspberry Pi runs the same bytes as a VPS and there is no build step and
no Node on the host; it is pulled alongside the API's by that compose file.

**[Full self-hosting docs](https://github.com/opendiving/opendiving/tree/main/docs)** — install,
every configuration variable, running behind your own reverse proxy instead of the bundled Caddy,
backup and restore, upgrades, and troubleshooting. They live in one place rather than half here and
half there.

### Building the image yourself

The published image is what a `docker build` in this repository produces, so a fork or a local
change is one build away:

```bash
docker build -t opendiving-web .
docker run -p 3000:3000 -e API_INTERNAL_URL=http://your-api-host:8000 opendiving-web
```

Nothing about your instance is baked into that image. The browser calls `/api/v1` on whatever origin
served the page, and this app's own route handler forwards each request to `API_INTERNAL_URL` — an
origin with no `/api/v1` on the end, read fresh on every request. Its default is `http://api:8000`,
the API's service name on a compose network, so a container swapped into the shipped bundle needs
the variable no more than the published image does. The rest of the settings are read on the server
at request time too, so they are plain environment variables on the container:
[`.env.example`](.env.example) documents each one.

The image carries its own `HEALTHCHECK` against `/healthz`, which reports that this process is
serving HTTP and deliberately nothing more — the web container talks to neither Postgres nor Redis,
and the API owns readiness. Two settings are worth knowing when you are the one deciding how this is
served rather than taking the bundle's answer: `WEB_HSTS=off` hands `Strict-Transport-Security` to a
proxy in front, or drops it for a plain-HTTP LAN address, and `WEB_NOINDEX=true` keeps an instance
that is reachable but private out of search engines.

The same image carries a second program, the map renderer, which draws the map tiles the API stores
once for every account and the dive and trip cards and the dive, trip and dive site page heads are
composed from. It is started with its own command and reached by the API alone:

```bash
docker run -e SITE_URL=https://dives.example.com opendiving-web node map-renderer/index.mjs
```

It reads the same basemap settings as the web server, answers the same `/healthz` — there, whether
it can draw — and has its own section in [`.env.example`](.env.example).

The single exception to runtime configuration is `NEXT_PUBLIC_API_URL`, for a split-origin
deployment where the API answers on a host of its own and the browser should reach it directly
instead of through this app:

```bash
docker build --build-arg NEXT_PUBLIC_API_URL=https://api.example.com/api/v1 -t opendiving-web .
```

It is the full base with the `/api/v1` prefix included, and `NEXT_PUBLIC_*` values are inlined into
the client bundle by the compiler — so that address is fixed at build time and changing it means
rebuilding, which is exactly why it is no longer the default path. It is also one of the two things
the CSP's `connect-src` is derived from — the other is the basemap, whose host comes from the
runtime settings `.env.example` documents — so an API or basemap host reached any other way is
blocked rather than merely misconfigured. Left unset, none of this applies.

## Development setup

The web app is the frontend for [opendiving-api](https://github.com/opendiving/opendiving-api) —
start that first (one `docker compose up`), then:

```bash
npm install
cp .env.example .env   # points at http://localhost:8000/api/v1 by default
npm run dev
```

Open [http://localhost:3000](http://localhost:3000), sign in with your email, and log your first
dive. Without a configured email provider on the API side, the magic link is printed to the API logs
— handy for local development.

### Scripts

| Command                       |                                                     |
| ----------------------------- | --------------------------------------------------- |
| `npm run dev`                 | Development server                                  |
| `npm run build` / `npm start` | Production build / serve                            |
| `npm run lint`                | ESLint                                              |
| `npm run type-check`          | TypeScript                                          |
| `npm test`                    | Unit tests (`test:watch`, `test:coverage` variants) |

## Tech stack

Next.js (App Router) + TypeScript, Tailwind CSS, shadcn/ui with Base UI components,
react-hook-form + Zod validation, axios. The dive-profile and consumption charts are hand-rolled SVG
— no charting library.

## Contributing

Issues and PRs are welcome — from a typo fix to a new importer. Open an issue first for bigger
features so we can agree on the shape. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the checks
CI runs, and the house rules, and [DECISIONS.md](DECISIONS.md) for the non-obvious choices already
made. Security problems go through [SECURITY.md](SECURITY.md) rather than the issue tracker.

## Related repositories

|                                                                |                                                         |
| -------------------------------------------------------------- | ------------------------------------------------------- |
| [opendiving](https://github.com/opendiving/opendiving)         | The product: install bundle, operator docs, the release |
| [opendiving-api](https://github.com/opendiving/opendiving-api) | FastAPI backend (Postgres, Redis, dive-file parsing)    |
| [opendiving-ios](https://github.com/opendiving/opendiving-ios) | SwiftUI app (in development, not yet installable)       |

## License

[AGPL-3.0](LICENSE). In short: run it, change it, self-host it freely — but if you offer a modified
version as a service, you share your changes. Nobody gets to take this closed-source and lock
divers' data away.

Third-party artwork and vendored assets that travel in this tree — Google's sign-in mark, the
MapLibre build, the OpenFreeMap styles — are credited in [NOTICE.md](NOTICE.md). The brand mark
isn't among them: it's original to this project.