Skip to content

Commit faa94d0

Browse files
Maselkovclaude
andauthored
Admin dialogs: rework Manage users and User history, readable buttons (#160)
Manage users is split into Visibility, Add a user and Users. New users default to operator, and the picked role's permissions are spelled out under the picker (and on each user's role badge). The list is one grid, so roles and buttons line up, with the app's usual row buttons. Each token shows when it was made and last used instead of its id. Your own row has no Delete, and a new token shows in its user's row. User history is drawn like Recent crafts: grouped by day, with the item's icon, amount, a result badge and the failure reason, and filters for requests, cancels and failures. Cancels now record what the CPU was making (a migration adds the columns), and the API says whether a cancel worked. Requests a user's stock targets made are left out unless asked for (Stock refills): they used to count toward the 100 events, so a busy target could push out everything the user did themselves. Backups gets a proper file button and Game data a Version heading. Primary and destructive buttons and the stale banner had white text at 2.2:1 and 2.8:1; they use darker fills (--accent-fill, --danger-fill) at about 4.8:1 in both themes. Co-authored-by: Maselkov <25772111+Maselkov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 22f1096 commit faa94d0

13 files changed

Lines changed: 443 additions & 127 deletions

File tree

‎docs/server.md‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ so a token can be replaced without losing data.
159159
| Role | Can |
160160
|---|---|
161161
| Viewer | View everything; keep personal pins and completion history |
162-
| Operator | Also request and cancel crafts, and see the last game update's [pattern check](using.md#pattern-check) |
163-
| Admin | Also manage users and tokens |
162+
| Operator | Also request and cancel crafts, set stock targets, and see the last game update's [pattern check](using.md#pattern-check) |
163+
| Admin | Also manage users, computers, game data, webhooks and backups |
164164

