tui/read.bend

cswap-bend-cli@0.1.0.0 · 265 lines · 52 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
4import cswap-bend-cli@0.1.0.0/tui/model.bend as M
5import cswap-bend-cli@0.1.0.0/tui/num.bend as N
6
7# Building a Snap from the text of the files the dashboard follows. Pure: the
8# caller reads the files, the clock and the daemon's liveness.
9#
10# The daemon's status file (<state>/daemon-status.json, src/daemon/status.bend)
11# has these fields, named here and nowhere else:
12
13def f_pid() -> String:
14  "pid"
15
16def f_next_poll() -> String:
17  "next_poll_at"
18
19def f_active() -> String:
20  "active"
21
22def f_accounts() -> String:
23  "accounts"
24
25def f_last_error() -> String:
26  "last_error"
27
28def f_backoff() -> String:
29  "backoff_until"
30
31# ---- windows -----------------------------------------------------------------------------
32
33def win_pct(+label: String, +w: Json.Json, +p: Maybe<&2, U32>) -> Maybe<&2, M.Win>:
34  match p:
35    case None{}:
36      None{}
37    case Some{c}:
38      Some{M.Win{label, p, N.iso_at("resets_at", w)}}
39
40# One window object of cache/usage.json: {"pct": 47.0, "resets_at": "<ISO>"}.
41def win_of(+label: String, +w: Json.Json) -> Maybe<&2, M.Win>:
42  win_pct(label, w, N.centi_at("pct", w))
43
44def win_opt(+label: String, +m: Maybe<&2, Json.Json>) -> Maybe<&2, M.Win>:
45  match m:
46    case None{}:
47      None{}
48    case Some{w}:
49      win_of(label, w)
50
51def win_key(+label: String, +key: String, +good: Json.Json) -> Maybe<&2, M.Win>:
52  win_opt(label, Json.get(key, good))
53
54def cons_win(+m: Maybe<&2, M.Win>, +rest: List<&2, M.Win>) -> List<&2, M.Win>:
55  match m:
56    case None{}:
57      rest
58    case Some{w}:
59      Con{w, rest}
60
61def scoped_win(+w: Json.Json) -> Maybe<&2, M.Win>:
62  win_of(N.str_at("name", w), w)
63
64def scoped(+items: List<&2, Json.Json>) -> List<&2, M.Win>:
65  match items:
66    case Nil{}:
67      Nil{}
68    case Con{w, t}:
69      cons_win(scoped_win(w), scoped(t))
70
71# The windows of a lastGood object, in the reference's order: 5h, 7d, then the
72# per-model windows.
73def wins_of(+good: Json.Json) -> List<&2, M.Win>:
74  cons_win(win_key("5h", "five_hour", good), cons_win(win_key("7d", "seven_day", good), scoped(Json.to_list(N.get_or_null("scoped", good)))))
75
76# ---- one account --------------------------------------------------------------------------
77
78def age_of(+fetched: Maybe<&2, U32>, +now: U32) -> Maybe<&2, U32>:
79  match fetched:
80    case None{}:
81      None{}
82    case Some{t}:
83      Some{N.sel(U32.is_gt(now, t), (now - t : U32), 0)}
84
85def api_key(+rec: Json.Json) -> Bool:
86  String.eq(N.str_at("kind", rec), "api_key")
87
88def non_empty_pick(+blank: Bool, +s: String) -> Maybe<&2, String>:
89  match blank:
90    case True{}:
91      None{}
92    case False{}:
93      Some{s}
94
95def non_empty(+s: String) -> Maybe<&2, String>:
96  non_empty_pick(String.is_empty(s), s)
97
98def first_text(+a: String, +b: String) -> String:
99  N.str_or(non_empty(a), b)
100
101def error_of(+row: Json.Json, +st: Json.Json) -> String:
102  first_text(N.str_at("lastError", row), N.str_at(f_last_error(), st))
103
104def note_pick(+key: Bool, +err: String) -> String:
105  match key:
106    case True{}:
107      "API key (no quota)"
108    case False{}:
109      err
110
111def note_of(+rec: Json.Json, +row: Json.Json, +st: Json.Json, +wins: List<&2, M.Win>) -> String:
112  note_pick(api_key(rec), error_of(row, st))
113
114def flag_of(+m: Maybe<&2, Json.Json>) -> Bool:
115  match m:
116    case Some{Json.JBool{b}}:
117      b
118    case _:
119      False{}
120
121# ---- every account ----------------------------------------------------------------------------
122
123def flag(+rec: Json.Json, +key: String) -> Bool:
124  flag_of(Json.get(key, rec))
125
126def acct_with(+slot: String, +rec: Json.Json, +row: Json.Json, +st: Json.Json, +active: Bool, +now: U32, +wins: List<&2, M.Win>) -> M.Acct:
127  M.Acct{slot, N.str_at("email", rec), N.str_at("alias", rec), SSeq.org_tag(rec), active, flag(rec, "disabled"), wins, age_of(N.secs_at("fetchedAt", row), now), note_of(rec, row, st, wins)}
128
129def acct_of(+slot: String, +rec: Json.Json, +row: Json.Json, +st: Json.Json, +active: Bool, +now: U32) -> M.Acct:
130  acct_with(slot, rec, row, st, active, now, wins_of(N.get_or_null("lastGood", row)))
131
132def is_active(+slot: String, +active: Maybe<&2, U32>) -> Bool:
133  match active:
134    case None{}:
135      False{}
136    case Some{n}:
137      String.eq(slot, U32.show(n))
138
139def first_some(+a: Maybe<&2, U32>, +b: Maybe<&2, U32>) -> Maybe<&2, U32>:
140  match a:
141    case None{}:
142      b
143    case Some{x}:
144      a
145
146# ---- the header --------------------------------------------------------------------------------
147
148# The live slot: the daemon's view first, else the roster's advisory field.
149def active_slot(+status: Json.Json, +seq: Json.Json) -> Maybe<&2, U32>:
150  first_some(N.uint_at(f_active(), status), N.uint_at("activeAccountNumber", seq))
151
152def settings_block(+settings: Json.Json) -> Json.Json:
153  N.get_or_null("autoswitch", settings)
154
155def whole(+m: Maybe<&2, U32>) -> Maybe<&2, U32>:
156  match m:
157    case None{}:
158      None{}
159    case Some{c}:
160      Some{(c / 100 : U32)}
161
162def f_config() -> String:
163  "config"
164
165def f_interval() -> String:
166  "interval_seconds"
167
168def first_u(+a: Maybe<&2, U32>, +b: Maybe<&2, U32>) -> Maybe<&2, U32>:
169  first_some(a, b)
170
171# The running daemon's own config (its status file) wins while it is alive;
172# a stopped daemon, or an older status file without one, reads settings.json.
173def pick_j(+live: Bool, +j: Json.Json) -> Json.Json:
174  match live:
175    case True{}:
176      j
177    case False{}:
178      Json.JNull{}
179
180def daemon_cfg(+live: Bool, +status: Json.Json) -> Json.Json:
181  pick_j(live, N.get_or_null(f_config(), status))
182
183def thr_of(+live: Bool, +status: Json.Json, +settings: Json.Json) -> Maybe<&2, U32>:
184  first_u(N.centi_at("threshold", daemon_cfg(live, status)), N.centi_at("threshold", settings_block(settings)))
185
186def every_of(+live: Bool, +status: Json.Json, +settings: Json.Json) -> Maybe<&2, U32>:
187  first_u(N.uint_at(f_interval(), daemon_cfg(live, status)), whole(N.centi_at("intervalSeconds", settings_block(settings))))
188
189def hdr_of(+version: String, +live: Bool, +status: Json.Json, +settings: Json.Json) -> M.Hdr:
190  M.Hdr{version, live, N.uint_at(f_pid(), status), thr_of(live, status, settings), every_of(live, status, settings), N.iso_at(f_next_poll(), status)}
191
192# ---- the log ----------------------------------------------------------------------------------
193
194# "12:03:11  info tick no-action": the time of day of ts, level, msg, outcome.
195def clock_part(+ts: String) -> String:
196  String.take(String.drop(ts, 11n), 8n)
197
198def join_pick(+empty: Bool, +a: String, +b: String) -> String:
199  match empty:
200    case True{}:
201      a
202    case False{}:
203      a ++ " " ++ b
204
205def join_sp(+a: String, +b: String) -> String:
206  join_pick(String.is_empty(b), a, b)
207
208def log_line(+j: Json.Json) -> String:
209  join_sp(join_sp(join_sp(clock_part(N.str_at("ts", j)) ++ " ", N.str_at("level", j)), N.str_at("msg", j)), N.str_at("outcome", j))
210
211def log_pick(+j: Json.Json, +line: String) -> String:
212  match j:
213    case Json.JObj{f}:
214      log_line(j)
215    case _:
216      String.trim(line)
217
218def log_text(+line: String) -> String:
219  log_pick(N.parse_text(line), line)
220
221def keep_line(+blank: Bool, +l: String, +rest: List<&2, String>) -> List<&2, String>:
222  match blank:
223    case True{}:
224      rest
225    case False{}:
226      Con{log_text(l), rest}
227
228# ---- the snapshot ------------------------------------------------------------------------------
229
230def log_lines(+ls: List<&2, String>) -> List<&2, String>:
231  match ls:
232    case Nil{}:
233      Nil{}
234    case Con{l, t}:
235      keep_line(String.is_empty(String.trim(l)), l, log_lines(t))
236
237def acct_opt(+slot: String, +rec: Maybe<&2, Json.Json>, +cache: Json.Json, +status: Json.Json, +active: Maybe<&2, U32>, +now: U32) -> Maybe<&2, M.Acct>:
238  match rec:
239    case None{}:
240      None{}
241    case Some{r}:
242      Some{acct_of(slot, r, N.get_or_null(slot, N.get_or_null(f_accounts(), cache)), N.get_or_null(slot, N.get_or_null(f_accounts(), status)), is_active(slot, active), now)}
243
244def acct_cons(+m: Maybe<&2, M.Acct>, +rest: List<&2, M.Acct>) -> List<&2, M.Acct>:
245  match m:
246    case None{}:
247      rest
248    case Some{a}:
249      Con{a, rest}
250
251def accts(+nums: List<&2, U32>, +seq: Json.Json, +cache: Json.Json, +status: Json.Json, +active: Maybe<&2, U32>, +now: U32) -> List<&2, M.Acct>:
252  match nums:
253    case Nil{}:
254      Nil{}
255    case Con{n, t}:
256      acct_cons(acct_opt(U32.show(n), Json.get(U32.show(n), SSeq.accounts_of(seq)), cache, status, active, now), accts(t, seq, cache, status, active, now))
257
258def snap_of(+version: String, +live: Bool, +now: U32, +off: U32, +seq: Json.Json, +cache: Json.Json, +status: Json.Json, +settings: Json.Json, +log_t: String) -> M.Snap:
259  M.Snap{now, off, hdr_of(version, live, status, settings), accts(SSeq.seq_nums(seq), seq, cache, status, active_slot(status, seq), now), log_lines(String.lines(log_t))}
260
261# Everything the dashboard shows, from the text of sequence.json,
262# cache/usage.json, daemon-status.json, settings.json and the tail of
263# daemon.log. Missing or unparseable files read as empty.
264def snap(+version: String, +live: Bool, +now: U32, +off: U32, +seq_t: String, +cache_t: String, +status_t: String, +settings_t: String, +log_t: String) -> M.Snap:
265  snap_of(version, live, now, off, N.parse_text(seq_t), N.parse_text(cache_t), N.parse_text(status_t), N.parse_text(settings_t), log_t)