Files
clockula/docs/BACKUP.md
T
makiolaj cac5b2dfca refactor(settings): remove the unused default timer duration
Settings > Timers offered a "Default duration", but nothing that creates
a timer ever read it: the keypad starts empty. The row, its preference,
ClockDefaults.timerDuration and the backup field are gone, and the
screen's hint and the hub's subtitle now only speak of the sound.

Backups no longer write default_timer_duration_millis. Older files that
carry it still import, since the codec ignores unknown keys.

Closes #5
2026-10-05 19:28:31 +02:00

140 lines
5.4 KiB
Markdown

# Clockula backup format
`clockula-backup-v1.json` — a documented, versioned JSON export of Clockula's
own data, written to (and read from) a user-chosen folder via SAF
(`docs/PLAN.md` §7). This file is the schema's source of truth; the codec
(`data/backup/BackupDto.kt`, `BackupCodec.kt`) implements exactly what is
written here.
## Top level
```json
{
"format": "clockula-backup",
"version": 1,
"exportedAt": 1700000000000,
"alarms": [ … ],
"timers": [ … ],
"worldClocks": [ … ],
"settings": { … }
}
```
- `format` — must be exactly `"clockula-backup"`.
- `version` — must be exactly `1`. Any other value rejects the whole file:
Clockula does not guess at a schema it has not implemented.
- `exportedAt` — epoch milliseconds, informational only (not re-imported).
## Parsing rules
- **Tolerant of unknown fields, at any level.** An older or newer client's
extra field is ignored, never an error.
- **Strict about known fields.** A malformed value in a field this schema
defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's
`zone_id`, a timer's `duration_millis` — rejects the **whole file** with a
specific reason. Nothing is imported half-way.
- Settings values (snooze minutes, volume ramp, …) are
**not** range-checked at parse time; they pass through the same clamps
`ClockPrefs`/`SettingsPrefs` already apply on every write, so a value from
a future, more permissive version degrades to the nearest valid one instead
of failing the import.
- An enum field with a name this version does not recognise (a `dismiss_
challenge` or `theme_mode` from a future version, say) degrades to that
field's default, the same forgiving-read posture `AlarmMapper` already uses
for a corrupt Room row — it does not fail the import.
## Import semantics
Import **replaces all state** — alarms, timers and world clocks are deleted
and re-created from the file, and every setting the file carries is written
over the current value. This is not a merge, and the backup screen says so
before the user confirms. A ringing alarm is dismissed and any running timer
is deleted *before* the replace, so the import never leaves an orphaned ring
session; alarm/timer/stopwatch scheduling is re-resolved immediately after.
Every imported alarm and timer gets a **fresh id and fresh timestamps** —
the backup carries no id and no `created_at`/`updated_at`, since those are
local-database facts, not portable ones.
## Alarms
One object per row of `alarms`, config only:
| Field | Type | Notes |
|---|---|---|
| `hour` | Int | 0..23. Out of range rejects the whole file. |
| `minute` | Int | 0..59. Out of range rejects the whole file. |
| `label` | String | |
| `enabled` | Boolean | |
| `repeat_days` | Int | The 7-bit mask (`RepeatDays`); bit *n* is ISO day *n+1*. |
| `skip_next_occurrence` | Boolean | |
| `ringtone_uri` | String? | Exported as-is. See "Ringtone URIs" below. |
| `vibrate` | Boolean? | `null` = inherit the app default. |
| `snooze_minutes` | Int? | `null` = inherit. |
| `snooze_limit` | Int? | `null` = inherit. |
| `volume_ramp_seconds` | Int? | `null` = inherit. |
| `dismiss_challenge` | String? | `NONE`/`MATH`/`HOLD`, or `null` = inherit. |
| `delete_after_use` | Boolean | A transient, contract-created alarm. |
**Not exported:** the `alarm_states` ring-state table (snooze/ringing/handled-
occurrence bookkeeping) — it is volatile, per-device state, not configuration,
and `docs/ARCHITECTURE.md` §4/§6 are explicit that it must never appear here.
## Timers
One object per row of `timers`, config only:
| Field | Type | Notes |
|---|---|---|
| `label` | String | |
| `duration_millis` | Long | ≥ 0. Negative rejects the whole file. |
| `ringtone_uri` | String? | |
| `sort_order` | Int | |
| `delete_after_use` | Boolean | |
**Not exported:** `state`, `remaining`, the elapsed-realtime anchors, or
`ends_at_wall_clock` — a timer's running state is meaningless on another
device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
`IDLE`, with `remaining` equal to `duration`.
## World clocks
| Field | Type | Notes |
|---|---|---|
| `zone_id` | String | Must be a zone id the *exporting* device's IANA tzdata knows (`Zones.isValid`). A bad id rejects the whole file. |
| `label` | String? | |
| `sort_order` | Int | |
## Settings
```json
"settings": {
"theme_mode": "SYSTEM",
"dynamic_color": true,
"default_snooze_minutes": 10,
"default_snooze_limit": 3,
"default_vibrate": true,
"default_volume_ramp_seconds": 15,
"default_alarm_ringtone_uri": null,
"default_timer_ringtone_uri": null,
"default_dismiss_challenge": "NONE",
"home_zone_id": null,
"timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000]
}
```
Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Older
files may also carry `default_timer_duration_millis`, from a default timer
length setting that has since been removed; it is ignored on import like any
other unknown field. Every
numeric/duration value is clamped on *apply*, through the existing pref
setters — never on parse.
## Ringtone URIs
Exported exactly as stored (a `content://` URI, typically). On import the
URI is written back unchanged; if the *importing* device cannot resolve it
at ring/preview time, the existing ringtone-resolution fallback already used
elsewhere in the app applies (falls through to the device default) — this is
not special-cased in the backup path itself.