@nano133/nanoterra 0.1.0 → 0.2.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +189 -2
  3. package/ai/SPEC.md +2 -1
  4. package/dist/admin/adapters/d1.d.ts +27 -0
  5. package/dist/admin/adapters/d1.js +73 -0
  6. package/dist/admin/adapters/firestore.d.ts +40 -0
  7. package/dist/admin/adapters/firestore.js +53 -0
  8. package/dist/admin/amount.d.ts +5 -0
  9. package/dist/admin/amount.js +11 -0
  10. package/dist/admin/conformance.d.ts +132 -0
  11. package/dist/admin/conformance.js +467 -0
  12. package/dist/admin/guard.d.ts +38 -0
  13. package/dist/admin/guard.js +57 -0
  14. package/dist/admin/handler.d.ts +63 -0
  15. package/dist/admin/handler.js +170 -0
  16. package/dist/admin/http.d.ts +17 -0
  17. package/dist/admin/http.js +76 -0
  18. package/dist/admin/index.d.ts +13 -0
  19. package/dist/admin/index.js +14 -0
  20. package/dist/admin/memory.d.ts +32 -0
  21. package/dist/admin/memory.js +40 -0
  22. package/dist/admin/panel/api.d.ts +11 -0
  23. package/dist/admin/panel/api.js +33 -0
  24. package/dist/admin/panel/dom.d.ts +7 -0
  25. package/dist/admin/panel/dom.js +33 -0
  26. package/dist/admin/panel/gameCard.d.ts +4 -0
  27. package/dist/admin/panel/gameCard.js +56 -0
  28. package/dist/admin/panel/index.d.ts +10 -0
  29. package/dist/admin/panel/index.js +13 -0
  30. package/dist/admin/panel/mount.d.ts +30 -0
  31. package/dist/admin/panel/mount.js +147 -0
  32. package/dist/admin/panel/shelf.d.ts +17 -0
  33. package/dist/admin/panel/shelf.js +247 -0
  34. package/dist/admin/panel/shelfData.d.ts +37 -0
  35. package/dist/admin/panel/shelfData.js +68 -0
  36. package/dist/admin/panel/signins.d.ts +6 -0
  37. package/dist/admin/panel/signins.js +69 -0
  38. package/dist/admin/store.d.ts +121 -0
  39. package/dist/admin/store.js +3 -0
  40. package/dist/admin/summary.d.ts +39 -0
  41. package/dist/admin/summary.js +85 -0
  42. package/dist/admin/switches.d.ts +58 -0
  43. package/dist/admin/switches.js +61 -0
  44. package/dist/looks/change.d.ts +8 -4
  45. package/dist/looks/change.js +25 -3
  46. package/dist/looks/index.d.ts +3 -3
  47. package/dist/looks/index.js +2 -2
  48. package/dist/looks/json.d.ts +2 -2
  49. package/dist/looks/json.js +6 -4
  50. package/dist/looks/look.d.ts +29 -3
  51. package/dist/looks/look.js +60 -4
  52. package/package.json +9 -2
  53. package/src/admin/adapters/d1.ts +94 -0
  54. package/src/admin/adapters/firestore.ts +78 -0
  55. package/src/admin/amount.ts +14 -0
  56. package/src/admin/conformance.ts +605 -0
  57. package/src/admin/guard.ts +86 -0
  58. package/src/admin/handler.ts +200 -0
  59. package/src/admin/http.ts +74 -0
  60. package/src/admin/index.ts +21 -0
  61. package/src/admin/memory.ts +59 -0
  62. package/src/admin/panel/api.ts +37 -0
  63. package/src/admin/panel/dom.ts +31 -0
  64. package/src/admin/panel/gameCard.ts +63 -0
  65. package/src/admin/panel/index.ts +18 -0
  66. package/src/admin/panel/mount.ts +227 -0
  67. package/src/admin/panel/shelf.ts +337 -0
  68. package/src/admin/panel/shelfData.ts +87 -0
  69. package/src/admin/panel/signins.ts +68 -0
  70. package/src/admin/panel/style.css +542 -0
  71. package/src/admin/store.ts +98 -0
  72. package/src/admin/summary.ts +103 -0
  73. package/src/admin/switches.ts +74 -0
  74. package/src/looks/change.ts +25 -6
  75. package/src/looks/index.ts +3 -3
  76. package/src/looks/json.ts +6 -4
  77. package/src/looks/look.ts +66 -5
