Move runbooks into domain directories

od-db-backup.md becomes od/db-backup.md -- the hyphen becomes a slash, so
the fetch command is exactly as long to type as before. That mattered: the
length of a hand-typed command is the constraint this repo is organized
around, and a reorganization that lengthened it would have been a net loss.

Scripts deliberately stay flat in scripts/ with their domain prefix.
Everything executable in one directory is the set worth reading before it
runs, and nesting five files by domain would add characters without adding
clarity.

Updates every reference: README Contents (now grouped by directory), the
layout section, both fetch examples, inter-runbook links, and the .NOTES
headers in all five scripts. Verified every markdown link resolves on disk
and that Contents and the filesystem agree in both directions.

Records the naming rule in CONTRIBUTING so the next file lands correctly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwcG1jLs1T425QRMxtjxP7
This commit is contained in:
2026-09-02 23:15:48 -07:00
parent f2a979f047
commit cb0c5b4614
14 changed files with 91 additions and 48 deletions
+144
View File
@@ -0,0 +1,144 @@
# Runbook: Open Dental — Schedule & monitor backups (Windows)
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** The Open Dental database server, once [`od/db-backup.md`](db-backup.md) is proven to run by hand.
**Goal:** Make the backup run **automatically every day**, replicate it **off-site**, and **monitor** it so a silent failure gets noticed within a day — not the day you need a restore.
Open Dental's floor is **at least one backup per day**, with a combination of methods and at least one **automated**. ([Open Dental — Backups](https://opendental.com/manual/backups.html)) This runbook automates and watches the cold backup.
> [!IMPORTANT]
> **Monitoring is the half everyone skips.** A backup job that silently stopped, a full disk, or a powered-off server sends **no error email** — the absence of failure looks exactly like success. The only reliable signal is a **positive heartbeat that goes missing** (a dead-man's-switch). Build that, or you don't have monitoring.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<DEST>` | Backup destination root (holds the `od-backup-<timestamp>` folders) |
| `<BACKUP_SCRIPT>` | Filled-in local copy of `od-db-backup.ps1` (paths baked in) — **private tier** |
| `<CHECK_SCRIPT>` | Filled-in local copy of `od-backup-check.ps1` (`<DEST>` + heartbeat baked in) — **private tier** |
| `<HEARTBEAT_URL>` | Dead-man's-switch ping URL (healthchecks.io, Uptime Kuma push, RMM, etc.) — **private tier** |
---
## The jobs and their cadence
| Job | What | When | Runbook |
|---|---|---|---|
| **1. Cold backup** | Stop DB, copy data + images, restart | Nightly, off-hours | [`od/db-backup.md`](db-backup.md) |
| **2. Off-site upload** | Replicate `<DEST>` to cloud/immutable | After job 1 finishes | [`od/db-backup.md` §6](db-backup.md) |
| **3. Health check** | Verify newest backup + heartbeat ping | After job 1 finishes | this runbook + [`od-backup-check.ps1`](../scripts/od-backup-check.ps1) |
| **4. Test-restore** | Full restore into isolated VM | Monthly | [`od/backup-verify.md`](backup-verify.md) |
Stagger them so nothing reads a half-written folder, e.g. **backup 23:30 → check 00:45 → off-site upload 01:00**.
## Why a scheduled *local copy*, not `irm | iex`
`irm | iex` prompts interactively — it can't run unattended. For scheduling, keep a **filled-in copy** of each script (`<BACKUP_SCRIPT>`, `<CHECK_SCRIPT>`) with `<DEST>`, service name, and paths baked in, stored in the **private tier** (never committed here). The public scripts stay the interactive/spot-check version.
---
## 1. Schedule the nightly cold backup
Register `<BACKUP_SCRIPT>` to run as **SYSTEM**, highest privileges, off-hours. From an elevated prompt:
```
schtasks /Create /TN "OD Nightly Cold Backup" ^
/TR "powershell -NoProfile -ExecutionPolicy Bypass -File <BACKUP_SCRIPT>" ^
/SC DAILY /ST 23:30 /RU SYSTEM /RL HIGHEST
```
- `<BACKUP_SCRIPT>` is the filled-in copy — it must **not** prompt (no `Read-Host` for the scheduled path) and must still do the stop → verify-stopped → copy → **always-restart** sequence.
- Ensure the task is set to **run whether or not a user is logged on** and, if the server sleeps, **wake the computer to run** (Task Scheduler → task → *Conditions*).
- Confirm the server actually **stays on** overnight (disable sleep/hibernate on the server).
## 2. Schedule the off-site upload (after the backup)
Point your off-site tool (Duplicati → Backblaze B2, `rclone`, Veeam, etc.) at **`<DEST>`** — it uploads *this backup*, it does **not** re-run the stop/copy against the live database. Schedule it **after** job 1 completes and stagger the start. Enable that tool's **own** email/report and **object-lock/immutability** for the ransomware-resistant off-site copy. Details: [`od/db-backup.md` §6](db-backup.md).
## 3. Schedule the health check (and heartbeat)
Register `<CHECK_SCRIPT>` to run shortly **after** the backup window. It verifies the newest `od-backup-<timestamp>` is fresh, complete, and sensibly sized, appends to `<DEST>\backup-check.log`, and — when healthy — pings `<HEARTBEAT_URL>`.
```
schtasks /Create /TN "OD Backup Health Check" ^
/TR "powershell -NoProfile -ExecutionPolicy Bypass -File <CHECK_SCRIPT>" ^
/SC DAILY /ST 00:45 /RU SYSTEM /RL HIGHEST
```
The check is **read-only** — no DB, no service, no file changes — so it's safe to run any time, including a manual spot-check:
```
irm rb.godwinsystems.com/scripts/od-backup-check.ps1 | iex
```
---
## Monitoring — the three layers of "is it actually working?"
Use all three; each catches what the others miss.
### Layer 1 — Did the job run? (Task Scheduler)
`Last Run Result = 0x0` and a recent `Last Run Time` on both tasks:
```
Get-ScheduledTaskInfo -TaskName "OD Nightly Cold Backup"
Get-ScheduledTaskInfo -TaskName "OD Backup Health Check"
```
Enable **All Tasks History** in Task Scheduler so you can see misfires. Catches: task disabled, wrong credentials, server was off.
### Layer 2 — Is the output good? (the health check)
`od-backup-check.ps1` / `<CHECK_SCRIPT>` confirms the newest backup is:
- **Fresh** — written within 24 h (else the job silently stopped),
- **Complete** — has `data\`, `OpenDentImages\`, `MANIFEST.txt`, and a `backup.log` that ends in success with no `ERROR`/`CRITICAL`,
- **Sane size** — `data\` isn't near-empty and isn't a fraction of the prior run (catches truncation / a filling disk).
Read `<DEST>\backup-check.log` for the running PASS/FAIL trail. Catches: partial copies, missing images, service that didn't restart, dying disk.
### Layer 3 — Dead-man's-switch (the one that catches everything)
Register a check with an **external** monitor — [healthchecks.io](https://healthchecks.io), Uptime Kuma (push), or your RMM — that expects a daily ping. `<CHECK_SCRIPT>` pings `<HEARTBEAT_URL>` **only when the backup is healthy**. If the backup breaks, the check fails, the script crashes, or **the whole server is offline**, the ping never arrives and the monitor alerts you.
- Set the monitor's **period to ~1 day** with a grace window past your backup+check schedule.
- healthchecks.io users: `<CHECK_SCRIPT>` can hit `<HEARTBEAT_URL>/fail` on failure for an **immediate** alert instead of waiting out the grace period.
- This is the layer that turns "no news" into an actual alarm. Without it, a dead backup is invisible until a restore fails.
### Layer 4 — Off-site tool's own report
Your cloud tool (Duplicati/Veeam/rclone wrapper) should send its **own** success/failure summary and expose versions/immutability in the provider console. Confirms the copy actually left the building.
---
## On failure — triage
When Layer 2/3 flags a problem, in rough order:
1. **Is the database up?** `Get-Service <DB_SERVICE>` — if the backup died mid-run, confirm the service **restarted** (the script's `finally` should have; verify). The practice being able to work comes first.
2. **Destination full / offline?** Free space on `<DEST>`; is the disk/UNC reachable? Prune old `od-backup-<timestamp>` generations if space-bound.
3. **Partial/most-recent folder incomplete?** Check that run's `backup.log` for the `ERROR`/`CRITICAL` line; re-run the backup by hand ([`od/db-backup.md`](db-backup.md)).
4. **Version drift?** A recent Open Dental/MySQL update can change paths — reconcile against `MANIFEST.txt`.
5. **Off-site not uploading?** Check the cloud tool's log and that it sources `<DEST>` (not the live datadir).
Then re-run the health check and confirm the heartbeat goes green.
## Records / evidence
Keep the `<DEST>\backup-check.log`, the monitor's uptime history, and the monthly **test-restore** results ([`od/backup-verify.md`](backup-verify.md)) together. That trail is your DR evidence for E&O / cyber insurance and HIPAA contingency-plan testing.
---
## Security note
`<HEARTBEAT_URL>` is a capability — anyone with it can spoof "backup healthy." Treat it as a secret: keep it in the **private tier**, out of this repo, out of screenshots. Don't let monitor check names or heartbeat URLs encode a client's identity. The `<DEST>` folders and `<BACKUP_SCRIPT>`/`<CHECK_SCRIPT>` may reference real paths — keep the filled-in copies private and the destination encrypted/access-controlled (it holds PHI).
## References
- Open Dental manual — Backups (daily minimum; automated + combined methods): <https://opendental.com/manual/backups.html>
- Microsoft — `schtasks` / Scheduled Tasks: <https://learn.microsoft.com/windows-server/administration/windows-commands/schtasks>
- healthchecks.io — dead-man's-switch cron monitoring: <https://healthchecks.io/>
- Companion runbooks — [`od/db-backup.md`](db-backup.md) (produce the backup), [`od/backup-verify.md`](backup-verify.md) (test-restore)
+119
View File
@@ -0,0 +1,119 @@
# Runbook: Open Dental — Verify a backup by test-restoring into an isolated Hyper-V VM
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** Any Open Dental cold backup produced by [`od/db-backup.md`](db-backup.md) (an `od-backup-<timestamp>` folder holding `data\`, `OpenDentImages\`, `FreeDentalConfig.xml`, `MANIFEST.txt`).
**Goal:** Prove a backup is actually restorable and complete by restoring it into a **throwaway, network-isolated Hyper-V virtual machine** and running a short health checklist. A backup you have never restored is a guess.
> [!IMPORTANT]
> **Never restore over a live production database** — Open Dental warns this can cause irreversible data loss. The whole point of this runbook is to restore somewhere that *cannot* touch production: an isolated VM, off the office network. ([Open Dental — Backups](https://opendental.com/manual/backups.html))
**Cadence:** Test-restore **at least monthly**, and any time you change backup jobs, versions, or hardware. Open Dental recommends restoring to a machine **not connected to the office network** to verify quality. ([Open Dental — Manual Backups](https://www.opendental.com/manual/backupsmanual.html))
**Why Hyper-V:** it gives you that isolated machine on demand, with a **checkpoint** you revert after every test — so each verification starts from a known-clean baseline and the PHI-laden copy is destroyed when you're done. Any spare offline PC or a doctor's disconnected laptop works too; the steps are identical inside.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<BACKUP_FOLDER>` | The `od-backup-<timestamp>` folder being verified |
| `<OD_VERSION>` | Open Dental version from `MANIFEST.txt` — must match on the VM |
| `<DB_VERSION>` | MySQL/MariaDB version from `MANIFEST.txt` — must match on the VM |
| `<DB_SERVICE>` | The MySQL/MariaDB service name **inside the VM** |
| `<VM_DATA_DIR>` | The VM's MySQL data directory (e.g. `C:\mysql\data`) |
| `<VM_IMAGES_DIR>` | The VM's A-to-Z images folder (e.g. `C:\OpenDentImages`) |
---
## Phase 0 — Read the manifest first
Open `<BACKUP_FOLDER>\MANIFEST.txt`. Note **`Open Dental ver`** (`<OD_VERSION>`) and **`DB engine ver`** (`<DB_VERSION>`). You must install **matching** versions in the VM — a version mismatch either refuses to open or silently converts the database. ([Open Dental — Manual Backups](https://www.opendental.com/manual/backupsmanual.html))
## Phase 1 — Build the isolated verification VM (once, then reuse)
Do this one time; you'll revert to a checkpoint for every future test.
1. **Enable Hyper-V** on a Windows Pro/Enterprise/Server host (elevated PowerShell; reboots):
```
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
```
2. **Create an isolated network** so the VM can never reach the office network or the live database. In Hyper-V Manager → *Virtual Switch Manager*, create a **Private** (or **Internal**) switch — **not** External. Or leave the VM with **no network adapter** at all.
> This isolation is the safety guarantee. An "External" switch would put the test VM on the practice LAN, where a mistake could reach production shares or the live DB. Don't.
3. **Create the VM** (Gen 2, enough RAM/disk for the database + images), install a supported **Windows** guest.
4. Inside the VM, install **MySQL/MariaDB `<DB_VERSION>`** and **Open Dental `<OD_VERSION>`** (matching the manifest). Do a normal Open Dental install so `<DB_SERVICE>`, `<VM_DATA_DIR>`, and a local `opendental` database exist.
5. Shut the VM down cleanly and take a **checkpoint** named `clean-baseline`. This is your reset point.
## Phase 2 — Get the backup into the VM (keep it offline)
Copy `<BACKUP_FOLDER>` into the VM **without** using the office network — e.g. Hyper-V Enhanced Session copy/paste, or attach the backup as a VHD/passthrough disk, or a scratch VHDX. The VM stays isolated the entire time.
## Phase 3 — Restore into the VM
Run these **inside the VM**, elevated. This mirrors Open Dental's documented restore, adapted for our **whole-datadir** cold copy.
1. **Stop the database:**
```
net stop <DB_SERVICE>
Get-Service <DB_SERVICE> # confirm Status = Stopped before touching files
```
2. **Move the VM's existing data aside** (don't delete — lets you retry):
```
Rename-Item "<VM_DATA_DIR>" "<VM_DATA_DIR>-old"
New-Item -ItemType Directory "<VM_DATA_DIR>" | Out-Null
```
3. **Restore the whole data directory** from the backup — copy the **entire contents of the backup's `data\` folder**, not just `data\opendental\`:
```
robocopy "<BACKUP_FOLDER>\data" "<VM_DATA_DIR>" /E /COPY:DAT /R:2 /W:5
```
> For **InnoDB** you must restore the shared tablespace and logs (`ibdata1`, `ib_logfile*`) that live at the datadir root alongside the `opendental` folder — that's why we restore the whole `data\`, matching how [`od/db-backup.md`](db-backup.md) captured it. Restoring only the `opendental` subfolder yields a database that won't start.
4. **Restore the images:**
```
robocopy "<BACKUP_FOLDER>\OpenDentImages" "<VM_IMAGES_DIR>" /E /COPY:DAT /R:2 /W:5
```
5. **Start the database:**
```
net start <DB_SERVICE>
Get-Service <DB_SERVICE> # confirm Running
```
6. **Launch Open Dental in the VM**, connecting to the **local** `opendental` database (localhost). Configure a fresh local connection — do **not** reuse the production `FreeDentalConfig.xml` connection string (it points at the real server). The captured `FreeDentalConfig.xml` is only a **reference** of production settings (e.g. the A-to-Z path).
7. If Open Dental reports it **can't find the images**, point the image path at `<VM_IMAGES_DIR>`: *Setup → Imaging / Image path* (the A-to-Z path stored in the database is production's; update it to the VM's local folder so restored images resolve).
## Phase 4 — Health checklist: is this backup good?
Work top to bottom. **All must pass** for the backup to count as verified.
- [ ] **Connects & opens** — Open Dental launches against the restored DB with **no connection or startup errors**.
- [ ] **Version matches** — *Help → About* shows `<OD_VERSION>` (no unexpected DB-conversion prompt).
- [ ] **Database Maintenance is clean** — *Main Menu → Tools → Database Maintenance*, run **Check** (not Fix). It "checks the database for improper settings, inconsistencies, or corruption." Review the results for **unexpected** corruption/integrity errors. ([Open Dental — Database Maintenance](https://www.opendental.com/manual/databasemaintenance.html))
- [ ] **Recent data is present and current** — open the **Appointments** module on a recent production date; open 2–3 known patients; check a recent **payment/ledger** entry. Confirm the newest data lines up with production **as of the backup time** (proves the backup is current, not stale).
- [ ] **Images actually open** — pick a patient known to have scanned docs/X-rays in the Imaging module and **open the files**. This proves the A-to-Z folder restored *and* the image path resolves — a DB-only restore silently loses every document.
- [ ] **No missing modules/tables** — click through Chart, Family, Account, Imaging; watch for "table doesn't exist" / read-only errors.
- [ ] *(Optional, deeper)* **Engine-level integrity** — from the VM's MySQL shell, `mysqlcheck -u root -p --check opendental` (or `CHECK TABLE`) to confirm no table-level corruption.
## Phase 5 — Record the result (keep as evidence)
Log, outside the VM: **date verified**, `<BACKUP_FOLDER>` timestamp, `<OD_VERSION>`/`<DB_VERSION>`, each checklist item **pass/fail**, who ran it, and any Database Maintenance findings. This is your DR evidence for E&O / cyber insurance and HIPAA contingency-plan testing.
> A **fail** on any item means the backup is not trustworthy — fix the backup job (not just this restore) and re-verify. Common causes: images folder never included; only `data\opendental\` copied (InnoDB won't start); backup taken hot without stopping the service; version drift.
## Phase 6 — Tear down / reset
1. In Open Dental, close out; **stop the MySQL service** in the VM before shutting down.
> Open Dental warns: if a machine used to verify backups **sleeps while MySQL is running, it can corrupt the database.** Stop the service first. ([Open Dental — Manual Backups](https://www.opendental.com/manual/backupsmanual.html))
2. **Revert the VM to the `clean-baseline` checkpoint.** This resets to a known-clean state for next time **and destroys the restored PHI copy**.
3. Never reconnect this VM to the office network with a restored database attached.
---
## Security note
The moment you restore, the VM's virtual disk holds the **entire practice's PHI** — treat it exactly like production. Keep the VM **isolated** (Private/Internal switch or no NIC), keep the host and VHDX **encrypted** (BitLocker / encrypted VHDX), and **revert/destroy** the restored state when done so PHI doesn't linger on a test box. HIPAA applies to the copy just as much as to production. See the security note in [`od/db-backup.md`](db-backup.md) — on direct-connection setups the captured `FreeDentalConfig.xml` also holds the (reversibly obfuscated) DB password.
## References
- Open Dental manual — Backups (overview; test-restore off-network): <https://opendental.com/manual/backups.html>
- Open Dental manual — Manual Backups (restore steps; verify on disconnected machine; sleep/corruption warning): <https://www.opendental.com/manual/backupsmanual.html>
- Open Dental manual — Database Maintenance (Check/Fix health tool): <https://www.opendental.com/manual/databasemaintenance.html>
- Open Dental manual — Backup Tool: <https://www.opendental.com/manual/backuptool.html>
- Companion runbook — producing the backup: [`od/db-backup.md`](db-backup.md)
+81
View File
@@ -0,0 +1,81 @@
# Runbook: Open Dental — Persist "Do not show this window on startup"
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** Windows workstation where Open Dental is run by a standard (non-admin) user.
**Symptom:** The **Choose Database** window appears on every launch even though **"Do not show this window on startup (this computer only)"** is checked. The checkbox appears to take but does not survive a restart.
**Root cause:** The setting is stored in `FreeDentalConfig.xml` in the Open Dental install directory. Standard users lack write permission to that directory, so the change is never saved. Per Open Dental's docs, saving this window's settings requires the process to write that file.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<OD_INSTALL_DIR>` | Open Dental install directory (holds `FreeDentalConfig.xml`) — commonly under `Program Files` or `Program Files (x86)`; varies by version/architecture |
---
## 1. Confirm the symptom
Launch Open Dental **as the standard user who normally runs it**. Check **"Do not show this window on startup (this computer only)"**, click **OK**, then fully close and relaunch.
- Window returns → the config file isn't being saved → continue.
- Window gone → nothing to do.
## 2. Choose the fix
| Situation | Use |
|---|---|
| One-off machine, or the daily user is already a local admin | **Option A** — one-time elevated save |
| Shared workstation, or the daily user is a standard (non-admin) user | **Option B** — persistent ACL grant *(preferred)* |
Option A saves the setting once under an elevated context. Option B makes the file writable under the normal user context so Open Dental can rewrite it itself, now and after future changes — the durable fix for standard-user machines.
## 3. Option A — One-time elevated save
1. Right-click the Open Dental shortcut → **Run as administrator**.
2. Re-check **"Do not show this window on startup (this computer only)."**
3. Click **OK**, then close Open Dental.
4. Relaunch **as the standard user** and confirm the window no longer appears.
> Because the elevated process could write the install directory, the setting saves. If the standard user later triggers another change to this window, it won't persist — for shared/standard-user machines prefer Option B.
## 4. Option B — Persistent ACL grant (preferred)
Grant the built-in **Users** group **Modify** on `FreeDentalConfig.xml` so Open Dental can rewrite it under the normal user context without elevation.
Fastest, from an **elevated** PowerShell:
```
irm rb.godwinsystems.com/scripts/od-cfg-acl.ps1 | iex
```
It resolves the install path (64-bit / 32-bit Program Files), confirms, and grants the permission.
Offline / manual equivalent (elevated PowerShell or Command Prompt) — grant Modify to Users via the well-known SID, not the localized name "Users":
```
icacls "<OD_INSTALL_DIR>\FreeDentalConfig.xml" /grant *S-1-5-32-545:M
```
> `S-1-5-32-545` is the built-in **Users** group on every Windows install regardless of OS language — safer in scripts than the display name "Users," which is localized. `:M` = Modify.
## 5. Verify
Relaunch Open Dental **as the standard user**. Confirm the **Choose Database** window no longer appears on startup.
If it still appears: confirm the ACL landed with `icacls "<OD_INSTALL_DIR>\FreeDentalConfig.xml"` (look for a `Users:(M)`-style entry), and confirm you edited the copy in the directory Open Dental actually launches from.
---
## Security note
On **direct-connection** setups, `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible**. Granting the Users group write access does not change that exposure — but widening read/write on a file that holds DB credentials is worth calling out.
- Use a **limited MySQL user** (scoped to the Open Dental database) for workstation connections rather than `root`.
- Longer-term, move to **Middle Tier**, which removes per-workstation DB credentials entirely (workstations talk to a service, not the database directly).
## References
- Open Dental manual — FreeDentalConfig.xml: <https://www.opendental.com/manual/freedentalconfig.html>
- Open Dental manual — Choose Database window: <https://www.opendental.com/manual/choosedatabase.html>
+151
View File
@@ -0,0 +1,151 @@
# Runbook: Open Dental — Rock-solid database + images backup (Windows)
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** The Windows machine hosting the Open Dental MySQL/MariaDB database (the "server").
**Goal:** A fully consistent, restorable backup of both halves of an Open Dental practice — the **database** and the **A-to-Z images folder** — following Open Dental and MySQL/MariaDB best practice, including cleanly **stopping the database during the copy and restarting it after**.
### What a complete Open Dental backup is (and isn't)
Per Open Dental's own docs, a full backup is **two things — the database and the A-to-Z images folder** — and both must be captured together. A database backup **without** the images folder (or vice-versa) is not a usable restore.
| Component | Default location | Back up? | Holds |
|---|---|---|---|
| MySQL/MariaDB **data directory** | `C:\mysql\data\` (contains the `opendental` DB) | **Yes — required** | All clinical/financial data |
| **A-to-Z / OpenDentImages** folder | `C:\OpenDentImages\` | **Yes — required** | Scanned docs, images, attachments |
| **`FreeDentalConfig.xml`** | Open Dental install dir | Nice-to-have | Connection / Choose-Database / AtoZ path config |
| Open Dental **program files** | `…\Program Files\Open Dental\` | **No — reinstall** | The application itself |
| Other office documents | (varies) | Per your policy | Non-Open-Dental files |
**There is no separate "application backup."** Open Dental does **not** back up the program itself — on restore you **reinstall the matching Open Dental version** (and matching MySQL/MariaDB) and point it at the restored data. So the two "recovery aids" worth grabbing are just: a copy of **`FreeDentalConfig.xml`** and a record of the **exact versions** to reinstall. The script captures both automatically into the backup folder (`FreeDentalConfig.xml` + `MANIFEST.txt`) — nothing extra to run.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<DB_SERVICE>` | Name of the MySQL/MariaDB Windows service (e.g. `MySQL`, `MySQL57`, `MariaDB`) |
| `<DATA_DIR>` | MySQL data directory — the folder **containing** the `opendental` subfolder (commonly `C:\mysql\data`) |
| `<IMAGES_DIR>` | A-to-Z images folder (commonly `C:\OpenDentImages`) |
| `<DEST>` | Backup destination — separate physical disk or UNC path, ideally replicated off-site |
| `<DB_USER>` / `<DB_PASSWORD>` | A MySQL account for `mysqldump` (supplemental method) — from the password manager, never committed |
---
## Why "cold copy" is the rock-solid method
Open Dental databases run on **MyISAM or InnoDB**. For a *file-level* backup to be consistent:
- A **hot copy** (copying the data directory while the service runs) can capture InnoDB mid-write → **corrupt, unrestorable** backup. Open Dental's built-in Backup tool and most "online" file backups **cannot even restore InnoDB**.
- A **cold copy** — stop the service so it flushes and closes cleanly, copy, restart — is **consistent for both engines**. This is the gold-standard local backup.
Two details that make or break a cold copy:
1. **Copy the *entire* data directory, not just `data\opendental\`.** InnoDB's shared tablespace and redo logs (`ibdata1`, `ib_logfile*`) live at the **root** of the data directory. Copy only the `opendental` subfolder and an InnoDB restore will fail.
2. **Verify the service actually stopped before copying.** If it won't stop, do **not** copy — you'd capture a live datadir.
The script below does both, and **always restarts the service** (even if the copy fails), so the practice is never left down.
---
## 1. Pre-flight
- Run **on the database server**, in an **elevated** PowerShell, **off-hours** — the copy causes downtime; Open Dental is unavailable on every workstation while the service is stopped.
- Make sure **no one is in Open Dental** (fully closed on all workstations).
- Have a `<DEST>` on a **different physical disk** (or UNC share) with enough free space for the data directory **+** images.
## 2. Run the cold backup (primary method)
From an **elevated** PowerShell on the server:
```
irm rb.godwinsystems.com/scripts/od-db-backup.ps1 | iex
```
It will:
1. Auto-detect the `<DB_SERVICE>`, `<DATA_DIR>`, and `<IMAGES_DIR>` (prompting to confirm/override).
2. Report sizes and destination free space, then confirm before doing anything.
3. **Stop `<DB_SERVICE>` and verify it reached *Stopped*** — aborting the copy if it doesn't.
4. `robocopy` the **whole** data directory to `<DEST>\od-backup-<timestamp>\data`, then the images to `…\OpenDentImages`.
5. **Restart `<DB_SERVICE>`** in a `finally` block — this runs even if the copy fails or is interrupted.
6. Capture recovery aids (best-effort, outside the downtime window): a copy of **`FreeDentalConfig.xml`** and the exact **Open Dental + MySQL/MariaDB versions**.
7. Write `MANIFEST.txt` and `backup.log` into the backup folder.
### Manual equivalent (offline / if you can't fetch the script)
Elevated PowerShell, no one in Open Dental:
```
net stop <DB_SERVICE>
Get-Service <DB_SERVICE> # confirm Status = Stopped BEFORE copying
robocopy "<DATA_DIR>" "<DEST>\od-backup\data" /E /COPY:DAT /R:2 /W:5
robocopy "<IMAGES_DIR>" "<DEST>\od-backup\OpenDentImages" /E /COPY:DAT /R:2 /W:5
net start <DB_SERVICE>
Get-Service <DB_SERVICE> # confirm Status = Running
```
> Copy `<DATA_DIR>` itself (the parent of `opendental`), **not** `<DATA_DIR>\opendental` — you need `ibdata1` / `ib_logfile*` at the datadir root for InnoDB. If `net start` fails, start it immediately: the practice cannot work until the DB is back up.
## 3. Supplemental logical backup (mysqldump)
A cold copy is engine-perfect but physical — a portable **logical** dump is a valuable second line (survives binary corruption, restores to any server/version). It runs **while the service is up**, so schedule it separately (e.g. mid-day incremental in addition to the nightly cold copy). Slight slowness while it runs.
```
mysqldump -u <DB_USER> -p --single-transaction --quick --max-allowed-packet=1024M --default-character-set=utf8 --routines --events opendental > "<DEST>\opendental-<date>.sql"
```
- `--single-transaction` gives a consistent snapshot of **InnoDB** without locking the practice out. (For **MyISAM**, that flag does not guarantee consistency — use the cold copy as the source of truth.)
- Compress the `.sql` afterward; it shrinks dramatically.
- The dump does **not** include the images folder — always pair it with an `<IMAGES_DIR>` copy.
## 4. Schedule it (daily minimum)
Open Dental's floor is **at least one backup per day**; combine an automated nightly job with an off-site copy.
Register the cold backup as a nightly **Task Scheduler** job (runs off-hours, as SYSTEM/admin). Because `irm | iex` prompts interactively, schedule a **local copy** of the script with the paths baked in (keep that filled-in copy in the **private tier**, not here), e.g.:
```
schtasks /Create /TN "OD Nightly Cold Backup" /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\ops\od-db-backup-local.ps1" /SC DAILY /ST 23:30 /RU SYSTEM /RL HIGHEST
```
Full scheduling **and monitoring** (staggering the off-site upload, a daily health check, and a dead-man's-switch heartbeat so a silent failure gets caught): **[`od/backup-schedule.md`](backup-schedule.md)**.
## 5. Verify — a backup you haven't restored is a guess
- **Test-restore** to an **isolated** machine periodically (matching Open Dental + MySQL/MariaDB versions): `net stop`, rename the existing `opendental` folder aside, drop in the backup's `data\` contents, `net start`, launch Open Dental, spot-check patients/images. Full step-by-step with a health checklist: **[`od/backup-verify.md`](backup-verify.md)** (isolated Hyper-V test-restore).
- **Never restore over a live production database** — data loss is irreversible.
- Confirm the nightly job is actually producing dated folders and that they leave the building (off-site / immutable copy) — ransomware that reaches the server will reach on-line backups too.
## 6. Retention & off-site (3-2-1)
- **3** copies, **2** media, **1** off-site. The `<DEST>` disk alone is not a backup strategy.
- Keep several daily generations plus weekly/monthly rollups; prune old `od-backup-<timestamp>` folders on a schedule.
- These files contain **PHI** — encrypt at rest and in transit; restrict access. HIPAA applies.
### Point off-site tools at `<DEST>`, not at the live database
Any off-site/cloud replication (Duplicati → Backblaze B2, Veeam, `rclone`, Wasabi, etc.) must use **`<DEST>` as its source** — it backs up *this backup*. It must **not** re-run the stop/copy against the live `<DATA_DIR>`.
- The `od-backup-<timestamp>` folders under `<DEST>` are already a **consistent, cold copy** produced with the service cleanly stopped. Copying *them* off-site is a safe file copy — no service stop, no downtime, no consistency risk.
- Never let a naive file-sync tool crawl the live `C:\mysql\data` directly. A hot copy of a running InnoDB datadir is **corrupt and unrestorable** — the exact failure this runbook exists to avoid. Off-site tools have no idea they need to stop the service first; that's *this* script's job, done once, up front.
- **Sequence:** this cold backup runs first (nightly, off-hours) → the off-site job runs **after** it completes, sourcing `<DEST>`. Stagger the schedules (e.g. cold backup 23:30, off-site upload 01:00) so the upload never reads a half-written `od-backup-<timestamp>` folder. If your tool supports it, exclude any in-progress/partial folder or upload only completed timestamps.
- Let the off-site tool own its **own** encryption + retention on top — B2/Wasabi object lock or Duplicati's immutability/versioning gives you the ransomware-resistant, off-site copy of the 3-2-1 rule.
---
## Security note
Backup media and dumps hold the **entire practice's PHI**. Treat them as the crown jewels: encrypt the destination, lock down share permissions, and keep at least one copy **off-line/immutable** so ransomware can't encrypt your backups along with production. Don't store `<DB_PASSWORD>` in the scheduled command line — use a MySQL option file / limited account.
Note that the captured `FreeDentalConfig.xml` stores the MySQL password **obfuscated but reversible** on **direct-connection** setups. The backup already contains all PHI so this doesn't change the requirement — but it's one more reason the destination must be encrypted and access-controlled. (Middle Tier setups don't put DB credentials in that file — see `od/cfg-persist.md`.)
## References
- Open Dental manual — Backups (overview): <https://opendental.com/manual/backups.html>
- Open Dental manual — Manual Backups: <https://www.opendental.com/manual/backupsmanual.html>
- Open Dental manual — Backup Tool: <https://www.opendental.com/manual/backuptool.html>
- Open Dental manual — FreeDentalConfig.xml: <https://www.opendental.com/manual/freedentalconfig.html>
- Open Dental — InnoDB (backup implications): <https://www.opendental.com/site/mysqlinnodb.html>
- Open Dental manual — MySQL Data Directory Management: <https://opendental.com/manual/mysqlmanage.html>
- MySQL — `mysqldump`, `--single-transaction`: <https://dev.mysql.com/doc/refman/en/mysqldump.html>
+119
View File
@@ -0,0 +1,119 @@
# Runbook: Open Dental — Duplex ADF scanner captures only one side
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** Any single-pass duplex ADF scanner feeding Open Dental over **TWAIN** on a Windows 11 workstation (standalone / self-enrolled is common). Written against a Canon imageFORMULA DR-series with the combined ISIS/TWAIN/WIA driver package, but the diagnostic tree is model-agnostic.
**Symptom:** A double-sided document run through the ADF into the Open Dental **Imaging** module lands as front-only — the back side is missing, blank, or pages come out in the wrong order.
**Most likely cause:** Duplex is not enabled on whichever setting is *authoritative*, and which one is authoritative depends on the **Show TWAIN UI** toggle. The classic miss is a checked-but-inert control on the branch that isn't in charge.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<WORKSTATION>` | The Windows 11 workstation with the scanner attached |
| `<SCANNER_MODEL>` | The duplex ADF scanner model (e.g. a Canon DR-series) |
| `<TWAIN_SOURCE>` | The native TWAIN data source name as it appears in Open Dental's **Twain Name** dropdown |
| `<TEST_PATIENT>` | A dummy patient/chart used for test scans only — never a live chart |
| `<OD_USER>` | The Open Dental user the front desk actually scans as |
---
## Key concept — where duplex is governed
> [!IMPORTANT]
> In Open Dental, duplex is controlled in **one of two places**, and only one is live at a time. The **Show TWAIN UI** toggle (Setup → Imaging → **Edit Imaging Device**) decides which:
>
> - **Show TWAIN UI = OFF** → **Open Dental's own Duplex checkbox** (Imaging Quality → **Multipage Scans**) is authoritative. The scanner's own dialog never appears.
> - **Show TWAIN UI = ON** → the **scanner's TWAIN dialog at scan time** is authoritative. Open Dental's Duplex checkbox is **inert** — it is not read.
>
> **The trap:** an OD Duplex checkbox that is checked *while Show TWAIN UI is ON* (or a scanner dialog set to duplex *while Show TWAIN UI is OFF*) changes nothing. A control set correctly on the **wrong branch** is the single most common misdiagnosis here. Always establish the toggle state **first**, then set duplex on the branch that matches it.
---
## Diagnostic steps
Work these in order. Steps a–c establish ground truth before you change anything.
### a. Confirm the Twain Name binds to the NATIVE source, not a WIA bridge
In **Edit Imaging Device**, check the **Twain Name** value.
- It should be the vendor's **native TWAIN** source (e.g. the `<SCANNER_MODEL>` TWAIN entry).
- If it reads **`WIA-<SCANNER_MODEL>`** or any `WIA-…` entry, that is the Windows WIA→TWAIN bridge. **The WIA bridge frequently drops the second side** and cannot be relied on for duplex. Re-select the native TWAIN source.
If the native source isn't in the list, the 32-bit TWAIN driver isn't registered — see [Gotchas](#gotchas).
### b. Record the Show TWAIN UI state
Note whether **Show TWAIN UI** is **ON** or **OFF**. This determines which fix branch applies below. Write it down — you'll restore or deliberately set it.
### c. Screenshot the current settings (baseline / rollback)
Capture the current **Edit Imaging Device** and **Imaging Quality → Multipage Scans** settings before touching anything, so you have a known-good rollback point and a record of what changed.
> [!WARNING]
> Screenshots for your own rollback are fine, but they may contain client-identifying detail (hostname, user, chart data). Keep them in the **private tier** — never attach them to this public repo.
### d. Confirm the scan action is multi-page (ADF), not single-page
The front-desk action must be **Scan Multi-Page Document** (ADF → multi-page PDF). The plain **Scan Document** action pulls a single page and **can never be duplex** regardless of every other setting. If the button in use is single-page, that alone explains front-only output.
### e. Reproduce with a real double-sided document
Feed a genuine two-sided document through the ADF and record the **exact** failure mode — they point at different causes:
| Observed | Points toward |
|---|---|
| Front pages only, backs never appear | Duplex off on the authoritative branch (step f) |
| Backs captured then dropped if blank/light | **Skip Blank Page** is on (see Gotchas) |
| Both sides present but interleaved/out of order | Scan-order / driver page-order setting |
### f. Apply the fix for the matching branch
Use the [decision table](#fix-decision-table) below, keyed to the Show TWAIN UI state from step b.
### g. If still failing, isolate with Twacker over TWAIN
Twacker is the reference TWAIN test application. Scan the same document through Twacker against the **same `<TWAIN_SOURCE>`**:
- **Works in Twacker, fails in Open Dental** → the driver and hardware are fine; the problem is Open Dental configuration or how it drives the source. Return to steps a–f.
- **Fails in Twacker too** → the problem is below Open Dental: driver settings or hardware. Fix it in the scanner's TWAIN dialog / driver, then retest.
### h. Verify end-to-end and set as default
1. Trigger the scan from the **actual front-desk button** as `<OD_USER>` — not a settings-screen test.
2. **Open the resulting PDF** and confirm **both sides are present and in reading order**.
3. Set the working configuration as the **default** so it survives an app restart or profile reset. A fix that only holds for the current session isn't done.
---
## Fix decision table
| Show TWAIN UI | Authoritative setting | Action |
|---|---|---|
| **OFF** | Open Dental **Duplex** checkbox (Imaging Quality → Multipage Scans) | **Check it.** |
| **ON** | Scanner's **TWAIN dialog** at scan time | Set **Scanning Side = Duplex**; set **Skip Blank Page = OFF**. |
> **Resolved example (OFF branch):** In the case that prompted this runbook, **Show TWAIN UI was OFF** and Open Dental's **Duplex** checkbox was **unchecked**. Checking it resolved the issue immediately — no driver or hardware change needed. This is the common OFF-branch outcome; still walk the tree above rather than assuming, since the ON branch fails differently.
---
## Gotchas
- **"Skip Blank Page" masquerades as simplex.** With this enabled in the TWAIN driver, a blank or light back side is **silently deleted on save** — the output looks exactly like a simplex scan even though both sides were captured. Turn it **OFF** while diagnosing duplex.
- **Open Dental is 32-bit and binds a 32-bit TWAIN source.** Open Dental only supports **32-bit TWAIN** drivers. The ISIS/TWAIN/WIA package installs a 32-bit source, and Open Dental (a 32-bit app) binds to it. A 64-bit-only driver, or picking the wrong source, means no working duplex — confirm the **native 32-bit TWAIN** source is what's selected in step a.
- **Windows Fax and Scan is NOT a valid isolation test.** It drives the scanner over **WIA, not TWAIN**. It can succeed while the TWAIN path fails (or vice versa) and tells you nothing about an Open Dental/TWAIN problem. Use **Twacker over TWAIN** (step g) for isolation.
- **Mechanical checks.** Before chasing software: the feed/separation **lever is in the separation (multi-sheet) position**, **double-feed detection** isn't misfiring and halting the second side, and the back side of the test document **genuinely has content**.
---
## Compliance note
> [!WARNING]
> Run all test scans into a **`<TEST_PATIENT>`** dummy account — **never a live chart**. When finished, **purge the test images** from `<TEST_PATIENT>` so no stray PHI or test scans are left behind. Confirm the dummy account holds nothing before leaving the workstation.
## References
- Open Dental manual — Imaging module / device setup: <https://www.opendental.com/manual/imaging.html>
- Open Dental manual — Edit Imaging Device (Twain Name, Show TWAIN UI): <https://www.opendental.com/manual/imagingdevices.html>
+137
View File
@@ -0,0 +1,137 @@
# Runbook: Open Dental SMB Share Access — Stored Credential Fix
> For qualified IT professionals, on systems they are authorized to administer.
> Provided as-is, without warranty — verify it fits your environment. See LICENSE.
**Applies to:** Entra-joined Windows workstation accessing an Open Dental A-to-Z share on a standalone (non-domain, non-Entra) server via local SAM credentials.
**Symptom:** Open Dental cannot reach `\\<SERVER>\<SHARE>` after workstation restart; works after manually connecting via File Explorer.
**Root causes covered:** missing/stale stored Windows credential; Credential Guard blocking saved credential replay; elevated process not seeing user-session credentials.
**Placeholders:**
| Placeholder | Meaning |
|---|---|
| `<SERVER>` | Application server hostname |
| `<SHARE>` | Share name (e.g. OpenDentImages) |
| `<SHARE_USER>` | Local account on `<SERVER>` used for share access |
| `<PASSWORD>` | From password manager — never stored in this file |
---
## 1. Confirm identity of the machine
```
hostname
```
Verify you're on the machine you think you're on before changing anything.
## 2. Check Credential Guard status
```
msinfo32
```
System Summary → **Virtualization-based security Services Running**
- Credential Guard **not listed** → skip to Step 4
- Credential Guard **listed** → do Step 3
> Context: Credential Guard blocks the replay of saved "Windows credentials" from Credential Manager. Symptom is exactly "works after manual Explorer connect, breaks on restart." Enabled by default on Windows 11 22H2+ on entitled SKUs (Enterprise/Business), not plain Pro.
## 3. Disable Credential Guard (ONLY if running)
Fastest, from an **elevated** PowerShell:
```
irm rb.godwinsystems.com/scripts/cg-disable.ps1 | iex
```
It confirms, clears the flags, and prompts to reboot.
Offline / manual equivalent (elevated PowerShell or Command Prompt):
```
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Lsa" /v LsaCfgFlags /t REG_DWORD /d 0 /f
reg add "HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\CredentialGuard" /v Enabled /t REG_DWORD /d 0 /f
shutdown /r /t 0
```
After reboot: re-run msinfo32 and confirm Credential Guard is no longer listed, then continue.
> If it's still running after reboot, it was enabled with UEFI lock — requires the bcdedit/physical-presence removal procedure. Also check whether MDM policy is re-enabling it; align with the environment's baseline rather than fighting it locally.
## 4. Review existing stored credentials
Regular (NON-elevated) Command Prompt, logged in as the user who runs Open Dental:
```
cmdkey /list
```
- Record any entries for `<SERVER>` (by hostname or IP) before deleting.
- Delete existing entries for the server:
```
cmdkey /delete:<SERVER>
```
Repeat for IP-based entries if present. Duplicate entries for the same server (hostname + IP) cause intermittent 1219-style conflicts — clear all of them.
## 5. Add the credential
Same non-elevated prompt (elevated prompts write to the wrong credential vault):
```
cmdkey /add:<SERVER> /user:<SERVER>\<SHARE_USER> /pass:"<PASSWORD>"
```
Quote the password if it contains special characters.
## 6. Verify it stored
```
cmdkey /list
```
Expect an entry with `Target: <SERVER>`.
## 7. Cold test
```
shutdown /r /t 0
```
Log in as the Open Dental user. Launch Open Dental **directly — do not open Explorer or touch the share first.**
Open the Imaging module → confirm images load.
**PASS → done.** Record results.
## 8. If Step 7 fails — isolate
```
dir \\<SERVER>\<SHARE>
```
| Result | Meaning | Action |
|---|---|---|
| `dir` fails | Stored credential not being used | `cmdkey /list` — confirm entry survived reboot; re-check msinfo32 for Credential Guard |
| `dir` works, Open Dental doesn't | Open Dental launching elevated | Shortcut → Properties → Advanced → uncheck "Run as administrator"; also check Compatibility tab. Retest. If elevation is required, set `EnableLinkedConnections` (HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System, DWORD=1) and reboot. |
## Fleet comparison (optional, ~2 min)
On a known-working workstation:
```
cmdkey /list
```
Record what it stores for `<SERVER>` — documents the fleet's credential state and reveals inconsistencies (per-machine manual setups, stale accounts).
---
## Notes
- The Open Dental A-to-Z path stored in Setup → Data Paths is **global** (database-stored). The "Path override for this computer" field is per-workstation. Never change the global path to a mapped drive letter to fix one machine.
- Prefer a dedicated low-privilege local account on `<SERVER>` scoped to the share only — not an administrative account.
- Rotating the share account's password silently breaks stored credentials on every workstation using it. Inventory which machines hold it (Step 4 on each) before rotating.