From 750c098bed989c3e53807e0dac817a23bc95110b Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:08:23 -0500 Subject: [PATCH 01/21] Updated to use object labels in column headers and fall back to object names when labels not set --- src/soa_builder/web/app.py | 35 ------------------------- src/soa_builder/web/templates/edit.html | 12 ++++----- 2 files changed, 6 insertions(+), 41 deletions(-) diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index 667a1ff..72c2f25 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -3116,9 +3116,6 @@ def delete_activity(soa_id: int, activity_id: int): return {"deleted_activity_id": activity_id} -# API endpoint for deleting an Epoch <- moved to routers/epochs.py - - @app.get("/", response_class=HTMLResponse) def ui_index(request: Request): """Render home page for the SoA Workbench.""" @@ -3280,38 +3277,6 @@ def ui_update_meta( ) -# Helper to fetch element audit rows with legacy-safe columns -> Deprecated (Moved to audits.py, audits.html) -""" -def _fetch_element_audits(soa_id: int): - conn_ea = _connect() - cur_ea = conn_ea.cursor() - cur_ea.execute("PRAGMA table_info(element_audit)") - cols = [row[1] for row in cur_ea.fetchall()] - want = [ - "id", - "element_id", - "action", - "before_json", - "after_json", - "performed_at", - ] - available = [c for c in want if c in cols] - element_audits = [] - if available: - select_sql = f"SELECT {', '.join(available)} FROM element_audit WHERE soa_id=? ORDER BY id DESC" - cur_ea.execute(select_sql, (soa_id,)) - for r in cur_ea.fetchall(): - item = {} - for i, c in enumerate(available): - item[c] = r[i] - for k in want: - item.setdefault(k, None) - element_audits.append(item) - conn_ea.close() - return element_audits -""" - - # UI endpoint for rendering SOA edit page @app.get("/ui/soa/{soa_id}/edit", response_class=HTMLResponse) def ui_edit(request: Request, soa_id: int): diff --git a/src/soa_builder/web/templates/edit.html b/src/soa_builder/web/templates/edit.html index 7564683..7e14525 100644 --- a/src/soa_builder/web/templates/edit.html +++ b/src/soa_builder/web/templates/edit.html @@ -240,16 +240,16 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ Epoch:-> {% for inst in timeline_instances %} - {% if inst.epoch_name %}{{ inst.epoch_name }}{% endif %} + {% if inst.epoch_label %}{{ inst.epoch_label }}{% else %}{{ inst.epoch_name }}{% endif %} {% endfor %} - Encounter Name:-> + Encounter:-> {% for inst in timeline_instances %} - {% if inst.encounter_name %}{{ inst.encounter_name }}{% endif %} + {% if inst.encounter_label %}{{ inst.encounter_label }}{% else %}{{ inst.encounter_name }}{% endif %} {% endfor %} @@ -264,10 +264,10 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ - Timing Label:-> + Timing:-> {% for inst in timeline_instances %} - {% if inst.timing_label %}{{ inst.timing_label }}{% endif %} + {% if inst.timing_label %}{{ inst.timing_label }}{% else %}{{ inst.timing_name }}{% endif %} {% endfor %} @@ -286,7 +286,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ Concepts {% for inst in timeline_instances %} -
{{ inst.name }}
+
{% if inst.label %}{{ inst.label }}{% else %}{{ inst.name }}{% endif %}
{% endfor %} From b51a2d3a9ac174876778de68b987a57993365977 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:29:26 -0500 Subject: [PATCH 02/21] Updated SQL query to populate values for sending to template edit.html --- src/soa_builder/web/app.py | 67 ++++++++++---------------------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index 72c2f25..a876d20 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -3547,51 +3547,16 @@ def ui_edit(request: Request, soa_id: int): cur_inst = conn_inst.cursor() cur_inst.execute( """ - SELECT i.id, - i.name, - i.instance_uid, - i.label, - i.member_of_timeline, - (SELECT t.name - FROM schedule_timelines t - WHERE t.schedule_timeline_uid = i.member_of_timeline - AND t.soa_id = i.soa_id) AS timeline_name, - (SELECT v.name - FROM visit v - WHERE v.encounter_uid = i.encounter_uid - AND v.soa_id = i.soa_id) AS encounter_name, - (SELECT e.name - FROM epoch e - WHERE e.epoch_uid = i.epoch_uid - AND e.soa_id = i.soa_id) AS epoch_name, - (SELECT tm.window_label - FROM visit v - JOIN timing tm - ON tm.id = v.scheduledAtId - AND tm.soa_id = v.soa_id - WHERE v.encounter_uid = i.encounter_uid - AND v.soa_id = i.soa_id - LIMIT 1) AS window_label, - (SELECT tm.label - FROM visit v - JOIN timing tm - ON tm.id = v.scheduledAtId - AND tm.soa_id = v.soa_id - WHERE v.encounter_uid = i.encounter_uid - AND v.soa_id = i.soa_id - LIMIT 1) AS timing_label, - (SELECT tm.value - FROM visit v - JOIN timing tm - ON tm.id = v.scheduledAtId - AND tm.soa_id = v.soa_id - WHERE v.encounter_uid = i.encounter_uid - AND v.soa_id = i.soa_id - LIMIT 1) AS study_day + SELECT i.id,i.name,i.instance_uid,i.label,i.member_of_timeline,st.name AS timeline_name,st.label AS timeline_label, + v.name AS encounter_name,v.label AS encounter_label,e.name AS epoch_name,e.epoch_label as epoch_label,tm.window_label,tm.label AS timing_label,tm.name AS timing_name,tm.value AS study_day FROM instances i - WHERE soa_id=? - ORDER BY member_of_timeline, length(instance_uid), instance_uid - """, + LEFT JOIN schedule_timelines st ON st.schedule_timeline_uid = i.member_of_timeline AND st.soa_id = i.soa_id + LEFT JOIN visit v ON v.encounter_uid = i.encounter_uid AND v.soa_id = i.soa_id + LEFT JOIN epoch e ON e.epoch_uid = i.epoch_uid AND e.soa_id = i.soa_id + LEFT JOIN timing tm ON tm.id = v.scheduledAtId AND tm.soa_id = v.soa_id + WHERE i.soa_id=? + ORDER BY COALESCE(i.member_of_timeline, 'zzz'), LENGTH(i.instance_uid), i.instance_uid + """, (soa_id,), ) instances = [ @@ -3602,11 +3567,15 @@ def ui_edit(request: Request, soa_id: int): "label": r[3], "member_of_timeline": r[4], "timeline_name": r[5], - "encounter_name": r[6], - "epoch_name": r[7], - "window_label": r[8], - "timing_label": r[9], - "study_day": iso_duration_to_days(r[10]), + "timeline_label": r[6], + "encounter_name": r[7], + "encounter_label": r[8], + "epoch_name": r[9], + "epoch_label": r[10], + "window_label": r[11], + "timing_label": r[12], + "timing_name": r[13], + "study_day": iso_duration_to_days(r[14]), } for r in cur_inst.fetchall() ] From 558dc40bb226885de04dadb574060980f1085d8c Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:53:27 -0500 Subject: [PATCH 03/21] Renamed column header 'UID' to 'id' --- src/soa_builder/web/templates/arms.html | 2 +- src/soa_builder/web/templates/elements.html | 2 +- src/soa_builder/web/templates/encounters.html | 2 +- src/soa_builder/web/templates/epochs.html | 2 +- src/soa_builder/web/templates/instances.html | 2 +- src/soa_builder/web/templates/rules.html | 2 +- src/soa_builder/web/templates/schedule_timelines.html | 2 +- src/soa_builder/web/templates/timings.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/soa_builder/web/templates/arms.html b/src/soa_builder/web/templates/arms.html index a963c64..0a7df12 100644 --- a/src/soa_builder/web/templates/arms.html +++ b/src/soa_builder/web/templates/arms.html @@ -40,7 +40,7 @@

Arms for SoA {{ soa_id }}

- + diff --git a/src/soa_builder/web/templates/elements.html b/src/soa_builder/web/templates/elements.html index 9b71e96..ea4ccde 100644 --- a/src/soa_builder/web/templates/elements.html +++ b/src/soa_builder/web/templates/elements.html @@ -40,7 +40,7 @@

Elements for SoA {{ soa_id }}

UIDid Name Label Description
- + diff --git a/src/soa_builder/web/templates/encounters.html b/src/soa_builder/web/templates/encounters.html index 5229fbd..cd6808c 100644 --- a/src/soa_builder/web/templates/encounters.html +++ b/src/soa_builder/web/templates/encounters.html @@ -85,7 +85,7 @@

Encounters for SoA {{ soa_id }}

UIDid Name Label Description
- + diff --git a/src/soa_builder/web/templates/epochs.html b/src/soa_builder/web/templates/epochs.html index 453be17..8c963d9 100644 --- a/src/soa_builder/web/templates/epochs.html +++ b/src/soa_builder/web/templates/epochs.html @@ -31,7 +31,7 @@

Epochs for SoA {{ soa_id }}

UIDid Name Label Description
- + diff --git a/src/soa_builder/web/templates/instances.html b/src/soa_builder/web/templates/instances.html index ae6f9bd..9b6bfd0 100644 --- a/src/soa_builder/web/templates/instances.html +++ b/src/soa_builder/web/templates/instances.html @@ -67,7 +67,7 @@

Scheduled Activity Instances for SoA {{ soa_id }}

UIDid Order Name Label
- + diff --git a/src/soa_builder/web/templates/rules.html b/src/soa_builder/web/templates/rules.html index fb562d3..edb1752 100644 --- a/src/soa_builder/web/templates/rules.html +++ b/src/soa_builder/web/templates/rules.html @@ -26,7 +26,7 @@

Transition Rules for SoA {{ soa_id }}

UIDid Name Label Description
- + diff --git a/src/soa_builder/web/templates/schedule_timelines.html b/src/soa_builder/web/templates/schedule_timelines.html index 12895e6..2f3259c 100644 --- a/src/soa_builder/web/templates/schedule_timelines.html +++ b/src/soa_builder/web/templates/schedule_timelines.html @@ -47,7 +47,7 @@

Schedule Timelines for SoA {{ soa_id }}

UIDid Name Label Description
- + diff --git a/src/soa_builder/web/templates/timings.html b/src/soa_builder/web/templates/timings.html index a47eceb..843cef4 100644 --- a/src/soa_builder/web/templates/timings.html +++ b/src/soa_builder/web/templates/timings.html @@ -87,7 +87,7 @@

Timings for SoA {{ soa_id }}

UIDid Name Label Description
- + From 91b7c73872561e4302a4ba9a5cf1b19568aad9e1 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:54:51 -0500 Subject: [PATCH 04/21] Added inline comments to clarify sections --- src/soa_builder/web/templates/edit.html | 59 ++++++++++++++----------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/soa_builder/web/templates/edit.html b/src/soa_builder/web/templates/edit.html index 7e14525..1d8ff5e 100644 --- a/src/soa_builder/web/templates/edit.html +++ b/src/soa_builder/web/templates/edit.html @@ -205,36 +205,35 @@

Editing SoA {{ soa_id }}

color: #fff; } - {% if timelines and timelines|length > 0 %} -
- Select Timeline: - {% for tl in timelines %} - - {% endfor %} -
- {% endif %} - - +{% if timelines and timelines|length > 0 %} +
+Select Timeline: +{% for tl in timelines %} + +{% endfor %} +
+{% endif %} + {% for timeline_uid, timeline_instances in instances_by_timeline.items() %}
+ data-timeline="{{ timeline_uid }}" + {% if timeline_uid == default_timeline %}style="display: block;"{% else %}style="display: none;"{% endif %}>

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{{ timeline_instances[0].timeline_name }}{% else %}{{ timeline_uid }}{% endif %}

{% if timeline_instances|length == 0 %}

No scheduled activity instances for this timeline.

{% else %} - +
UIDid Name Label Description
- + @@ -244,6 +243,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endfor %}

+ @@ -253,6 +253,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endfor %}

+ @@ -262,6 +263,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endfor %}

+ @@ -271,7 +273,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endfor %}

- + @@ -284,6 +286,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{

+ {% for inst in timeline_instances %} {% endfor %} @@ -313,6 +320,7 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endif %} {% endfor %} +

Generate Normalized Summary (JSON)

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ title="Download Excel workbook" >⬇ XLSX
+ + + \ No newline at end of file From 9129560f0d59b8a39e60b41ff25ac38faa3abd98 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:57:08 -0500 Subject: [PATCH 07/21] Added href column to activity_concept table --- .gitignore | 1 + src/soa_builder/web/app.py | 2 ++ src/soa_builder/web/initialize_database.py | 3 ++- src/soa_builder/web/migrate_database.py | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 84b94ff..d766364 100644 --- a/.gitignore +++ b/.gitignore @@ -98,5 +98,6 @@ files/~* output/* SOA Workbench Wishlist.docx NCT01750580_limited.json +CLAUDE.md # End of file diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index a876d20..4024c0d 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -56,6 +56,7 @@ _migrate_timing_add_member_of_timeline, _migrate_instances_add_member_of_timeline, _migrate_matrix_cells_add_instance_id, + _migrate_activity_concept_add_href, ) from .routers import activities as activities_router from .routers import arms as arms_router @@ -150,6 +151,7 @@ def _configure_logging(): # Database migration steps +_migrate_activity_concept_add_href() _migrate_matrix_cells_add_instance_id() _migrate_instances_add_member_of_timeline() _migrate_timing_add_member_of_timeline() diff --git a/src/soa_builder/web/initialize_database.py b/src/soa_builder/web/initialize_database.py index aa178f0..2e7e08f 100644 --- a/src/soa_builder/web/initialize_database.py +++ b/src/soa_builder/web/initialize_database.py @@ -31,7 +31,8 @@ def _init_db(): concept_title TEXT, concept_uid TEXT, -- immutable BiomedicalConcept_N identifier unique within an SOA activity_uid TEXT, -- joins to the activity table using this uid unique within an SOA - soa_id INT + soa_id INT, + href TEXT -- stores the API address where the BC exists; codeSystem & codeSystemVersion )""" ) diff --git a/src/soa_builder/web/migrate_database.py b/src/soa_builder/web/migrate_database.py index 6bf4195..ae2fa28 100644 --- a/src/soa_builder/web/migrate_database.py +++ b/src/soa_builder/web/migrate_database.py @@ -979,3 +979,19 @@ def _migrate_matrix_cells_add_instance_id(): conn.close() except Exception as e: logger.warning("matrix_cells instance_id migration failed: %s", e) + + +def _migrate_activity_concept_add_href(): + """Add href column to store value for API from which the codeSystem and codeSystemVersion USDM properties can be derived""" + try: + conn = _connect() + cur = conn.cursor() + cur.execute("PRAGMA table_info(activity_concept)") + cols = {r[1] for r in cur.fetchall()} + if "href" not in cols: + cur.execute("ALTER TABLE activity_concept ADD COLUMN href TEXT") + conn.commit() + logger.info("Added href column to the activity_concept table") + conn.close() + except Exception as e: + logger.warning("activity_concept href migration failed: %s", e) From 28a0c92da2656cb34d8d1a9f40631a99f7e427cb Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:15:50 -0500 Subject: [PATCH 08/21] Reorganized column header rows --- src/soa_builder/web/templates/edit.html | 37 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/soa_builder/web/templates/edit.html b/src/soa_builder/web/templates/edit.html index 1d8ff5e..f29650f 100644 --- a/src/soa_builder/web/templates/edit.html +++ b/src/soa_builder/web/templates/edit.html @@ -237,10 +237,25 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{

+ {% set prev_epoch = namespace(value=None, count=0) %} {% for inst in timeline_instances %} - + {% set current_epoch = inst.epoch_label if inst.epoch_label else inst.epoch_name %} + {% if prev_epoch.value == current_epoch %} + {# Same as previous, increment count but don't output #} + {% set prev_epoch.count = prev_epoch.count + 1 %} + {% else %} + {# Different epoch, output previous if exists #} + {% if prev_epoch.value is not none %} + + {% endif %} + {# Start tracking new epoch #} + {% set prev_epoch.value = current_epoch %} + {% set prev_epoch.count = 1 %} + {% endif %} + {# On last iteration, output the accumulated cell #} + {% if loop.last %} + + {% endif %} {% endfor %} @@ -253,6 +268,16 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% endfor %} + +

+ + + {% for inst in timeline_instances %} + + {% endfor %} + @@ -286,11 +311,9 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{

- + {% for inst in timeline_instances %} - + {% endfor %} {% for a in activities %} From 95c6b0f1fb4b63736610efa86d0312250193ee2d Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:16:24 -0500 Subject: [PATCH 09/21] Removed backup file of edit.html --- .../web/templates/edit_backup.html | 642 ------------------ 1 file changed, 642 deletions(-) delete mode 100644 src/soa_builder/web/templates/edit_backup.html diff --git a/src/soa_builder/web/templates/edit_backup.html b/src/soa_builder/web/templates/edit_backup.html deleted file mode 100644 index e9bfa37..0000000 --- a/src/soa_builder/web/templates/edit_backup.html +++ /dev/null @@ -1,642 +0,0 @@ -{% extends 'base.html' %} -{% block content %} -

Editing SoA {{ soa_id }}

-
- Study Metadata -
-
- - -
-
- - -
-
- - -
-
- - Study ID must be unique and cannot be cleared once set. Other fields may be left blank to clear. -
- -
-
-
-
- - - -
-
- Count: {{ freeze_count }} - {% if last_frozen_at %} • Last: {{ last_frozen_at }}{% endif %} -
-
- {% if freezes %} -
- Versions: - {% for f in freezes %} -
- {{ f.version_label }} - -
- {% endfor %} -
-
- - - Rollback Audit XLSX - Reorder Audit XLSX - Reorder CSV -
-
- Diff: - - - -
- {% else %} -
No versions frozen yet.
- {% endif %} -
- -
-
- - -
- Last fetch: - {% if concepts_last_fetch_relative %} - {{ concepts_last_fetch_relative }} - {% else %} - n/a - {% endif %} -
- (forces remote re-fetch & cache reset) -
-
-
-
- Visits/Encounters ({{ visits|length }}) (drag to reorder) -
    - {% for v in visits %} -
  • - {{ v.order_index }}. {{ v.name }} - - {% if epochs %} -
    - - - - {% endif %} - {% if timings %} -
    - - - - {% endif %} - {% if transition_rules %} -
    - - - -
    - - - - {% endif %} - -
    - - - - - -
    - - - -
    -
  • - {% endfor %} -
-
- - - - {% if epochs %} - - {% endif %} - - -
-
- Activities ({{ activities|length }}) (drag to reorder) -
    - {% for a in activities %} -
  • - {{ a.order_index }}. {{ a.name }} - -
    - - - - - - -
    - - - -
    -
  • - {% endfor %} -
-
- - - - - -
- -
- Epochs ({{ epochs|length }}) (drag to reorder) -
    - {% for e in epochs %} -
  • - {{ e.order_index }}. {{ e.name }} - -
    - - - - - - - -
    - - - -
    -
  • - {% endfor %} -
-
- - - - - - -
-
- Elements ({{ elements|length }}) (drag to reorder) -
    - {% for el in elements %} -
  • - {{ el.order_index }}. {% if el.label %}{{ el.label }}{% else %}{{ el.name }}{% endif %} - - {% if transition_rules %} -
    - - - -
    - - - - {% endif %} -
    - - - - - - -
    - - - -
    -
  • - {% endfor %} -
-
- - - - - - -
-
- Arms ({{ arms|length }}) (drag to reorder) -
    - {% for arm in arms %} -
  • - {{ arm.order_index }}. {% if arm.label %}{{ arm.label }}{% else %}{{ arm.name }}{% endif %} - -
    - - - - - {# Type selection from protocol terminology C174222 #} - {% if protocol_terminology_C174222 %} - - {% endif %} - {# Type selection from ddf terminology C188727 #} - {% if ddf_terminology_C188727 %} - - {% endif %} - - -
    - - - -
    -
  • - {% endfor %} -
-
- - - - {% if protocol_terminology_C174222 %} - - {% endif %} - {% if ddf_terminology_C188727 %} - - {% endif %} - - -
-
- Study Cells ({{ study_cells|length }}) -
-
- - -
-
- - -
-
- - - Select one or more elements (Cmd/Ctrl+Click). -
-
- -
- -
-
Epoch:->
Encounter:->
Study Day:->
Timing:->
Visit Window:->
Activity Concepts
{% if inst.label %}{{ inst.label }}{% else %}{{ inst.name }}{% endif %}
@@ -301,10 +304,14 @@

Matrix: {% if timeline_instances and timeline_instances[0].timeline_name %}{ {% for inst in timeline_instances %} {% set raw_status = cell_map.get((inst.id, a.id), '') %} {% set display = 'X' if raw_status == 'X' else '' %} -

{{ display }} + + {{ display }}
Epoch:-> - {% if inst.epoch_label %}{{ inst.epoch_label }}{% else %}{{ inst.epoch_name }}{% endif %} - {{ prev_epoch.value }}{{ prev_epoch.value }}
Instance:-> +
{% if inst.label %}{{ inst.label }}{% else %}{{ inst.name }}{% endif %}
+
Activity Concepts -
{% if inst.label %}{{ inst.label }}{% else %}{{ inst.name }}{% endif %}
-
- - - - - - - - {% for sc in study_cells %} - - - - - - - - {% else %} - - {% endfor %} -
UIDArmEpochElementActions
{{ sc.study_cell_uid }} - {% set arm_match = arms | selectattr('arm_uid', 'equalto', sc.arm_uid) | list %} - {% if arm_match and arm_match[0] and arm_match[0].name %} - {{ arm_match[0].name }} - {% else %} - {{ sc.arm_uid }} - {% endif %} - {{ sc.epoch_name or sc.epoch_uid }}{{ sc.element_name or sc.element_uid }} -
- - -
-
No study cells yet.
- - -
- Transition Rules ({{ transition_rules|length }}) - -
- - - - - -
-
- - - -
-

Matrix

- - - - - {% for v in visits %} - - {% endfor %} - - {% for a in activities %} - - - {% set concepts_list = activity_concepts.get(a.id, []) %} - {% set selected_list = concepts_list %} - {% set selected_codes = concepts_list | map(attribute='code') | list %} - {% set activity_id = a.id %} - {% include 'concepts_cell.html' %} - {% for v in visits %} - {% set raw_status = cell_map.get((v.id, a.id), '') %} - {% set display = 'X' if raw_status == 'X' else '' %} - - {% endfor %} - - {% endfor %} -
ActivityConcepts -
{{ v.name }}
-
Encounter: {{ v.name }}
- {% if v.epoch_id %} - {% set ep = (epochs | selectattr('id','equalto', v.epoch_id) | list) %} - {% if ep and ep[0] %}
Epoch: {{ ep[0].name }}
{% endif %} - {% endif %} -
{{ a.name }}{{ display }}
-

Generate Normalized Summary (JSON)

-
- ⬇ XLSX -
- - -{% endblock %} From 32eba89a42d5497d6284647641340e160e18153a Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:18:00 -0500 Subject: [PATCH 10/21] Aligned export XLSX SoA matrix with that shown in the UI --- src/soa_builder/web/app.py | 118 +++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index 4024c0d..e3fde12 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -1963,6 +1963,47 @@ def _matrix_arrays(soa_id: int): return instance_headers, rows +def _fetch_enriched_instances(soa_id: int): + """Return enriched instance data with all header information for XLSX export.""" + conn = _connect() + cur = conn.cursor() + cur.execute( + """ + SELECT i.id,i.name,i.instance_uid,i.label,i.member_of_timeline, + v.name AS encounter_name,v.label AS encounter_label, + e.name AS epoch_name,e.epoch_label as epoch_label, + tm.window_label,tm.label AS timing_label,tm.name AS timing_name,tm.value AS study_day + FROM instances i + LEFT JOIN visit v ON v.encounter_uid = i.encounter_uid AND v.soa_id = i.soa_id + LEFT JOIN epoch e ON e.epoch_uid = i.epoch_uid AND e.soa_id = i.soa_id + LEFT JOIN timing tm ON tm.id = v.scheduledAtId AND tm.soa_id = v.soa_id + WHERE i.soa_id=? + ORDER BY COALESCE(i.member_of_timeline, 'zzz'), LENGTH(i.instance_uid), i.instance_uid + """, + (soa_id,), + ) + instances = [ + { + "id": r[0], + "name": r[1], + "instance_uid": r[2], + "label": r[3], + "member_of_timeline": r[4], + "encounter_name": r[5], + "encounter_label": r[6], + "epoch_name": r[7], + "epoch_label": r[8], + "window_label": r[9], + "timing_label": r[10], + "timing_name": r[11], + "study_day": r[12], + } + for r in cur.fetchall() + ] + conn.close() + return instances + + # API endpoint for creating new Study/SOA @app.post("/soa") def create_soa(payload: SOACreate): @@ -2724,6 +2765,9 @@ def export_xlsx(soa_id: int, left: Optional[int] = None, right: Optional[int] = except Exception as e: # Provide an error sheet to highlight issue rather than failing entire export concept_diff_df = pd.DataFrame([[str(e)]], columns=["ConceptDiffError"]) + # Fetch enriched instances for header rows + enriched_instances = _fetch_enriched_instances(soa_id) + with pd.ExcelWriter(bio, engine="openpyxl") as writer: study_df.to_excel(writer, index=False, sheet_name="Study") df.to_excel(writer, index=False, sheet_name="SoA") @@ -2731,6 +2775,80 @@ def export_xlsx(soa_id: int, left: Optional[int] = None, right: Optional[int] = audit_df.to_excel(writer, index=False, sheet_name="RollbackAudit") if concept_diff_df is not None: concept_diff_df.to_excel(writer, index=False, sheet_name="ConceptDiff") + + # Add header rows to SoA sheet to match web UI + # workbook = writer.book + worksheet = writer.sheets["SoA"] + + # Insert 6 rows at the top for header rows + worksheet.insert_rows(1, 6) + + # Build header rows + # Row 1: Epoch (with merged cells for consecutive same values) + worksheet.cell(1, 1, "") + worksheet.cell(1, 2, "Epoch:") + col_idx = 3 + epoch_groups = [] # Track (value, start_col, end_col) for merging + prev_epoch = None + start_col = 3 + for i, inst in enumerate(enriched_instances): + epoch_val = inst.get("epoch_label") or inst.get("epoch_name") or "" + if prev_epoch is None: + prev_epoch = epoch_val + start_col = col_idx + elif prev_epoch != epoch_val: + epoch_groups.append((prev_epoch, start_col, col_idx - 1)) + prev_epoch = epoch_val + start_col = col_idx + col_idx += 1 + # Add last group + if prev_epoch is not None: + epoch_groups.append((prev_epoch, start_col, col_idx - 1)) + + # Write and merge epoch cells + for epoch_val, start, end in epoch_groups: + worksheet.cell(1, start, epoch_val) + if start != end: + worksheet.merge_cells( + start_row=1, start_column=start, end_row=1, end_column=end + ) + + # Row 2: Encounter + worksheet.cell(2, 1, "") + worksheet.cell(2, 2, "Encounter:") + for i, inst in enumerate(enriched_instances): + encounter_val = ( + inst.get("encounter_label") or inst.get("encounter_name") or "" + ) + worksheet.cell(2, i + 3, encounter_val) + + # Row 3: Instance (ScheduledActivityInstance) + worksheet.cell(3, 1, "") + worksheet.cell(3, 2, "Instance:") + for i, inst in enumerate(enriched_instances): + instance_val = inst.get("label") or inst.get("name") or "" + worksheet.cell(3, i + 3, instance_val) + + # Row 4: Study Day + worksheet.cell(4, 1, "") + worksheet.cell(4, 2, "Study Day:") + for i, inst in enumerate(enriched_instances): + study_day_val = inst.get("study_day") or "" + worksheet.cell(4, i + 3, study_day_val) + + # Row 5: Timing + worksheet.cell(5, 1, "") + worksheet.cell(5, 2, "Timing:") + for i, inst in enumerate(enriched_instances): + timing_val = inst.get("timing_label") or inst.get("timing_name") or "" + worksheet.cell(5, i + 3, timing_val) + + # Row 6: Visit Window + worksheet.cell(6, 1, "") + worksheet.cell(6, 2, "Visit Window:") + for i, inst in enumerate(enriched_instances): + window_val = inst.get("window_label") or "" + worksheet.cell(6, i + 3, window_val) bio.seek(0) # Dynamic filename pattern: studyid_version.xlsx # Determine study_id and version context From c2ef2525993d38b4b9b03289e7059f0a55262e52 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:24:43 -0500 Subject: [PATCH 11/21] XLSX export matrix now includes a worksheet per timeline --- src/soa_builder/web/app.py | 231 +++++++++++++++++++++++++------------ 1 file changed, 160 insertions(+), 71 deletions(-) diff --git a/src/soa_builder/web/app.py b/src/soa_builder/web/app.py index e3fde12..56bf8a1 100644 --- a/src/soa_builder/web/app.py +++ b/src/soa_builder/web/app.py @@ -2004,6 +2004,77 @@ def _fetch_enriched_instances(soa_id: int): return instances +def _add_header_rows_to_worksheet(worksheet, enriched_instances): + """Add header rows to a worksheet with instance metadata.""" + # Insert 6 rows at the top for header rows + worksheet.insert_rows(1, 6) + + # Build header rows + # Row 1: Epoch (with merged cells for consecutive same values) + worksheet.cell(1, 1, "") + worksheet.cell(1, 2, "Epoch:") + col_idx = 3 + epoch_groups = [] # Track (value, start_col, end_col) for merging + prev_epoch = None + start_col = 3 + for i, inst in enumerate(enriched_instances): + epoch_val = inst.get("epoch_label") or inst.get("epoch_name") or "" + if prev_epoch is None: + prev_epoch = epoch_val + start_col = col_idx + elif prev_epoch != epoch_val: + epoch_groups.append((prev_epoch, start_col, col_idx - 1)) + prev_epoch = epoch_val + start_col = col_idx + col_idx += 1 + # Add last group + if prev_epoch is not None: + epoch_groups.append((prev_epoch, start_col, col_idx - 1)) + + # Write and merge epoch cells + for epoch_val, start, end in epoch_groups: + worksheet.cell(1, start, epoch_val) + if start != end: + worksheet.merge_cells( + start_row=1, start_column=start, end_row=1, end_column=end + ) + + # Row 2: Encounter + worksheet.cell(2, 1, "") + worksheet.cell(2, 2, "Encounter:") + for i, inst in enumerate(enriched_instances): + encounter_val = inst.get("encounter_label") or inst.get("encounter_name") or "" + worksheet.cell(2, i + 3, encounter_val) + + # Row 3: Instance (ScheduledActivityInstance) + worksheet.cell(3, 1, "") + worksheet.cell(3, 2, "Instance:") + for i, inst in enumerate(enriched_instances): + instance_val = inst.get("label") or inst.get("name") or "" + worksheet.cell(3, i + 3, instance_val) + + # Row 4: Study Day + worksheet.cell(4, 1, "") + worksheet.cell(4, 2, "Study Day:") + for i, inst in enumerate(enriched_instances): + study_day_val = inst.get("study_day") or "" + worksheet.cell(4, i + 3, study_day_val) + + # Row 5: Timing + worksheet.cell(5, 1, "") + worksheet.cell(5, 2, "Timing:") + for i, inst in enumerate(enriched_instances): + timing_val = inst.get("timing_label") or inst.get("timing_name") or "" + worksheet.cell(5, i + 3, timing_val) + + # Row 6: Visit Window + worksheet.cell(6, 1, "") + worksheet.cell(6, 2, "Visit Window:") + for i, inst in enumerate(enriched_instances): + window_val = inst.get("window_label") or "" + worksheet.cell(6, i + 3, window_val) + + # API endpoint for creating new Study/SOA @app.post("/soa") def create_soa(payload: SOACreate): @@ -2768,87 +2839,105 @@ def export_xlsx(soa_id: int, left: Optional[int] = None, right: Optional[int] = # Fetch enriched instances for header rows enriched_instances = _fetch_enriched_instances(soa_id) + # Fetch timelines + conn_tl = _connect() + cur_tl = conn_tl.cursor() + cur_tl.execute( + """ + SELECT schedule_timeline_uid,name,main_timeline + FROM schedule_timelines + WHERE soa_id=? + ORDER BY main_timeline DESC, name + """, + (soa_id,), + ) + timelines = [ + { + "schedule_timeline_uid": r[0], + "name": r[1], + "main_timeline": bool(r[2]), + } + for r in cur_tl.fetchall() + ] + conn_tl.close() + + # Group enriched instances by timeline + instances_by_timeline = {} + for inst in enriched_instances: + timeline_key = inst.get("member_of_timeline") or "unassigned" + if timeline_key not in instances_by_timeline: + instances_by_timeline[timeline_key] = [] + instances_by_timeline[timeline_key].append(inst) + with pd.ExcelWriter(bio, engine="openpyxl") as writer: study_df.to_excel(writer, index=False, sheet_name="Study") - df.to_excel(writer, index=False, sheet_name="SoA") mapping_df.to_excel(writer, index=False, sheet_name="ConceptMappings") audit_df.to_excel(writer, index=False, sheet_name="RollbackAudit") if concept_diff_df is not None: concept_diff_df.to_excel(writer, index=False, sheet_name="ConceptDiff") - # Add header rows to SoA sheet to match web UI - # workbook = writer.book - worksheet = writer.sheets["SoA"] - - # Insert 6 rows at the top for header rows - worksheet.insert_rows(1, 6) - - # Build header rows - # Row 1: Epoch (with merged cells for consecutive same values) - worksheet.cell(1, 1, "") - worksheet.cell(1, 2, "Epoch:") - col_idx = 3 - epoch_groups = [] # Track (value, start_col, end_col) for merging - prev_epoch = None - start_col = 3 - for i, inst in enumerate(enriched_instances): - epoch_val = inst.get("epoch_label") or inst.get("epoch_name") or "" - if prev_epoch is None: - prev_epoch = epoch_val - start_col = col_idx - elif prev_epoch != epoch_val: - epoch_groups.append((prev_epoch, start_col, col_idx - 1)) - prev_epoch = epoch_val - start_col = col_idx - col_idx += 1 - # Add last group - if prev_epoch is not None: - epoch_groups.append((prev_epoch, start_col, col_idx - 1)) + # Create a worksheet for each timeline + if timelines: + for timeline in timelines: + timeline_uid = timeline["schedule_timeline_uid"] + timeline_name = timeline["name"] + timeline_instances = instances_by_timeline.get(timeline_uid, []) + + if not timeline_instances: + continue + + # Build matrix data for this timeline + cell_lookup = { + (c["instance_id"], c["activity_id"]): c.get("status", "") + for c in cells + if c.get("instance_id") is not None + and c.get("activity_id") is not None + } + + # Build instance headers for this timeline + instance_headers_tl = [inst["name"] for inst in timeline_instances] + + # Build rows for this timeline + rows_tl = [] + for a in activities: + row = [a["name"]] + for inst in timeline_instances: + row.append(cell_lookup.get((inst["id"], a["id"]), "")) + rows_tl.append(row) - # Write and merge epoch cells - for epoch_val, start, end in epoch_groups: - worksheet.cell(1, start, epoch_val) - if start != end: - worksheet.merge_cells( - start_row=1, start_column=start, end_row=1, end_column=end + # Create DataFrame for this timeline + df_tl = pd.DataFrame( + rows_tl, columns=["Activity"] + instance_headers_tl ) - # Row 2: Encounter - worksheet.cell(2, 1, "") - worksheet.cell(2, 2, "Encounter:") - for i, inst in enumerate(enriched_instances): - encounter_val = ( - inst.get("encounter_label") or inst.get("encounter_name") or "" - ) - worksheet.cell(2, i + 3, encounter_val) - - # Row 3: Instance (ScheduledActivityInstance) - worksheet.cell(3, 1, "") - worksheet.cell(3, 2, "Instance:") - for i, inst in enumerate(enriched_instances): - instance_val = inst.get("label") or inst.get("name") or "" - worksheet.cell(3, i + 3, instance_val) - - # Row 4: Study Day - worksheet.cell(4, 1, "") - worksheet.cell(4, 2, "Study Day:") - for i, inst in enumerate(enriched_instances): - study_day_val = inst.get("study_day") or "" - worksheet.cell(4, i + 3, study_day_val) - - # Row 5: Timing - worksheet.cell(5, 1, "") - worksheet.cell(5, 2, "Timing:") - for i, inst in enumerate(enriched_instances): - timing_val = inst.get("timing_label") or inst.get("timing_name") or "" - worksheet.cell(5, i + 3, timing_val) - - # Row 6: Visit Window - worksheet.cell(6, 1, "") - worksheet.cell(6, 2, "Visit Window:") - for i, inst in enumerate(enriched_instances): - window_val = inst.get("window_label") or "" - worksheet.cell(6, i + 3, window_val) + # Add concepts columns + if len(concepts_strings) == len(df_tl): + df_tl.insert(1, "Concepts", concepts_strings) + df_tl["Concept UIDs"] = concept_titles_strings + + # Sanitize sheet name (max 31 chars, no special chars) + sheet_name = f"SoA - {timeline_name}"[:31] + sheet_name = ( + sheet_name.replace("/", "-") + .replace("\\", "-") + .replace("*", "-") + .replace("?", "-") + .replace(":", "-") + .replace("[", "-") + .replace("]", "-") + ) + + # Write to Excel + df_tl.to_excel(writer, index=False, sheet_name=sheet_name) + + # Add header rows + worksheet_tl = writer.sheets[sheet_name] + _add_header_rows_to_worksheet(worksheet_tl, timeline_instances) + else: + # No timelines, create single SoA sheet as before + df.to_excel(writer, index=False, sheet_name="SoA") + worksheet = writer.sheets["SoA"] + _add_header_rows_to_worksheet(worksheet, enriched_instances) bio.seek(0) # Dynamic filename pattern: studyid_version.xlsx # Determine study_id and version context From 821c15fc00452c262d94a6f67dd650541c52eea7 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:33:46 -0500 Subject: [PATCH 12/21] All timelines now collpase/expand biomedical concepts --- .../web/templates/concepts_cell.html | 23 ++++++++++--------- src/soa_builder/web/templates/edit.html | 1 + 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/soa_builder/web/templates/concepts_cell.html b/src/soa_builder/web/templates/concepts_cell.html index 02b1614..51b1715 100644 --- a/src/soa_builder/web/templates/concepts_cell.html +++ b/src/soa_builder/web/templates/concepts_cell.html @@ -1,8 +1,9 @@ {# Partial for concepts cell in matrix (add/remove only; concepts immutable) #} {% set concept_count = selected_list | length %} - +{% set cell_id_suffix = timeline_context ~ '-' ~ activity_id if timeline_context is defined else activity_id %} + {% if edit %} -
+
@@ -13,12 +14,12 @@
- +
+ {% endblock %} \ No newline at end of file From 198c4e580ef0266b25125541ddcb0b4d234c27a9 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:03:44 -0500 Subject: [PATCH 14/21] Added reorder instances functionality --- src/soa_builder/web/routers/instances.py | 48 +++++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/soa_builder/web/routers/instances.py b/src/soa_builder/web/routers/instances.py index fcb63af..712575d 100644 --- a/src/soa_builder/web/routers/instances.py +++ b/src/soa_builder/web/routers/instances.py @@ -1,8 +1,8 @@ import logging import os -from typing import Optional +from typing import Optional, List -from fastapi import APIRouter, HTTPException, Request, Form +from fastapi import APIRouter, HTTPException, Request, Form, Body from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates @@ -420,3 +420,47 @@ def delete_instance(soa_id: int, instance_id: int): def ui_del_instance(request: Request, soa_id: int, instance_id: int): delete_instance(soa_id, instance_id) return RedirectResponse(url=f"/ui/soa/{int(soa_id)}/instances", status_code=303) + + +# API endpoint to reorder instances +@router.post("/soa/{soa_id}/instances/reorder", response_class=JSONResponse) +def reorder_instances_api( + soa_id: int, + order: List[int] = Body(..., embed=True), # JSON body: {"order":[...]} +): + if not soa_exists(soa_id): + raise HTTPException(404, "SOA not found") + if not order: + raise HTTPException(400, "Order list required") + + conn = _connect() + cur = conn.cursor() + cur.execute( + "SELECT id,name FROM instances WHERE soa_id=? ORDER BY order_index", (soa_id,) + ) + rows = cur.fetchall() + old_order = [r[0] for r in rows] # IDs for API response + id_to_name = {r[0]: r[1] for r in rows} + old_order_names = [r[1] for r in rows] # Names for audit + + cur.execute("SELECT id,name FROM instances WHERE soa_id=?", (soa_id,)) + existing = {r[0] for r in cur.fetchall()} + if set(order) - existing: + conn.close() + raise HTTPException(400, "Order contains invalid instance id") + + for idx, instance_id in enumerate(order, start=1): + cur.execute("UPDATE instances SET order_index=? WHERE id=?", (idx, instance_id)) + conn.commit() + conn.close() + + new_order_names = [id_to_name.get(iid, str(iid)) for iid in order] + + _record_instance_audit( + soa_id, + "reorder", + instance_id=None, + before={"old_order": old_order_names}, + after={"new_order": new_order_names}, + ) + return JSONResponse({"ok": True, "old_order": old_order, "new_order": order}) From 116ebffd6e4f78aa4b2f887b6f703df4053eb20e Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:04:51 -0500 Subject: [PATCH 15/21] Added return to edit page navigation --- src/soa_builder/web/templates/arms.html | 6 +++++ src/soa_builder/web/templates/elements.html | 6 +++++ src/soa_builder/web/templates/encounters.html | 6 +++++ src/soa_builder/web/templates/epochs.html | 24 +++++++++++-------- src/soa_builder/web/templates/rules.html | 6 +++++ .../web/templates/schedule_timelines.html | 6 +++++ src/soa_builder/web/templates/timings.html | 6 +++++ 7 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/soa_builder/web/templates/arms.html b/src/soa_builder/web/templates/arms.html index 0a7df12..a15dddf 100644 --- a/src/soa_builder/web/templates/arms.html +++ b/src/soa_builder/web/templates/arms.html @@ -2,6 +2,12 @@ {% block content %}

Arms for SoA {{ soa_id }}

+
+ + ← Return to Edit Page + +
+
diff --git a/src/soa_builder/web/templates/elements.html b/src/soa_builder/web/templates/elements.html index ea4ccde..d1f56e6 100644 --- a/src/soa_builder/web/templates/elements.html +++ b/src/soa_builder/web/templates/elements.html @@ -2,6 +2,12 @@ {% block content %}

Elements for SoA {{ soa_id }}

+
+ + ← Return to Edit Page + +
+
diff --git a/src/soa_builder/web/templates/encounters.html b/src/soa_builder/web/templates/encounters.html index cd6808c..0c526d4 100644 --- a/src/soa_builder/web/templates/encounters.html +++ b/src/soa_builder/web/templates/encounters.html @@ -2,6 +2,12 @@ {% block content %}

Encounters for SoA {{ soa_id }}

+ +
diff --git a/src/soa_builder/web/templates/epochs.html b/src/soa_builder/web/templates/epochs.html index 8c963d9..7ca1648 100644 --- a/src/soa_builder/web/templates/epochs.html +++ b/src/soa_builder/web/templates/epochs.html @@ -2,6 +2,12 @@ {% block content %}

Epochs for SoA {{ soa_id }}

+ +
@@ -39,7 +45,14 @@

Epochs for SoA {{ soa_id }}

Type Save Delete - Reorder + + + {% for e in epochs %} @@ -79,15 +92,6 @@

Epochs for SoA {{ soa_id }}

{% endfor %} -
- -
- From 67f245c47d74f65c74168657171f810353b95a18 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:49:04 -0500 Subject: [PATCH 19/21] Update src/soa_builder/web/templates/concepts_cell.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/soa_builder/web/templates/concepts_cell.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/soa_builder/web/templates/concepts_cell.html b/src/soa_builder/web/templates/concepts_cell.html index 51b1715..bdf2cc7 100644 --- a/src/soa_builder/web/templates/concepts_cell.html +++ b/src/soa_builder/web/templates/concepts_cell.html @@ -1,6 +1,6 @@ {# Partial for concepts cell in matrix (add/remove only; concepts immutable) #} {% set concept_count = selected_list | length %} -{% set cell_id_suffix = timeline_context ~ '-' ~ activity_id if timeline_context is defined else activity_id %} +{% set cell_id_suffix = timeline_context ~ '-' ~ activity_id if timeline_context is defined else 'soa-' ~ soa_id ~ '-act-' ~ activity_id %} {% if edit %} From de0b06d7c3054a84f223c745ae288e92316addb9 Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:49:27 -0500 Subject: [PATCH 20/21] Update src/soa_builder/web/migrate_database.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/soa_builder/web/migrate_database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/soa_builder/web/migrate_database.py b/src/soa_builder/web/migrate_database.py index ae2fa28..ae7eaa9 100644 --- a/src/soa_builder/web/migrate_database.py +++ b/src/soa_builder/web/migrate_database.py @@ -982,7 +982,7 @@ def _migrate_matrix_cells_add_instance_id(): def _migrate_activity_concept_add_href(): - """Add href column to store value for API from which the codeSystem and codeSystemVersion USDM properties can be derived""" + """Add href column to store the API URI from which codeSystem and codeSystemVersion USDM properties can be derived""" try: conn = _connect() cur = conn.cursor() From ac25f173e66e09191446b7a5b72605a70e6fc49b Mon Sep 17 00:00:00 2001 From: Darren <3921919+pendingintent@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:50:26 -0500 Subject: [PATCH 21/21] Update src/soa_builder/web/templates/instances.html Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/soa_builder/web/templates/instances.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/soa_builder/web/templates/instances.html b/src/soa_builder/web/templates/instances.html index 2b875c8..0d376f2 100644 --- a/src/soa_builder/web/templates/instances.html +++ b/src/soa_builder/web/templates/instances.html @@ -154,7 +154,7 @@

Scheduled Activity Instances for SoA {{ soa_id }}

{% else %} - No instances yet. + No instances yet. {% endfor %}