Day-to-day operations¶
Re-printing a label that failed mid-batch¶
DDB's batch print pauses 1 s between jobs and retries on Bluetooth
[Errno 16] Device or resource busy, so most batches go clean. If a
vial still ends up in the DB without a printed label — printer sleeping,
BT drops, out of tape — the dialog tells you how many of the batch
failed. The vials exist in the DB either way; only the physical label
is missing.
To re-print one at a time:
- Scan tab → type the print code into the Code field (top-right of the controls row, next to the camera role dropdown).
- Press Enter. The right-hand Detail panel loads the vial.
- Click Print at the bottom of the Detail panel.
Repeat for each missing label. The print code comes from the dialog's
failure message, or from data/labels/ on disk where every rendered
label PNG lives under its print code.
If printing from Detail also fails, open a terminal and try:
ddb printer label <PRINT_CODE>
which bypasses the GUI but takes the same BT path; the same retry logic applies.
Offline backups¶
On a shared tablet the DB is written to by whichever biologist happens
to be logged in — nobody's home cron / Syncthing can be assumed to be
running. Backups therefore live at the system level: a
ddb-backup.timer fires hourly regardless of who is (or isn't) signed
in, and pushes snapshots to a cloud remote via rclone if configured.
One-time install¶
sudo bash scripts/setup_system_backup.sh
This is idempotent — re-run it after any git pull that touches the
scripts. It:
- Creates
/srv/ddb/backups/{,history}(root:ddb, mode 2775 so the group inherits automatically). - Installs the backup script to
/usr/local/sbin/ddb-backup. - Installs
ddb-backup.service(oneshot) +ddb-backup.timer(hourly withPersistent=true) under/etc/systemd/system/. - Seeds
/etc/ddb/backup.envwith commented defaults (won't overwrite operator edits on re-runs). - Enables + starts the timer.
How it works¶
- Runs as root from
ddb-backup.service, so it's independent of any user's login session. - Uses Python's stdlib
sqlite3.Connection.backup()— a consistent copy is guaranteed even while the GUI writes during the snapshot. - Content-addressed dedupe. Every hour it takes a snapshot, then
SHA-256s the result and compares to
/srv/ddb/backups/.last-hash(the hash of the last successfully-shipped snapshot). If they match, the candidate is discarded — no history entry, no rclone push, onlyddb.latest.sqlite3's mtime gets touched to show "we checked, still current". Because the DB is idle for long stretches (weekends, overnight), this collapses most days to zero cloud traffic. - Retention (
DDB_BACKUP_RETAIN_HOURLY, default 168) therefore counts unique states, not hours. On a low-turnover DB, 168 entries cover months, not one week — the ladder always shows real change events instead of empty repeats. - Files:
/srv/ddb/backups/ddb.latest.sqlite3— stable filename, updated on any change./srv/ddb/backups/history/ddb_<UTC-timestamp>.sqlite3— one entry per unique state, oldest auto-prune once retention is hit./srv/ddb/backups/.last-hash— bookkeeping for the dedupe check; only bumped after the whole pipeline (snapshot + optional rclone push) succeeds, so a failed push retries next hour.- Timer runs hourly on
OnCalendar=hourlywithPersistent=true, so a missed hour (tablet powered off) catches up on next boot. - Logs to the systemd journal —
journalctl -u ddb-backup.service.
Enabling offsite push (optional)¶
The local snapshots survive a DB corruption but not a stolen tablet.
To ship copies off the machine via rclone:
sudo apt install rclone
sudo rclone --config /etc/ddb/rclone.conf config
# → walk through the wizard to add a remote; call it e.g. "ddb-backup"
sudo $EDITOR /etc/ddb/backup.env
# → set DDB_BACKUP_RCLONE_REMOTE=ddb-backup:drosodb (or your bucket path)
The next timer tick will rclone copy the latest snapshot and
rclone sync the history dir. Cloud remote works with anything rclone
speaks — S3, Google Drive, WebDAV, Nextcloud, ownCloud, Backblaze B2,
etc.
Config knobs (edit /etc/ddb/backup.env)¶
| Variable | Default | Use for |
|---|---|---|
DDB_BACKUP_SRC |
/srv/ddb/ddb.sqlite3 |
Alternative DB path |
DDB_BACKUP_DEST |
/srv/ddb/backups |
Different backup target |
DDB_BACKUP_RETAIN_HOURLY |
168 |
Snapshots to retain (one week) |
DDB_BACKUP_RCLONE_CONFIG |
/etc/ddb/rclone.conf |
rclone config path |
DDB_BACKUP_RCLONE_REMOTE |
(empty → skip push) | e.g. ddb-backup:drosodb |
DDB_BACKUP_RCLONE_BIN |
/usr/bin/rclone |
Non-standard rclone binary |
Verifying a snapshot¶
Any of the history/ files is a full, valid SQLite database:
sqlite3 /srv/ddb/backups/history/ddb_20260703T143014Z.sqlite3 \
"SELECT COUNT(*) FROM vial WHERE is_active = 1;"
Or fire a snapshot manually right now:
sudo systemctl start ddb-backup.service
journalctl -u ddb-backup.service -n 30
Restoring from backup¶
- Stop everyone's GUI (close the app on every logged-in account).
sudo cp /srv/ddb/backups/ddb.latest.sqlite3 /srv/ddb/ddb.sqlite3- Fix ownership + perms:
sudo chown geuba03p:ddb /srv/ddb/ddb.sqlite3 && sudo chmod 664 /srv/ddb/ddb.sqlite3 - Relaunch with
ddb gui.
Pick a specific history/ file instead of ddb.latest.sqlite3 if you
need to roll back further than the last hour.
Migrating from the old per-user cron backup¶
The pre-shared-tablet setup ran a personal per-user backup script from a crontab, dropping snapshots into a Syncthing folder in the maintainer's home directory. Once the system timer above has fired at least once and you've confirmed the new snapshots are landing, retire the old setup:
crontab -l # confirm the old line is there
crontab -e # delete the line pointing at the old script
rm -f /path/to/your/old-drosodb_backup.sh # old script (wherever you kept it)