File size: 8,548 Bytes
c14ceee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""harness/views.py β€” saved views + spawned dashboards: the store and the RE-RUNNER (OM-3 viewer).

The Analyst's save_view/compose_dashboard tools persist chart SPECS + their semantic QUERY (never
rows) into data/store/views.json. This module is the read side: load the specs, re-execute each
view's governed query against the tenant store at render time, and hand the page a fresh artifact
dict in exactly the shape `_render_analyst_artifact` already renders. The viewer therefore shows
LIVE (store-fresh) numbers, not the numbers from whenever the view was saved β€” same discipline as
every dashboard (a saved view is a query, not a snapshot).

Legacy note: views saved before 2026-07-11 carry no 'query' (only a session-scoped result_id) and
cannot be re-run β€” run_view raises a readable error the page surfaces per view.
"""
import json
import time

import harness.semantic as SEM
from harness.tools import VIEWS_PATH

_QUERY_KEYS = ("topic", "measures", "group_by", "grain", "date_from", "date_to",
               "team_id", "filters", "sort", "limit", "exclude_services")


def load():
    if VIEWS_PATH.exists():
        return json.loads(VIEWS_PATH.read_text(encoding="utf-8"))
    return {"views": {}, "dashboards": {}}


def _save(d):
    VIEWS_PATH.parent.mkdir(parents=True, exist_ok=True)
    VIEWS_PATH.write_text(json.dumps(d, indent=1), encoding="utf-8")


def views():
    return load().get("views", {})


def dashboards():
    return load().get("dashboards", {})


def delete_view(name):
    d = load()
    d.get("views", {}).pop(name, None)
    for dash in d.get("dashboards", {}).values():
        dash["views"] = [v for v in dash.get("views", []) if v != name]
    _save(d)


def delete_dashboard(name):
    d = load()
    d.get("dashboards", {}).pop(name, None)
    _save(d)


# ------------------------------------------------------------------ UPDATE (owner directive
# 2026-07-12: the Analyst can CREATE **and UPDATE** each workbook in the workspace)

_QUERY_PATCH_KEYS = ("measures", "group_by", "grain", "date_from", "date_to", "team_id",
                     "filters", "sort", "limit", "exclude_services", "topic")
_SPEC_PATCH_KEYS = ("title", "kind", "x", "y", "series", "metric",
                    "y2", "size", "value", "facet", "columns", "transforms")


def update_view(name, changes):
    """Patch a saved view's chart spec and/or query, VALIDATED BY RE-EXECUTION before saving β€”
    an update that cannot run does not land. `changes` may contain spec keys (title/kind/x/y/
    series/metric) and/or a `query` dict of query-key patches (a None value REMOVES the key)."""
    d = load()
    v = d.get("views", {}).get(name)
    if not v:
        raise SEM.ModelError(f"unknown view {name!r} (use list_workspace)")
    spec = dict(v.get("chart") or {})
    q = dict(spec.get("query") or {})
    if not q:
        raise SEM.ModelError(f"view {name!r} carries no query β€” rebuild it instead")
    changes = dict(changes or {})
    qpatch = changes.pop("query", None) or {}
    bad = [k for k in changes if k not in _SPEC_PATCH_KEYS]
    bad += [k for k in qpatch if k not in _QUERY_PATCH_KEYS]
    if bad:
        raise SEM.ModelError(f"unknown patch keys {bad} (spec: {list(_SPEC_PATCH_KEYS)}; "
                             f"query: {list(_QUERY_PATCH_KEYS)})")
    for k, val in qpatch.items():
        if val is None:
            q.pop(k, None)
        else:
            q[k] = val
    res = _run_query(q)                                  # validation: it must RUN
    new_spec = {**spec, **changes, "query": q}
    rows = res["rows"]
    if new_spec.get("transforms"):                       # the transform chain must replay too
        import harness.transforms as TR
        rows, _ = TR.apply({**res, "query": q}, new_spec["transforms"], run_query=_run_query)
    cols = set(rows[0]) if rows else set()
    for ref in ("x", "y", "series", "y2", "size", "value", "facet"):
        if new_spec.get(ref) and cols and new_spec[ref] not in cols:
            raise SEM.ModelError(f"{ref}={new_spec[ref]!r} not in the patched result columns "
                                 f"{sorted(cols)} β€” patch {ref} too")
    d["views"][name] = {"chart": new_spec,
                        "saved_at": time.strftime("%Y-%m-%d %H:%M")}
    _save(d)
    return {"updated": name, "row_count": res["row_count"],
            "query": q, "note": "patched query re-ran successfully before saving"}


def update_dashboard(name, views_list=None, new_name=None):
    """Update a workbook (dashboard): recompose its views (order = display order) and/or rename
    it. Renames follow through to scheduled reports that reference it."""
    d = load()
    dash = d.get("dashboards", {}).get(name)
    if dash is None:
        raise SEM.ModelError(f"unknown dashboard {name!r} (use list_workspace)")
    if views_list is not None:
        missing = [v for v in views_list if v not in d.get("views", {})]
        if missing:
            raise SEM.ModelError(f"unknown views {missing} β€” save_view them first")
        dash["views"] = list(views_list)
    dash["updated_at"] = time.strftime("%Y-%m-%d %H:%M")
    if new_name and new_name != name:
        if new_name in d.get("dashboards", {}) or new_name in d.get("views", {}):
            raise SEM.ModelError(f"{new_name!r} already exists β€” pick another name")
        d["dashboards"][new_name] = d["dashboards"].pop(name)
        try:                                             # follow the rename into schedules
            import harness.routines as R
            rt = R.load()
            if name in rt.get("reports", {}):
                rt["reports"][new_name] = rt["reports"].pop(name)
                R._save(rt)
        except Exception:
            pass
        name = new_name
    _save(d)
    return {"dashboard": name, "views": d["dashboards"][name]["views"]}


def _run_query(q):
    return SEM.store_query(**{k: q.get(k) for k in _QUERY_KEYS if q.get(k) is not None})


def run_view(name):
    """Re-execute one saved view's governed query β€” then REPLAY its recorded transform chain β€”
    and return a fresh artifact dict ({'chart': {..., 'rows': [...]}}, {'table': {...}} or
    {'kpi': {...}}) for the house renderer."""
    v = views().get(name)
    if not v:
        raise SEM.ModelError(f"unknown view {name!r}")
    spec = v.get("chart") or {}
    q = spec.get("query")
    if not q:
        raise SEM.ModelError(f"view {name!r} was saved without its query (pre-2026-07-11) β€” "
                             "ask the Analyst to rebuild and re-save it")
    res = _run_query(q)
    if spec.get("kind") == "kpi" or (spec.get("metric") and spec.get("kind") != "table"):
        metric = spec.get("metric") or spec.get("y")       # KPI-shaped view
        val = (res["rows"][0].get(metric) if res["rows"] else None)
        kpi = {"metric": metric, "value": val or 0}
        cq = spec.get("compare_query")
        if cq:
            prev_rows = _run_query(cq)["rows"]
            prev = prev_rows[0].get(metric) if prev_rows else None
            if prev:
                kpi["delta_pct"] = ((val or 0) - prev) / abs(prev)
        return {"kpi": kpi}
    rows = res["rows"]
    if spec.get("transforms"):                             # a saved view replays its transforms
        import harness.transforms as TR
        rows, _ = TR.apply({**res, "query": q}, spec["transforms"], run_query=_run_query)
    if spec.get("kind") == "table":
        return {"table": {**{k: spec.get(k) for k in ("kind", "title", "columns")},
                          "rows": rows}}
    return {"chart": {**{k: spec.get(k) for k in ("kind", "x", "y", "series", "title",
                                                  "y2", "size", "value", "facet")},
                      "rows": rows}}


def run_dashboard(name):
    """All of a dashboard's views, freshly re-queried. Returns [(view_name, artifact|None, err)]."""
    dash = dashboards().get(name)
    if dash is None:
        raise SEM.ModelError(f"unknown dashboard {name!r}")
    out = []
    for vn in dash.get("views", []):
        try:
            out.append((vn, run_view(vn), None))
        except Exception as e:
            out.append((vn, None, str(e)[:200]))
    return out


def store_freshness():
    """Newest sync checkpoint across entities β€” the staleness footer ('data as of …')."""
    try:
        import harness.datastore as DS
        ts = [s.get("updated") for s in DS.status().values() if s.get("updated")]
        return max(ts) if ts else None
    except Exception:
        return None