@mxpicture/tesla-superchargers-http 0.1.64 → 0.1.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@
18
18
  * POST /notify-failures body: { threshold? }
19
19
  * POST /notify-unmatched-invoices
20
20
  * GET / (the web UI — a static page driving the above)
21
+ * GET /favourites (the web UI — the charger favourites table)
22
+ * GET /additional-costs (the web UI — the additional-costs table)
21
23
  * GET /health (unauthenticated liveness probe)
22
24
  *
23
25
  * The database connection comes from the standard `PG*` environment variables;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CACX,qEAAqE,CACtE,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;AACnD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,QAAQ,GAAG,GAAS,EAAE;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC;AAC1C,IAAI,CAAC,KAAK,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CACX,qEAAqE,CACtE,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;AACnD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC;AAElE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC,CAAC;AAE5E,qEAAqE;AACrE,MAAM,QAAQ,GAAG,GAAS,EAAE;IAC1B,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EAEZ,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,YAAY,CAAC;AAM1D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAuED,mFAAmF;AACnF,eAAO,MAAM,YAAY,GAAI,MAAM,aAAa,KAAG,MAmB/C,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,MAAM,EAEZ,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,eAAe,EAAU,MAAM,YAAY,CAAC;AAqB1D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAoED,mFAAmF;AACnF,eAAO,MAAM,YAAY,GAAI,MAAM,aAAa,KAAG,MAmB/C,CAAC"}
package/dist/server.js CHANGED
@@ -1,9 +1,23 @@
1
1
  import { createServer as createHttpServer, } from "node:http";
2
2
  import { isAuthorized } from "./auth.js";
3
3
  import { HttpError } from "./errors.js";
4
- import { UI_HTML } from "./ui.js";
4
+ import { ADDITIONAL_COSTS_HTML } from "./ui/additional-costs.js";
5
+ import { COMMANDS_HTML } from "./ui/commands.js";
6
+ import { FAVOURITES_HTML } from "./ui/favourites.js";
5
7
  /** Reject bodies larger than this (bytes) before buffering them. */
6
8
  const MAX_BODY_BYTES = 1_000_000;
