3v324v23 commited on
Commit
96e58a0
ยท
1 Parent(s): 4ce5a39

feat: add deployment metadata display for teacher panel with versioning and commit info

Browse files
Files changed (3) hide show
  1. .env.example +7 -0
  2. app/main.py +34 -1
  3. static/index.html +25 -1
.env.example CHANGED
@@ -27,3 +27,10 @@ SUPABASE_SECRET_KEY=
27
  # right and the system scored wrong. A flag is kept in the Supabase table `flags` with its recording (see the README),
28
  # so it needs SUPABASE_SECRET_KEY. These accounts may sign in without being in a class.
29
  TUTORS_EMAIL=
 
 
 
 
 
 
 
 
27
  # right and the system scored wrong. A flag is kept in the Supabase table `flags` with its recording (see the README),
28
  # so it needs SUPABASE_SECRET_KEY. These accounts may sign in without being in a class.
29
  TUTORS_EMAIL=
30
+
31
+ # Optional deployment metadata shown to teachers at the bottom of the management panel.
32
+ # Without these, the app uses a content hash, the newest source-file time, and the local git commit when available.
33
+ APP_VERSION=
34
+ BUILD_DATE=
35
+ GIT_COMMIT=
36
+ GIT_COMMIT_MESSAGE=
app/main.py CHANGED
@@ -14,6 +14,7 @@ import hashlib
14
  import logging
15
  import os
16
  import re
 
17
  import time
18
  import urllib.parse
19
  import uuid
@@ -248,6 +249,38 @@ def build_version() -> str:
248
  return f"{h.hexdigest()[:7]} ยท {time.strftime('%Y-%m-%d %H:%M', time.gmtime(newest))} UTC"
249
 
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  def utc(stamp: str) -> datetime:
252
  """A Supabase timestamp. The seconds part only: the fraction's length varies, which Python 3.10 does not parse."""
