refactor: rename pages directory to views

Renames the `app/pages` module to `app/views` to better reflect the
purpose of the directory, aligning with conventional MVC naming
conventions where view-related logic is housed under `views`.
This commit is contained in:
2026-06-10 09:29:02 -04:00
parent faa7d20b3e
commit 253ff38118
6 changed files with 0 additions and 0 deletions

135
app/views/versions.py Normal file
View File

@@ -0,0 +1,135 @@
"""Version history tab — list, diff, save, restore."""
from __future__ import annotations
import streamlit as st
from app.pages._helpers import safe
from core.tei_client import TEIClient
def _flatten_values(values: list[dict]) -> dict[str, dict]:
"""Index a values list by field_key for easy diffing."""
return {v.get("field_key", ""): v for v in values}
def _diff_rows(a: dict[str, dict], b: dict[str, dict]) -> list[dict]:
"""Return one row per field with side-by-side year values."""
keys = sorted(set(a.keys()) | set(b.keys()))
rows: list[dict] = []
def _years_of(v: dict) -> dict:
"""Accept both friendly (year_values) and wire (nested years) shapes."""
if isinstance(v.get("year_values"), dict):
return {str(k): val for k, val in v["year_values"].items()}
if isinstance(v.get("years"), dict):
return {
str(k): (cell or {}).get("value")
for k, cell in v["years"].items()
}
if v.get("value") is not None:
return {"1": v["value"]}
return {}
for k in keys:
av = a.get(k, {}) or {}
bv = b.get(k, {}) or {}
ay = _years_of(av)
by = _years_of(bv)
years = sorted(set(ay.keys()) | set(by.keys()), key=lambda x: int(x))
for y in years:
a_val = float(ay.get(y) or 0)
b_val = float(by.get(y) or 0)
if abs(a_val - b_val) < 1e-9:
continue
rows.append(
{
"field_key": k,
"year": y,
"left": a_val,
"right": b_val,
"delta": b_val - a_val,
}
)
return rows
def render(client: TEIClient, tool: dict) -> None:
st.header("🕒 Versions")
public_id = tool["id"]
versions = safe(client.list_versions, public_id) or []
versions = sorted(
versions, key=lambda v: int(v.get("version_number") or 0), reverse=True
)
# Save new version
with st.expander(" Save current state as a new version", expanded=not versions):
note = st.text_area(
"Version note",
placeholder=(
"What changed? E.g. 'CFO confirmed 1.8M contacts/month; "
"raised legacy license cost from $160 to $180/agent.'"
),
)
if st.button("💾 Save version", disabled=not note.strip()):
result = safe(client.save_version, public_id, note.strip())
if result:
st.success(
f"Saved version {result.get('version_number', '?')}."
)
st.rerun()
if not versions:
st.info("No versions saved yet.")
return
# Listing
st.subheader("History")
rows = []
for v in versions:
snap = v.get("summary_snapshot") or v.get("summary") or {}
rows.append(
{
"Version": v.get("version_number"),
"Date": v.get("created_at") or v.get("date"),
"NPV": float(snap.get("net_present_value") or snap.get("npv") or 0),
"ROI %": float(
snap.get("roi_percentage")
or snap.get("roi")
or snap.get("roi_pct")
or 0
),
"Note": v.get("note", ""),
}
)
st.dataframe(rows, use_container_width=True, hide_index=True)
# Compare two versions
st.subheader("Compare")
if len(versions) < 2:
st.caption("Save two or more versions to compare.")
return
labels = {f"v{v['version_number']}{v.get('note', '')[:40]}": v for v in versions}
keys = list(labels.keys())
c1, c2 = st.columns(2)
with c1:
left_label = st.selectbox("Left (older)", keys, index=min(1, len(keys) - 1))
with c2:
right_label = st.selectbox("Right (newer)", keys, index=0)
if left_label == right_label:
st.caption("Pick two different versions to see a diff.")
return
left = safe(client.get_version, public_id, labels[left_label]["version_number"])
right = safe(client.get_version, public_id, labels[right_label]["version_number"])
if not (left and right):
return
a_values = left.get("values_snapshot") or left.get("values") or []
b_values = right.get("values_snapshot") or right.get("values") or []
diff = _diff_rows(_flatten_values(a_values), _flatten_values(b_values))
if not diff:
st.success("No value differences between these versions.")
else:
st.dataframe(diff, use_container_width=True, hide_index=True)