Showing 21 changed files with 931 additions and 441 deletions
+0 -439
.doc/database.md
@@ -1,439 +0,0 @@
1
-# SQLite Database
2
-
3
-Madagascar Local Authority folosește SQLite ca sursă de adevăr runtime pentru hosturi, aliasuri, vhosturi, Work Orders, workeri de date și certificate.
4
-
5
-Locația implicită în checkout:
6
-
7
-```text
8
-var/host-manager.sqlite
9
-```
10
-
11
-Locația runtime pe jumper:
12
-
13
-```text
14
-/usr/local/xdev-host-manager/var/host-manager.sqlite
15
-```
16
-
17
-Path-ul poate fi schimbat cu:
18
-
19
-```text
20
-HOST_MANAGER_DB=/path/to/host-manager.sqlite
21
-```
22
-
23
-## Principii
24
-
25
-Hosturile sunt identificate prin FQDN complet, nu prin short name. Exemplu: `gw.local` și `gw.remote` sunt identități diferite. Coloana compatibilă `legacy_id` păstrează ID-ul scurt folosit de UI-ul curent, dar cheia reală este `hosts.fqdn`.
26
-
27
-Schema evită să transforme `hosts` într-un tabel cu prea multe coloane. Datele specializate sunt în tabele separate:
28
-
29
-- aliasuri: `host_aliases`
30
-- roluri: `host_roles`
31
-- surse: `host_sources`
32
-- flaguri: `host_flags`
33
-- SSH: `host_ssh`
34
-- vhosturi mutabile: `vhosts`
35
-- certificate: `certificates`, `certificate_dns_names`
36
-- workeri și observații: `data_workers`, `dhcp_leases`, `mdns_observations`
37
-
38
-`documents` rămâne doar tabel legacy pentru migrarea din modelul vechi document-store. Aplicația nu îl mai folosește ca sursă de adevăr.
39
-
40
-## Schema Version
41
-
42
-Schema curentă este versiunea `2`.
43
-
44
-```sql
45
-schema_meta('schema_version') = '2'
46
-```
47
-
48
-`schema_meta` păstrează și metadate runtime precum `registry_updated_at`.
49
-
50
-## Catalog
51
-
52
-| Tabel | Rol |
53
-|-------|-----|
54
-| `schema_meta` | metadate de schemă/runtime |
55
-| `documents` | document-store legacy pentru migrare |
56
-| `hosts` | hosturi canonice, identificate prin FQDN |
57
-| `host_aliases` | aliasuri păstrate inclusiv după retragere |
58
-| `host_roles` | roluri active/retrase per host |
59
-| `host_sources` | surse active/retrase per host |
60
-| `host_flags` | flaguri extensibile per host |
61
-| `host_ssh` | profile SSH per host |
62
-| `vhosts` | vhosturi mutabile între hosturi |
63
-| `data_workers` | workeri/surse care colectează date |
64
-| `dhcp_leases` | date observate din DHCP lease/reservation |
65
-| `mdns_observations` | date observate din mDNS |
66
-| `certificates` | certificate emise de CA locală |
67
-| `certificate_dns_names` | SAN DNS names pentru certificate |
68
-| `work_orders` | Work Orders |
69
-| `work_order_checklist` | checklist items pentru Work Orders |
70
-| `work_order_actions` | acțiuni confirmabile pentru Work Orders |
71
-
72
-## Tabele
73
-
74
-### `hosts`
75
-
76
-```sql
77
-CREATE TABLE hosts (
78
-    fqdn TEXT PRIMARY KEY,
79
-    legacy_id TEXT NOT NULL UNIQUE,
80
-    status TEXT NOT NULL DEFAULT 'active',
81
-    hosts_ip TEXT NOT NULL DEFAULT '',
82
-    dns_ip TEXT NOT NULL DEFAULT '',
83
-    monitoring TEXT NOT NULL DEFAULT 'pending',
84
-    notes TEXT NOT NULL DEFAULT '',
85
-    created_at TEXT NOT NULL,
86
-    updated_at TEXT NOT NULL
87
-);
88
-```
89
-
90
-Chei și indexuri:
91
-
92
-- PK: `hosts(fqdn)`
93
-- UNIQUE: `hosts(legacy_id)`
94
-
95
-### `host_aliases`
96
-
97
-```sql
98
-CREATE TABLE host_aliases (
99
-    alias_name TEXT NOT NULL,
100
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
101
-        ON UPDATE CASCADE ON DELETE RESTRICT,
102
-    alias_kind TEXT NOT NULL DEFAULT 'declared',
103
-    status TEXT NOT NULL DEFAULT 'active',
104
-    is_dns_published INTEGER NOT NULL DEFAULT 1,
105
-    created_at TEXT NOT NULL,
106
-    retired_at TEXT,
107
-    notes TEXT NOT NULL DEFAULT '',
108
-    PRIMARY KEY (alias_name, host_fqdn)
109
-);
110
-```
111
-
112
-Indexuri:
113
-
114
-```sql
115
-CREATE UNIQUE INDEX idx_host_aliases_active_name
116
-ON host_aliases(alias_name)
117
-WHERE status = 'active';
118
-
119
-CREATE INDEX idx_host_aliases_host_status
120
-ON host_aliases(host_fqdn, status);
121
-```
122
-
123
-Reguli:
124
-
125
-- aliasurile retrase nu se șterg; se setează `status = 'retired'`
126
-- un alias activ poate aparține unui singur host
127
-- aliasurile scurte derivate, cum ar fi `baobab`, sunt păstrate cu `alias_kind = 'derived'`
128
-- short aliases derivate din vhosturi, cum ar fi `pmx.baobab`, sunt păstrate cu `alias_kind = 'derived-vhost'`
129
-
130
-### `vhosts`
131
-
132
-```sql
133
-CREATE TABLE vhosts (
134
-    vhost_fqdn TEXT PRIMARY KEY,
135
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
136
-        ON UPDATE CASCADE ON DELETE RESTRICT,
137
-    status TEXT NOT NULL DEFAULT 'active',
138
-    service_name TEXT NOT NULL DEFAULT '',
139
-    upstream_url TEXT NOT NULL DEFAULT '',
140
-    tls_mode TEXT NOT NULL DEFAULT 'local-ca',
141
-    certificate_id TEXT REFERENCES certificates(certificate_id)
142
-        ON UPDATE CASCADE ON DELETE SET NULL,
143
-    notes TEXT NOT NULL DEFAULT '',
144
-    created_at TEXT NOT NULL,
145
-    updated_at TEXT NOT NULL
146
-);
147
-```
148
-
149
-Indexuri:
150
-
151
-```sql
152
-CREATE INDEX idx_vhosts_host_status
153
-ON vhosts(host_fqdn, status);
154
-```
155
-
156
-Un vhost se mută de pe un host pe altul prin update pe `vhosts.host_fqdn`. Vhosturile retrase rămân în tabel cu `status = 'retired'`.
157
-
158
-### `host_roles`, `host_sources`, `host_flags`, `host_ssh`
159
-
160
-```sql
161
-CREATE TABLE host_roles (
162
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
163
-        ON UPDATE CASCADE ON DELETE RESTRICT,
164
-    role TEXT NOT NULL,
165
-    status TEXT NOT NULL DEFAULT 'active',
166
-    created_at TEXT NOT NULL,
167
-    retired_at TEXT,
168
-    PRIMARY KEY (host_fqdn, role)
169
-);
170
-
171
-CREATE TABLE host_sources (
172
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
173
-        ON UPDATE CASCADE ON DELETE RESTRICT,
174
-    source TEXT NOT NULL,
175
-    status TEXT NOT NULL DEFAULT 'active',
176
-    created_at TEXT NOT NULL,
177
-    retired_at TEXT,
178
-    PRIMARY KEY (host_fqdn, source)
179
-);
180
-
181
-CREATE TABLE host_flags (
182
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
183
-        ON UPDATE CASCADE ON DELETE RESTRICT,
184
-    flag TEXT NOT NULL,
185
-    value TEXT NOT NULL DEFAULT '1',
186
-    created_at TEXT NOT NULL,
187
-    updated_at TEXT NOT NULL,
188
-    PRIMARY KEY (host_fqdn, flag)
189
-);
190
-
191
-CREATE TABLE host_ssh (
192
-    host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn)
193
-        ON UPDATE CASCADE ON DELETE RESTRICT,
194
-    profile_name TEXT NOT NULL DEFAULT 'default',
195
-    username TEXT NOT NULL DEFAULT '',
196
-    port INTEGER NOT NULL DEFAULT 22,
197
-    identity_file TEXT NOT NULL DEFAULT '',
198
-    address TEXT NOT NULL DEFAULT '',
199
-    local_forward_host TEXT NOT NULL DEFAULT '',
200
-    local_forward_port INTEGER,
201
-    remote_forward_host TEXT NOT NULL DEFAULT '',
202
-    remote_forward_port INTEGER,
203
-    notes TEXT NOT NULL DEFAULT '',
204
-    created_at TEXT NOT NULL,
205
-    updated_at TEXT NOT NULL,
206
-    PRIMARY KEY (host_fqdn, profile_name)
207
-);
208
-```
209
-
210
-### `data_workers`
211
-
212
-```sql
213
-CREATE TABLE data_workers (
214
-    worker_id TEXT PRIMARY KEY,
215
-    worker_type TEXT NOT NULL,
216
-    name TEXT NOT NULL DEFAULT '',
217
-    status TEXT NOT NULL DEFAULT 'active',
218
-    source TEXT NOT NULL DEFAULT '',
219
-    last_run_at TEXT,
220
-    notes TEXT NOT NULL DEFAULT '',
221
-    created_at TEXT NOT NULL,
222
-    updated_at TEXT NOT NULL
223
-);
224
-```
225
-
226
-Indexuri:
227
-
228
-```sql
229
-CREATE INDEX idx_data_workers_type_status
230
-ON data_workers(worker_type, status);
231
-```
232
-
233
-Seed implicit:
234
-
235
-- `dhcp-router`, type `dhcp`
236
-- `mdns-listener`, type `mdns`
237
-
238
-### `dhcp_leases`
239
-
240
-```sql
241
-CREATE TABLE dhcp_leases (
242
-    lease_key TEXT PRIMARY KEY,
243
-    worker_id TEXT NOT NULL REFERENCES data_workers(worker_id)
244
-        ON UPDATE CASCADE ON DELETE RESTRICT,
245
-    host_fqdn TEXT REFERENCES hosts(fqdn)
246
-        ON UPDATE CASCADE ON DELETE SET NULL,
247
-    observed_name TEXT NOT NULL DEFAULT '',
248
-    ip_address TEXT NOT NULL,
249
-    mac_address TEXT NOT NULL DEFAULT '',
250
-    lease_state TEXT NOT NULL DEFAULT '',
251
-    first_seen TEXT NOT NULL,
252
-    last_seen TEXT NOT NULL,
253
-    raw TEXT NOT NULL DEFAULT ''
254
-);
255
-```
256
-
257
-Indexuri:
258
-
259
-```sql
260
-CREATE INDEX idx_dhcp_leases_ip ON dhcp_leases(ip_address);
261
-CREATE INDEX idx_dhcp_leases_mac ON dhcp_leases(mac_address);
262
-CREATE INDEX idx_dhcp_leases_worker_last_seen
263
-ON dhcp_leases(worker_id, last_seen);
264
-```
265
-
266
-### `mdns_observations`
267
-
268
-```sql
269
-CREATE TABLE mdns_observations (
270
-    observation_key TEXT PRIMARY KEY,
271
-    worker_id TEXT NOT NULL REFERENCES data_workers(worker_id)
272
-        ON UPDATE CASCADE ON DELETE RESTRICT,
273
-    host_fqdn TEXT REFERENCES hosts(fqdn)
274
-        ON UPDATE CASCADE ON DELETE SET NULL,
275
-    observed_name TEXT NOT NULL,
276
-    ip_address TEXT NOT NULL,
277
-    rr_type TEXT NOT NULL DEFAULT 'A',
278
-    ttl INTEGER NOT NULL DEFAULT 0,
279
-    first_seen TEXT NOT NULL,
280
-    last_seen TEXT NOT NULL,
281
-    seen_count INTEGER NOT NULL DEFAULT 1,
282
-    last_peer TEXT NOT NULL DEFAULT '',
283
-    raw TEXT NOT NULL DEFAULT ''
284
-);
285
-```
286
-
287
-Indexuri:
288
-
289
-```sql
290
-CREATE INDEX idx_mdns_observations_name ON mdns_observations(observed_name);
291
-CREATE INDEX idx_mdns_observations_ip ON mdns_observations(ip_address);
292
-CREATE INDEX idx_mdns_observations_worker_last_seen
293
-ON mdns_observations(worker_id, last_seen);
294
-```
295
-
296
-### `certificates` și `certificate_dns_names`
297
-
298
-```sql
299
-CREATE TABLE certificates (
300
-    certificate_id TEXT PRIMARY KEY,
301
-    host_fqdn TEXT REFERENCES hosts(fqdn)
302
-        ON UPDATE CASCADE ON DELETE SET NULL,
303
-    common_name TEXT NOT NULL DEFAULT '',
304
-    subject TEXT NOT NULL DEFAULT '',
305
-    issuer TEXT NOT NULL DEFAULT '',
306
-    serial TEXT UNIQUE,
307
-    status TEXT NOT NULL DEFAULT 'issued',
308
-    not_before TEXT NOT NULL DEFAULT '',
309
-    not_after TEXT NOT NULL DEFAULT '',
310
-    fingerprint_sha256 TEXT UNIQUE,
311
-    cert_path TEXT NOT NULL DEFAULT '',
312
-    csr_path TEXT NOT NULL DEFAULT '',
313
-    created_at TEXT NOT NULL,
314
-    updated_at TEXT NOT NULL,
315
-    notes TEXT NOT NULL DEFAULT ''
316
-);
317
-
318
-CREATE TABLE certificate_dns_names (
319
-    certificate_id TEXT NOT NULL REFERENCES certificates(certificate_id)
320
-        ON UPDATE CASCADE ON DELETE CASCADE,
321
-    dns_name TEXT NOT NULL,
322
-    PRIMARY KEY (certificate_id, dns_name)
323
-);
324
-```
325
-
326
-Indexuri:
327
-
328
-```sql
329
-CREATE INDEX idx_certificate_dns_names_dns_name
330
-ON certificate_dns_names(dns_name);
331
-```
332
-
333
-Certificatele emise sunt sincronizate când aplicația citește lista CA prin `ca_manager.sh list-json`.
334
-
335
-### Work Orders
336
-
337
-```sql
338
-CREATE TABLE work_orders (
339
-    id TEXT PRIMARY KEY,
340
-    status TEXT NOT NULL DEFAULT 'pending',
341
-    title TEXT NOT NULL DEFAULT '',
342
-    reason TEXT NOT NULL DEFAULT '',
343
-    created_at TEXT NOT NULL,
344
-    confirmed_at TEXT NOT NULL DEFAULT '',
345
-    result TEXT NOT NULL DEFAULT '',
346
-    updated_at TEXT NOT NULL
347
-);
348
-
349
-CREATE TABLE work_order_checklist (
350
-    work_order_id TEXT NOT NULL REFERENCES work_orders(id)
351
-        ON UPDATE CASCADE ON DELETE CASCADE,
352
-    item_id TEXT NOT NULL,
353
-    text TEXT NOT NULL DEFAULT '',
354
-    status TEXT NOT NULL DEFAULT 'pending',
355
-    owner TEXT NOT NULL DEFAULT '',
356
-    notes TEXT NOT NULL DEFAULT '',
357
-    updated_at TEXT NOT NULL DEFAULT '',
358
-    PRIMARY KEY (work_order_id, item_id)
359
-);
360
-
361
-CREATE TABLE work_order_actions (
362
-    work_order_id TEXT NOT NULL REFERENCES work_orders(id)
363
-        ON UPDATE CASCADE ON DELETE CASCADE,
364
-    position INTEGER NOT NULL,
365
-    type TEXT NOT NULL,
366
-    host_fqdn TEXT REFERENCES hosts(fqdn)
367
-        ON UPDATE CASCADE ON DELETE SET NULL,
368
-    host_legacy_id TEXT NOT NULL DEFAULT '',
369
-    name TEXT NOT NULL DEFAULT '',
370
-    payload TEXT NOT NULL DEFAULT '',
371
-    PRIMARY KEY (work_order_id, position)
372
-);
373
-```
374
-
375
-## Migrare și Seed
376
-
377
-La prima inițializare a unei baze fără rânduri în `hosts`, aplicația seed-uiește din:
378
-
379
-1. `documents.name = 'hosts_yaml'`, dacă există din vechiul model
380
-2. `config/hosts.yaml`, dacă documentul legacy lipsește
381
-3. document gol valid, dacă lipsește și seed-ul
382
-
383
-Work Orders se seed-uiesc similar din `documents.name = 'work_orders_yaml'` sau `config/work-orders.yaml`.
384
-
385
-Seed-ul curent produce:
386
-
387
-- 11 hosturi cunoscute
388
-- aliasuri scurte derivate pentru hosturi și vhosturi
389
-- vhosturile `hosts.*`, `pmx.*` și `pbs.*`
390
-- workerii `dhcp-router` și `mdns-listener`
391
-- Work Order-ul existent pentru retragerea numelor legacy
392
-
393
-`config/local-hosts.tsv` rămâne manifest generat explicit din tabelele runtime.
394
-
395
-## Inspecție
396
-
397
-Pe jumper:
398
-
399
-```bash
400
-cd /usr/local/xdev-host-manager
401
-sqlite3 var/host-manager.sqlite '.tables'
402
-sqlite3 var/host-manager.sqlite '.schema hosts'
403
-sqlite3 var/host-manager.sqlite '.schema host_aliases'
404
-sqlite3 var/host-manager.sqlite '.schema vhosts'
405
-sqlite3 var/host-manager.sqlite 'pragma foreign_key_list(vhosts);'
406
-sqlite3 var/host-manager.sqlite 'pragma index_list(host_aliases);'
407
-sqlite3 var/host-manager.sqlite 'select fqdn, legacy_id, status, dns_ip from hosts order by legacy_id;'
408
-sqlite3 var/host-manager.sqlite 'select alias_name, host_fqdn, alias_kind, status from host_aliases order by alias_name;'
409
-sqlite3 var/host-manager.sqlite 'select vhost_fqdn, host_fqdn, status from vhosts order by vhost_fqdn;'
410
-```
411
-
412
-## Backup
413
-
414
-Backup recomandat:
415
-
416
-```bash
417
-cd /usr/local/xdev-host-manager
418
-sqlite3 var/host-manager.sqlite ".backup 'backups/host-manager/host-manager.sqlite.$(date +%Y%m%d_%H%M%S).bak'"
419
-```
420
-
421
-Cu WAL activ, o copie brută trebuie să trateze fișierele ca set coerent:
422
-
423
-```text
424
-var/host-manager.sqlite
425
-var/host-manager.sqlite-wal
426
-var/host-manager.sqlite-shm
427
-```
428
-
429
-## Restore
430
-
431
-Restore-ul înlocuiește sursa de adevăr runtime:
432
-
433
-```bash
434
-sudo systemctl stop host-manager
435
-sudo cp backups/host-manager/host-manager.sqlite.YYYYMMDD_HHMMSS.bak var/host-manager.sqlite
436
-sudo chown host-manager:host-manager var/host-manager.sqlite
437
-sudo systemctl start host-manager
438
-curl -fsS http://127.0.0.1:8088/healthz >/dev/null
439
-```
+157 -0
.doc/database/README.md
@@ -0,0 +1,157 @@
1
+# SQLite Database
2
+
3
+Madagascar Local Authority folosește SQLite ca sursă de adevăr runtime pentru hosturi, aliasuri, vhosturi, Work Orders, workeri de date și certificate.
4
+
5
+Locația implicită în checkout:
6
+
7
+```text
8
+var/host-manager.sqlite
9
+```
10
+
11
+Locația runtime pe jumper:
12
+
13
+```text
14
+/usr/local/xdev-host-manager/var/host-manager.sqlite
15
+```
16
+
17
+Path-ul poate fi schimbat cu:
18
+
19
+```text
20
+HOST_MANAGER_DB=/path/to/host-manager.sqlite
21
+```
22
+
23
+## Principii
24
+
25
+Hosturile sunt identificate prin FQDN complet, nu prin short name. Exemplu: `gw.local` și `gw.remote` sunt identități diferite. Coloana compatibilă `legacy_id` păstrează ID-ul scurt folosit de UI-ul curent, dar cheia reală este `hosts.fqdn`.
26
+
27
+Schema evită să transforme `hosts` într-un tabel cu prea multe coloane. Datele specializate stau în tabele separate:
28
+
29
+- aliasuri: [`host_aliases`](tables/host_aliases.md)
30
+- roluri: [`host_roles`](tables/host_roles.md)
31
+- surse: [`host_sources`](tables/host_sources.md)
32
+- flaguri: [`host_flags`](tables/host_flags.md)
33
+- SSH: [`host_ssh`](tables/host_ssh.md)
34
+- vhosturi mutabile: [`vhosts`](tables/vhosts.md)
35
+- certificate: [`certificates`](tables/certificates.md), [`certificate_dns_names`](tables/certificate_dns_names.md)
36
+- workeri și observații: [`data_workers`](tables/data_workers.md), [`dhcp_leases`](tables/dhcp_leases.md), [`mdns_observations`](tables/mdns_observations.md)
37
+
38
+[`documents`](tables/documents.md) rămâne doar tabel legacy pentru migrarea din modelul vechi document-store. Aplicația nu îl mai folosește ca sursă de adevăr.
39
+
40
+## Schema Version
41
+
42
+Schema curentă este versiunea `2`.
43
+
44
+```sql
45
+schema_meta('schema_version') = '2'
46
+```
47
+
48
+[`schema_meta`](tables/schema_meta.md) păstrează și metadate runtime precum `registry_updated_at`.
49
+
50
+## Catalog
51
+
52
+| Tabel | Rol |
53
+|-------|-----|
54
+| [`schema_meta`](tables/schema_meta.md) | metadate de schemă/runtime |
55
+| [`documents`](tables/documents.md) | document-store legacy pentru migrare |
56
+| [`hosts`](tables/hosts.md) | hosturi canonice, identificate prin FQDN |
57
+| [`host_aliases`](tables/host_aliases.md) | aliasuri păstrate inclusiv după retragere |
58
+| [`host_roles`](tables/host_roles.md) | roluri active/retrase per host |
59
+| [`host_sources`](tables/host_sources.md) | surse active/retrase per host |
60
+| [`host_flags`](tables/host_flags.md) | flaguri extensibile per host |
61
+| [`host_ssh`](tables/host_ssh.md) | profile SSH per host |
62
+| [`vhosts`](tables/vhosts.md) | vhosturi mutabile între hosturi |
63
+| [`data_workers`](tables/data_workers.md) | workeri/surse care colectează date |
64
+| [`dhcp_leases`](tables/dhcp_leases.md) | date observate din DHCP lease/reservation |
65
+| [`mdns_observations`](tables/mdns_observations.md) | date observate din mDNS |
66
+| [`certificates`](tables/certificates.md) | certificate emise de CA locală |
67
+| [`certificate_dns_names`](tables/certificate_dns_names.md) | SAN DNS names pentru certificate |
68
+| [`work_orders`](tables/work_orders.md) | Work Orders |
69
+| [`work_order_checklist`](tables/work_order_checklist.md) | checklist items pentru Work Orders |
70
+| [`work_order_actions`](tables/work_order_actions.md) | acțiuni confirmabile pentru Work Orders |
71
+
72
+## Relații Principale
73
+
74
+```mermaid
75
+erDiagram
76
+  hosts ||--o{ host_aliases : has
77
+  hosts ||--o{ vhosts : serves
78
+  hosts ||--o{ host_roles : has
79
+  hosts ||--o{ host_sources : has
80
+  hosts ||--o{ host_flags : has
81
+  hosts ||--o{ host_ssh : has
82
+  hosts ||--o{ dhcp_leases : may_match
83
+  hosts ||--o{ mdns_observations : may_match
84
+  hosts ||--o{ certificates : may_own
85
+  certificates ||--o{ certificate_dns_names : has
86
+  data_workers ||--o{ dhcp_leases : collects
87
+  data_workers ||--o{ mdns_observations : collects
88
+  work_orders ||--o{ work_order_checklist : has
89
+  work_orders ||--o{ work_order_actions : has
90
+  hosts ||--o{ work_order_actions : targets
91
+```
92
+
93
+## Migrare și Seed
94
+
95
+La prima inițializare a unei baze fără rânduri în `hosts`, aplicația seed-uiește din:
96
+
97
+1. `documents.name = 'hosts_yaml'`, dacă există din vechiul model
98
+2. `config/hosts.yaml`, dacă documentul legacy lipsește
99
+3. document gol valid, dacă lipsește și seed-ul
100
+
101
+Work Orders se seed-uiesc similar din `documents.name = 'work_orders_yaml'` sau `config/work-orders.yaml`.
102
+
103
+Seed-ul curent produce:
104
+
105
+- 11 hosturi cunoscute
106
+- aliasuri scurte derivate pentru hosturi și vhosturi
107
+- vhosturile `hosts.*`, `pmx.*` și `pbs.*`
108
+- workerii `dhcp-router` și `mdns-listener`
109
+- Work Order-ul existent pentru retragerea numelor legacy
110
+
111
+`config/local-hosts.tsv` rămâne manifest generat explicit din tabelele runtime.
112
+
113
+## Inspecție
114
+
115
+Pe jumper:
116
+
117
+```bash
118
+cd /usr/local/xdev-host-manager
119
+sqlite3 var/host-manager.sqlite '.tables'
120
+sqlite3 var/host-manager.sqlite '.schema hosts'
121
+sqlite3 var/host-manager.sqlite '.schema host_aliases'
122
+sqlite3 var/host-manager.sqlite '.schema vhosts'
123
+sqlite3 var/host-manager.sqlite 'pragma foreign_key_list(vhosts);'
124
+sqlite3 var/host-manager.sqlite 'pragma index_list(host_aliases);'
125
+sqlite3 var/host-manager.sqlite 'select fqdn, legacy_id, status, dns_ip from hosts order by legacy_id;'
126
+sqlite3 var/host-manager.sqlite 'select alias_name, host_fqdn, alias_kind, status from host_aliases order by alias_name;'
127
+sqlite3 var/host-manager.sqlite 'select vhost_fqdn, host_fqdn, status from vhosts order by vhost_fqdn;'
128
+```
129
+
130
+## Backup
131
+
132
+Backup recomandat:
133
+
134
+```bash
135
+cd /usr/local/xdev-host-manager
136
+sqlite3 var/host-manager.sqlite ".backup 'backups/host-manager/host-manager.sqlite.$(date +%Y%m%d_%H%M%S).bak'"
137
+```
138
+
139
+Cu WAL activ, o copie brută trebuie să trateze fișierele ca set coerent:
140
+
141
+```text
142
+var/host-manager.sqlite
143
+var/host-manager.sqlite-wal
144
+var/host-manager.sqlite-shm
145
+```
146
+
147
+## Restore
148
+
149
+Restore-ul înlocuiește sursa de adevăr runtime:
150
+
151
+```bash
152
+sudo systemctl stop host-manager
153
+sudo cp backups/host-manager/host-manager.sqlite.YYYYMMDD_HHMMSS.bak var/host-manager.sqlite
154
+sudo chown host-manager:host-manager var/host-manager.sqlite
155
+sudo systemctl start host-manager
156
+curl -fsS http://127.0.0.1:8088/healthz >/dev/null
157
+```
+33 -0
.doc/database/tables/certificate_dns_names.md
@@ -0,0 +1,33 @@
1
+# Table: `certificate_dns_names`
2
+
3
+Stores DNS Subject Alternative Names for certificates.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `certificate_id` | `TEXT` | no | none | Certificate identifier. References `certificates(certificate_id)`. |
10
+| `dns_name` | `TEXT` | no | none | DNS SAN value. |
11
+
12
+## Keys And Indexes
13
+
14
+- Primary key: `(certificate_id, dns_name)`
15
+- Lookup index: `idx_certificate_dns_names_dns_name` on `dns_name`
16
+
17
+## Relationships
18
+
19
+- `certificate_id` references `certificates(certificate_id)` with `ON UPDATE CASCADE ON DELETE CASCADE`
20
+
21
+## Definition
22
+
23
+```sql
24
+CREATE TABLE IF NOT EXISTS certificate_dns_names (
25
+    certificate_id TEXT NOT NULL,
26
+    dns_name TEXT NOT NULL,
27
+    PRIMARY KEY (certificate_id, dns_name),
28
+    FOREIGN KEY (certificate_id) REFERENCES certificates(certificate_id) ON UPDATE CASCADE ON DELETE CASCADE
29
+);
30
+
31
+CREATE INDEX IF NOT EXISTS idx_certificate_dns_names_dns_name
32
+ON certificate_dns_names(dns_name);
33
+```
+60 -0
.doc/database/tables/certificates.md
@@ -0,0 +1,60 @@
1
+# Table: `certificates`
2
+
3
+Stores metadata for certificates issued by the local CA.
4
+
5
+Certificate rows are synchronized when the app reads `ca_manager.sh list-json`.
6
+
7
+## Columns
8
+
9
+| Column | Type | Null | Default | Notes |
10
+|--------|------|------|---------|-------|
11
+| `certificate_id` | `TEXT` | no | none | Certificate identifier, currently derived from issued cert filename. Primary key. |
12
+| `host_fqdn` | `TEXT` | yes | `NULL` | Matched host if known. References `hosts(fqdn)`. |
13
+| `common_name` | `TEXT` | no | `''` | Common name or primary DNS name. |
14
+| `subject` | `TEXT` | no | `''` | Certificate subject. |
15
+| `issuer` | `TEXT` | no | `''` | Certificate issuer. |
16
+| `serial` | `TEXT` | yes | `NULL` | Certificate serial. Unique when present. |
17
+| `status` | `TEXT` | no | `'issued'` | Certificate lifecycle state. |
18
+| `not_before` | `TEXT` | no | `''` | Certificate validity start. |
19
+| `not_after` | `TEXT` | no | `''` | Certificate validity end. |
20
+| `fingerprint_sha256` | `TEXT` | yes | `NULL` | SHA256 fingerprint. Unique when present. |
21
+| `cert_path` | `TEXT` | no | `''` | Certificate file path. |
22
+| `csr_path` | `TEXT` | no | `''` | CSR file path. |
23
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
24
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
25
+| `notes` | `TEXT` | no | `''` | Operator notes. |
26
+
27
+## Keys And Indexes
28
+
29
+- Primary key: `certificate_id`
30
+- Unique: `serial`
31
+- Unique: `fingerprint_sha256`
32
+
33
+## Relationships
34
+
35
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE SET NULL`
36
+- Referenced by `certificate_dns_names.certificate_id`
37
+- Referenced by `vhosts.certificate_id`
38
+
39
+## Definition
40
+
41
+```sql
42
+CREATE TABLE IF NOT EXISTS certificates (
43
+    certificate_id TEXT PRIMARY KEY,
44
+    host_fqdn TEXT,
45
+    common_name TEXT NOT NULL DEFAULT '',
46
+    subject TEXT NOT NULL DEFAULT '',
47
+    issuer TEXT NOT NULL DEFAULT '',
48
+    serial TEXT UNIQUE,
49
+    status TEXT NOT NULL DEFAULT 'issued',
50
+    not_before TEXT NOT NULL DEFAULT '',
51
+    not_after TEXT NOT NULL DEFAULT '',
52
+    fingerprint_sha256 TEXT UNIQUE,
53
+    cert_path TEXT NOT NULL DEFAULT '',
54
+    csr_path TEXT NOT NULL DEFAULT '',
55
+    created_at TEXT NOT NULL,
56
+    updated_at TEXT NOT NULL,
57
+    notes TEXT NOT NULL DEFAULT '',
58
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL
59
+);
60
+```
+53 -0
.doc/database/tables/data_workers.md
@@ -0,0 +1,53 @@
1
+# Table: `data_workers`
2
+
3
+Stores worker/source definitions for collected external data.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `worker_id` | `TEXT` | no | none | Worker identifier. Primary key. |
10
+| `worker_type` | `TEXT` | no | none | Worker type, for example `dhcp` or `mdns`. |
11
+| `name` | `TEXT` | no | `''` | Human-readable name. |
12
+| `status` | `TEXT` | no | `'active'` | Worker lifecycle state. |
13
+| `source` | `TEXT` | no | `''` | Source endpoint/file. |
14
+| `last_run_at` | `TEXT` | yes | `NULL` | Last successful collection timestamp. |
15
+| `notes` | `TEXT` | no | `''` | Operator notes. |
16
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
17
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
18
+
19
+## Keys And Indexes
20
+
21
+- Primary key: `worker_id`
22
+- Lookup index: `idx_data_workers_type_status` on `(worker_type, status)`
23
+
24
+## Relationships
25
+
26
+Referenced by:
27
+
28
+- `dhcp_leases.worker_id`
29
+- `mdns_observations.worker_id`
30
+
31
+## Seed Rows
32
+
33
+- `dhcp-router`, type `dhcp`
34
+- `mdns-listener`, type `mdns`
35
+
36
+## Definition
37
+
38
+```sql
39
+CREATE TABLE IF NOT EXISTS data_workers (
40
+    worker_id TEXT PRIMARY KEY,
41
+    worker_type TEXT NOT NULL,
42
+    name TEXT NOT NULL DEFAULT '',
43
+    status TEXT NOT NULL DEFAULT 'active',
44
+    source TEXT NOT NULL DEFAULT '',
45
+    last_run_at TEXT,
46
+    notes TEXT NOT NULL DEFAULT '',
47
+    created_at TEXT NOT NULL,
48
+    updated_at TEXT NOT NULL
49
+);
50
+
51
+CREATE INDEX IF NOT EXISTS idx_data_workers_type_status
52
+ON data_workers(worker_type, status);
53
+```
+54 -0
.doc/database/tables/dhcp_leases.md
@@ -0,0 +1,54 @@
1
+# Table: `dhcp_leases`
2
+
3
+Stores observed DHCP lease/reservation data.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `lease_key` | `TEXT` | no | none | Stable lease observation key. Primary key. |
10
+| `worker_id` | `TEXT` | no | none | Collector worker. References `data_workers(worker_id)`. |
11
+| `host_fqdn` | `TEXT` | yes | `NULL` | Matched host if known. References `hosts(fqdn)`. |
12
+| `observed_name` | `TEXT` | no | `''` | Name reported by DHCP. |
13
+| `ip_address` | `TEXT` | no | none | Observed IP address. |
14
+| `mac_address` | `TEXT` | no | `''` | Observed MAC address. |
15
+| `lease_state` | `TEXT` | no | `''` | DHCP state, if known. |
16
+| `first_seen` | `TEXT` | no | none | First observation timestamp. |
17
+| `last_seen` | `TEXT` | no | none | Last observation timestamp. |
18
+| `raw` | `TEXT` | no | `''` | Raw source payload or diagnostic text. |
19
+
20
+## Keys And Indexes
21
+
22
+- Primary key: `lease_key`
23
+- `idx_dhcp_leases_ip` on `ip_address`
24
+- `idx_dhcp_leases_mac` on `mac_address`
25
+- `idx_dhcp_leases_worker_last_seen` on `(worker_id, last_seen)`
26
+
27
+## Relationships
28
+
29
+- `worker_id` references `data_workers(worker_id)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
30
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE SET NULL`
31
+
32
+## Definition
33
+
34
+```sql
35
+CREATE TABLE IF NOT EXISTS dhcp_leases (
36
+    lease_key TEXT PRIMARY KEY,
37
+    worker_id TEXT NOT NULL,
38
+    host_fqdn TEXT,
39
+    observed_name TEXT NOT NULL DEFAULT '',
40
+    ip_address TEXT NOT NULL,
41
+    mac_address TEXT NOT NULL DEFAULT '',
42
+    lease_state TEXT NOT NULL DEFAULT '',
43
+    first_seen TEXT NOT NULL,
44
+    last_seen TEXT NOT NULL,
45
+    raw TEXT NOT NULL DEFAULT '',
46
+    FOREIGN KEY (worker_id) REFERENCES data_workers(worker_id) ON UPDATE CASCADE ON DELETE RESTRICT,
47
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL
48
+);
49
+
50
+CREATE INDEX IF NOT EXISTS idx_dhcp_leases_ip ON dhcp_leases(ip_address);
51
+CREATE INDEX IF NOT EXISTS idx_dhcp_leases_mac ON dhcp_leases(mac_address);
52
+CREATE INDEX IF NOT EXISTS idx_dhcp_leases_worker_last_seen
53
+ON dhcp_leases(worker_id, last_seen);
54
+```
+32 -0
.doc/database/tables/documents.md
@@ -0,0 +1,32 @@
1
+# Table: `documents`
2
+
3
+Legacy document-store table kept only for migration from the first SQLite implementation.
4
+
5
+The application no longer uses this table as source of truth after relational tables are seeded.
6
+
7
+## Columns
8
+
9
+| Column | Type | Null | Default | Notes |
10
+|--------|------|------|---------|-------|
11
+| `name` | `TEXT` | no | none | Legacy document name. Known values: `hosts_yaml`, `work_orders_yaml`. |
12
+| `content` | `TEXT` | no | none | Legacy YAML payload. |
13
+| `updated_at` | `TEXT` | no | none | Legacy document update timestamp. |
14
+
15
+## Keys And Indexes
16
+
17
+- Primary key: `name`
18
+- SQLite creates the internal primary-key index.
19
+
20
+## Relationships
21
+
22
+None. Migration code reads this table and imports the YAML into relational tables when `hosts` or `work_orders` are empty.
23
+
24
+## Definition
25
+
26
+```sql
27
+CREATE TABLE IF NOT EXISTS documents (
28
+    name TEXT PRIMARY KEY,
29
+    content TEXT NOT NULL,
30
+    updated_at TEXT NOT NULL
31
+);
32
+```
+57 -0
.doc/database/tables/host_aliases.md
@@ -0,0 +1,57 @@
1
+# Table: `host_aliases`
2
+
3
+Stores aliases for canonical hosts. Aliases are retained after retirement for audit and collision prevention.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `alias_name` | `TEXT` | no | none | Alias DNS name or short alias. |
10
+| `host_fqdn` | `TEXT` | no | none | Target host. References `hosts(fqdn)`. |
11
+| `alias_kind` | `TEXT` | no | `'declared'` | `declared`, `derived`, or `derived-vhost`. |
12
+| `status` | `TEXT` | no | `'active'` | Alias lifecycle state. |
13
+| `is_dns_published` | `INTEGER` | no | `1` | Whether this alias should appear in generated DNS exports. |
14
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
15
+| `retired_at` | `TEXT` | yes | `NULL` | ISO UTC retirement timestamp. |
16
+| `notes` | `TEXT` | no | `''` | Operator notes. |
17
+
18
+## Keys And Indexes
19
+
20
+- Primary key: `(alias_name, host_fqdn)`
21
+- Unique partial index: `idx_host_aliases_active_name` on `alias_name` where `status = 'active'`
22
+- Lookup index: `idx_host_aliases_host_status` on `(host_fqdn, status)`
23
+
24
+## Relationships
25
+
26
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
27
+
28
+## Rules
29
+
30
+- A retired alias is not deleted; `status` changes to `retired` and `is_dns_published` is set to `0`.
31
+- One active alias can point to only one host.
32
+- Derived short names such as `baobab` are persisted with `alias_kind = 'derived'`.
33
+- Derived vhost short names such as `pmx.baobab` are persisted with `alias_kind = 'derived-vhost'`.
34
+
35
+## Definition
36
+
37
+```sql
38
+CREATE TABLE IF NOT EXISTS host_aliases (
39
+    alias_name TEXT NOT NULL,
40
+    host_fqdn TEXT NOT NULL,
41
+    alias_kind TEXT NOT NULL DEFAULT 'declared',
42
+    status TEXT NOT NULL DEFAULT 'active',
43
+    is_dns_published INTEGER NOT NULL DEFAULT 1,
44
+    created_at TEXT NOT NULL,
45
+    retired_at TEXT,
46
+    notes TEXT NOT NULL DEFAULT '',
47
+    PRIMARY KEY (alias_name, host_fqdn),
48
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT
49
+);
50
+
51
+CREATE UNIQUE INDEX IF NOT EXISTS idx_host_aliases_active_name
52
+ON host_aliases(alias_name)
53
+WHERE status = 'active';
54
+
55
+CREATE INDEX IF NOT EXISTS idx_host_aliases_host_status
56
+ON host_aliases(host_fqdn, status);
57
+```
+35 -0
.doc/database/tables/host_flags.md
@@ -0,0 +1,35 @@
1
+# Table: `host_flags`
2
+
3
+Stores extensible boolean/string flags for hosts without adding one column per flag to `hosts`.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `host_fqdn` | `TEXT` | no | none | Target host. References `hosts(fqdn)`. |
10
+| `flag` | `TEXT` | no | none | Flag name. |
11
+| `value` | `TEXT` | no | `'1'` | Flag value. |
12
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
13
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
14
+
15
+## Keys And Indexes
16
+
17
+- Primary key: `(host_fqdn, flag)`
18
+
19
+## Relationships
20
+
21
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
22
+
23
+## Definition
24
+
25
+```sql
26
+CREATE TABLE IF NOT EXISTS host_flags (
27
+    host_fqdn TEXT NOT NULL,
28
+    flag TEXT NOT NULL,
29
+    value TEXT NOT NULL DEFAULT '1',
30
+    created_at TEXT NOT NULL,
31
+    updated_at TEXT NOT NULL,
32
+    PRIMARY KEY (host_fqdn, flag),
33
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT
34
+);
35
+```
+35 -0
.doc/database/tables/host_roles.md
@@ -0,0 +1,35 @@
1
+# Table: `host_roles`
2
+
3
+Stores host roles separately from `hosts` so the host table does not grow a role-specific column set.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `host_fqdn` | `TEXT` | no | none | Target host. References `hosts(fqdn)`. |
10
+| `role` | `TEXT` | no | none | Role label, for example `proxmox`, `pbs`, `storage`. |
11
+| `status` | `TEXT` | no | `'active'` | Role lifecycle state. |
12
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
13
+| `retired_at` | `TEXT` | yes | `NULL` | ISO UTC retirement timestamp. |
14
+
15
+## Keys And Indexes
16
+
17
+- Primary key: `(host_fqdn, role)`
18
+
19
+## Relationships
20
+
21
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
22
+
23
+## Definition
24
+
25
+```sql
26
+CREATE TABLE IF NOT EXISTS host_roles (
27
+    host_fqdn TEXT NOT NULL,
28
+    role TEXT NOT NULL,
29
+    status TEXT NOT NULL DEFAULT 'active',
30
+    created_at TEXT NOT NULL,
31
+    retired_at TEXT,
32
+    PRIMARY KEY (host_fqdn, role),
33
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT
34
+);
35
+```
+35 -0
.doc/database/tables/host_sources.md
@@ -0,0 +1,35 @@
1
+# Table: `host_sources`
2
+
3
+Stores evidence/source labels for a host.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `host_fqdn` | `TEXT` | no | none | Target host. References `hosts(fqdn)`. |
10
+| `source` | `TEXT` | no | none | Source label, for example `local-hosts.tsv` or `madagascar.json`. |
11
+| `status` | `TEXT` | no | `'active'` | Source lifecycle state. |
12
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
13
+| `retired_at` | `TEXT` | yes | `NULL` | ISO UTC retirement timestamp. |
14
+
15
+## Keys And Indexes
16
+
17
+- Primary key: `(host_fqdn, source)`
18
+
19
+## Relationships
20
+
21
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
22
+
23
+## Definition
24
+
25
+```sql
26
+CREATE TABLE IF NOT EXISTS host_sources (
27
+    host_fqdn TEXT NOT NULL,
28
+    source TEXT NOT NULL,
29
+    status TEXT NOT NULL DEFAULT 'active',
30
+    created_at TEXT NOT NULL,
31
+    retired_at TEXT,
32
+    PRIMARY KEY (host_fqdn, source),
33
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT
34
+);
35
+```
+51 -0
.doc/database/tables/host_ssh.md
@@ -0,0 +1,51 @@
1
+# Table: `host_ssh`
2
+
3
+Stores SSH access profiles separately from `hosts`.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `host_fqdn` | `TEXT` | no | none | Target host. References `hosts(fqdn)`. |
10
+| `profile_name` | `TEXT` | no | `'default'` | SSH profile name. |
11
+| `username` | `TEXT` | no | `''` | SSH username. |
12
+| `port` | `INTEGER` | no | `22` | SSH port. |
13
+| `identity_file` | `TEXT` | no | `''` | SSH identity path or key label. |
14
+| `address` | `TEXT` | no | `''` | Optional override address. |
15
+| `local_forward_host` | `TEXT` | no | `''` | Optional local-forward host. |
16
+| `local_forward_port` | `INTEGER` | yes | `NULL` | Optional local-forward port. |
17
+| `remote_forward_host` | `TEXT` | no | `''` | Optional remote-forward host. |
18
+| `remote_forward_port` | `INTEGER` | yes | `NULL` | Optional remote-forward port. |
19
+| `notes` | `TEXT` | no | `''` | Operator notes. |
20
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
21
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
22
+
23
+## Keys And Indexes
24
+
25
+- Primary key: `(host_fqdn, profile_name)`
26
+
27
+## Relationships
28
+
29
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
30
+
31
+## Definition
32
+
33
+```sql
34
+CREATE TABLE IF NOT EXISTS host_ssh (
35
+    host_fqdn TEXT NOT NULL,
36
+    profile_name TEXT NOT NULL DEFAULT 'default',
37
+    username TEXT NOT NULL DEFAULT '',
38
+    port INTEGER NOT NULL DEFAULT 22,
39
+    identity_file TEXT NOT NULL DEFAULT '',
40
+    address TEXT NOT NULL DEFAULT '',
41
+    local_forward_host TEXT NOT NULL DEFAULT '',
42
+    local_forward_port INTEGER,
43
+    remote_forward_host TEXT NOT NULL DEFAULT '',
44
+    remote_forward_port INTEGER,
45
+    notes TEXT NOT NULL DEFAULT '',
46
+    created_at TEXT NOT NULL,
47
+    updated_at TEXT NOT NULL,
48
+    PRIMARY KEY (host_fqdn, profile_name),
49
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT
50
+);
51
+```
+55 -0
.doc/database/tables/hosts.md
@@ -0,0 +1,55 @@
1
+# Table: `hosts`
2
+
3
+Canonical host registry. Hosts are identified by full DNS name in `fqdn`.
4
+
5
+`legacy_id` exists for compatibility with the current UI/API, but it is not the canonical identity.
6
+
7
+## Columns
8
+
9
+| Column | Type | Null | Default | Notes |
10
+|--------|------|------|---------|-------|
11
+| `fqdn` | `TEXT` | no | none | Canonical full host name. Primary key. |
12
+| `legacy_id` | `TEXT` | no | none | Short ID used by the existing UI/API. Unique. |
13
+| `status` | `TEXT` | no | `'active'` | Host lifecycle state, currently `active`, `planned`, or `retired`. |
14
+| `hosts_ip` | `TEXT` | no | `''` | IP used for `/etc/hosts` on jumper. |
15
+| `dns_ip` | `TEXT` | no | `''` | IP published to clients through local DNS. |
16
+| `monitoring` | `TEXT` | no | `'pending'` | Monitoring state. |
17
+| `notes` | `TEXT` | no | `''` | Operator notes. |
18
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
19
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
20
+
21
+## Keys And Indexes
22
+
23
+- Primary key: `fqdn`
24
+- Unique key: `legacy_id`
25
+
26
+## Relationships
27
+
28
+Referenced by:
29
+
30
+- `host_aliases.host_fqdn`
31
+- `host_roles.host_fqdn`
32
+- `host_sources.host_fqdn`
33
+- `host_flags.host_fqdn`
34
+- `host_ssh.host_fqdn`
35
+- `vhosts.host_fqdn`
36
+- `dhcp_leases.host_fqdn`
37
+- `mdns_observations.host_fqdn`
38
+- `certificates.host_fqdn`
39
+- `work_order_actions.host_fqdn`
40
+
41
+## Definition
42
+
43
+```sql
44
+CREATE TABLE IF NOT EXISTS hosts (
45
+    fqdn TEXT PRIMARY KEY,
46
+    legacy_id TEXT NOT NULL UNIQUE,
47
+    status TEXT NOT NULL DEFAULT 'active',
48
+    hosts_ip TEXT NOT NULL DEFAULT '',
49
+    dns_ip TEXT NOT NULL DEFAULT '',
50
+    monitoring TEXT NOT NULL DEFAULT 'pending',
51
+    notes TEXT NOT NULL DEFAULT '',
52
+    created_at TEXT NOT NULL,
53
+    updated_at TEXT NOT NULL
54
+);
55
+```
+58 -0
.doc/database/tables/mdns_observations.md
@@ -0,0 +1,58 @@
1
+# Table: `mdns_observations`
2
+
3
+Stores observed mDNS records.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `observation_key` | `TEXT` | no | none | Stable observation key. Primary key. |
10
+| `worker_id` | `TEXT` | no | none | Collector worker. References `data_workers(worker_id)`. |
11
+| `host_fqdn` | `TEXT` | yes | `NULL` | Matched host if known. References `hosts(fqdn)`. |
12
+| `observed_name` | `TEXT` | no | none | Observed mDNS name. |
13
+| `ip_address` | `TEXT` | no | none | Observed IP address. |
14
+| `rr_type` | `TEXT` | no | `'A'` | DNS record type. |
15
+| `ttl` | `INTEGER` | no | `0` | Observed TTL. |
16
+| `first_seen` | `TEXT` | no | none | First observation timestamp. |
17
+| `last_seen` | `TEXT` | no | none | Last observation timestamp. |
18
+| `seen_count` | `INTEGER` | no | `1` | Number of times observed. |
19
+| `last_peer` | `TEXT` | no | `''` | Last sender peer. |
20
+| `raw` | `TEXT` | no | `''` | Raw source payload or diagnostic text. |
21
+
22
+## Keys And Indexes
23
+
24
+- Primary key: `observation_key`
25
+- `idx_mdns_observations_name` on `observed_name`
26
+- `idx_mdns_observations_ip` on `ip_address`
27
+- `idx_mdns_observations_worker_last_seen` on `(worker_id, last_seen)`
28
+
29
+## Relationships
30
+
31
+- `worker_id` references `data_workers(worker_id)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
32
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE SET NULL`
33
+
34
+## Definition
35
+
36
+```sql
37
+CREATE TABLE IF NOT EXISTS mdns_observations (
38
+    observation_key TEXT PRIMARY KEY,
39
+    worker_id TEXT NOT NULL,
40
+    host_fqdn TEXT,
41
+    observed_name TEXT NOT NULL,
42
+    ip_address TEXT NOT NULL,
43
+    rr_type TEXT NOT NULL DEFAULT 'A',
44
+    ttl INTEGER NOT NULL DEFAULT 0,
45
+    first_seen TEXT NOT NULL,
46
+    last_seen TEXT NOT NULL,
47
+    seen_count INTEGER NOT NULL DEFAULT 1,
48
+    last_peer TEXT NOT NULL DEFAULT '',
49
+    raw TEXT NOT NULL DEFAULT '',
50
+    FOREIGN KEY (worker_id) REFERENCES data_workers(worker_id) ON UPDATE CASCADE ON DELETE RESTRICT,
51
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL
52
+);
53
+
54
+CREATE INDEX IF NOT EXISTS idx_mdns_observations_name ON mdns_observations(observed_name);
55
+CREATE INDEX IF NOT EXISTS idx_mdns_observations_ip ON mdns_observations(ip_address);
56
+CREATE INDEX IF NOT EXISTS idx_mdns_observations_worker_last_seen
57
+ON mdns_observations(worker_id, last_seen);
58
+```
+37 -0
.doc/database/tables/schema_meta.md
@@ -0,0 +1,37 @@
1
+# Table: `schema_meta`
2
+
3
+Stores schema/runtime metadata as key-value rows.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `key` | `TEXT` | no | none | Metadata key. |
10
+| `value` | `TEXT` | no | none | Metadata value. |
11
+| `updated_at` | `TEXT` | no | none | ISO UTC timestamp written by the application. |
12
+
13
+## Keys And Indexes
14
+
15
+- Primary key: `key`
16
+- SQLite creates the internal primary-key index.
17
+
18
+## Known Keys
19
+
20
+| Key | Meaning |
21
+|-----|---------|
22
+| `schema_version` | Current relational schema version. Current value: `2`. |
23
+| `registry_updated_at` | Last registry update timestamp used for API/export metadata. |
24
+
25
+## Relationships
26
+
27
+None.
28
+
29
+## Definition
30
+
31
+```sql
32
+CREATE TABLE IF NOT EXISTS schema_meta (
33
+    key TEXT PRIMARY KEY,
34
+    value TEXT NOT NULL,
35
+    updated_at TEXT NOT NULL
36
+);
37
+```
+55 -0
.doc/database/tables/vhosts.md
@@ -0,0 +1,55 @@
1
+# Table: `vhosts`
2
+
3
+Stores virtual hosts separately from physical/logical hosts so a vhost can be moved between hosts.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `vhost_fqdn` | `TEXT` | no | none | Vhost DNS name. Primary key. |
10
+| `host_fqdn` | `TEXT` | no | none | Current host serving the vhost. References `hosts(fqdn)`. |
11
+| `status` | `TEXT` | no | `'active'` | Vhost lifecycle state. |
12
+| `service_name` | `TEXT` | no | `''` | Service label, often inferred from first DNS label. |
13
+| `upstream_url` | `TEXT` | no | `''` | Optional upstream URL. |
14
+| `tls_mode` | `TEXT` | no | `'local-ca'` | TLS handling mode. |
15
+| `certificate_id` | `TEXT` | yes | `NULL` | Optional issued certificate. References `certificates(certificate_id)`. |
16
+| `notes` | `TEXT` | no | `''` | Operator notes. |
17
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
18
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
19
+
20
+## Keys And Indexes
21
+
22
+- Primary key: `vhost_fqdn`
23
+- Lookup index: `idx_vhosts_host_status` on `(host_fqdn, status)`
24
+
25
+## Relationships
26
+
27
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE RESTRICT`
28
+- `certificate_id` references `certificates(certificate_id)` with `ON UPDATE CASCADE ON DELETE SET NULL`
29
+
30
+## Rules
31
+
32
+- Moving a vhost means updating `host_fqdn`.
33
+- Retiring a vhost means setting `status = 'retired'`; the row remains.
34
+
35
+## Definition
36
+
37
+```sql
38
+CREATE TABLE IF NOT EXISTS vhosts (
39
+    vhost_fqdn TEXT PRIMARY KEY,
40
+    host_fqdn TEXT NOT NULL,
41
+    status TEXT NOT NULL DEFAULT 'active',
42
+    service_name TEXT NOT NULL DEFAULT '',
43
+    upstream_url TEXT NOT NULL DEFAULT '',
44
+    tls_mode TEXT NOT NULL DEFAULT 'local-ca',
45
+    certificate_id TEXT,
46
+    notes TEXT NOT NULL DEFAULT '',
47
+    created_at TEXT NOT NULL,
48
+    updated_at TEXT NOT NULL,
49
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT,
50
+    FOREIGN KEY (certificate_id) REFERENCES certificates(certificate_id) ON UPDATE CASCADE ON DELETE SET NULL
51
+);
52
+
53
+CREATE INDEX IF NOT EXISTS idx_vhosts_host_status
54
+ON vhosts(host_fqdn, status);
55
+```
+41 -0
.doc/database/tables/work_order_actions.md
@@ -0,0 +1,41 @@
1
+# Table: `work_order_actions`
2
+
3
+Stores ordered actions attached to Work Orders.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `work_order_id` | `TEXT` | no | none | Parent Work Order. References `work_orders(id)`. |
10
+| `position` | `INTEGER` | no | none | Ordered action position. |
11
+| `type` | `TEXT` | no | none | Action type, for example `remove_name`. |
12
+| `host_fqdn` | `TEXT` | yes | `NULL` | Target host if resolved. References `hosts(fqdn)`. |
13
+| `host_legacy_id` | `TEXT` | no | `''` | Compatibility target ID from old Work Order format. |
14
+| `name` | `TEXT` | no | `''` | Target name for action. |
15
+| `payload` | `TEXT` | no | `''` | Reserved structured payload field. |
16
+
17
+## Keys And Indexes
18
+
19
+- Primary key: `(work_order_id, position)`
20
+
21
+## Relationships
22
+
23
+- `work_order_id` references `work_orders(id)` with `ON UPDATE CASCADE ON DELETE CASCADE`
24
+- `host_fqdn` references `hosts(fqdn)` with `ON UPDATE CASCADE ON DELETE SET NULL`
25
+
26
+## Definition
27
+
28
+```sql
29
+CREATE TABLE IF NOT EXISTS work_order_actions (
30
+    work_order_id TEXT NOT NULL,
31
+    position INTEGER NOT NULL,
32
+    type TEXT NOT NULL,
33
+    host_fqdn TEXT,
34
+    host_legacy_id TEXT NOT NULL DEFAULT '',
35
+    name TEXT NOT NULL DEFAULT '',
36
+    payload TEXT NOT NULL DEFAULT '',
37
+    PRIMARY KEY (work_order_id, position),
38
+    FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON UPDATE CASCADE ON DELETE CASCADE,
39
+    FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL
40
+);
41
+```
+39 -0
.doc/database/tables/work_order_checklist.md
@@ -0,0 +1,39 @@
1
+# Table: `work_order_checklist`
2
+
3
+Stores checklist items for Work Orders.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `work_order_id` | `TEXT` | no | none | Parent Work Order. References `work_orders(id)`. |
10
+| `item_id` | `TEXT` | no | none | Checklist item ID. |
11
+| `text` | `TEXT` | no | `''` | Checklist item text. |
12
+| `status` | `TEXT` | no | `'pending'` | Item state. |
13
+| `owner` | `TEXT` | no | `''` | Optional owner. |
14
+| `notes` | `TEXT` | no | `''` | Operator notes. |
15
+| `updated_at` | `TEXT` | no | `''` | Item update timestamp. |
16
+
17
+## Keys And Indexes
18
+
19
+- Primary key: `(work_order_id, item_id)`
20
+
21
+## Relationships
22
+
23
+- `work_order_id` references `work_orders(id)` with `ON UPDATE CASCADE ON DELETE CASCADE`
24
+
25
+## Definition
26
+
27
+```sql
28
+CREATE TABLE IF NOT EXISTS work_order_checklist (
29
+    work_order_id TEXT NOT NULL,
30
+    item_id TEXT NOT NULL,
31
+    text TEXT NOT NULL DEFAULT '',
32
+    status TEXT NOT NULL DEFAULT 'pending',
33
+    owner TEXT NOT NULL DEFAULT '',
34
+    notes TEXT NOT NULL DEFAULT '',
35
+    updated_at TEXT NOT NULL DEFAULT '',
36
+    PRIMARY KEY (work_order_id, item_id),
37
+    FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON UPDATE CASCADE ON DELETE CASCADE
38
+);
39
+```
+42 -0
.doc/database/tables/work_orders.md
@@ -0,0 +1,42 @@
1
+# Table: `work_orders`
2
+
3
+Stores Work Order headers.
4
+
5
+## Columns
6
+
7
+| Column | Type | Null | Default | Notes |
8
+|--------|------|------|---------|-------|
9
+| `id` | `TEXT` | no | none | Work Order ID. Primary key. |
10
+| `status` | `TEXT` | no | `'pending'` | Work Order state. |
11
+| `title` | `TEXT` | no | `''` | Title. |
12
+| `reason` | `TEXT` | no | `''` | Reason/context. |
13
+| `created_at` | `TEXT` | no | none | ISO UTC creation timestamp. |
14
+| `confirmed_at` | `TEXT` | no | `''` | Confirmation timestamp, empty until confirmed. |
15
+| `result` | `TEXT` | no | `''` | Result summary. |
16
+| `updated_at` | `TEXT` | no | none | ISO UTC update timestamp. |
17
+
18
+## Keys And Indexes
19
+
20
+- Primary key: `id`
21
+
22
+## Relationships
23
+
24
+Referenced by:
25
+
26
+- `work_order_checklist.work_order_id`
27
+- `work_order_actions.work_order_id`
28
+
29
+## Definition
30
+
31
+```sql
32
+CREATE TABLE IF NOT EXISTS work_orders (
33
+    id TEXT PRIMARY KEY,
34
+    status TEXT NOT NULL DEFAULT 'pending',
35
+    title TEXT NOT NULL DEFAULT '',
36
+    reason TEXT NOT NULL DEFAULT '',
37
+    created_at TEXT NOT NULL,
38
+    confirmed_at TEXT NOT NULL DEFAULT '',
39
+    result TEXT NOT NULL DEFAULT '',
40
+    updated_at TEXT NOT NULL
41
+);
42
+```
+1 -1
README.md
@@ -41,7 +41,7 @@ The web UI is OTP-protected for all registry data, downloads, exports, and write
41 41
 For agent/operator context, see:
42 42
 
43 43
 - [`agents.md`](agents.md)
44
-- [`.doc/database.md`](.doc/database.md)
44
+- [`.doc/database/`](.doc/database/README.md)
45 45
 - [`.doc/development-log.md`](.doc/development-log.md)
46 46
 - [`.doc/host-manager.md`](.doc/host-manager.md)
47 47
 - [`.doc/local-hosts.md`](.doc/local-hosts.md)
+1 -1
agents.md
@@ -5,7 +5,7 @@ Madagascar Local Authority is the local authority application for the Madagascar
5 5
 Start with these documents:
6 6
 
7 7
 - [README.md](README.md) - current repository, deployment model, runtime paths, GitPrep remote.
8
-- [.doc/database.md](.doc/database.md) - SQLite runtime store schema, seed rules, backup and restore.
8
+- [.doc/database/](.doc/database/README.md) - SQLite runtime store schema, table docs, seed rules, backup and restore.
9 9
 - [.doc/host-manager.md](.doc/host-manager.md) - application behavior, OTP, Work Orders, local CA, registry rules.
10 10
 - [.doc/local-hosts.md](.doc/local-hosts.md) - local DNS rules, resolver sync, source priority.
11 11
 - [.doc/development-log.md](.doc/development-log.md) - scope and architecture decisions over time.