kryten-webqueue 0.14.2__tar.gz → 0.15.0__tar.gz
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.
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/CHANGELOG.md +12 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/PKG-INFO +1 -1
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/api_gate/client.py +15 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/catalog/db.py +10 -3
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/queue.py +8 -4
- kryten_webqueue-0.15.0/kryten_webqueue/routes/user.py +89 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/static/css/main.css +164 -0
- kryten_webqueue-0.15.0/kryten_webqueue/templates/user/dashboard.html +449 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/pyproject.toml +1 -1
- kryten_webqueue-0.14.2/kryten_webqueue/routes/user.py +0 -35
- kryten_webqueue-0.14.2/kryten_webqueue/templates/user/dashboard.html +0 -126
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/.github/workflows/python-publish.yml +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/.github/workflows/release.yml +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/.gitignore +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/README.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/config.example.json +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/deploy/kryten-webqueue.service +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/deploy/nginx-queue.conf +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/IMPLEMENTATION_SPEC.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/IMPL_API_GATE.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/IMPL_ECONOMY.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/IMPL_KRYTEN_PY.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/IMPL_ROBOT.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/PLAN_PRESENCE_AND_PROMOS.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/PRE_PLAN_GAPS.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/PRODUCT_PLAN.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/docs/SPEC_JOBS_AND_BROWSE.md +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/__main__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/api_gate/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/app.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/auth/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/auth/otp.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/auth/rate_limit.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/auth/session.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/catalog/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/catalog/images.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/catalog/mediacms.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/catalog/sync.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/config.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/_common.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/enrichmeta.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/enrichtitles.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/enrichtv.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/cmsutils/fetchurls.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/ytpipe/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/integrations/ytpipe/downloader.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/jobs/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/jobs/fetchurls_auth.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/jobs/manager.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/jobs/tasks.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/bulk_add.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/fire.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/importer.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/ordering.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/playlists/scheduler.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/promos/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/promos/director.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/queue/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/queue/ordering.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/queue/poller.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/queue/presence.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/queue/shadow.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_catalog.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_jobs.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_playlists.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_promos.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_queue.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/admin_schedules.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/auth.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/catalog.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/routes/pages.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/static/js/main.js +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/admin/index.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/admin/playlists.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/admin/promos.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/admin/queue_mgmt.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/admin/schedules.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/auth/login.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/base.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/catalog/browse.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/catalog/item_detail.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/catalog/item_not_found.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/templates/queue/index.html +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/ws/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/ws/handler.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/kryten_webqueue/ws/manager.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/__init__.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_config_persistence.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_fetchurls_sharepoint.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_phase1.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_phase2_jobs.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_phase3_jobs.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_phase4_live_fixes.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_playlist_import.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_presence_refund.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_promo_director.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_promo_pool_exclusion.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_queue_announce.py +0 -0
- {kryten_webqueue-0.14.2 → kryten_webqueue-0.15.0}/tests/test_save_results_to_playlist.py +0 -0
|
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.15.0] — 2026-06-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Account progression panel on the Z-Coin dashboard.** The left column now surfaces the user's economy account in full: current rank/level, a progress bar toward the next milestone (remaining Z + percent), active perks (including spend discount), and their purchased vanity items. Powered by the new api-gate `GET /economy/account/{username}` endpoint (economy `account.summary`), proxied via `GET /user/account`.
|
|
14
|
+
- **Vanity item editing dialogs.** Inline **Edit** buttons open dialogs to purchase/update the custom greeting (textarea, 200-char limit) and custom chat color (native color picker synced with a 6-digit hex field). Backed by `POST /user/vanity/greeting` and `POST /user/vanity/color`, which proxy the economy `vanity.set_greeting` / `vanity.set_color` commands. The username is taken from the authenticated session, never the request body.
|
|
15
|
+
- **Transaction credit/debit filter.** The Recent Transactions column gains an All / Credits / Debits toggle and a **Load more** control, with friendlier titles derived from each transaction's `trigger_id` (e.g. `presence.base` → "Watching reward", `spend.vanity.chat_color` → "Custom color") instead of raw `earn` labels.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Queue history pagination.** `GET /queue/history` now accepts `limit`/`offset` and returns a `total` count; the dashboard's middle column paginates through the full history with Prev/Next controls instead of showing only the most recent 20 entries.
|
|
20
|
+
|
|
9
21
|
## [0.14.2] — 2026-06-13
|
|
10
22
|
|
|
11
23
|
### Added
|
|
@@ -93,6 +93,21 @@ class ApiGateClient:
|
|
|
93
93
|
async def get_transactions(self, username: str, limit: int = 20, offset: int = 0) -> dict:
|
|
94
94
|
return await self.get(f"/economy/transactions/{username}", limit=limit, offset=offset)
|
|
95
95
|
|
|
96
|
+
async def get_account_summary(self, username: str) -> dict:
|
|
97
|
+
return await self.get(f"/economy/account/{username}")
|
|
98
|
+
|
|
99
|
+
async def set_vanity_greeting(self, username: str, value: str) -> dict:
|
|
100
|
+
return await self.post("/economy/vanity/greeting", json={
|
|
101
|
+
"username": username,
|
|
102
|
+
"value": value,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
async def set_vanity_color(self, username: str, value: str) -> dict:
|
|
106
|
+
return await self.post("/economy/vanity/color", json={
|
|
107
|
+
"username": username,
|
|
108
|
+
"value": value,
|
|
109
|
+
})
|
|
110
|
+
|
|
96
111
|
async def queue_preview(self, username: str, duration_sec: int, tier: str = "queue") -> dict:
|
|
97
112
|
return await self.post("/economy/queue-preview", json={
|
|
98
113
|
"username": username,
|
|
@@ -893,12 +893,19 @@ class Database:
|
|
|
893
893
|
[username, friendly_token, title, tier, z_cost],
|
|
894
894
|
)
|
|
895
895
|
|
|
896
|
-
async def get_user_queue_history(self, username: str, limit: int = 50) -> list[dict]:
|
|
896
|
+
async def get_user_queue_history(self, username: str, limit: int = 50, offset: int = 0) -> list[dict]:
|
|
897
897
|
return await self._fetch_all(
|
|
898
|
-
"SELECT * FROM queue_history WHERE username=? ORDER BY id DESC LIMIT ?",
|
|
899
|
-
[username, limit],
|
|
898
|
+
"SELECT * FROM queue_history WHERE username=? ORDER BY id DESC LIMIT ? OFFSET ?",
|
|
899
|
+
[username, limit, offset],
|
|
900
900
|
)
|
|
901
901
|
|
|
902
|
+
async def get_user_queue_history_count(self, username: str) -> int:
|
|
903
|
+
row = await self._fetch_one(
|
|
904
|
+
"SELECT COUNT(*) AS c FROM queue_history WHERE username=?",
|
|
905
|
+
[username],
|
|
906
|
+
)
|
|
907
|
+
return int(row["c"]) if row else 0
|
|
908
|
+
|
|
902
909
|
# --- Saved playlists ---
|
|
903
910
|
|
|
904
911
|
async def get_saved_playlists(self) -> list[dict]:
|
|
@@ -223,11 +223,15 @@ async def cost_preview(request: Request, friendly_token: str, tier: str = "queue
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
@router.get("/history")
|
|
226
|
-
async def queue_history(request: Request,
|
|
227
|
-
|
|
226
|
+
async def queue_history(request: Request, limit: int = 20, offset: int = 0,
|
|
227
|
+
user: dict = Depends(get_current_user)):
|
|
228
|
+
"""Get user's queue history (paginated, most recent first)."""
|
|
229
|
+
limit = max(1, min(limit, 100))
|
|
230
|
+
offset = max(0, offset)
|
|
228
231
|
db = request.app.state.db
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
items = await db.get_user_queue_history(user["username"], limit=limit, offset=offset)
|
|
233
|
+
total = await db.get_user_queue_history_count(user["username"])
|
|
234
|
+
return {"items": items, "total": total, "limit": limit, "offset": offset}
|
|
231
235
|
|
|
232
236
|
|
|
233
237
|
@router.get("/next-schedule")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
from ..auth.session import get_current_user
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/user", tags=["user"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("/balance")
|
|
10
|
+
async def get_balance(request: Request, user: dict = Depends(get_current_user)):
|
|
11
|
+
"""Get user's economy balance."""
|
|
12
|
+
api_gate = request.app.state.api_gate
|
|
13
|
+
return await api_gate.get_balance(user["username"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/account")
|
|
17
|
+
async def get_account(request: Request, user: dict = Depends(get_current_user)):
|
|
18
|
+
"""Get the user's full economy account summary (rank, progress, perks, vanity)."""
|
|
19
|
+
api_gate = request.app.state.api_gate
|
|
20
|
+
return await api_gate.get_account_summary(user["username"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GreetingUpdate(BaseModel):
|
|
24
|
+
value: str
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ColorUpdate(BaseModel):
|
|
28
|
+
value: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/vanity/greeting")
|
|
32
|
+
async def set_vanity_greeting(
|
|
33
|
+
body: GreetingUpdate, request: Request, user: dict = Depends(get_current_user)
|
|
34
|
+
):
|
|
35
|
+
"""Purchase/update the current user's custom greeting."""
|
|
36
|
+
api_gate = request.app.state.api_gate
|
|
37
|
+
try:
|
|
38
|
+
return await api_gate.set_vanity_greeting(user["username"], body.value)
|
|
39
|
+
except Exception as exc: # noqa: BLE001
|
|
40
|
+
raise HTTPException(status_code=400, detail=_economy_error(exc)) from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.post("/vanity/color")
|
|
44
|
+
async def set_vanity_color(
|
|
45
|
+
body: ColorUpdate, request: Request, user: dict = Depends(get_current_user)
|
|
46
|
+
):
|
|
47
|
+
"""Purchase/update the current user's custom chat color (6-digit hex)."""
|
|
48
|
+
api_gate = request.app.state.api_gate
|
|
49
|
+
try:
|
|
50
|
+
return await api_gate.set_vanity_color(user["username"], body.value)
|
|
51
|
+
except Exception as exc: # noqa: BLE001
|
|
52
|
+
raise HTTPException(status_code=400, detail=_economy_error(exc)) from exc
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _economy_error(exc: Exception) -> str:
|
|
56
|
+
"""Extract a human-readable message from an api-gate HTTP error."""
|
|
57
|
+
import httpx
|
|
58
|
+
|
|
59
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
60
|
+
try:
|
|
61
|
+
detail = exc.response.json().get("detail")
|
|
62
|
+
if detail:
|
|
63
|
+
return str(detail)
|
|
64
|
+
except Exception: # noqa: BLE001
|
|
65
|
+
pass
|
|
66
|
+
return "Purchase failed. Please try again."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.get("/transactions")
|
|
70
|
+
async def get_transactions(request: Request, limit: int = 20, offset: int = 0,
|
|
71
|
+
user: dict = Depends(get_current_user)):
|
|
72
|
+
"""Get user's transaction history."""
|
|
73
|
+
api_gate = request.app.state.api_gate
|
|
74
|
+
return await api_gate.get_transactions(user["username"], limit=limit, offset=offset)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get("/profile")
|
|
78
|
+
async def get_profile(request: Request, user: dict = Depends(get_current_user)):
|
|
79
|
+
"""Get current user profile info from api-gate."""
|
|
80
|
+
api_gate = request.app.state.api_gate
|
|
81
|
+
try:
|
|
82
|
+
user_data = await api_gate.get_user(user["username"])
|
|
83
|
+
except Exception:
|
|
84
|
+
user_data = {}
|
|
85
|
+
return {
|
|
86
|
+
"username": user["username"],
|
|
87
|
+
"rank": user["rank"],
|
|
88
|
+
"online": user_data.get("online", False) if user_data else False,
|
|
89
|
+
}
|
|
@@ -790,6 +790,170 @@ a.np-chip {
|
|
|
790
790
|
color: var(--danger);
|
|
791
791
|
}
|
|
792
792
|
|
|
793
|
+
/* Account rank + progression (dashboard left column) */
|
|
794
|
+
.account-rank {
|
|
795
|
+
margin-top: 1.25rem;
|
|
796
|
+
padding-top: 1.25rem;
|
|
797
|
+
border-top: 1px solid var(--border);
|
|
798
|
+
}
|
|
799
|
+
.rank-name {
|
|
800
|
+
font-size: 1.1rem;
|
|
801
|
+
font-weight: 700;
|
|
802
|
+
}
|
|
803
|
+
.rank-level {
|
|
804
|
+
font-size: 0.8rem;
|
|
805
|
+
color: var(--text-secondary);
|
|
806
|
+
margin-top: 0.15rem;
|
|
807
|
+
}
|
|
808
|
+
.rank-progress {
|
|
809
|
+
margin-top: 0.85rem;
|
|
810
|
+
}
|
|
811
|
+
.rank-progress-head, .rank-progress-foot {
|
|
812
|
+
display: flex;
|
|
813
|
+
justify-content: space-between;
|
|
814
|
+
font-size: 0.8rem;
|
|
815
|
+
color: var(--text-secondary);
|
|
816
|
+
}
|
|
817
|
+
.rank-progress-head {
|
|
818
|
+
margin-bottom: 0.35rem;
|
|
819
|
+
}
|
|
820
|
+
.rank-progress-foot {
|
|
821
|
+
margin-top: 0.35rem;
|
|
822
|
+
}
|
|
823
|
+
.progress-track {
|
|
824
|
+
height: 8px;
|
|
825
|
+
border-radius: 999px;
|
|
826
|
+
background: var(--bg-secondary, #1a1a24);
|
|
827
|
+
overflow: hidden;
|
|
828
|
+
border: 1px solid var(--border);
|
|
829
|
+
}
|
|
830
|
+
.progress-fill {
|
|
831
|
+
height: 100%;
|
|
832
|
+
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
|
|
833
|
+
transition: width 0.4s ease;
|
|
834
|
+
}
|
|
835
|
+
.account-perks {
|
|
836
|
+
margin-top: 1.25rem;
|
|
837
|
+
}
|
|
838
|
+
.account-perks h3 {
|
|
839
|
+
font-size: 0.85rem;
|
|
840
|
+
color: var(--text-secondary);
|
|
841
|
+
margin-bottom: 0.4rem;
|
|
842
|
+
}
|
|
843
|
+
.perk-list {
|
|
844
|
+
list-style: none;
|
|
845
|
+
padding: 0;
|
|
846
|
+
margin: 0;
|
|
847
|
+
display: flex;
|
|
848
|
+
flex-direction: column;
|
|
849
|
+
gap: 0.2rem;
|
|
850
|
+
}
|
|
851
|
+
.perk-list li {
|
|
852
|
+
font-size: 0.85rem;
|
|
853
|
+
}
|
|
854
|
+
.perk-list li::before {
|
|
855
|
+
content: "✓ ";
|
|
856
|
+
color: var(--success);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/* Vanity items (dashboard left column) */
|
|
860
|
+
.vanity-section {
|
|
861
|
+
margin-top: 1.25rem;
|
|
862
|
+
padding-top: 1.25rem;
|
|
863
|
+
border-top: 1px solid var(--border);
|
|
864
|
+
}
|
|
865
|
+
.vanity-section h3 {
|
|
866
|
+
font-size: 0.85rem;
|
|
867
|
+
color: var(--text-secondary);
|
|
868
|
+
margin-bottom: 0.6rem;
|
|
869
|
+
}
|
|
870
|
+
.vanity-item {
|
|
871
|
+
margin-bottom: 0.85rem;
|
|
872
|
+
}
|
|
873
|
+
.vanity-row {
|
|
874
|
+
display: flex;
|
|
875
|
+
align-items: center;
|
|
876
|
+
justify-content: space-between;
|
|
877
|
+
gap: 0.5rem;
|
|
878
|
+
}
|
|
879
|
+
.vanity-label {
|
|
880
|
+
font-size: 0.85rem;
|
|
881
|
+
font-weight: 600;
|
|
882
|
+
}
|
|
883
|
+
.vanity-value {
|
|
884
|
+
font-size: 0.85rem;
|
|
885
|
+
color: var(--text-primary);
|
|
886
|
+
margin-top: 0.25rem;
|
|
887
|
+
word-break: break-word;
|
|
888
|
+
display: flex;
|
|
889
|
+
align-items: center;
|
|
890
|
+
gap: 0.4rem;
|
|
891
|
+
}
|
|
892
|
+
.vanity-unset {
|
|
893
|
+
color: var(--text-secondary);
|
|
894
|
+
font-style: italic;
|
|
895
|
+
}
|
|
896
|
+
.color-swatch {
|
|
897
|
+
display: inline-block;
|
|
898
|
+
width: 1rem;
|
|
899
|
+
height: 1rem;
|
|
900
|
+
border-radius: 3px;
|
|
901
|
+
border: 1px solid var(--border);
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/* Color picker dialog */
|
|
906
|
+
.color-picker-row {
|
|
907
|
+
display: flex;
|
|
908
|
+
align-items: center;
|
|
909
|
+
gap: 0.75rem;
|
|
910
|
+
margin: 0.5rem 0 1rem;
|
|
911
|
+
}
|
|
912
|
+
.color-picker-row input[type="color"] {
|
|
913
|
+
width: 48px;
|
|
914
|
+
height: 40px;
|
|
915
|
+
padding: 0;
|
|
916
|
+
border: 1px solid var(--border);
|
|
917
|
+
border-radius: var(--radius);
|
|
918
|
+
background: transparent;
|
|
919
|
+
cursor: pointer;
|
|
920
|
+
}
|
|
921
|
+
.color-hex {
|
|
922
|
+
flex: 1;
|
|
923
|
+
text-transform: uppercase;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/* Transaction filter toggle */
|
|
927
|
+
.tx-toggle {
|
|
928
|
+
display: flex;
|
|
929
|
+
gap: 0.25rem;
|
|
930
|
+
margin: 0.5rem 0 0.75rem;
|
|
931
|
+
}
|
|
932
|
+
.tx-toggle-btn {
|
|
933
|
+
flex: 1;
|
|
934
|
+
padding: 0.35rem 0.5rem;
|
|
935
|
+
font-size: 0.8rem;
|
|
936
|
+
background: var(--bg-secondary, #1a1a24);
|
|
937
|
+
color: var(--text-secondary);
|
|
938
|
+
border: 1px solid var(--border);
|
|
939
|
+
border-radius: var(--radius);
|
|
940
|
+
cursor: pointer;
|
|
941
|
+
}
|
|
942
|
+
.tx-toggle-btn.active {
|
|
943
|
+
background: var(--accent);
|
|
944
|
+
border-color: var(--accent);
|
|
945
|
+
color: #fff;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/* Dashboard history / transaction pagers */
|
|
949
|
+
.history-pager, .tx-pager {
|
|
950
|
+
display: flex;
|
|
951
|
+
align-items: center;
|
|
952
|
+
justify-content: center;
|
|
953
|
+
gap: 0.5rem;
|
|
954
|
+
margin-top: 1rem;
|
|
955
|
+
}
|
|
956
|
+
|
|
793
957
|
/* Admin */
|
|
794
958
|
.admin-nav {
|
|
795
959
|
display: flex;
|