165165
The first admin is made with the setup link from the server's log (see the
166166
[Quick start](../README.md#quick-start)). Opening the page without it shows
@@ -177,11 +177,18 @@ docker compose exec gtnh-craft-monitor python app.py create-admin "Your name"
177177

178178
Managing users:
179179

180-
- Each generated token is shown once. Send it to its owner over a secure
181-
channel.
180+
- New users are operators unless you pick another role; the dialog says
181+
what the picked role can do.
182+
- Each generated token is shown once, in its user's row. Send it to its
183+
owner over a secure channel.
184+
- Each user's tokens show when they were made and last used.
185+
- **History** lists the user's craft requests and cancels, newest first:
186+
the item, whether it started or failed (and why), and which CPU.
187+
**Stock refills** adds the requests their keep-in-stock targets made;
188+
the server makes those, under whoever last set the target.
182189
- **Revoke** invalidates a token and ends its sessions immediately.
183190
- **New token** replaces a user's current token in one step.
184-
- **Delete** permanently removes a user with their tokens, sessions,
191+
- **Delete** (not on your own account) permanently removes a user with their tokens, sessions,
185192
pins and pending notifications. Their craft request history is kept.
186193

187194
### Private servers

‎server/gcm/db.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,14 @@ def _app_provider_stalls(conn):
623623
""")
624624

625625

626+
def _app_cancel_items(conn):
627+
# What the CPU was making when a cancel was asked for, for the admin
628+
# user history (gcm/store/requests.py). Rows from before only have
629+
# the CPU.
630+
conn.execute("ALTER TABLE craft_cancel_history ADD COLUMN item_label TEXT")
631+
conn.execute("ALTER TABLE craft_cancel_history ADD COLUMN item_icon TEXT")
632+
633+
626634
APP_MIGRATIONS = [
627635
_app_baseline,
628636
_app_drop_orphaned_user_rows,
@@ -634,6 +642,7 @@ def _app_provider_stalls(conn):
634642
_app_craft_event_auto,
635643
_app_settings,
636644
_app_provider_stalls,
645+
_app_cancel_items,
637646
]
638647
POWER_MIGRATIONS = [_power_baseline, _power_machines, _power_drop_impossible_times]
639648
ITEM_HISTORY_MIGRATIONS = [

‎server/gcm/routes/craft_requests.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ def craft_cancel_post():
185185
"reason": None,
186186
"created_at": created_at,
187187
},
188-
lambda req_id: store.requests.record_cancel(req_id, user_id, cpu_name, created_at),
188+
lambda req_id: store.requests.record_cancel(
189+
req_id, user_id, cpu_name, created_at, job.get("final_output"), job.get("final_output_icon")
190+
),
189191
)
190192
return jsonify({"ok": True, "id": req_id})
191193

‎server/gcm/routes/users.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from flask import Blueprint, g, jsonify, request
55

6-
from gcm import auth, config, notices, store
6+
from gcm import auth, config, icons, notices, store
77

88

99
bp = Blueprint("users", __name__)
@@ -178,13 +178,26 @@ def admin_user_token_regenerate(user_id):
178178
@bp.route("/api/admin/users/<user_id>/history", methods=["GET"])
179179
@auth.admin_required
180180
def admin_user_history_get(user_id):
181+
"""?auto=1 includes the requests the user's keep-in-stock targets made;
182+
auto_count is how many there are either way."""
181183
display_name = store.users.display_name(user_id)
182184
if display_name is None:
183185
return jsonify({"error": "unknown user"}), 404
186+
events = store.requests.user_activity(user_id, include_auto=request.args.get("auto") == "1")
187+
for event in events:
188+
# A request's icon is looked up in the game data in use now; a
189+
# cancel kept the one its CPU showed, moved to the live bundle.
190+
item_icon = event.pop("item_icon")
191+
event["icon"] = (
192+
icons.resolve_icon(event["mod"], event["internal"], event["damage"], event["target"], kind=event["kind"])
193+
if event["type"] == "request"
194+
else icons.current_path(item_icon)
195+
)
184196
return jsonify(
185197
{
186198
"user": {"id": user_id, "display_name": display_name},
187-
"events": store.requests.user_activity(user_id),
199+
"events": events,
200+
"auto_count": store.requests.user_auto_request_count(user_id),
188201
}
189202
)
190203

‎server/gcm/store/requests.py‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ def resolve_request(request_id, status, reason, cpu_name, only_if_open=False):
4444
)
4545

4646

47-
def record_cancel(request_id, user_id, cpu_name, created_at):
47+
def record_cancel(request_id, user_id, cpu_name, created_at, item_label=None, item_icon=None):
4848
with db.transaction(db.app_db) as conn:
4949
conn.execute(
5050
"INSERT INTO craft_cancel_history "
51-
"(request_id, user_id, cpu_name, status, created_at) "
52-
"VALUES (?, ?, ?, 'pending', ?)",
53-
(request_id, user_id, cpu_name, created_at),
51+
"(request_id, user_id, cpu_name, status, created_at, item_label, item_icon) "
52+
"VALUES (?, ?, ?, 'pending', ?, ?, ?)",
53+
(request_id, user_id, cpu_name, created_at, item_label, item_icon),
5454
)
5555

5656

@@ -94,17 +94,26 @@ def close_orphaned():
9494
)
9595

9696

97-
def user_activity(user_id, limit=100):
98-
"""A user's latest craft requests and cancellations, newest first."""
97+
def user_activity(user_id, limit=100, include_auto=False):
98+
"""A user's latest craft requests and cancellations, newest first.
99+
Requests a keep-in-stock target made (source 'auto', filed under who
100+
last set the target) only with include_auto: the server made them,
101+
and a busy target would otherwise push everything else past limit.
102+
target: the item (None for a cancel recorded without one); success:
103+
whether a resolved cancel worked (None for requests). A request's
104+
mod/internal/damage name its item; a cancel's item_icon is the icon
105+
path as it was then."""
99106
with db.transaction(db.app_db) as conn:
100107
rows = conn.execute(
101-
"SELECT 'request', label, status, reason, cpu_name, created_at, resolved_at, source "
102-
"FROM craft_request_history WHERE user_id = ? "
108+
"SELECT 'request', label, status, reason, cpu_name, created_at, resolved_at, source, "
109+
"amount, kind, NULL, mod, internal, damage, NULL "
110+
"FROM craft_request_history WHERE user_id = ? AND (? OR source != 'auto') "
103111
"UNION ALL "
104-
"SELECT 'cancel', cpu_name, status, reason, cpu_name, created_at, resolved_at, 'manual' "
112+
"SELECT 'cancel', item_label, status, reason, cpu_name, created_at, resolved_at, 'manual', "
113+
"NULL, NULL, success, NULL, NULL, NULL, item_icon "
105114
"FROM craft_cancel_history WHERE user_id = ? "
106115
"ORDER BY created_at DESC LIMIT ?",
107-
(user_id, user_id, limit),
116+
(user_id, include_auto, user_id, limit),
108117
).fetchall()
109118
return [
110119
{
@@ -116,6 +125,22 @@ def user_activity(user_id, limit=100):
116125
"created_at": row[5],
117126
"resolved_at": row[6],
118127
"source": row[7],
128+
"amount": row[8],
129+
"kind": row[9],
130+
"success": None if row[10] is None else bool(row[10]),
131+
"mod": row[11],
132+
"internal": row[12],
133+
"damage": row[13],
134+
"item_icon": row[14],
119135
}
120136
for row in rows
121137
]
138+
139+
140+
def user_auto_request_count(user_id):
141+
"""How many requests a user's keep-in-stock targets have made."""
142+
with db.transaction(db.app_db) as conn:
143+
return conn.execute(
144+
"SELECT COUNT(*) FROM craft_request_history WHERE user_id = ? AND source = 'auto'",
145+
(user_id,),
146+
).fetchone()[0]

‎server/index.html‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -351,36 +351,30 @@ <h2>This base is private</h2>
351351
</div>
352352

353353
<div id="adminUsersModal" class="modal-overlay" style="display:none;">
354-
<div class="modal-box admin-box">
354+
<div class="modal-box admin-box admin-users-box">
355355
<div class="modal-header">
356356
<div class="modal-item-name">User management</div>
357357
<button class="history-close-btn" data-action="close-admin" title="Close">&times;</button>
358358
</div>
359+
<div class="status-heading">Visibility</div>
359360
<div class="agent-settings">
360361
<label class="agent-setting"><input type="checkbox" id="privateToggle">
361362
<span>Only signed-in users can see this base
362363
<span class="agent-line">Signed out, the crafts, network and power are hidden, and links to this base show no preview.</span></span></label>
363364
</div>
364-
<div class="modal-note">Create a user and share the generated access token securely. It is shown only once.</div>
365+
<div class="status-heading admin-section-heading">Add a user</div>
365366
<div class="admin-form-grid">
366-
<input id="adminUserName" class="modal-amount-input" style="margin:0;" maxlength="100" placeholder="Display name" autocomplete="off">
367-
<select id="adminUserRole" class="modal-amount-input" style="margin:0;">
367+
<input id="adminUserName" class="modal-amount-input" maxlength="100" placeholder="Display name" autocomplete="off">
368+
<select id="adminUserRole" class="modal-amount-input" aria-describedby="adminRoleHelp">
368369
<option value="viewer">Viewer</option>
369-
<option value="operator">Operator</option>
370+
<option value="operator" selected>Operator</option>
370371
<option value="admin">Admin</option>
371372
</select>
372-
</div>
373-
<div id="adminUsersError" class="modal-error" style="display:none;"></div>
374-
<div class="modal-buttons" style="margin-top:12px;">
375373
<button class="modal-btn primary" id="adminCreateUserBtn" data-action="create-user">Create user</button>
376374
</div>
377-
<div id="adminTokenResult" class="admin-token-result" style="display:none;">
378-
<label class="modal-label">New access token</label>
379-
<textarea id="adminTokenValue" class="modal-amount-input admin-token-value" readonly></textarea>
380-
<div class="modal-buttons">
381-
<button class="modal-btn secondary" data-action="copy-token">Copy token</button>
382-
</div>
383-
</div>
375+
<div id="adminRoleHelp" class="modal-note admin-role-help"></div>
376+
<div id="adminUsersError" class="modal-error" style="display:none;"></div>
377+
<div class="status-heading admin-section-heading">Users <span id="adminUsersCount"></span></div>
384378
<div id="adminUsersList" class="admin-users"></div>
385379
</div>
386380
</div>
@@ -391,6 +385,7 @@ <h2>This base is private</h2>
391385
<div class="modal-item-name">Game data</div>
392386
<button class="history-close-btn" data-action="close-gamedata" title="Close">&times;</button>
393387
</div>
388+
<div class="status-heading">Version</div>
394389
<div class="modal-note">Item icons and the network scanner's item list, for the GTNH version your server runs. The download is about 300 MB and is used without a restart; the scanner switches on its next scan.</div>
395390
<div id="gamedataCurrent" class="gamedata-current"></div>
396391
<details id="gamedataUnknown" class="gamedata-unknown" hidden>
@@ -449,7 +444,11 @@ <h2>This base is private</h2>
449444
</div>
450445
<div class="status-heading backups-restore-heading">Restore</div>
451446
<div class="modal-note">Replaces everything with the backup's: every database, account, token and key. Anything since the backup was made is lost, and you're signed out if your session is newer than it.</div>
452-
<input type="file" id="restoreFile" class="restore-file" accept=".zip,application/zip">
447+
<div class="restore-pick">
448+
<label class="range-btn restore-pick-btn">Choose backup&hellip;
449+
<input type="file" id="restoreFile" class="restore-file" accept=".zip,application/zip"></label>
450+
<span id="restoreFileName" class="restore-file-name">No file chosen</span>
451+
</div>
453452
<div id="restoreError" class="modal-error" style="display:none;"></div>
454453
<div class="modal-buttons">
455454
<button class="modal-btn cancel-confirm-btn" id="restoreBtn" data-action="restore-backup" disabled>Restore</button>
@@ -551,12 +550,23 @@ <h2>This base is private</h2>
551550
</div>
552551

553552
<div id="userHistoryModal" class="modal-overlay" style="display:none;">
554-
<div class="modal-box admin-box">
553+
<div class="modal-box admin-box user-history-box">
555554
<div class="modal-header">
556-
<div id="userHistoryTitle" class="modal-item-name">User history</div>
555+
<div class="history-name-wrap">
556+
<div id="userHistoryTitle" class="modal-item-name">User history</div>
557+
<div id="userHistorySummary" class="cpu-id"></div>
558+
</div>
557559
<button class="history-close-btn" data-action="close-user-history" title="Close">&times;</button>
558560
</div>
559-
<div id="userHistoryList" class="user-history-list"></div>
561+
<div id="userHistoryFilters" class="range-buttons user-history-filters">
562+
<button class="range-btn active" data-action="user-history-filter" data-filter="all">All</button>
563+
<button class="range-btn" data-action="user-history-filter" data-filter="request">Requests</button>
564+
<button class="range-btn" data-action="user-history-filter" data-filter="cancel">Cancels</button>
565+
<button class="range-btn" data-action="user-history-filter" data-filter="problem">Failed</button>
566+
<button id="userHistoryAuto" class="range-btn user-history-auto" data-action="user-history-auto" hidden
567+
title="Requests their keep-in-stock targets made"></button>
568+
</div>
569+
<div id="userHistoryList" class="craft-history-list user-history-list"></div>
560570
</div>
561571
</div>
562572

0 commit comments

Comments
 (0)