@@ -0,0 +1,68 @@
1
+ // The "Sign-ins" card of /admin: the last admin sign-ins (when, the rough country, a short browser name) and "Sign out everywhere". It shows only what the store gave for a
2
+ // sign-in: the three things of `AdminLogin`. The sign-out is two presses, so that one slip does not end every login.
3
+
4
+ import type { AdminSummary } from "../store.js";
5
+ import { ApiError, type Api } from "./api.js";
6
+ import { h } from "./dom.js";
7
+
8
+ /** What the card says about what is kept, unless the game says its own (its retention). */
9
+ export const DEFAULT_LOGINS_NOTE = "For each sign-in only the time, the rough country and a short browser name are kept: no address and no full browser text.";
10
+
11
+ /** "United States (US)", or "US" when the name is not known, or "unknown country". */
12
+ function countryText(code: string | null): string {
13
+ if (!code) return "unknown country";
14
+ try {
15
+ const name = new Intl.DisplayNames(["en"], { type: "region" }).of(code);
16
+ return name && name !== code ? `${name} (${code})` : code;
17
+ } catch {
18
+ return code;
19
+ }
20
+ }
21
+
22
+ /** The card, or null when the store lists no sign-ins and cannot sign out. `done` runs after a sign-out everywhere (the panel loads again and asks for a sign-in). */
23
+ export function signinsCard(s: AdminSummary, api: Api, done: () => void, note: string = DEFAULT_LOGINS_NOTE): HTMLElement | null {
24
+ if (!s.logins && !s.canSignOutEverywhere) return null;
25
+ const card = h("section", { class: "admin-card signins", "aria-labelledby": "signins-title" }, h("h2", { id: "signins-title" }, "Sign-ins"));
26
+ if (s.logins) {
27
+ card.append(h("p", { class: "fine" }, "The last sign-ins to this admin, newest first. If one is not yours, press Sign out everywhere."));
28
+ const list = h("ul", { class: "signin-list" });
29
+ for (const l of s.logins) list.append(h("li", {}, h("strong", {}, new Date(l.at).toLocaleString()), h("span", {}, ` · ${countryText(l.country)} · ${l.browser}`)));
30
+ if (!s.logins.length) list.append(h("li", { class: "fine" }, "No sign-in is listed yet."));
31
+ card.append(list, h("p", { class: "fine" }, note));
32
+ }
33
+ if (s.canSignOutEverywhere) {
34
+ const box = h("div", { class: "signout-box" });
35
+ const status = h("span", { class: "fine", role: "status" });
36
+ const first = h("button", { class: "btn", type: "button" }, "Sign out everywhere");
37
+ const confirm = h("div", { class: "signout-confirm" });
38
+ confirm.hidden = true;
39
+ const yes = h("button", { class: "btn btn-primary", type: "button" }, "Yes, sign out everywhere");
40
+ const cancel = h("button", { class: "btn", type: "button" }, "Cancel");
41
+ confirm.append(h("p", {}, "This ends every admin sign-in, in every browser, this one too. You will sign in again."), yes, " ", cancel);
42
+ first.addEventListener("click", () => {
43
+ first.hidden = true;
44
+ confirm.hidden = false;
45
+ status.textContent = "";
46
+ });
47
+ cancel.addEventListener("click", () => {
48
+ confirm.hidden = true;
49
+ first.hidden = false;
50
+ });
51
+ yes.addEventListener("click", async () => {
52
+ yes.disabled = true;
53
+ cancel.disabled = true;
54
+ status.textContent = "Signing out…";
55
+ try {
56
+ await api.post("/signout-all");
57
+ done();
58
+ } catch (e) {
59
+ status.textContent = e instanceof ApiError ? e.message : "The game's server did not answer. Nothing was changed.";
60
+ yes.disabled = false;
61
+ cancel.disabled = false;
62
+ }
63
+ });
64
+ box.append(first, confirm, status);
65
+ card.append(box);
66
+ }
67
+ return card;
68
+ }
@@ -0,0 +1,542 @@
1
+ /* The admin panel's styles (mountAdmin in ./mount.ts). Every rule is under .n133-admin, so nothing leaks into the page that holds the panel and the page's own
2
+ styles do not reach in by accident. Phone first: every control is at least 40 px high (the order tabs), the rest 44 px or more; one column, two from 640px.
3
+ To theme it, set the --n133-* custom properties on the element that holds the panel (or on any parent): --n133-ink, --n133-ink-soft, --n133-paper, --n133-line,
4
+ --n133-gem, --n133-gem-dark, --n133-sea, --n133-radius and --n133-font. */
5
+
6
+ .n133-admin {
7
+ --font: var(--n133-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
8
+ --gem: var(--n133-gem, #20c9a6);
9
+ --gem-dark: var(--n133-gem-dark, #0e8a72);
10
+ --ink: var(--n133-ink, #16202e);
11
+ --ink-soft: var(--n133-ink-soft, #4a5568);
12
+ --line: var(--n133-line, #dbe4ee);
13
+ --paper: var(--n133-paper, #ffffff);
14
+ --radius: var(--n133-radius, 18px);
15
+ --sea: var(--n133-sea, #1f8fc4);
16
+ box-sizing: border-box;
17
+ max-width: 900px;
18
+ margin: 0 auto;
19
+ color: var(--ink);
20
+ font-family: var(--font);
21
+ -webkit-text-size-adjust: 100%;
22
+ }
23
+
24
+ .n133-admin *,
25
+ .n133-admin *::before,
26
+ .n133-admin *::after {
27
+ box-sizing: border-box;
28
+ }
29
+
30
+ .n133-admin [hidden] {
31
+ display: none !important;
32
+ }
33
+
34
+ .n133-admin .btn {
35
+ display: inline-flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ gap: 8px;
39
+ min-height: 48px;
40
+ padding: 0 18px;
41
+ border: 2px solid var(--line);
42
+ border-radius: 999px;
43
+ background: var(--paper);
44
+ color: var(--ink);
45
+ font: 700 16px/1 var(--font);
46
+ text-decoration: none;
47
+ cursor: pointer;
48
+ touch-action: manipulation;
49
+ }
50
+
51
+ .n133-admin .btn:focus-visible {
52
+ outline: 3px solid var(--sea);
53
+ outline-offset: 2px;
54
+ }
55
+
56
+ .n133-admin .btn:disabled {
57
+ opacity: 0.55;
58
+ cursor: default;
59
+ }
60
+
61
+ .n133-admin .btn-primary {
62
+ border-color: var(--gem-dark);
63
+ background: var(--gem);
64
+ color: #062b23;
65
+ }
66
+
67
+ .n133-admin .btn-small {
68
+ min-height: 40px;
69
+ padding: 0 12px;
70
+ font-size: 14px;
71
+ }
72
+
73
+ .n133-admin .fine {
74
+ color: var(--ink-soft);
75
+ font-size: 13px;
76
+ }
77
+
78
+ .n133-admin .admin-card {
79
+ margin: 12px 0;
80
+ padding: 16px;
81
+ border: 1px solid var(--line);
82
+ border-radius: var(--radius);
83
+ background: #fff;
84
+ }
85
+
86
+ .n133-admin .admin-card h2 {
87
+ margin: 0 0 10px;
88
+ font-size: 19px;
89
+ }
90
+
91
+ .n133-admin .card-head {
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: 8px;
97
+ margin-bottom: 8px;
98
+ }
99
+
100
+ .n133-admin .card-head h2 {
101
+ margin: 0;
102
+ }
103
+
104
+ .n133-admin .warnings {
105
+ border-color: #f2b8c1;
106
+ background: #fff4f6;
107
+ }
108
+
109
+ .n133-admin .warnings ul {
110
+ margin: 0;
111
+ padding-left: 20px;
112
+ }
113
+
114
+ .n133-admin .totals {
115
+ display: grid;
116
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
117
+ gap: 12px;
118
+ }
119
+
120
+ .n133-admin .total {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 4px;
124
+ padding: 16px;
125
+ border: 1px solid var(--line);
126
+ border-radius: var(--radius);
127
+ background: #fff;
128
+ }
129
+
130
+ .n133-admin .total span {
131
+ color: var(--ink-soft);
132
+ font-size: 14px;
133
+ }
134
+
135
+ .n133-admin .total strong {
136
+ font-size: 24px;
137
+ font-variant-numeric: tabular-nums;
138
+ }
139
+
140
+ .n133-admin .tabs {
141
+ display: flex;
142
+ gap: 6px;
143
+ overflow-x: auto;
144
+ padding-bottom: 6px;
145
+ }
146
+
147
+ .n133-admin .tab {
148
+ flex: none;
149
+ min-height: 40px;
150
+ padding: 0 14px;
151
+ border: 2px solid var(--line);
152
+ border-radius: 999px;
153
+ background: #fff;
154
+ font: 700 14px/1 var(--font);
155
+ cursor: pointer;
156
+ }
157
+
158
+ .n133-admin .tab-on {
159
+ border-color: var(--ink);
160
+ background: var(--ink);
161
+ color: #fff;
162
+ }
163
+
164
+ .n133-admin .order {
165
+ padding: 12px 0;
166
+ border-bottom: 1px solid var(--line);
167
+ }
168
+
169
+ .n133-admin .order-top {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: space-between;
173
+ gap: 8px;
174
+ }
175
+
176
+ .n133-admin .order-amount {
177
+ margin: 4px 0;
178
+ font-variant-numeric: tabular-nums;
179
+ }
180
+
181
+ .n133-admin .badge {
182
+ padding: 4px 10px;
183
+ border-radius: 999px;
184
+ background: #eef1f4;
185
+ font-size: 13px;
186
+ font-weight: 700;
187
+ }
188
+
189
+ .n133-admin .badge-paid {
190
+ background: #dcf8ef;
191
+ color: #0b6d5a;
192
+ }
193
+
194
+ .n133-admin .badge-open {
195
+ background: #e3f1ff;
196
+ color: #134e86;
197
+ }
198
+
199
+ .n133-admin .badge-paid_late {
200
+ background: #ffe3e8;
201
+ color: #a1122a;
202
+ }
203
+
204
+ .n133-admin .late-note {
205
+ margin-top: 6px;
206
+ padding: 8px 10px;
207
+ border-radius: 10px;
208
+ background: #fff4f6;
209
+ font-size: 14px;
210
+ }
211
+
212
+ .n133-admin .late-note code,
213
+ .n133-admin .player code,
214
+ .n133-admin .admin-card code {
215
+ word-break: break-all;
216
+ font-size: 12px;
217
+ }
218
+
219
+ .n133-admin .hash {
220
+ word-break: break-all;
221
+ }
222
+
223
+ .n133-admin .player {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 2px;
227
+ padding: 8px 0;
228
+ border-bottom: 1px solid var(--line);
229
+ }
230
+
231
+ /* The Shelf (src/client/shelf.ts): phone first, one column; two from 640px. */
232
+ .n133-admin .shelf-list {
233
+ display: grid;
234
+ grid-template-columns: 1fr;
235
+ gap: 10px;
236
+ }
237
+
238
+ @media (min-width: 640px) {
239
+ .n133-admin .shelf-list {
240
+ grid-template-columns: 1fr 1fr;
241
+ }
242
+ }
243
+
244
+ .n133-admin .shelf-card {
245
+ padding: 12px;
246
+ border: 1px solid var(--line);
247
+ border-radius: 14px;
248
+ background: #fbfdff;
249
+ }
250
+
251
+ .n133-admin .shelf-top {
252
+ display: flex;
253
+ align-items: center;
254
+ gap: 12px;
255
+ }
256
+
257
+ .n133-admin .shelf-pic {
258
+ display: grid;
259
+ flex: none;
260
+ width: 68px;
261
+ height: 76px;
262
+ place-items: center;
263
+ border-radius: 12px;
264
+ background: #e8f3fb;
265
+ }
266
+
267
+ .n133-admin .shelf-text {
268
+ display: grid;
269
+ min-width: 0;
270
+ gap: 2px;
271
+ }
272
+
273
+ .n133-admin .shelf-text strong {
274
+ overflow-wrap: anywhere;
275
+ }
276
+
277
+ .n133-admin .shelf-price {
278
+ font-weight: 700;
279
+ font-variant-numeric: tabular-nums;
280
+ }
281
+
282
+ .n133-admin .shelf-field {
283
+ display: grid;
284
+ gap: 4px;
285
+ margin-top: 10px;
286
+ }
287
+
288
+ .n133-admin .shelf-field > span {
289
+ color: var(--ink-soft);
290
+ font-size: 13px;
291
+ font-weight: 700;
292
+ }
293
+
294
+ .n133-admin .shelf-input {
295
+ width: 100%;
296
+ min-height: 44px;
297
+ padding: 0 12px;
298
+ border: 2px solid var(--line);
299
+ border-radius: 12px;
300
+ background: #fff;
301
+ color: var(--ink);
302
+ font: 16px/1 var(--font);
303
+ }
304
+
305
+ .n133-admin .shelf-color {
306
+ width: 100%;
307
+ min-height: 44px;
308
+ padding: 2px;
309
+ border: 2px solid var(--line);
310
+ border-radius: 12px;
311
+ background: #fff;
312
+ }
313
+
314
+ .n133-admin .shelf-color:disabled {
315
+ opacity: 0.45;
316
+ }
317
+
318
+ .n133-admin .shelf-colors {
319
+ display: grid;
320
+ grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
321
+ gap: 8px;
322
+ }
323
+
324
+ .n133-admin .shelf-ladder {
325
+ display: flex;
326
+ flex-wrap: wrap;
327
+ gap: 6px;
328
+ }
329
+
330
+ .n133-admin .chip {
331
+ min-width: 44px;
332
+ min-height: 44px;
333
+ padding: 0 12px;
334
+ border: 2px solid var(--line);
335
+ border-radius: 999px;
336
+ background: #fff;
337
+ color: var(--ink);
338
+ font: 700 14px/1 var(--font);
339
+ cursor: pointer;
340
+ }
341
+
342
+ .n133-admin .shelf-actions,
343
+ .n133-admin .shelf-add {
344
+ display: flex;
345
+ flex-wrap: wrap;
346
+ gap: 8px;
347
+ margin-top: 12px;
348
+ }
349
+
350
+ .n133-admin .shelf-foot {
351
+ display: grid;
352
+ gap: 10px;
353
+ margin-top: 14px;
354
+ }
355
+
356
+ .n133-admin .shelf-changes ul,
357
+ .n133-admin .late-note ul {
358
+ margin: 4px 0 0;
359
+ padding-left: 20px;
360
+ }
361
+
362
+ .n133-admin .shelf-file pre {
363
+ max-height: 320px;
364
+ margin: 8px 0 0;
365
+ padding: 12px;
366
+ overflow: auto;
367
+ border-radius: 12px;
368
+ background: #0f172a;
369
+ color: #e6edf6;
370
+ font-size: 13px;
371
+ }
372
+
373
+ /* WebKit draws a select at its own height and ignores min-height: take over the drawing, so it is 44px (a finger) on an iPad too. */
374
+ .n133-admin select.shelf-input {
375
+ height: 44px;
376
+ padding-right: 32px;
377
+ background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(135deg, var(--ink) 50%, transparent 50%);
378
+ background-position: calc(100% - 18px) 19px, calc(100% - 13px) 19px;
379
+ background-repeat: no-repeat;
380
+ background-size: 5px 5px, 5px 5px;
381
+ -webkit-appearance: none;
382
+ appearance: none;
383
+ }
384
+
385
+ /* The "Game" card: two switches (src/client/gameSwitches.ts). Each is a button of at least 44 px, on a phone first. */
386
+ .n133-admin .game-card .switch-row {
387
+ display: grid;
388
+ gap: 6px;
389
+ padding: 14px 0;
390
+ border-top: 1px solid var(--line);
391
+ }
392
+
393
+ .n133-admin .game-card .switch-head {
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: space-between;
397
+ gap: 12px;
398
+ flex-wrap: wrap;
399
+ }
400
+
401
+ .n133-admin .game-card .switch-title {
402
+ font: 700 16px/1.3 var(--font);
403
+ flex: 1 1 14rem;
404
+ min-width: 0;
405
+ }
406
+
407
+ .n133-admin .game-card .switch-note {
408
+ margin: 0;
409
+ color: var(--ink-soft);
410
+ font-size: 15px;
411
+ line-height: 1.45;
412
+ max-width: 60ch;
413
+ }
414
+
415
+ .n133-admin .game-card .switch-review {
416
+ margin: 4px 0 0;
417
+ padding-top: 12px;
418
+ border-top: 1px solid var(--line);
419
+ }
420
+
421
+ .n133-admin .game-card .switch-review a {
422
+ display: inline-block;
423
+ padding: 10px 0;
424
+ color: var(--ink);
425
+ font-weight: 700;
426
+ }
427
+
428
+ .n133-admin .switch {
429
+ display: inline-flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ gap: 10px;
433
+ min-width: 96px;
434
+ min-height: 44px;
435
+ /* Off: the knob is on the left and the word on the right. On: the word on the left and the knob on the right (the usual switch). */
436
+ flex-direction: row-reverse;
437
+ padding: 0 14px 0 6px;
438
+ border: 2px solid var(--line);
439
+ border-radius: 999px;
440
+ background: var(--paper);
441
+ color: var(--ink);
442
+ font: 700 14px/1 var(--font);
443
+ cursor: pointer;
444
+ }
445
+
446
+ .n133-admin .switch .switch-knob {
447
+ width: 28px;
448
+ height: 28px;
449
+ border-radius: 50%;
450
+ background: var(--line);
451
+ }
452
+
453
+ .n133-admin .switch.switch-on {
454
+ border-color: var(--gem-dark);
455
+ background: var(--gem);
456
+ color: #052b23;
457
+ flex-direction: row;
458
+ padding: 0 6px 0 14px;
459
+ }
460
+
461
+ .n133-admin .switch.switch-on .switch-knob {
462
+ background: var(--paper);
463
+ }
464
+
465
+ .n133-admin .switch:focus-visible {
466
+ outline: 3px solid var(--sea);
467
+ outline-offset: 2px;
468
+ }
469
+
470
+ .n133-admin .switch:disabled {
471
+ opacity: 0.6;
472
+ cursor: progress;
473
+ }
474
+
475
+ /* The slots of a look in the Shelf's editor: a name and a value in a row, and Remove; on a narrow phone Remove goes under them. */
476
+ .n133-admin .shelf-slots-edit {
477
+ gap: 8px;
478
+ }
479
+
480
+ .n133-admin .shelf-slot-list {
481
+ display: grid;
482
+ gap: 8px;
483
+ }
484
+
485
+ .n133-admin .shelf-slot {
486
+ display: grid;
487
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr) auto;
488
+ gap: 8px;
489
+ align-items: center;
490
+ }
491
+
492
+ @media (max-width: 480px) {
493
+ .n133-admin .shelf-slot {
494
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
495
+ }
496
+
497
+ .n133-admin .shelf-slot .btn {
498
+ grid-column: 1 / -1;
499
+ }
500
+ }
501
+
502
+ .n133-admin .shelf-slots-edit > .btn {
503
+ justify-self: start;
504
+ }
505
+
506
+ .n133-admin .shelf-slots {
507
+ overflow-wrap: anywhere;
508
+ }
509
+
510
+ /* The Sign-ins card: a list of the last sign-ins, and the two-step "Sign out everywhere". */
511
+ .n133-admin .signin-list {
512
+ display: grid;
513
+ gap: 8px;
514
+ margin: 8px 0;
515
+ padding: 0;
516
+ list-style: none;
517
+ }
518
+
519
+ .n133-admin .signin-list li {
520
+ padding: 8px 0;
521
+ border-bottom: 1px solid var(--line);
522
+ overflow-wrap: anywhere;
523
+ }
524
+
525
+ .n133-admin .signout-box {
526
+ display: grid;
527
+ gap: 8px;
528
+ justify-items: start;
529
+ margin-top: 8px;
530
+ }
531
+
532
+ .n133-admin .signout-confirm {
533
+ display: grid;
534
+ gap: 8px;
535
+ padding: 12px;
536
+ border-radius: 12px;
537
+ background: #fff4f6;
538
+ }
539
+
540
+ .n133-admin .signout-confirm p {
541
+ margin: 0;
542
+ }
@@ -0,0 +1,98 @@
1
+ // What a game gives the admin handler: a store (an adapter for its database) and a status. The package has no database of its own.
2
+
3
+ import type { GamePatch } from "./switches.js";
4
+
5
+ /** Money as the admin page shows it: a count, the XNO text and the US dollars as text. */
6
+ export type Money = { count: number; xno: string; usd: string };
7
+
8
+ export type OrderStatus = "open" | "paid" | "expired" | "paid_late";
9
+
10
+ export type AdminOrder = {
11
+ id: string;
12
+ /** The player's Nano address. */
13
+ player: string;
14
+ lookId: string;
15
+ /** What the player reads for the look (the handler fills it from `lookName`). */
16
+ look: string;
17
+ usd: string;
18
+ xno: string;
19
+ status: OrderStatus | string;
20
+ createdAt: number;
21
+ expiresAt: number;
22
+ paidAt: number | null;
23
+ payer: string | null;
24
+ hash: string | null;
25
+ };
26
+
27
+ export type AdminPlayer = { player: string; looks: { id: string; name: string; at: number }[] };
28
+
29
+ /** The numbers of the orders and the looks that players own. */
30
+ export type StoreSummary = {
31
+ counts: { open: number; paid: number; expired: number; paid_late: number };
32
+ totals: { paid: Money; paidLate: Money };
33
+ /** Newest first, at most 500. `look` is filled by the handler. */
34
+ orders: Omit<AdminOrder, "look">[];
35
+ /** Players and the looks that a paid order gave them. `name` is filled by the handler. */
36
+ players: { player: string; looks: { id: string; at: number }[] }[];
37
+ /** The price of XNO in US dollars that the game uses, and where it came from. */
38
+ rate: { usd: number; sources: string[]; at: number } | null;
39
+ };
40
+
41
+ /**
42
+ * One admin sign-in, as the panel lists it: when, the rough country (two letters, or null when the game did not know it) and a short browser name ("Chrome on macOS").
43
+ * Nothing else: no address, no IP, no browser text. The game writes these when it makes an admin login; the package only reads them.
44
+ */
45
+ export type AdminLogin = { at: number; country: string | null; browser: string };
46
+
47
+ /** How many sign-ins the panel lists. */
48
+ export const ADMIN_LOGINS_SHOWN = 10;
49
+
50
+ /**
51
+ * The game's database, as the handler needs it. An adapter writes ONLY what these calls say: the two settings and, when the game turns it on, the deletion of the admin sessions
52
+ * (`signOutEverywhere`). Nothing else.
53
+ * (There is no call that writes a look, a price or an order: the Shelf gives text to copy, and a change of looks is a change of the repository.)
54
+ */
55
+ export interface AdminStore {
56
+ /** The settings that /admin has saved. A value that is not a known word is ignored by the handler, so an adapter returns what it holds. */
57
+ readSettings(): Promise<{ character?: unknown; signin?: unknown }>;
58
+ /** Saves a checked patch: all of it or none of it. */
59
+ saveSettings(patch: GamePatch, now: number): Promise<void>;
60
+ summary(now: number): Promise<StoreSummary>;
61
+ /**
62
+ * Optional. The last sign-ins to the admin, newest first, at most `limit`, with only the three things of `AdminLogin`. With it the summary lists them (the "Sign-ins" card).
63
+ * An adapter that has it must never return more than those three things for a sign-in.
64
+ */
65
+ recentLogins?(limit: number): Promise<AdminLogin[]>;
66
+ /**
67
+ * Optional. Deletes EVERY admin session (the admin login of the owner, in every browser, this one too) and no other session; returns how many it deleted. With it the handler has
68
+ * the route `POST {base}/signout-all` and the panel has "Sign out everywhere". An adapter must delete only sessions of the ADMIN scope.
69
+ */
70
+ signOutEverywhere?(): Promise<number>;
71
+ }
72
+
73
+ /** What the game says about itself on the Status card: only the game knows its address, its test mode and what is still off. */
74
+ export type AdminStatus = {
75
+ address: string | null;
76
+ testMode: boolean;
77
+ /** Whether Turnstile (the bot check) is on. */
78
+ turnstile?: boolean;
79
+ /** The names of the Nano nodes that check payments. */
80
+ nodes?: string[];
81
+ /** "owner/repo" on GitHub, for the link to looks.json. */
82
+ repo?: string | null;
83
+ /** Plain sentences about what is still off. */
84
+ warnings: string[];
85
+ };
86
+
87
+ /** The whole of GET summary. */
88
+ export type AdminSummary = AdminStatus &
89
+ Omit<StoreSummary, "orders" | "players"> & {
90
+ orders: AdminOrder[];
91
+ players: AdminPlayer[];
92
+ /** Whether the game gave a payment check (POST check). The panel shows "Check payments now" only then. */
93
+ canCheck: boolean;
94
+ /** The last admin sign-ins (newest first), when the store can list them. */
95
+ logins?: AdminLogin[];
96
+ /** Whether the route "sign out everywhere" exists (the store can do it). */
97
+ canSignOutEverywhere?: boolean;
98
+ };