9
+ /**
10
+ * The web UI: a static, secret-free page per path. Served unauthenticated —
11
+ * behind Caddy the SSO portal gates the browser before it reaches here, and a
12
+ * page's own command calls carry the Caddy-injected bearer token; the pages
13
+ * hold nothing sensitive on their own.
14
+ */
15
+ const PAGES = {
16
+ "/": COMMANDS_HTML,
17
+ "/index.html": COMMANDS_HTML,
18
+ "/favourites": FAVOURITES_HTML,
19
+ "/additional-costs": ADDITIONAL_COSTS_HTML,
20
+ };
7
21
  const sendJson = (res, status, payload) => {
8
22
  res.writeHead(status, { "content-type": "application/json" });
9
23
  res.end(JSON.stringify(payload ?? null));
@@ -36,13 +50,10 @@ const handle = async (req, res, opts) => {
36
50
  // unauthenticated liveness probe
37
51
  if (req.method === "GET" && url.pathname === "/health")
38
52
  return sendJson(res, 200, { status: "ok" });
39
- // The web UI (a static, secret-free shell). Served unauthenticated: behind
40
- // Caddy the SSO portal gates the browser before it reaches here, and its own
41
- // command calls carry the Caddy-injected bearer token; the page holds nothing
42
- // sensitive on its own.
43
- if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
53
+ const page = req.method === "GET" ? PAGES[url.pathname] : undefined;
54
+ if (page !== undefined) {
44
55
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
45
- res.end(UI_HTML);
56
+ res.end(page);
46
57
  return;
47
58
  }
48
59
  if (!isAuthorized(req.headers.authorization, opts.token))
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The additional-costs page, served at `GET /additional-costs`.
3
+ *
4
+ * The costs TeslaMate never sees — service, repairs, tyres, insurance, tax,
5
+ * subsidies — added, edited inline and deleted straight from the table.
6
+ *
7
+ * Its endpoints are not command cards on `/` either: the generic grid's
8
+ * confirm-on-every-POST would fire on every inline save.
9
+ */
10
+ export declare const ADDITIONAL_COSTS_HTML: string;
11
+ //# sourceMappingURL=additional-costs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"additional-costs.d.ts","sourceRoot":"","sources":["../../src/ui/additional-costs.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,QAiRhC,CAAC"}
@@ -0,0 +1,286 @@
1
+ import { COST_CATEGORIES, COST_RECURRENCES, } from "@mxpicture/tesla-superchargers-api/types";
2
+ import { renderPage } from "./shell.js";
3
+ /**
4
+ * The additional-costs page, served at `GET /additional-costs`.
5
+ *
6
+ * The costs TeslaMate never sees — service, repairs, tyres, insurance, tax,
7
+ * subsidies — added, edited inline and deleted straight from the table.
8
+ *
9
+ * Its endpoints are not command cards on `/` either: the generic grid's
10
+ * confirm-on-every-POST would fire on every inline save.
11
+ */
12
+ export const ADDITIONAL_COSTS_HTML = renderPage({
13
+ title: "Additional costs · Tesla Superchargers",
14
+ nav: "/additional-costs",
15
+ body: `<section id="addcosts">
16
+ <div class="card">
17
+ <div class="title"><h2>Additional costs</h2></div>
18
+ <p class="desc">
19
+ Costs TeslaMate never sees — service, repairs, tyres, insurance, tax, subsidies.
20
+ A monthly cost is one entry: its date is the first month, its end date the last
21
+ (leave it empty while it still runs) and its cost the amount per month.
22
+ </p>
23
+ <div class="ac-wrap">
24
+ <table class="fav ac">
25
+ <thead>
26
+ <tr>
27
+ <th>Car</th>
28
+ <th>Category</th>
29
+ <th>Date</th>
30
+ <th>Description</th>
31
+ <th class="num">Cost (€)</th>
32
+ <th></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody id="ac-form" class="form"></tbody>
36
+ <tbody id="ac-body"></tbody>
37
+ </table>
38
+ </div>
39
+ <pre id="ac-status" class="result" hidden></pre>
40
+ </div>
41
+ </section>`,
42
+ script: `
43
+ // Deliberately no filter, no sort control and no pager: this is a handful
44
+ // of rows per car per year.
45
+
46
+ // Interpolated from the generated enums, so the DDL's own value lists are
47
+ // what the dropdowns offer — the same values the endpoints validate against.
48
+ var AC_CATEGORIES = ${JSON.stringify(COST_CATEGORIES)};
49
+ var AC_RECURRENCES = ${JSON.stringify(COST_RECURRENCES)};
50
+
51
+ var acForm = document.getElementById("ac-form");
52
+ var acBody = document.getElementById("ac-body");
53
+ var acStatus = document.getElementById("ac-status");
54
+
55
+ var acRows = []; // every stored entry
56
+ var acCars = []; // TeslaMate's cars, for the picker and the row labels
57
+
58
+ function acFail(res, text) {
59
+ if (res.status === 401) show(acStatus, "err", "401 Unauthorized — sign in via the SSO portal (running locally, the command endpoints need the bearer token).");
60
+ else show(acStatus, "err", res.status + " — " + pretty(text));
61
+ }
62
+
63
+ function acSend(url, body, btn, done) {
64
+ show(acStatus, "running", "Saving…");
65
+ fetch(url, {
66
+ method: "POST",
67
+ headers: { "content-type": "application/json" },
68
+ body: JSON.stringify(body),
69
+ }).then(function (res) {
70
+ return res.text().then(function (text) {
71
+ if (res.ok) { acStatus.hidden = true; done(JSON.parse(text)); }
72
+ else acFail(res, text);
73
+ });
74
+ }).catch(function (e) {
75
+ show(acStatus, "err", "Request failed: " + (e && e.message ? e.message : e));
76
+ }).then(function () { btn.disabled = false; });
77
+ }
78
+
79
+ function carLabel(id) {
80
+ for (var i = 0; i < acCars.length; i++)
81
+ if (acCars[i].id === id) return acCars[i].name || ("Car #" + acCars[i].id);
82
+ return "Car #" + id; // a car TeslaMate no longer has; nothing links them
83
+ }
84
+
85
+ // The five editable cells, shared by the add row and an inline edit: the
86
+ // same inputs, populated from the row when editing one.
87
+ function acEditor(row) {
88
+ var car = el("select", { name: "car_id" });
89
+ acCars.forEach(function (c) {
90
+ car.appendChild(el("option", { value: String(c.id) }, c.name || ("Car #" + c.id)));
91
+ });
92
+ var category = el("select", { name: "category" });
93
+ AC_CATEGORIES.forEach(function (c) { category.appendChild(el("option", { value: c }, c)); });
94
+ var recurrence = el("select", { name: "recurrence" });
95
+ AC_RECURRENCES.forEach(function (r) { recurrence.appendChild(el("option", { value: r }, r)); });
96
+ var date = el("input", { type: "date", name: "date", title: "Invoice date, or the first month of a recurring cost" });
97
+ var endDate = el("input", { type: "date", name: "end_date", title: "Last month; empty means still running" });
98
+ var descr = el("input", { type: "text", name: "description", class: "descr", placeholder: "What it was for" });
99
+ var cost = el("input", { type: "number", step: "0.01", name: "cost", class: "cost", placeholder: "0.00" });
100
+
101
+ // The table rejects an end date on a one-off row, so do not offer one.
102
+ function syncRecurrence() {
103
+ endDate.disabled = recurrence.value !== "monthly";
104
+ if (endDate.disabled) endDate.value = "";
105
+ }
106
+ recurrence.addEventListener("change", syncRecurrence);
107
+
108
+ if (row) {
109
+ car.value = String(row.car_id);
110
+ category.value = row.category;
111
+ recurrence.value = row.recurrence;
112
+ date.value = row.date;
113
+ endDate.value = row.end_date || "";
114
+ descr.value = row.description;
115
+ cost.value = row.cost;
116
+ }
117
+ syncRecurrence();
118
+
119
+ var dates = el("div", { class: "dates" });
120
+ dates.appendChild(date);
121
+ dates.appendChild(recurrence);
122
+ dates.appendChild(endDate);
123
+
124
+ var carCell = el("td");
125
+ carCell.appendChild(car);
126
+ var categoryCell = el("td");
127
+ categoryCell.appendChild(category);
128
+ var dateCell = el("td");
129
+ dateCell.appendChild(dates);
130
+ var descrCell = el("td");
131
+ descrCell.appendChild(descr);
132
+ var costCell = el("td", { class: "num" });
133
+ costCell.appendChild(cost);
134
+
135
+ return {
136
+ cells: [carCell, categoryCell, dateCell, descrCell, costCell],
137
+ // The request body, or null after saying which field is still missing.
138
+ body: function () {
139
+ if (!car.value) { show(acStatus, "err", "Pick a car first."); return null; }
140
+ if (!date.value) { show(acStatus, "err", "Please fill in a date."); return null; }
141
+ if (!descr.value.trim()) { show(acStatus, "err", "Please fill in a description."); return null; }
142
+ if (cost.value === "") { show(acStatus, "err", "Please fill in a cost."); return null; }
143
+ return {
144
+ car_id: Number(car.value),
145
+ category: category.value,
146
+ date: date.value,
147
+ description: descr.value.trim(),
148
+ cost: Number(cost.value),
149
+ recurrence: recurrence.value,
150
+ end_date: endDate.value || null,
151
+ };
152
+ },
153
+ clear: function () { descr.value = ""; cost.value = ""; },
154
+ };
155
+ }
156
+
157
+ function acMessage(text) {
158
+ var tr = el("tr");
159
+ tr.appendChild(el("td", { colspan: "6", class: "msg" }, text));
160
+ acBody.appendChild(tr);
161
+ }
162
+
163
+ // A monthly entry shows the period it covers and the rate it is charged at;
164
+ // the UI never multiplies the two out.
165
+ function acPeriod(row) {
166
+ if (row.recurrence !== "monthly") return row.date;
167
+ return row.date + " → " + (row.end_date || "…");
168
+ }
169
+
170
+ function acAmount(row) {
171
+ return row.recurrence === "monthly" ? row.cost + " / mo" : row.cost;
172
+ }
173
+
174
+ function acRowNode(row) {
175
+ var tr = el("tr");
176
+ tr.appendChild(el("td", null, carLabel(row.car_id)));
177
+ tr.appendChild(el("td", null, row.category));
178
+ tr.appendChild(el("td", null, acPeriod(row)));
179
+ tr.appendChild(el("td", null, row.description));
180
+ tr.appendChild(el("td", { class: "num" }, acAmount(row)));
181
+
182
+ var actions = el("td", { class: "actions" });
183
+ var edit = el("button", { type: "button", class: "icon", title: "Edit" }, "✎");
184
+ edit.addEventListener("click", function () { acEditRow(tr, row); });
185
+ var remove = el("button", { type: "button", class: "icon", title: "Delete" }, "🗑");
186
+ remove.addEventListener("click", function () {
187
+ if (!window.confirm('Delete "' + row.description + '"? Nothing can rebuild it.')) return;
188
+ remove.disabled = true;
189
+ acSend("/delete-additional-cost", { id: String(row.id) }, remove, function () {
190
+ acRows = acRows.filter(function (r) { return r.id !== row.id; });
191
+ renderAc();
192
+ });
193
+ });
194
+ actions.appendChild(edit);
195
+ actions.appendChild(remove);
196
+ tr.appendChild(actions);
197
+ return tr;
198
+ }
199
+
200
+ function acEditRow(tr, row) {
201
+ var editor = acEditor(row);
202
+ var editing = el("tr");
203
+ editor.cells.forEach(function (td) { editing.appendChild(td); });
204
+
205
+ var actions = el("td", { class: "actions" });
206
+ var save = el("button", { type: "button", class: "small" }, "Save");
207
+ save.addEventListener("click", function () {
208
+ var body = editor.body();
209
+ if (!body) return;
210
+ body.id = String(row.id);
211
+ save.disabled = true;
212
+ acSend("/update-additional-cost", body, save, function (updated) {
213
+ if (!updated) { show(acStatus, "err", "Entry " + row.id + " is gone — reload the page."); return; }
214
+ acRows = acRows.map(function (r) { return r.id === row.id ? updated : r; });
215
+ renderAc();
216
+ });
217
+ });
218
+ var cancel = el("button", { type: "button", class: "icon", title: "Cancel" }, "✕");
219
+ cancel.addEventListener("click", function () { renderAc(); });
220
+ actions.appendChild(save);
221
+ actions.appendChild(cancel);
222
+ editing.appendChild(actions);
223
+
224
+ tr.replaceWith(editing);
225
+ }
226
+
227
+ function renderAc() {
228
+ // Newest day first, and within a day the newest entry — what the endpoint
229
+ // orders by, kept up after a local add or edit.
230
+ acRows.sort(function (a, b) {
231
+ if (a.date !== b.date) return a.date < b.date ? 1 : -1;
232
+ return Number(b.id) - Number(a.id);
233
+ });
234
+ acBody.innerHTML = "";
235
+ if (!acRows.length) acMessage("No entries yet.");
236
+ else acRows.forEach(function (row) { acBody.appendChild(acRowNode(row)); });
237
+ }
238
+
239
+ function renderAcForm() {
240
+ var editor = acEditor(null);
241
+ var tr = el("tr");
242
+ editor.cells.forEach(function (td) { tr.appendChild(td); });
243
+ var actions = el("td", { class: "actions" });
244
+ var add = el("button", { type: "button", class: "small" }, "Add");
245
+ add.addEventListener("click", function () {
246
+ var body = editor.body();
247
+ if (!body) return;
248
+ add.disabled = true;
249
+ acSend("/create-additional-cost", body, add, function (created) {
250
+ acRows.push(created);
251
+ editor.clear();
252
+ renderAc();
253
+ });
254
+ });
255
+ actions.appendChild(add);
256
+ tr.appendChild(actions);
257
+ acForm.innerHTML = "";
258
+ acForm.appendChild(tr);
259
+ }
260
+
261
+ // The cars first: the add form's picker and every row's car label read them.
262
+ function loadAdditionalCosts() {
263
+ show(acStatus, "running", "Loading additional costs…");
264
+ fetch("/query-cars").then(function (res) {
265
+ return res.text().then(function (text) {
266
+ if (!res.ok) return acFail(res, text);
267
+ acCars = JSON.parse(text);
268
+ return fetch("/query-additional-costs").then(function (costRes) {
269
+ return costRes.text().then(function (costText) {
270
+ if (!costRes.ok) return acFail(costRes, costText);
271
+ acRows = JSON.parse(costText);
272
+ acStatus.hidden = true;
273
+ renderAcForm();
274
+ renderAc();
275
+ });
276
+ });
277
+ });
278
+ }).catch(function (e) {
279
+ show(acStatus, "err", "Request failed: " + (e && e.message ? e.message : e));
280
+ });
281
+ }
282
+
283
+ loadAdditionalCosts();
284
+ `,
285
+ });
286
+ //# sourceMappingURL=additional-costs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"additional-costs.js","sourceRoot":"","sources":["../../src/ui/additional-costs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,gBAAgB,GACjB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,UAAU,CAAC;IAC9C,KAAK,EAAE,wCAAwC;IAC/C,GAAG,EAAE,mBAAmB;IACxB,IAAI,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;WA0BG;IACT,MAAM,EAAE;;;;;;wBAMc,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC;yBAC9B,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2OxD;CACA,CAAC,CAAC"}
@@ -1,7 +1,5 @@
1
1
  /**
2
- * The web UI, served at `GET /` by the server (see `server.ts`). It is one
3
- * self-contained HTML document — inline CSS + JS, no build step, no assets — so
4
- * it compiles into `dist` as a plain string and is served as-is.
2
+ * The command page, served at `GET /` and `GET /index.html`.
5
3
  *
6
4
  * Each command is a small form that calls the matching endpoint on the same
7
5
  * origin (read-only lookups as GET, side-effecting actions as POST with a
@@ -9,9 +7,6 @@
9
7
  * behind Caddy the SSO portal authenticates the browser and Caddy injects the
10
8
  * bearer token upstream, so a same-origin fetch is already authorized. Run
11
9
  * locally without that front, the command calls answer 401 (the page says so).
12
- *
13
- * The inner <script> deliberately avoids template literals so it can live inside
14
- * this outer template string without escaping.
15
10
  */
16
- export declare const UI_HTML: string;
17
- //# sourceMappingURL=ui.d.ts.map
11
+ export declare const COMMANDS_HTML: string;
12
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/ui/commands.ts"],"names":[],"mappings":"AAUA;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,QA0JxB,CAAC"}
@@ -0,0 +1,168 @@
1
+ import { FAILURE_DEPRIORITIZE_THRESHOLD, MAX_CHARGER_FETCHES, } from "@mxpicture/tesla-superchargers-api/common";
2
+ import { COVERED_MARGIN, COVERED_RADIUS_KM, } from "@mxpicture/tesla-superchargers-mate/common";
3
+ import { renderPage } from "./shell.js";
4
+ /**
5
+ * The command page, served at `GET /` and `GET /index.html`.
6
+ *
7
+ * Each command is a small form that calls the matching endpoint on the same
8
+ * origin (read-only lookups as GET, side-effecting actions as POST with a
9
+ * confirm) and renders the JSON response. The page never sends a credential:
10
+ * behind Caddy the SSO portal authenticates the browser and Caddy injects the
11
+ * bearer token upstream, so a same-origin fetch is already authorized. Run
12
+ * locally without that front, the command calls answer 401 (the page says so).
13
+ */
14
+ export const COMMANDS_HTML = renderPage({
15
+ title: "Tesla Superchargers",
16
+ nav: "/",
17
+ intro: "Run a command against the price-history database. Actions are highlighted and ask to confirm.",
18
+ body: `<main id="commands"></main>`,
19
+ script: `
20
+ // "value" pre-fills the field with the default the endpoint would apply
21
+ // anyway; "placeholder" is only a hint for fields that have no fixed default.
22
+ var F = {
23
+ lat: { name: "lat", label: "Latitude", type: "text", value: "49.3135644", required: true },
24
+ lon: { name: "lon", label: "Longitude", type: "text", value: "12.1445587", required: true },
25
+ radiusKm: { name: "radiusKm", label: "Radius (km)", type: "number", value: "${COVERED_RADIUS_KM * COVERED_MARGIN}" },
26
+ locale: { name: "locale", label: "Locale", type: "text", value: "de-DE" },
27
+ maxFetches: { name: "maxFetches", label: "Max fetches", type: "number", value: "${MAX_CHARGER_FETCHES}" },
28
+ at: { name: "at", label: "At (local)", type: "datetime-local", wide: true },
29
+ source: { name: "source", label: "Source", type: "select", options: ["all", "charges", "chargers", "slugs"] },
30
+ sourceCon: { name: "source", label: "Source", type: "select", options: ["charges", "chargers", "slugs", "all"] },
31
+ threshold: { name: "threshold", label: "Threshold", type: "number", value: "${FAILURE_DEPRIORITIZE_THRESHOLD}" },
32
+ id: { name: "id", label: "Id", type: "text", placeholder: "reset all if empty", wide: true },
33
+ latOpt: { name: "lat", label: "Latitude", type: "text", placeholder: "49.3135644" },
34
+ lonOpt: { name: "lon", label: "Longitude", type: "text", placeholder: "12.1445587" },
35
+ slug: { name: "slug", label: "Slug", type: "text", placeholder: "location_url_slug", wide: true },
36
+ chargerId: { name: "id", label: "Charger id", type: "text", placeholder: "pic_chargers id" },
37
+ countries: { name: "countries", label: "Countries", type: "text", placeholder: "DE or DE,AT", required: true, wide: true }
38
+ };
39
+
40
+ var COMMANDS = [
41
+ { name: "query-slugs", method: "GET", kind: "read", fields: [F.lat, F.lon, F.radiusKm],
42
+ desc: "Nearest location slugs (tesla_uuid) to a coordinate." },
43
+ { name: "query-chargers", method: "GET", kind: "read", fields: [F.lat, F.lon, F.radiusKm],
44
+ desc: "Chargers already stored near a coordinate. Never fetches tesla.com — fast." },
45
+ { name: "fetch-charger", method: "GET", kind: "read", fields: [F.latOpt, F.lonOpt, F.slug, F.radiusKm, F.locale],
46
+ desc: "One charger by slug, or the nearest to a coordinate. May fetch tesla.com on demand." },
47
+ { name: "fetch-chargers", method: "GET", kind: "read", fields: [F.lat, F.lon, F.locale, F.radiusKm, F.maxFetches],
48
+ desc: "Nearest chargers to a coordinate. May fetch tesla.com on demand — can be slow." },
49
+ { name: "fetch-prices", method: "GET", kind: "read", fields: [F.latOpt, F.lonOpt, F.slug, F.chargerId, F.at, F.radiusKm],
50
+ desc: "Supercharger prices valid at a time (default now), by coordinate, slug or charger id." },
51
+ { name: "query-unpriced-charges", method: "GET", kind: "read", fields: [],
52
+ desc: "TeslaMate Supercharger charges with no cost yet." },
53
+ { name: "query-failures", method: "GET", kind: "read", fields: [F.source, F.threshold],
54
+ desc: "Rows with repeated failures at or above the threshold." },
55
+ { name: "query-unmatched-invoices", method: "GET", kind: "read", fields: [],
56
+ desc: "Supercharger charges without a matched invoice." },
57
+ { name: "sync-invoices-dropbox", method: "POST", kind: "action", fields: [],
58
+ desc: "Import Tesla charging-history CSV exports from Dropbox." },
59
+ { name: "import-zip-codes", method: "POST", kind: "action", fields: [F.countries],
60
+ desc: "Import postal codes so rural addresses render their post town, not their hamlet." },
61
+ { name: "run-unpriced-charges", method: "POST", kind: "action", fields: [],
62
+ desc: "Backfill cost on unpriced charges from stored Supercharger prices." },
63
+ { name: "build-refs", method: "POST", kind: "action", fields: [],
64
+ desc: "Rebuild pic_refs, the drive/idle/charge cost table the cost dashboards read." },
65
+ { name: "assign-clusters", method: "POST", kind: "action", fields: [],
66
+ desc: "Cluster the TeslaMate addresses that have no cluster yet." },
67
+ { name: "sync-slugs", method: "POST", kind: "action", fields: [],
68
+ desc: "Snapshot-sync pic_slugs against the live Tesla map feed." },
69
+ { name: "fetch-fav-chargers", method: "POST", kind: "action", fields: [F.locale],
70
+ desc: "Refresh the favourite chargers whose cached row went stale." },
71
+ { name: "reset-failures", method: "POST", kind: "action", fields: [F.sourceCon, F.id],
72
+ desc: "Clear failure state. Leave id empty to reset ALL rows of the source.",
73
+ buildBody: function (v) { return v.id ? { source: v.source, id: v.id } : { source: v.source, all: true }; } },
74
+ { name: "notify-failures", method: "POST", kind: "action", fields: [F.threshold],
75
+ desc: "Send a Pushover alert for not-yet-notified failures." },
76
+ { name: "notify-unmatched-invoices", method: "POST", kind: "action", fields: [],
77
+ desc: "Send a Pushover alert for Supercharger charges without a matched invoice." }
78
+ ];
79
+
80
+
81
+ function fieldNode(f) {
82
+ var wrap = el("div", { class: "field" + (f.wide ? " wide" : "") });
83
+ wrap.appendChild(el("label", null, f.label));
84
+ var input;
85
+ if (f.type === "select") {
86
+ input = el("select", { name: f.name });
87
+ f.options.forEach(function (o) { input.appendChild(el("option", { value: o }, o)); });
88
+ } else {
89
+ input = el("input", { name: f.name, type: f.type });
90
+ if (f.placeholder) input.setAttribute("placeholder", f.placeholder);
91
+ if (f.value) input.setAttribute("value", f.value);
92
+ }
93
+ wrap.appendChild(input);
94
+ return wrap;
95
+ }
96
+
97
+ function collect(cmd, form) {
98
+ var values = {};
99
+ cmd.fields.forEach(function (f) {
100
+ var input = form.elements[f.name];
101
+ var val = input ? String(input.value).trim() : "";
102
+ if (f.type === "datetime-local" && val) val = new Date(val).toISOString();
103
+ if (val !== "") values[f.name] = val;
104
+ });
105
+ return values;
106
+ }
107
+
108
+
109
+ function run(cmd, form, out, btn) {
110
+ var values = collect(cmd, form);
111
+ for (var i = 0; i < cmd.fields.length; i++) {
112
+ var f = cmd.fields[i];
113
+ if (f.required && !values[f.name]) { show(out, "err", "Please fill in " + f.label + "."); return; }
114
+ }
115
+
116
+ var url = "/" + cmd.name;
117
+ var opts = { method: cmd.method, headers: {} };
118
+ if (cmd.method === "GET") {
119
+ var qs = new URLSearchParams(values).toString();
120
+ if (qs) url += "?" + qs;
121
+ } else {
122
+ if (!window.confirm("Run " + cmd.name + "? This performs a real action.")) return;
123
+ var body = cmd.buildBody ? cmd.buildBody(values) : values;
124
+ opts.headers["content-type"] = "application/json";
125
+ opts.body = JSON.stringify(body);
126
+ }
127
+
128
+ btn.disabled = true;
129
+ show(out, "running", "Running…");
130
+ fetch(url, opts).then(function (res) {
131
+ return res.text().then(function (text) {
132
+ if (res.ok) show(out, "ok", pretty(text));
133
+ else if (res.status === 401) show(out, "err", "401 Unauthorized — sign in via the SSO portal (running locally, the command endpoints need the bearer token).");
134
+ else show(out, "err", res.status + " — " + pretty(text));
135
+ });
136
+ }).catch(function (e) {
137
+ show(out, "err", "Request failed: " + (e && e.message ? e.message : e));
138
+ }).then(function () { btn.disabled = false; });
139
+ }
140
+
141
+ function card(cmd) {
142
+ var form = el("form", { class: "card " + cmd.kind });
143
+ var title = el("div", { class: "title" });
144
+ title.appendChild(el("span", { class: "badge " + (cmd.method === "GET" ? "get" : "post") }, cmd.method));
145
+ title.appendChild(el("h2", null, cmd.name));
146
+ form.appendChild(title);
147
+ form.appendChild(el("p", { class: "desc" }, cmd.desc));
148
+
149
+ if (cmd.fields.length) {
150
+ var fields = el("div", { class: "fields" });
151
+ cmd.fields.forEach(function (f) { fields.appendChild(fieldNode(f)); });
152
+ form.appendChild(fields);
153
+ }
154
+
155
+ var btn = el("button", { type: "submit" }, cmd.kind === "action" ? "Run action" : "Run");
156
+ form.appendChild(btn);
157
+ var out = el("pre", { class: "result", hidden: "hidden" });
158
+ form.appendChild(out);
159
+
160
+ form.addEventListener("submit", function (e) { e.preventDefault(); run(cmd, form, out, btn); });
161
+ return form;
162
+ }
163
+
164
+ var root = document.getElementById("commands");
165
+ COMMANDS.forEach(function (cmd) { root.appendChild(card(cmd)); });
166
+ `,
167
+ });
168
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/ui/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,mBAAmB,GACpB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EACL,cAAc,EACd,iBAAiB,GAClB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;IACtC,KAAK,EAAE,qBAAqB;IAC5B,GAAG,EAAE,GAAG;IACR,KAAK,EACH,+FAA+F;IACjG,IAAI,EAAE,6BAA6B;IACnC,MAAM,EAAE;;;;;;+FAMqF,iBAAiB,GAAG,cAAc;;+FAElC,mBAAmB;;;;+FAInB,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuI5H;CACA,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The favourites page, served at `GET /favourites`.
3
+ *
4
+ * A table of the charger favourites, with an inline star toggle that persists
5
+ * each change through `update-fav-charger`. Filter, sort and paging all run
6
+ * against the full dataset `query-fav-chargers` hands back in one go.
7
+ *
8
+ * Those two endpoints are not offered as command cards on `/`: this table is
9
+ * their UI, and a raw-JSON card would only duplicate it.
10
+ */
11
+ export declare const FAVOURITES_HTML: string;
12
+ //# sourceMappingURL=favourites.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"favourites.d.ts","sourceRoot":"","sources":["../../src/ui/favourites.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,QA8M1B,CAAC"}