253
  return datetime.strptime(stamp[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
@@ -1114,7 +1147,7 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
1114
  @app.get("/api/health")
1115
  def health():
1116
  s: Scorer = app.state.scorer
1117
- return {"version": build_version(), "model": MODEL_ID, "tts": app.state.tts is not None,
1118
  "references": app.state.refs is not None, "sign_in": app.state.auth is not None,
1119
  "phoneme_fallbacks": s.vocab.downgraded}
1120
 
 
14
  import logging
15
  import os
16
  import re
17
+ import subprocess
18
  import time
19
  import urllib.parse
20
  import uuid
 
249
  return f"{h.hexdigest()[:7]} ยท {time.strftime('%Y-%m-%d %H:%M', time.gmtime(newest))} UTC"
250
 
251
 
252
+ def build_metadata() -> dict[str, str]:
253
+ """Deployment details for the teacher panel, with useful local-development fallbacks."""
254
+ files = sorted(Path(__file__).resolve().parent.glob("*.py")) + [STATIC / "index.html"]
255
+ h = hashlib.sha1()
256
+ for f in files:
257
+ h.update(f.read_bytes())
258
+ newest = max(f.stat().st_mtime for f in files)
259
+ commit = next((os.environ.get(name, "").strip() for name in
260
+ ("GIT_COMMIT", "COMMIT_SHA", "SOURCE_VERSION", "SPACE_REVISION")
261
+ if os.environ.get(name, "").strip()), "")
262
+ message = os.environ.get("GIT_COMMIT_MESSAGE", "").strip()
263
+ if not commit or not message:
264
+ try:
265
+ git = subprocess.run(
266
+ ["git", "log", "-1", "--format=%H%n%s"], cwd=STATIC.parent, check=True,
267
+ capture_output=True, text=True, timeout=2,
268
+ ).stdout.splitlines()
269
+ if not commit and git:
270
+ commit = git[0].strip()
271
+ if not message and len(git) > 1:
272
+ message = git[1].strip()
273
+ except (OSError, subprocess.SubprocessError):
274
+ pass
275
+ return {
276
+ "version": os.environ.get("APP_VERSION", "").strip() or h.hexdigest()[:7],
277
+ "updated_at": os.environ.get("BUILD_DATE", "").strip()
278
+ or datetime.fromtimestamp(newest, timezone.utc).isoformat(timespec="seconds"),
279
+ "commit": commit or "unknown",
280
+ "commit_message": message,
281
+ }
282
+
283
+
284
  def utc(stamp: str) -> datetime:
285
  """A Supabase timestamp. The seconds part only: the fraction's length varies, which Python 3.10 does not parse."""
286
  return datetime.strptime(stamp[:19], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc)
 
1147
  @app.get("/api/health")
1148
  def health():
1149
  s: Scorer = app.state.scorer
1150
+ return {"version": build_version(), "build": build_metadata(), "model": MODEL_ID, "tts": app.state.tts is not None,
1151
  "references": app.state.refs is not None, "sign_in": app.state.auth is not None,
1152
  "phoneme_fallbacks": s.vocab.downgraded}
1153
 
static/index.html CHANGED
@@ -308,6 +308,16 @@ body:not(.manage) #manage,body.manage #wrap>header,body.manage #path,body.manage
308
  .mtitle{font:400 24px/1 var(--f-display);margin:0}
309
  #mState.setup{padding:10px 12px;border:1px solid var(--sumac);border-radius:10px;color:var(--ink)}
310
  #mState.setup code{display:block;margin-top:6px;font-size:12px;overflow-wrap:anywhere;user-select:all}
 
 
 
 
 
 
 
 
 
 
311
  .hard>.note{margin:0;padding:4px 16px 8px;color:var(--muted);font-size:14px}
312
  /* inside a student, each part (the best sentences, the weakest, every lesson) opens and closes on its heading */
313
  .sec>summary{cursor:pointer;list-style:none;display:grid;grid-template-columns:1fr auto 3em;gap:4px 10px;align-items:center;padding:8px 16px;border-top:1px solid var(--line);font:700 15px/1.4 var(--f-body)}
@@ -839,6 +849,7 @@ body.quiet :is(#mic,#playMine,#driveBtn,.phone-btn),body.driving :is(#quietBtn,#
839
  <div class="card hard" id="mLessons" hidden></div>
840
  <div class="card hard roster" id="mRoster" hidden></div>
841
  <div id="mStudent"></div>
 
842
  </section>
843
  </div>
844
  <dialog id="listDlg" aria-labelledby="listTitle">
@@ -2634,7 +2645,8 @@ async function loadManage(){
2634
  const state=$("mState");
2635
  state.textContent="ื˜ื•ืขืŸโ€ฆ"; state.classList.remove("setup");
2636
  try{
2637
- const res=await api("/api/class"); await ready;
 
2638
  if(!res.ok){
2639
  const detail=(await res.json().catch(()=>({}))).detail||res.status;
2640
  state.textContent=res.status===403?"ื”ืคืื ืœ ืคืชื•ื— ืœืžื•ืจื™ื ื‘ืœื‘ื“.":`ื ืชื•ื ื™ ื”ืชืœืžื™ื“ื™ื ืœื ื–ืžื™ื ื™ื ื›ืจื’ืข (${detail}).`; return;
@@ -2648,6 +2660,18 @@ async function loadManage(){
2648
  const code=mark("",x.sql,"code"); code.dir="ltr"; state.append(mark("",x.what,"b"),mark(""," ื”ืจื™ืฆื• ื‘-SQL Editor ืฉืœ Supabase:"),code); }
2649
  drawManage();
2650
  }
 
 
 
 
 
 
 
 
 
 
 
 
2651
  function drawManage(){
2652
  const classes=CLASS.classes||[];
2653
  if(shownClass!=="*"&&!classes.some(c=>c.name===shownClass)) shownClass="*";
 
308
  .mtitle{font:400 24px/1 var(--f-display);margin:0}
309
  #mState.setup{padding:10px 12px;border:1px solid var(--sumac);border-radius:10px;color:var(--ink)}
310
  #mState.setup code{display:block;margin-top:6px;font-size:12px;overflow-wrap:anywhere;user-select:all}
311
+ .build-info{padding:14px 16px;margin-top:4px;font-variant-numeric:tabular-nums}
312
+ .build-info h2{margin:0;font:400 20px/1.2 var(--f-display)}
313
+ .build-info>p{margin:3px 0 12px;color:var(--muted);font-size:13px}
314
+ .build-info dl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin:0}
315
+ .build-info dl>div{min-width:0;padding:9px 10px;border-radius:9px;background:var(--bg)}
316
+ .build-info dt{margin-bottom:3px;color:var(--muted);font-size:12px;font-weight:600}
317
+ .build-info dd{margin:0;color:var(--ink);font-size:14px;overflow-wrap:anywhere}
318
+ .build-info code{direction:ltr;unicode-bidi:isolate;user-select:all}
319
+ #manage:not(.flags) #mBuild{display:none}
320
+ @media(max-width:560px){.build-info dl{grid-template-columns:1fr}}
321
  .hard>.note{margin:0;padding:4px 16px 8px;color:var(--muted);font-size:14px}
322
  /* inside a student, each part (the best sentences, the weakest, every lesson) opens and closes on its heading */
323
  .sec>summary{cursor:pointer;list-style:none;display:grid;grid-template-columns:1fr auto 3em;gap:4px 10px;align-items:center;padding:8px 16px;border-top:1px solid var(--line);font:700 15px/1.4 var(--f-body)}
 
849
  <div class="card hard" id="mLessons" hidden></div>
850
  <div class="card hard roster" id="mRoster" hidden></div>
851
  <div id="mStudent"></div>
852
+ <section class="build-info card" id="mBuild" aria-labelledby="mBuildTitle" hidden></section>
853
  </section>
854
  </div>
855
  <dialog id="listDlg" aria-labelledby="listTitle">
 
2645
  const state=$("mState");
2646
  state.textContent="ื˜ื•ืขืŸโ€ฆ"; state.classList.remove("setup");
2647
  try{
2648
+ const [res,healthRes]=await Promise.all([api("/api/class"),fetch("/api/health").catch(()=>null)]); await ready;
2649
+ if(healthRes?.ok) drawBuild((await healthRes.json()).build);
2650
  if(!res.ok){
2651
  const detail=(await res.json().catch(()=>({}))).detail||res.status;
2652
  state.textContent=res.status===403?"ื”ืคืื ืœ ืคืชื•ื— ืœืžื•ืจื™ื ื‘ืœื‘ื“.":`ื ืชื•ื ื™ ื”ืชืœืžื™ื“ื™ื ืœื ื–ืžื™ื ื™ื ื›ืจื’ืข (${detail}).`; return;
 
2660
  const code=mark("",x.sql,"code"); code.dir="ltr"; state.append(mark("",x.what,"b"),mark(""," ื”ืจื™ืฆื• ื‘-SQL Editor ืฉืœ Supabase:"),code); }
2661
  drawManage();
2662
  }
2663
+ function drawBuild(info){
2664
+ const box=$("mBuild");
2665
+ if(!info) return;
2666
+ const updated=new Date(info.updated_at), date=Number.isNaN(updated.valueOf())?info.updated_at:
2667
+ new Intl.DateTimeFormat("he-IL",{dateStyle:"short",timeStyle:"short"}).format(updated);
2668
+ const list=document.createElement("dl");
2669
+ const item=(label,value,code=false)=>{ const row=document.createElement("div"), dt=document.createElement("dt"), dd=document.createElement("dd"), val=document.createElement(code?"code":"span"); dt.textContent=label; val.textContent=value||"ืœื ื–ืžื™ืŸ"; dd.append(val); row.append(dt,dd); return row; };
2670
+ list.append(item("ืžืกืคืจ ื’ืจืกื”",info.version),item("ืขื“ื›ื•ืŸ ืื—ืจื•ืŸ",date),item("Commit ID",info.commit,true),item("ื”ื•ื“ืขืช commit",info.commit_message));
2671
+ const title=mark("","ืคืจื˜ื™ ื”ืžืขืจื›ืช","h2"); title.id="mBuildTitle";
2672
+ box.replaceChildren(title,mark("","ืžื™ื“ืข ื˜ื›ื ื™ ืœื–ื™ื”ื•ื™ ื”ื’ืจืกื” ืฉืคื•ืขืœืช ื›ืขืช.","p"),list);
2673
+ box.hidden=false;
2674
+ }
2675
  function drawManage(){
2676
  const classes=CLASS.classes||[];
2677
  if(shownClass!=="*"&&!classes.some(c=>c.name===shownClass)) shownClass="*";