Rick commited on
Commit
b4e9a3a
·
1 Parent(s): 459a547

Add DB write-lock setting with HF-friendly runtime controls

Browse files
Files changed (4) hide show
  1. app.py +36 -0
  2. db_store.py +33 -6
  3. static/js/settings.js +14 -2
  4. templates/index.html +7 -0
app.py CHANGED
@@ -111,6 +111,8 @@ from db_store import (
111
  set_context_payload,
112
  update_item_verified,
113
  upsert_inventory_item,
 
 
114
  )
115
 
116
  logger = logging.getLogger("uvicorn.error")
@@ -327,6 +329,8 @@ PREVIOUS_DATA_ROOT_DB = DATA_ROOT / "app.db"
327
  SEED_DB_LOCAL = APP_HOME / "seed" / "app.db"
328
  # Remote seeding disabled by default to avoid unintended downloads; set SEED_DB_URL to enable.
329
  SEED_DB_URL = os.environ.get("SEED_DB_URL") or None
 
 
330
 
331
 
332
  def _is_valid_sqlite(path: Path) -> bool:
@@ -418,6 +422,30 @@ def _bootstrap_db(force: bool = False):
418
  _bootstrap_db()
419
  configure_db(DB_PATH)
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  DEFAULT_store_LABEL = "Default"
422
  DEFAULT_store = None
423
 
@@ -1121,6 +1149,8 @@ def get_defaults():
1121
  "rep_penalty": 1.1,
1122
  "mission_context": "Isolated Medical Station offshore.",
1123
  "user_mode": "user",
 
 
1124
  "last_prompt_verbatim": "",
1125
  "vaccine_types": [
1126
  "Diphtheria, Tetanus, and Pertussis (DTaP/Tdap)",
@@ -1260,7 +1290,9 @@ def db_op(cat, data=None, store=None):
1260
  set_settings_meta(
1261
  user_mode=data.get("user_mode"),
1262
  offline_force_flags=data.get("offline_force_flags"),
 
1263
  )
 
1264
  return {**get_defaults(), **data}
1265
  if cat == "inventory":
1266
  if not isinstance(data, list):
@@ -2526,6 +2558,8 @@ async def db_status():
2526
  "stores": 1,
2527
  "crew_rows": crew,
2528
  "vessel_rows": vessel,
 
 
2529
  }
2530
  except Exception as e:
2531
  return JSONResponse({"error": str(e)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
@@ -2550,6 +2584,7 @@ async def db_create():
2550
  if DB_PATH.exists():
2551
  DB_PATH.unlink()
2552
  configure_db(DB_PATH)
 
2553
  _store_dirs(DEFAULT_store_LABEL)
2554
  return {"status": "created"}
2555
  except Exception as e:
@@ -2580,6 +2615,7 @@ async def db_upload(file: UploadFile = File(...)):
2580
  tmp.close()
2581
  shutil.move(tmp.name, DB_PATH)
2582
  configure_db(DB_PATH)
 
2583
  _store_dirs(DEFAULT_store_LABEL)
2584
  return {"status": "uploaded"}
2585
  except Exception as e:
 
111
  set_context_payload,
112
  update_item_verified,
113
  upsert_inventory_item,
114
+ set_db_write_lock,
115
+ get_db_write_lock,
116
  )
117
 
118
  logger = logging.getLogger("uvicorn.error")
 
329
  SEED_DB_LOCAL = APP_HOME / "seed" / "app.db"
330
  # Remote seeding disabled by default to avoid unintended downloads; set SEED_DB_URL to enable.
331
  SEED_DB_URL = os.environ.get("SEED_DB_URL") or None
332
+ _DB_WRITE_LOCK_ENV = (os.environ.get("DB_WRITE_LOCK") or "").strip().lower()
333
+ DB_WRITE_LOCK_FORCED = _DB_WRITE_LOCK_ENV in {"1", "true", "yes", "on"} if _DB_WRITE_LOCK_ENV else None
334
 
335
 
336
  def _is_valid_sqlite(path: Path) -> bool:
 
422
  _bootstrap_db()
423
  configure_db(DB_PATH)
424
 
425
+
426
+ def _apply_db_write_lock_setting(candidate=None):
427
+ """
428
+ Apply DB write-lock policy.
429
+
430
+ Priority:
431
+ 1) Environment override DB_WRITE_LOCK (if set)
432
+ 2) Persisted settings_meta.db_write_lock
433
+ """
434
+ if DB_WRITE_LOCK_FORCED is not None:
435
+ set_db_write_lock(DB_WRITE_LOCK_FORCED)
436
+ return DB_WRITE_LOCK_FORCED
437
+ if candidate is None:
438
+ try:
439
+ meta = get_settings_meta() or {}
440
+ candidate = bool(meta.get("db_write_lock"))
441
+ except Exception:
442
+ candidate = False
443
+ set_db_write_lock(bool(candidate))
444
+ return bool(candidate)
445
+
446
+
447
+ _apply_db_write_lock_setting()
448
+
449
  DEFAULT_store_LABEL = "Default"
450
  DEFAULT_store = None
451
 
 
1149
  "rep_penalty": 1.1,
1150
  "mission_context": "Isolated Medical Station offshore.",
1151
  "user_mode": "user",
1152
+ "db_write_lock": bool(get_db_write_lock()),
1153
+ "db_write_lock_forced": DB_WRITE_LOCK_FORCED is not None,
1154
  "last_prompt_verbatim": "",
1155
  "vaccine_types": [
1156
  "Diphtheria, Tetanus, and Pertussis (DTaP/Tdap)",
 
1290
  set_settings_meta(
1291
  user_mode=data.get("user_mode"),
1292
  offline_force_flags=data.get("offline_force_flags"),
1293
+ db_write_lock=data.get("db_write_lock"),
1294
  )
1295
+ _apply_db_write_lock_setting(data.get("db_write_lock"))
1296
  return {**get_defaults(), **data}
1297
  if cat == "inventory":
1298
  if not isinstance(data, list):
 
2558
  "stores": 1,
2559
  "crew_rows": crew,
2560
  "vessel_rows": vessel,
2561
+ "db_write_lock": bool(get_db_write_lock()),
2562
+ "db_write_lock_forced": DB_WRITE_LOCK_FORCED is not None,
2563
  }
2564
  except Exception as e:
2565
  return JSONResponse({"error": str(e)}, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
2584
  if DB_PATH.exists():
2585
  DB_PATH.unlink()
2586
  configure_db(DB_PATH)
2587
+ _apply_db_write_lock_setting()
2588
  _store_dirs(DEFAULT_store_LABEL)
2589
  return {"status": "created"}
2590
  except Exception as e:
 
2615
  tmp.close()
2616
  shutil.move(tmp.name, DB_PATH)
2617
  configure_db(DB_PATH)
2618
+ _apply_db_write_lock_setting()
2619
  _store_dirs(DEFAULT_store_LABEL)
2620
  return {"status": "uploaded"}
2621
  except Exception as e:
db_store.py CHANGED
@@ -26,6 +26,7 @@ from typing import Optional, Any, Dict
26
  logger = logging.getLogger("uvicorn.error")
27
 
28
  DB_PATH: Path
 
29
  TRIAGE_TREE_DEFAULT_JSON_PATH = Path(__file__).resolve().parent / "seed" / "triage_prompt_tree.default.json"
30
 
31
 
@@ -39,6 +40,17 @@ def configure_db(path: Path):
39
  _upgrade_schema()
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
42
  def _conn():
43
  """
44
  Conn helper.
@@ -50,6 +62,10 @@ def _conn():
50
  conn.execute("PRAGMA foreign_keys = ON;")
51
  # Keep temp tables in memory to avoid filesystem issues when sorting large BLOB rows
52
  conn.execute("PRAGMA temp_store = MEMORY;")
 
 
 
 
53
  except Exception:
54
  pass
55
  return conn
@@ -242,6 +258,7 @@ def _init_db():
242
  id INTEGER PRIMARY KEY CHECK (id = 1),
243
  user_mode TEXT,
244
  offline_force_flags INTEGER DEFAULT 0,
 
245
  last_prompt_verbatim TEXT,
246
  updated_at TEXT NOT NULL
247
  );
@@ -857,21 +874,24 @@ def _maybe_migrate_settings_meta(conn, now):
857
  data = {}
858
  user_mode = data.get("user_mode")
859
  offline_force_flags = 1 if data.get("offline_force_flags") else 0
 
860
  last_prompt_verbatim = data.get("last_prompt_verbatim")
861
  _ensure_settings_meta_columns(conn)
862
  conn.execute(
863
  """
864
- INSERT INTO settings_meta(id, user_mode, offline_force_flags, last_prompt_verbatim, updated_at)
865
- VALUES(1, :user_mode, :offline_force_flags, :last_prompt_verbatim, :updated_at)
866
  ON CONFLICT(id) DO UPDATE SET
867
  user_mode=excluded.user_mode,
868
  offline_force_flags=excluded.offline_force_flags,
 
869
  last_prompt_verbatim=excluded.last_prompt_verbatim,
870
  updated_at=excluded.updated_at;
871
  """,
872
  {
873
  "user_mode": user_mode,
874
  "offline_force_flags": offline_force_flags,
 
875
  "last_prompt_verbatim": last_prompt_verbatim,
876
  "updated_at": now,
877
  },
@@ -2215,6 +2235,8 @@ def _ensure_settings_meta_columns(conn):
2215
  names = {c["name"] for c in cols}
2216
  if "last_prompt_verbatim" not in names:
2217
  conn.execute("ALTER TABLE settings_meta ADD COLUMN last_prompt_verbatim TEXT;")
 
 
2218
  except Exception as exc:
2219
  logger.warning("Unable to add settings_meta columns: %s", exc)
2220
 
@@ -3781,13 +3803,14 @@ def get_settings_meta():
3781
  with _conn() as conn:
3782
  _ensure_settings_meta_columns(conn)
3783
  row = conn.execute(
3784
- "SELECT user_mode, offline_force_flags, last_prompt_verbatim FROM settings_meta WHERE id=1"
3785
  ).fetchone()
3786
  if not row:
3787
  return {}
3788
  return {
3789
  "user_mode": row["user_mode"],
3790
  "offline_force_flags": bool(row["offline_force_flags"]),
 
3791
  "last_prompt_verbatim": row["last_prompt_verbatim"],
3792
  }
3793
 
@@ -3795,6 +3818,7 @@ def get_settings_meta():
3795
  def set_settings_meta(
3796
  user_mode: str = None,
3797
  offline_force_flags: bool = None,
 
3798
  last_prompt_verbatim: str = None,
3799
  ):
3800
  """
@@ -3805,17 +3829,18 @@ def set_settings_meta(
3805
  with _conn() as conn:
3806
  _ensure_settings_meta_columns(conn)
3807
  existing = conn.execute(
3808
- "SELECT user_mode, offline_force_flags, last_prompt_verbatim FROM settings_meta WHERE id=1"
3809
  ).fetchone()
3810
  if existing is None:
3811
  conn.execute(
3812
  """
3813
- INSERT INTO settings_meta(id, user_mode, offline_force_flags, last_prompt_verbatim, updated_at)
3814
- VALUES(1, :user_mode, :offline_force_flags, :last_prompt_verbatim, :updated_at)
3815
  """,
3816
  {
3817
  "user_mode": user_mode,
3818
  "offline_force_flags": 1 if offline_force_flags else 0,
 
3819
  "last_prompt_verbatim": last_prompt_verbatim,
3820
  "updated_at": now,
3821
  },
@@ -3826,6 +3851,7 @@ def set_settings_meta(
3826
  UPDATE settings_meta
3827
  SET user_mode=COALESCE(:user_mode, user_mode),
3828
  offline_force_flags=COALESCE(:offline_force_flags, offline_force_flags),
 
3829
  last_prompt_verbatim=COALESCE(:last_prompt_verbatim, last_prompt_verbatim),
3830
  updated_at=:updated_at
3831
  WHERE id=1
@@ -3833,6 +3859,7 @@ def set_settings_meta(
3833
  {
3834
  "user_mode": user_mode,
3835
  "offline_force_flags": None if offline_force_flags is None else (1 if offline_force_flags else 0),
 
3836
  "last_prompt_verbatim": last_prompt_verbatim,
3837
  "updated_at": now,
3838
  },
 
26
  logger = logging.getLogger("uvicorn.error")
27
 
28
  DB_PATH: Path
29
+ DB_WRITE_LOCK = False
30
  TRIAGE_TREE_DEFAULT_JSON_PATH = Path(__file__).resolve().parent / "seed" / "triage_prompt_tree.default.json"
31
 
32
 
 
40
  _upgrade_schema()
41
 
42
 
43
+ def set_db_write_lock(enabled: bool):
44
+ """Set database query-only mode for all future connections."""
45
+ global DB_WRITE_LOCK
46
+ DB_WRITE_LOCK = bool(enabled)
47
+
48
+
49
+ def get_db_write_lock() -> bool:
50
+ """Return whether DB write lock (query-only mode) is enabled."""
51
+ return bool(DB_WRITE_LOCK)
52
+
53
+
54
  def _conn():
55
  """
56
  Conn helper.
 
62
  conn.execute("PRAGMA foreign_keys = ON;")
63
  # Keep temp tables in memory to avoid filesystem issues when sorting large BLOB rows
64
  conn.execute("PRAGMA temp_store = MEMORY;")
65
+ if DB_WRITE_LOCK:
66
+ conn.execute("PRAGMA query_only = ON;")
67
+ else:
68
+ conn.execute("PRAGMA query_only = OFF;")
69
  except Exception:
70
  pass
71
  return conn
 
258
  id INTEGER PRIMARY KEY CHECK (id = 1),
259
  user_mode TEXT,
260
  offline_force_flags INTEGER DEFAULT 0,
261
+ db_write_lock INTEGER DEFAULT 0,
262
  last_prompt_verbatim TEXT,
263
  updated_at TEXT NOT NULL
264
  );
 
874
  data = {}
875
  user_mode = data.get("user_mode")
876
  offline_force_flags = 1 if data.get("offline_force_flags") else 0
877
+ db_write_lock = 1 if data.get("db_write_lock") else 0
878
  last_prompt_verbatim = data.get("last_prompt_verbatim")
879
  _ensure_settings_meta_columns(conn)
880
  conn.execute(
881
  """
882
+ INSERT INTO settings_meta(id, user_mode, offline_force_flags, db_write_lock, last_prompt_verbatim, updated_at)
883
+ VALUES(1, :user_mode, :offline_force_flags, :db_write_lock, :last_prompt_verbatim, :updated_at)
884
  ON CONFLICT(id) DO UPDATE SET
885
  user_mode=excluded.user_mode,
886
  offline_force_flags=excluded.offline_force_flags,
887
+ db_write_lock=excluded.db_write_lock,
888
  last_prompt_verbatim=excluded.last_prompt_verbatim,
889
  updated_at=excluded.updated_at;
890
  """,
891
  {
892
  "user_mode": user_mode,
893
  "offline_force_flags": offline_force_flags,
894
+ "db_write_lock": db_write_lock,
895
  "last_prompt_verbatim": last_prompt_verbatim,
896
  "updated_at": now,
897
  },
 
2235
  names = {c["name"] for c in cols}
2236
  if "last_prompt_verbatim" not in names:
2237
  conn.execute("ALTER TABLE settings_meta ADD COLUMN last_prompt_verbatim TEXT;")
2238
+ if "db_write_lock" not in names:
2239
+ conn.execute("ALTER TABLE settings_meta ADD COLUMN db_write_lock INTEGER DEFAULT 0;")
2240
  except Exception as exc:
2241
  logger.warning("Unable to add settings_meta columns: %s", exc)
2242
 
 
3803
  with _conn() as conn:
3804
  _ensure_settings_meta_columns(conn)
3805
  row = conn.execute(
3806
+ "SELECT user_mode, offline_force_flags, db_write_lock, last_prompt_verbatim FROM settings_meta WHERE id=1"
3807
  ).fetchone()
3808
  if not row:
3809
  return {}
3810
  return {
3811
  "user_mode": row["user_mode"],
3812
  "offline_force_flags": bool(row["offline_force_flags"]),
3813
+ "db_write_lock": bool(row["db_write_lock"]),
3814
  "last_prompt_verbatim": row["last_prompt_verbatim"],
3815
  }
3816
 
 
3818
  def set_settings_meta(
3819
  user_mode: str = None,
3820
  offline_force_flags: bool = None,
3821
+ db_write_lock: bool = None,
3822
  last_prompt_verbatim: str = None,
3823
  ):
3824
  """
 
3829
  with _conn() as conn:
3830
  _ensure_settings_meta_columns(conn)
3831
  existing = conn.execute(
3832
+ "SELECT user_mode, offline_force_flags, db_write_lock, last_prompt_verbatim FROM settings_meta WHERE id=1"
3833
  ).fetchone()
3834
  if existing is None:
3835
  conn.execute(
3836
  """
3837
+ INSERT INTO settings_meta(id, user_mode, offline_force_flags, db_write_lock, last_prompt_verbatim, updated_at)
3838
+ VALUES(1, :user_mode, :offline_force_flags, :db_write_lock, :last_prompt_verbatim, :updated_at)
3839
  """,
3840
  {
3841
  "user_mode": user_mode,
3842
  "offline_force_flags": 1 if offline_force_flags else 0,
3843
+ "db_write_lock": 1 if db_write_lock else 0,
3844
  "last_prompt_verbatim": last_prompt_verbatim,
3845
  "updated_at": now,
3846
  },
 
3851
  UPDATE settings_meta
3852
  SET user_mode=COALESCE(:user_mode, user_mode),
3853
  offline_force_flags=COALESCE(:offline_force_flags, offline_force_flags),
3854
+ db_write_lock=COALESCE(:db_write_lock, db_write_lock),
3855
  last_prompt_verbatim=COALESCE(:last_prompt_verbatim, last_prompt_verbatim),
3856
  updated_at=:updated_at
3857
  WHERE id=1
 
3859
  {
3860
  "user_mode": user_mode,
3861
  "offline_force_flags": None if offline_force_flags is None else (1 if offline_force_flags else 0),
3862
+ "db_write_lock": None if db_write_lock is None else (1 if db_write_lock else 0),
3863
  "last_prompt_verbatim": last_prompt_verbatim,
3864
  "updated_at": now,
3865
  },
static/js/settings.js CHANGED
@@ -113,7 +113,9 @@ const DEFAULT_SETTINGS = {
113
  pharmacy_labels: ["Antibiotic", "Analgesic", "Cardiac", "Respiratory", "Gastrointestinal", "Endocrine", "Emergency"],
114
  equipment_categories: [],
115
  consumable_categories: [],
116
- offline_force_flags: false
 
 
117
  };
118
 
119
  // ============================================================================
@@ -349,6 +351,16 @@ function applySettingsToUI(data = {}) {
349
  }
350
  }
351
  });
 
 
 
 
 
 
 
 
 
 
352
  setUserMode(merged.user_mode);
353
  try { localStorage.setItem('user_mode', merged.user_mode || 'user'); } catch (err) { /* ignore */ }
354
  window.CACHED_SETTINGS = merged;
@@ -1575,7 +1587,7 @@ async function saveSettings(showAlert = true, reason = 'manual') {
1575
  try {
1576
  const s = {};
1577
  const numeric = new Set(['tr_temp','tr_tok','tr_p','tr_k','in_temp','in_tok','in_p','in_k','rep_penalty']);
1578
- ['triage_instruction','inquiry_instruction','tr_temp','tr_tok','tr_p','tr_k','in_temp','in_tok','in_p','in_k','mission_context','rep_penalty','user_mode'].forEach(k => {
1579
  const el = document.getElementById(k);
1580
  if (!el) return;
1581
  const val = el.type === 'checkbox' ? el.checked : el.value;
 
113
  pharmacy_labels: ["Antibiotic", "Analgesic", "Cardiac", "Respiratory", "Gastrointestinal", "Endocrine", "Emergency"],
114
  equipment_categories: [],
115
  consumable_categories: [],
116
+ offline_force_flags: false,
117
+ db_write_lock: false,
118
+ db_write_lock_forced: false,
119
  };
120
 
121
  // ============================================================================
 
351
  }
352
  }
353
  });
354
+ const dbWriteLockEl = document.getElementById('db_write_lock');
355
+ const dbWriteLockForcedNote = document.getElementById('db-write-lock-forced-note');
356
+ if (dbWriteLockEl) {
357
+ const forced = !!merged.db_write_lock_forced;
358
+ dbWriteLockEl.disabled = forced;
359
+ dbWriteLockEl.title = forced ? 'Locked by environment variable DB_WRITE_LOCK' : '';
360
+ if (dbWriteLockForcedNote) {
361
+ dbWriteLockForcedNote.style.display = forced ? 'block' : 'none';
362
+ }
363
+ }
364
  setUserMode(merged.user_mode);
365
  try { localStorage.setItem('user_mode', merged.user_mode || 'user'); } catch (err) { /* ignore */ }
366
  window.CACHED_SETTINGS = merged;
 
1587
  try {
1588
  const s = {};
1589
  const numeric = new Set(['tr_temp','tr_tok','tr_p','tr_k','in_temp','in_tok','in_p','in_k','rep_penalty']);
1590
+ ['triage_instruction','inquiry_instruction','tr_temp','tr_tok','tr_p','tr_k','in_temp','in_tok','in_p','in_k','mission_context','rep_penalty','user_mode','db_write_lock'].forEach(k => {
1591
  const el = document.getElementById(k);
1592
  if (!el) return;
1593
  const val = el.type === 'checkbox' ? el.checked : el.value;
templates/index.html CHANGED
@@ -1181,6 +1181,13 @@
1181
  <div style="margin-top:10px;">
1182
  <a class="btn btn-sm" style="background:#b23b00; display:inline-block;" href="/logout?fresh=1" onclick="if (typeof forceClearCache === 'function') { forceClearCache(); return false; }"><span class="dev-tag">dev:cache-bust</span>Force Reload</a>
1183
  </div>
 
 
 
 
 
 
 
1184
  <div style="margin-top:8px; font-size:12px; color:#555;">
1185
  User: hides advanced controls. Advanced: shows prompt tools. Developer: also shows dev tags.
1186
  </div>
 
1181
  <div style="margin-top:10px;">
1182
  <a class="btn btn-sm" style="background:#b23b00; display:inline-block;" href="/logout?fresh=1" onclick="if (typeof forceClearCache === 'function') { forceClearCache(); return false; }"><span class="dev-tag">dev:cache-bust</span>Force Reload</a>
1183
  </div>
1184
+ <label style="display:flex; align-items:center; gap:8px; font-size:12px; color:#333; margin-top:10px;">
1185
+ <input type="checkbox" id="db_write_lock" />
1186
+ Database Write Lock (read-only mode)
1187
+ </label>
1188
+ <div id="db-write-lock-forced-note" style="margin-top:6px; font-size:12px; color:#b23b00; display:none;">
1189
+ Write lock is forced by environment variable <code>DB_WRITE_LOCK</code>.
1190
+ </div>
1191
  <div style="margin-top:8px; font-size:12px; color:#555;">
1192
  User: hides advanced controls. Advanced: shows prompt tools. Developer: also shows dev tags.
1193
  </div>