From 410dd3b3225a1a0c3e41a110c48adfb3caddfae2 Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 17:16:47 +0530 Subject: [PATCH 1/7] feat: implement editing activities and sessions --- public/teach.html | 237 ++++++++++++++++++++++++++++++++++++---------- src/worker.py | 167 ++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 51 deletions(-) diff --git a/public/teach.html b/public/teach.html index 8c00bff..4f397fa 100644 --- a/public/teach.html +++ b/public/teach.html @@ -46,11 +46,14 @@

Host Hub

- -
-
-

🌟 Create New Activity

-

Descriptions are encrypted at rest in D1.

+ +
+
+
+

🌟 Create New Activity

+

Descriptions are encrypted at rest in D1.

+
+
@@ -100,55 +103,69 @@

🌟 Create New Activity

- +
- -
-
-

📅 Add Session

-

Location and description are encrypted at rest.

+ +
+
+
+

📅 Add Session

+

Location and description are encrypted at rest.

+
+
-
-
- - + + -
+ +
- - +
- - + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
-
-
- - -
-
- -
- +
@@ -239,6 +256,7 @@

My Hosted Activities

'
' + '
' + '
' + + '' + 'View' + '
' + ''; @@ -252,17 +270,61 @@

My Hosted Activities

setTimeout(() => el.classList.add('hidden'), 4000); } + let editActivityId = null; + let editSessionId = null; + let currentActivitySessions = []; + + async function startEditActivity(id) { + const a = hostedActivities.find(act => act.id === id); + if (!a) return; + + // fetch details and get description + const res = await fetch('/api/activities/' + id, { headers: { Authorization: 'Bearer ' + token } }); + const data = await res.json(); + if (!res.ok) { showMsg('act-err', 'Failed to load activity details', true); return; } + + const fullAct = data.activity; + + document.getElementById('a-title').value = fullAct.title; + document.getElementById('a-desc').value = fullAct.description || ''; + document.getElementById('a-type').value = fullAct.type; + document.getElementById('a-format').value = fullAct.format; + document.getElementById('a-schedule').value = fullAct.schedule_type; + document.getElementById('a-tags').value = (fullAct.tags || []).join(', '); + + document.getElementById('act-form-title').innerHTML = '✏️ Edit Activity'; + document.getElementById('btn-submit-act').textContent = 'Update Activity'; + document.getElementById('btn-cancel-act').classList.remove('hidden'); + + editActivityId = id; + document.getElementById('act-section').scrollIntoView({ behavior: 'smooth' }); + } + + function cancelEditActivity() { + document.getElementById('form-activity').reset(); + document.getElementById('act-form-title').innerHTML = '🌟 Create New Activity'; + document.getElementById('btn-submit-act').textContent = 'Create Activity'; + document.getElementById('btn-cancel-act').classList.add('hidden'); + editActivityId = null; + } + document.getElementById('form-activity').addEventListener('submit', async e => { e.preventDefault(); document.getElementById('act-err').classList.add('hidden'); document.getElementById('act-ok').classList.add('hidden'); const btn = e.target.querySelector('button[type=submit]'); - btn.textContent = 'Creating...'; btn.disabled = true; + btn.textContent = editActivityId ? 'Updating...' : 'Creating...'; + btn.disabled = true; + const rawTags = document.getElementById('a-tags').value; const tags = rawTags ? rawTags.split(',').map(t => t.trim()).filter(Boolean) : []; + + const method = editActivityId ? 'PUT' : 'POST'; + const url = editActivityId ? `/api/activities/${editActivityId}` : '/api/activities'; + try { - const res = await fetch('/api/activities', { - method:'POST', headers:{ 'Content-Type':'application/json', Authorization:'Bearer ' + token }, + const res = await fetch(url, { + method, headers:{ 'Content-Type':'application/json', Authorization:'Bearer ' + token }, body: JSON.stringify({ title: document.getElementById('a-title').value.trim(), description: document.getElementById('a-desc').value.trim(), @@ -274,25 +336,100 @@

My Hosted Activities

}); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed'); - showMsg('act-ok', 'Activity created: ' + data.data.title, false); - e.target.reset(); + + showMsg('act-ok', `Activity ${editActivityId ? 'updated' : 'created'} successfully!`, false); + cancelEditActivity(); await loadHostedActivities(); } catch (err) { showMsg('act-err', err.message, true); } - finally { btn.textContent = 'Create Activity'; btn.disabled = false; } + finally { btn.disabled = false; btn.textContent = editActivityId ? 'Update Activity' : 'Create Activity'; } }); + async function onActivitySelect() { + const actId = document.getElementById('s-activity').value; + const listDiv = document.getElementById('existing-sessions'); + const ul = document.getElementById('s-session-list'); + cancelEditSession(); + + if (!actId) { + listDiv.classList.add('hidden'); + return; + } + + const res = await fetch('/api/activities/' + actId, { headers: { Authorization: 'Bearer ' + token } }); + if (!res.ok) return; + const data = await res.json(); + currentActivitySessions = data.sessions || []; + + if (currentActivitySessions.length === 0) { + listDiv.classList.add('hidden'); + } else { + listDiv.classList.remove('hidden'); + ul.innerHTML = currentActivitySessions.map(s => { + return `
  • +
    + ${esc(s.title)} +
    ${s.start_time || ''}
    +
    + +
  • `; + }).join(''); + } + } + + function startEditSession(id) { + const s = currentActivitySessions.find(ses => ses.id === id); + if (!s) return; + + document.getElementById('s-title').value = s.title; + document.getElementById('s-desc').value = s.description || ''; + + // browsers require YYYY-MM-DDThh:mm format for datetime-local + const fmt = v => v ? String(v).replace(' ', 'T') : ''; + document.getElementById('s-start').value = fmt(s.start_time); + document.getElementById('s-end').value = fmt(s.end_time); + document.getElementById('s-location').value = s.location || ''; + + document.getElementById('ses-form-title').innerHTML = '✏️ Edit Session'; + document.getElementById('ses-fields-title').textContent = 'Editing Session Details'; + document.getElementById('btn-submit-ses').textContent = 'Update Session'; + document.getElementById('btn-cancel-ses').classList.remove('hidden'); + + editSessionId = id; + } + + function cancelEditSession() { + document.getElementById('s-title').value = ''; + document.getElementById('s-desc').value = ''; + document.getElementById('s-start').value = ''; + document.getElementById('s-end').value = ''; + document.getElementById('s-location').value = ''; + + document.getElementById('ses-form-title').innerHTML = '📅 Add Session'; + document.getElementById('ses-fields-title').textContent = 'New Session Details'; + document.getElementById('btn-submit-ses').textContent = 'Add Session'; + document.getElementById('btn-cancel-ses').classList.add('hidden'); + editSessionId = null; + } + document.getElementById('form-session').addEventListener('submit', async e => { e.preventDefault(); document.getElementById('ses-err').classList.add('hidden'); document.getElementById('ses-ok').classList.add('hidden'); - const btn = e.target.querySelector('button[type=submit]'); + const actId = document.getElementById('s-activity').value; if (!actId) { showMsg('ses-err', 'Please select an activity', true); return; } - btn.textContent = 'Adding...'; btn.disabled = true; + + const btn = e.target.querySelector('button[type=submit]'); + btn.textContent = editSessionId ? 'Updating...' : 'Adding...'; + btn.disabled = true; + const fmt = v => v ? v.replace('T',' ') : ''; + const method = editSessionId ? 'PUT' : 'POST'; + const url = editSessionId ? `/api/sessions/${editSessionId}` : '/api/sessions'; + try { - const res = await fetch('/api/sessions', { - method:'POST', headers:{ 'Content-Type':'application/json', Authorization:'Bearer ' + token }, + const res = await fetch(url, { + method, headers:{ 'Content-Type':'application/json', Authorization:'Bearer ' + token }, body: JSON.stringify({ activity_id: actId, title: document.getElementById('s-title').value.trim(), @@ -304,15 +441,13 @@

    My Hosted Activities

    }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Failed'); - showMsg('ses-ok', 'Session added successfully!', false); - document.getElementById('s-title').value = ''; - document.getElementById('s-desc').value = ''; - document.getElementById('s-start').value = ''; - document.getElementById('s-end').value = ''; - document.getElementById('s-location').value = ''; + + showMsg('ses-ok', `Session ${editSessionId ? 'updated' : 'added'} successfully!`, false); + cancelEditSession(); + await onActivitySelect(); // refresh session list await loadHostedActivities(); } catch (err) { showMsg('ses-err', err.message, true); } - finally { btn.textContent = 'Add Session'; btn.disabled = false; } + finally { btn.textContent = editSessionId ? 'Update Session' : 'Add Session'; btn.disabled = false; } }); diff --git a/src/worker.py b/src/worker.py index 9656277..61d3861 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1062,6 +1062,167 @@ async def api_add_activity_tags(req, env): return ok(None, "Tags updated") +async def api_update_activity(act_id, req, env): + user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET) + if not user: + return err("Authentication required", 401) + + body, bad_resp = await parse_json_object(req) + if bad_resp: + return bad_resp + + title = body.get("title") + description = body.get("description") + atype = body.get("type") + fmt = body.get("format") + schedule_type = body.get("schedule_type") + + owned = await env.DB.prepare( + "SELECT id FROM activities WHERE id=? AND host_id=?" + ).bind(act_id, user["id"]).first() + if not owned: + return err("Activity not found or access denied", 404) + + updates = [] + params = [] + + if title is not None: + title = title.strip() + if not title: + return err("title cannot be empty") + updates.append("title=(?)") + params.append(title) + + if description is not None: + description = description.strip() + enc = env.ENCRYPTION_KEY + updates.append("description=(?)") + params.append(encrypt(description, enc) if description else "") + + if atype is not None: + atype = atype.strip() + if atype not in ("course", "meetup", "workshop", "seminar", "other"): + atype = "course" + updates.append("type=(?)") + params.append(atype) + + if fmt is not None: + fmt = fmt.strip() + if fmt not in ("live", "self_paced", "hybrid"): + fmt = "self_paced" + updates.append("format=(?)") + params.append(fmt) + + if schedule_type is not None: + schedule_type = schedule_type.strip() + if schedule_type not in ("one_time", "multi_session", "recurring", "ongoing"): + schedule_type = "ongoing" + updates.append("schedule_type=(?)") + params.append(schedule_type) + + if updates: + params.append(act_id) + query = "UPDATE activities SET " + ", ".join(updates) + " WHERE id=(?)" + try: + await env.DB.prepare(query).bind(*params).run() + except Exception as e: + capture_exception(e, req, env, "api_update_activity.update_activity") + return err("Failed to update activity, please try again", 500) + + if "tags" in body: + tags = body.get("tags") or [] + try: + await env.DB.prepare("DELETE FROM activity_tags WHERE activity_id=?").bind(act_id).run() + except Exception: + pass + + for tag_name in tags: + tag_name = tag_name.strip() + if not tag_name: + continue + t_row = await env.DB.prepare("SELECT id FROM tags WHERE name=?").bind(tag_name).first() + if t_row: + tag_id = t_row.id + else: + tag_id = new_id() + try: + await env.DB.prepare("INSERT INTO tags (id,name) VALUES (?,?)").bind(tag_id, tag_name).run() + except Exception: + continue + try: + await env.DB.prepare("INSERT OR IGNORE INTO activity_tags (activity_id,tag_id) VALUES (?,?)").bind(act_id, tag_id).run() + except Exception: + pass + + return ok(None, "Activity updated") + + +async def api_update_session(ses_id, req, env): + user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET) + if not user: + return err("Authentication required", 401) + + body, bad_resp = await parse_json_object(req) + if bad_resp: + return bad_resp + + row = await env.DB.prepare( + "SELECT s.activity_id, a.host_id FROM sessions s JOIN activities a ON s.activity_id = a.id WHERE s.id=?" + ).bind(ses_id).first() + + if not row or row.host_id != user["id"]: + return err("Session not found or access denied", 404) + + title = body.get("title") + description = body.get("description") + start_time = body.get("start_time") + end_time = body.get("end_time") + location = body.get("location") + + updates = [] + params = [] + + if title is not None: + title = title.strip() + if not title: + return err("title cannot be empty") + updates.append("title=(?)") + params.append(title) + + if description is not None: + description = description.strip() + enc = env.ENCRYPTION_KEY + updates.append("description=(?)") + params.append(encrypt(description, enc) if description else "") + + if start_time is not None: + start_time = start_time.strip() + updates.append("start_time=(?)") + params.append(start_time) + + if end_time is not None: + end_time = end_time.strip() + updates.append("end_time=(?)") + params.append(end_time) + + if location is not None: + location = location.strip() + enc = env.ENCRYPTION_KEY + updates.append("location=(?)") + params.append(encrypt(location, enc) if location else "") + + if updates: + params.append(ses_id) + query = "UPDATE sessions SET " + ", ".join(updates) + " WHERE id=(?)" + try: + await env.DB.prepare(query).bind(*params).run() + except Exception as e: + capture_exception(e, req, env, "api_update_session.update_session") + return err("Failed to update session, please try again", 500) + + return ok(None, "Session updated") + + async def api_admin_table_counts(req, env): if not _is_basic_auth_valid(req, env): return _unauthorized_basic() @@ -1179,6 +1340,8 @@ async def _dispatch(request, env): m = re.fullmatch(r"/api/activities/([A-Za-z0-9_-]+)", path) if m and method == "GET": return await api_get_activity(m.group(1), request, env) + if m and method == "PUT": + return await api_update_activity(m.group(1), request, env) if path == "/api/join" and method == "POST": return await api_join(request, env) @@ -1188,6 +1351,10 @@ async def _dispatch(request, env): if path == "/api/sessions" and method == "POST": return await api_create_session(request, env) + + m_ses = re.fullmatch(r"/api/sessions/([A-Za-z0-9_-]+)", path) + if m_ses and method == "PUT": + return await api_update_session(m_ses.group(1), request, env) if path == "/api/tags" and method == "GET": return await api_list_tags(request, env) From c3a25353dc7aad095c06885bfa78833b7226b991 Mon Sep 17 00:00:00 2001 From: Shubhashish Chakraborty <164302071+Shubhashish-Chakraborty@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:51:51 +0530 Subject: [PATCH 2/7] update-teach.html, add for labels Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- public/teach.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/teach.html b/public/teach.html index 4f397fa..cfe8d1e 100644 --- a/public/teach.html +++ b/public/teach.html @@ -136,29 +136,29 @@

    📅 Add SessionNew Session Details
    - +
    - +
    - +
    - +
    - +
    From 58df5820476251f9d79cb1f8dd662d80d0f843d8 Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 18:01:58 +0530 Subject: [PATCH 3/7] implemented suggested fix --- public/teach.html | 8 ++++++-- src/worker.py | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/public/teach.html b/public/teach.html index cfe8d1e..e62419a 100644 --- a/public/teach.html +++ b/public/teach.html @@ -356,7 +356,11 @@

    My Hosted Activities

    } const res = await fetch('/api/activities/' + actId, { headers: { Authorization: 'Bearer ' + token } }); - if (!res.ok) return; + if (!res.ok) { + showMsg('ses-err', 'Failed to load sessions for this activity', true); + listDiv.classList.add('hidden'); + return; + } const data = await res.json(); currentActivitySessions = data.sessions || []; @@ -370,7 +374,7 @@

    My Hosted Activities

    ${esc(s.title)}
    ${s.start_time || ''}
    - + `; }).join(''); } diff --git a/src/worker.py b/src/worker.py index 61d3861..71b4ac2 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1128,31 +1128,34 @@ async def api_update_activity(act_id, req, env): except Exception as e: capture_exception(e, req, env, "api_update_activity.update_activity") return err("Failed to update activity, please try again", 500) + else: + return ok(None, "No changes provided") if "tags" in body: tags = body.get("tags") or [] try: await env.DB.prepare("DELETE FROM activity_tags WHERE activity_id=?").bind(act_id).run() - except Exception: - pass + except Exception as e: + capture_exception(e, req, env, "api_update_activity.delete_activity_tags") for tag_name in tags: - tag_name = tag_name.strip() - if not tag_name: + tag_name_clean = tag_name.strip() + if not tag_name_clean: continue - t_row = await env.DB.prepare("SELECT id FROM tags WHERE name=?").bind(tag_name).first() + t_row = await env.DB.prepare("SELECT id FROM tags WHERE name=?").bind(tag_name_clean).first() if t_row: tag_id = t_row.id else: tag_id = new_id() try: - await env.DB.prepare("INSERT INTO tags (id,name) VALUES (?,?)").bind(tag_id, tag_name).run() - except Exception: + await env.DB.prepare("INSERT INTO tags (id,name) VALUES (?,?)").bind(tag_id, tag_name_clean).run() + except Exception as e: + capture_exception(e, req, env, f"api_update_activity.insert_tag: tag_name={tag_name_clean}") continue try: await env.DB.prepare("INSERT OR IGNORE INTO activity_tags (activity_id,tag_id) VALUES (?,?)").bind(act_id, tag_id).run() - except Exception: - pass + except Exception as e: + capture_exception(e, req, env, f"api_update_activity.insert_activity_tag: tag_id={tag_id}") return ok(None, "Activity updated") @@ -1219,6 +1222,8 @@ async def api_update_session(ses_id, req, env): except Exception as e: capture_exception(e, req, env, "api_update_session.update_session") return err("Failed to update session, please try again", 500) + else: + return ok(None, "No changes provided") return ok(None, "Session updated") From 0e07e3df3c76895116da66a0689653986bdda21b Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 18:15:25 +0530 Subject: [PATCH 4/7] fixes --- public/teach.html | 2 +- src/worker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/teach.html b/public/teach.html index e62419a..5205d44 100644 --- a/public/teach.html +++ b/public/teach.html @@ -372,7 +372,7 @@

    My Hosted Activities

    return `
  • ${esc(s.title)} -
    ${s.start_time || ''}
    +
    ${esc(s.start_time || '')}
  • `; diff --git a/src/worker.py b/src/worker.py index 71b4ac2..73017ed 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1128,7 +1128,7 @@ async def api_update_activity(act_id, req, env): except Exception as e: capture_exception(e, req, env, "api_update_activity.update_activity") return err("Failed to update activity, please try again", 500) - else: + elif "tags" not in body: return ok(None, "No changes provided") if "tags" in body: From 02cb151eb7894dba086636ed629f925f2c321fc6 Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 22:35:42 +0530 Subject: [PATCH 5/7] implement fixes --- public/teach.html | 10 ++++++++-- src/worker.py | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/public/teach.html b/public/teach.html index 5205d44..43b85fb 100644 --- a/public/teach.html +++ b/public/teach.html @@ -127,13 +127,13 @@

    📅 Add Session
    - +

    New Session Details

    @@ -273,14 +273,18 @@

    My Hosted Activities

    let editActivityId = null; let editSessionId = null; let currentActivitySessions = []; + let activityEditLoadSeq = 0; + let sessionListLoadSeq = 0; async function startEditActivity(id) { + const loadSeq = ++activityEditLoadSeq; const a = hostedActivities.find(act => act.id === id); if (!a) return; // fetch details and get description const res = await fetch('/api/activities/' + id, { headers: { Authorization: 'Bearer ' + token } }); const data = await res.json(); + if (loadSeq !== activityEditLoadSeq) return; if (!res.ok) { showMsg('act-err', 'Failed to load activity details', true); return; } const fullAct = data.activity; @@ -346,6 +350,7 @@

    My Hosted Activities

    async function onActivitySelect() { const actId = document.getElementById('s-activity').value; + const loadSeq = ++sessionListLoadSeq; const listDiv = document.getElementById('existing-sessions'); const ul = document.getElementById('s-session-list'); cancelEditSession(); @@ -362,6 +367,7 @@

    My Hosted Activities

    return; } const data = await res.json(); + if (loadSeq !== sessionListLoadSeq || document.getElementById('s-activity').value !== actId) return; currentActivitySessions = data.sessions || []; if (currentActivitySessions.length === 0) { diff --git a/src/worker.py b/src/worker.py index 73017ed..2334078 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1087,6 +1087,8 @@ async def api_update_activity(act_id, req, env): params = [] if title is not None: + if not isinstance(title, str): + return err("title must be a string", 400) title = title.strip() if not title: return err("title cannot be empty") @@ -1094,12 +1096,16 @@ async def api_update_activity(act_id, req, env): params.append(title) if description is not None: + if not isinstance(description, str): + return err("description must be a string", 400) description = description.strip() enc = env.ENCRYPTION_KEY updates.append("description=(?)") params.append(encrypt(description, enc) if description else "") if atype is not None: + if not isinstance(atype, str): + return err("type must be a string", 400) atype = atype.strip() if atype not in ("course", "meetup", "workshop", "seminar", "other"): atype = "course" @@ -1107,6 +1113,8 @@ async def api_update_activity(act_id, req, env): params.append(atype) if fmt is not None: + if not isinstance(fmt, str): + return err("format must be a string", 400) fmt = fmt.strip() if fmt not in ("live", "self_paced", "hybrid"): fmt = "self_paced" @@ -1114,6 +1122,8 @@ async def api_update_activity(act_id, req, env): params.append(fmt) if schedule_type is not None: + if not isinstance(schedule_type, str): + return err("schedule_type must be a string", 400) schedule_type = schedule_type.strip() if schedule_type not in ("one_time", "multi_session", "recurring", "ongoing"): schedule_type = "ongoing" @@ -1139,6 +1149,8 @@ async def api_update_activity(act_id, req, env): capture_exception(e, req, env, "api_update_activity.delete_activity_tags") for tag_name in tags: + if not isinstance(tag_name, str): + continue tag_name_clean = tag_name.strip() if not tag_name_clean: continue @@ -1186,6 +1198,8 @@ async def api_update_session(ses_id, req, env): params = [] if title is not None: + if not isinstance(title, str): + return err("title must be a string", 400) title = title.strip() if not title: return err("title cannot be empty") @@ -1193,22 +1207,30 @@ async def api_update_session(ses_id, req, env): params.append(title) if description is not None: + if not isinstance(description, str): + return err("description must be a string", 400) description = description.strip() enc = env.ENCRYPTION_KEY updates.append("description=(?)") params.append(encrypt(description, enc) if description else "") if start_time is not None: + if not isinstance(start_time, str): + return err("start_time must be a string", 400) start_time = start_time.strip() updates.append("start_time=(?)") params.append(start_time) if end_time is not None: + if not isinstance(end_time, str): + return err("end_time must be a string", 400) end_time = end_time.strip() updates.append("end_time=(?)") params.append(end_time) if location is not None: + if not isinstance(location, str): + return err("location must be a string", 400) location = location.strip() enc = env.ENCRYPTION_KEY updates.append("location=(?)") @@ -1227,7 +1249,6 @@ async def api_update_session(ses_id, req, env): return ok(None, "Session updated") - async def api_admin_table_counts(req, env): if not _is_basic_auth_valid(req, env): return _unauthorized_basic() From 7b2b0e5bcd7f85281c44ee9f5cf7fa9ce41366be Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 23:29:26 +0530 Subject: [PATCH 6/7] fixesss --- public/teach.html | 3 ++- src/worker.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/teach.html b/public/teach.html index 43b85fb..2ed5122 100644 --- a/public/teach.html +++ b/public/teach.html @@ -454,8 +454,9 @@

    My Hosted Activities

    showMsg('ses-ok', `Session ${editSessionId ? 'updated' : 'added'} successfully!`, false); cancelEditSession(); - await onActivitySelect(); // refresh session list await loadHostedActivities(); + document.getElementById('s-activity').value = actId; + await onActivitySelect(); // refresh session list } catch (err) { showMsg('ses-err', err.message, true); } finally { btn.textContent = editSessionId ? 'Update Session' : 'Add Session'; btn.disabled = false; } }); diff --git a/src/worker.py b/src/worker.py index 2334078..4b03715 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1108,7 +1108,7 @@ async def api_update_activity(act_id, req, env): return err("type must be a string", 400) atype = atype.strip() if atype not in ("course", "meetup", "workshop", "seminar", "other"): - atype = "course" + return err("type must be one of: course, meetup, workshop, seminar, other", 400) updates.append("type=(?)") params.append(atype) @@ -1117,7 +1117,7 @@ async def api_update_activity(act_id, req, env): return err("format must be a string", 400) fmt = fmt.strip() if fmt not in ("live", "self_paced", "hybrid"): - fmt = "self_paced" + return err("format must be one of: live, self_paced, hybrid", 400) updates.append("format=(?)") params.append(fmt) @@ -1126,7 +1126,7 @@ async def api_update_activity(act_id, req, env): return err("schedule_type must be a string", 400) schedule_type = schedule_type.strip() if schedule_type not in ("one_time", "multi_session", "recurring", "ongoing"): - schedule_type = "ongoing" + return err("schedule_type must be one of: one_time, multi_session, recurring, ongoing", 400) updates.append("schedule_type=(?)") params.append(schedule_type) @@ -1142,7 +1142,7 @@ async def api_update_activity(act_id, req, env): return ok(None, "No changes provided") if "tags" in body: - tags = body.get("tags") or [] + tags = body.get("tags") try: await env.DB.prepare("DELETE FROM activity_tags WHERE activity_id=?").bind(act_id).run() except Exception as e: From 4bec205fdd1868ad250bee2ba2d0ea3d08a15d6e Mon Sep 17 00:00:00 2001 From: Shubhashish-Chakraborty Date: Wed, 25 Mar 2026 23:46:12 +0530 Subject: [PATCH 7/7] validate tags as an array of strings before iterating --- src/worker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/worker.py b/src/worker.py index 4b03715..cedc938 100644 --- a/src/worker.py +++ b/src/worker.py @@ -1143,6 +1143,10 @@ async def api_update_activity(act_id, req, env): if "tags" in body: tags = body.get("tags") + if tags is None: + tags = [] + if not isinstance(tags, list) or any(not isinstance(tag, str) for tag in tags): + return err("tags must be an array of strings", 400) try: await env.DB.prepare("DELETE FROM activity_tags WHERE activity_id=?").bind(act_id).run() except Exception as e: