v3.0.4: eigene Werkzeug-Seite, Logs der MBMNET-Server (HTTPS mit MBMNET-CA), Admin nur mit signiertem Nachweis vom Server
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
718c605f6d
commit
d1f5f5add3
@@ -0,0 +1,511 @@
|
||||
// Werkzeuge > Logs: Log-Zeilen aller MBMNET-Server, wie auf der Log-Webseite (MBMNET-Tools/logserver).
|
||||
//
|
||||
// Der Log-Server ist nur übers Accounting erreichbar (/mbmnet/logs/..., logsproxy.js), und zwar nur über
|
||||
// HTTPS mit der MBMNET-CA (dieselbe wie für Miiverse): Das Log-Passwort geht nie im Klartext übers Netz.
|
||||
// Oben die Zeilen (neueste unten, Steuerkreuz blättert, ganz oben werden ältere nachgeladen), unten die
|
||||
// Quellen zum Filtern. Ist man ganz unten, lädt die Ansicht alle 5 Sekunden neu (wie "live" im Browser).
|
||||
#include <3ds.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "logview.h"
|
||||
#include "https.h"
|
||||
#include "ui.h"
|
||||
|
||||
#define TOKEN_FILE "sdmc:/3ds/mbmnet/logs-token"
|
||||
#define MAX_LINES 600
|
||||
#define MAX_SOURCES 48
|
||||
#define ROW_H 13
|
||||
#define ROWS 16
|
||||
#define SRC_ROW_H 24
|
||||
#define SRC_ROWS 7
|
||||
#define RESP_MAX (192 * 1024)
|
||||
|
||||
typedef struct {
|
||||
u32 id;
|
||||
u64 t; // ms seit 1970 (UTC, vom Server)
|
||||
char level; // 'i', 'w', 'e'
|
||||
char src[28]; // Quelle (ohne Host)
|
||||
char msg[404];
|
||||
} Line;
|
||||
|
||||
typedef struct {
|
||||
char key[80], name[32], host[32];
|
||||
bool online;
|
||||
u32 errors, warns;
|
||||
} Source;
|
||||
|
||||
typedef enum { LV_NEED_PASSWORD, LV_LOADING, LV_READY, LV_ERROR } LvState;
|
||||
enum { JOB_NONE, JOB_LOGIN, JOB_RELOAD, JOB_OLDER };
|
||||
|
||||
// --- geteilt mit dem Lade-Thread (lock) ---
|
||||
static LightLock lock;
|
||||
static LightEvent wake;
|
||||
static Thread thread;
|
||||
static volatile bool quit, busy;
|
||||
static volatile int job;
|
||||
static char server[64], token[72], password[72];
|
||||
static Line *lines;
|
||||
static int lineCount;
|
||||
static Source sources[MAX_SOURCES];
|
||||
static int sourceCount;
|
||||
static volatile LvState state = LV_LOADING;
|
||||
static char status[128];
|
||||
static s64 timeOffset; // Sekunden: Konsolenuhr - Serveruhr (für die Uhrzeit der Zeilen)
|
||||
static u64 lastLoad; // osGetTime() des letzten Ladens
|
||||
|
||||
// --- nur Hauptthread ---
|
||||
static int cursor; // gewählte Zeile
|
||||
static int top; // erste sichtbare Zeile
|
||||
static int srcSel; // 0 = alle, sonst sources[srcSel - 1]
|
||||
static int srcTop;
|
||||
static bool errorsOnly, detail, following = true;
|
||||
static char filterKey[80]; // gewählte Quelle (key), leer = alle
|
||||
|
||||
|
||||
// --- HTTP ----------------------------------------------------------------------------
|
||||
|
||||
static int parse_month(const char *m)
|
||||
{
|
||||
static const char *names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
|
||||
for (int i = 0; i < 12; i++) if (!strncmp(m, names[i], 3)) return i + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Tage seit 1970-01-01 (proleptischer gregorianischer Kalender)
|
||||
static s64 days_from_civil(int y, int m, int d)
|
||||
{
|
||||
y -= m <= 2;
|
||||
s64 era = (y >= 0 ? y : y - 399) / 400;
|
||||
s64 yoe = y - era * 400;
|
||||
s64 doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
||||
s64 doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
// Server-Uhrzeit aus der Date-Kopfzeile ("Mon, 26 Oct 2026 12:34:56 GMT") -> Abstand zur Konsolenuhr
|
||||
static void sync_clock(const char *date)
|
||||
{
|
||||
int d, y, hh, mm, ss;
|
||||
char mon[4] = "";
|
||||
if (sscanf(date, "%*3s, %d %3s %d %d:%d:%d", &d, mon, &y, &hh, &mm, &ss) != 6) return;
|
||||
int m = parse_month(mon);
|
||||
if (!m) return;
|
||||
s64 serverNow = days_from_civil(y, m, d) * 86400 + hh * 3600 + mm * 60 + ss;
|
||||
timeOffset = (s64)time(NULL) - serverNow;
|
||||
}
|
||||
|
||||
// HTTPS-Anfrage ans Accounting (https.c, MBMNET-CA). Status 0 = keine Verbindung (status bekommt den Grund).
|
||||
static u32 request(const char *path, const char *form, char *out, u32 cap, u32 *outLen)
|
||||
{
|
||||
char date[64];
|
||||
Result rc = 0;
|
||||
u32 code = https_request(server, path, form, token[0] ? "X-Logs-Token" : NULL, token, out, cap, outLen, date, sizeof(date), &rc);
|
||||
if (date[0]) sync_clock(date);
|
||||
if (!code) snprintf(status, sizeof(status), rc == -1 ? "MBMNET-Zertifikat fehlt (Server nicht erreichbar?)"
|
||||
: "Keine Verbindung (%08lX)", (unsigned long)rc);
|
||||
return code;
|
||||
}
|
||||
|
||||
static void url_encode(char *out, size_t len, const char *in)
|
||||
{
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
size_t n = 0;
|
||||
for (; *in && n + 4 < len; in++) {
|
||||
unsigned char c = *in;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') out[n++] = c;
|
||||
else { out[n++] = '%'; out[n++] = hex[c >> 4]; out[n++] = hex[c & 15]; }
|
||||
}
|
||||
out[n] = 0;
|
||||
}
|
||||
|
||||
static void save_token(const char *t)
|
||||
{
|
||||
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||
FILE *f = fopen(TOKEN_FILE, "w");
|
||||
if (f) { fputs(t, f); fclose(f); }
|
||||
}
|
||||
|
||||
static void load_token(void)
|
||||
{
|
||||
token[0] = 0;
|
||||
FILE *f = fopen(TOKEN_FILE, "r");
|
||||
if (!f) return;
|
||||
if (fgets(token, sizeof(token), f)) token[strcspn(token, "\r\n")] = 0;
|
||||
fclose(f);
|
||||
if (strlen(token) != 64) token[0] = 0;
|
||||
}
|
||||
|
||||
static void forget_token(void)
|
||||
{
|
||||
token[0] = 0;
|
||||
remove(TOKEN_FILE);
|
||||
}
|
||||
|
||||
// Zeile mit Tabs zerlegen
|
||||
static int split(char *line, char **f, int max)
|
||||
{
|
||||
int n = 0;
|
||||
while (n < max) {
|
||||
f[n++] = line;
|
||||
char *tab = strchr(line, '\t');
|
||||
if (!tab) break;
|
||||
*tab = 0;
|
||||
line = tab + 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void parse_line(Line *l, char **f)
|
||||
{
|
||||
l->id = strtoul(f[0], NULL, 10);
|
||||
l->t = strtoull(f[1], NULL, 10);
|
||||
l->level = f[2][0] == 'e' ? 'e' : f[2][0] == 'w' ? 'w' : 'i';
|
||||
char *bar = strchr(f[3], '|'); // key = "quelle|host"
|
||||
if (bar) *bar = 0;
|
||||
snprintf(l->src, sizeof(l->src), "%s", f[3]);
|
||||
snprintf(l->msg, sizeof(l->msg), "%s", f[4]);
|
||||
}
|
||||
|
||||
// --- Lade-Thread ---------------------------------------------------------------------
|
||||
|
||||
static bool load_sources(char *buf)
|
||||
{
|
||||
u32 len;
|
||||
u32 code = request("/mbmnet/logs/sources", NULL, buf, RESP_MAX, &len);
|
||||
if (code == 401) return false;
|
||||
if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; }
|
||||
Source tmp[MAX_SOURCES];
|
||||
int n = 0;
|
||||
char *save = NULL, *f[8];
|
||||
for (char *line = strtok_r(buf, "\n", &save); line && n < MAX_SOURCES; line = strtok_r(NULL, "\n", &save)) {
|
||||
if (split(line, f, 8) < 6) continue;
|
||||
Source *s = &tmp[n++];
|
||||
snprintf(s->key, sizeof(s->key), "%s", f[0]);
|
||||
snprintf(s->name, sizeof(s->name), "%s", f[1]);
|
||||
snprintf(s->host, sizeof(s->host), "%s", f[2]);
|
||||
s->online = f[3][0] == '1';
|
||||
s->errors = strtoul(f[4], NULL, 10);
|
||||
s->warns = strtoul(f[5], NULL, 10);
|
||||
}
|
||||
LightLock_Lock(&lock);
|
||||
memcpy(sources, tmp, sizeof(Source) * n);
|
||||
sourceCount = n;
|
||||
LightLock_Unlock(&lock);
|
||||
return true;
|
||||
}
|
||||
|
||||
// older = vor der ältesten Zeile weiterlesen (sonst die neuesten). false = nicht angemeldet.
|
||||
static bool load_lines(char *buf, bool older)
|
||||
{
|
||||
char path[200], key[96];
|
||||
url_encode(key, sizeof(key), filterKey);
|
||||
LightLock_Lock(&lock);
|
||||
u32 before = older && lineCount ? lines[0].id : 0;
|
||||
LightLock_Unlock(&lock);
|
||||
snprintf(path, sizeof(path), "/mbmnet/logs/lines?limit=%d%s%s%s%s", older ? 100 : 200, key[0] ? "&source=" : "", key,
|
||||
errorsOnly ? "&level=error" : "", "");
|
||||
if (before) snprintf(path + strlen(path), sizeof(path) - strlen(path), "&before=%lu", (unsigned long)before);
|
||||
u32 len;
|
||||
u32 code = request(path, NULL, buf, RESP_MAX, &len);
|
||||
if (code == 401) return false;
|
||||
if (code != 200) { if (code) snprintf(status, sizeof(status), "Fehler beim Laden (HTTP %lu)", (unsigned long)code); return true; }
|
||||
|
||||
// Neue Zeilen zählen und einlesen
|
||||
int n = 0;
|
||||
for (char *p = buf; *p; p++) if (*p == '\n') n++;
|
||||
if (len && buf[len - 1] != '\n') n++;
|
||||
Line *fresh = n ? malloc(sizeof(Line) * n) : NULL;
|
||||
int got = 0;
|
||||
char *save = NULL, *f[6];
|
||||
for (char *line = strtok_r(buf, "\n", &save); line && got < n; line = strtok_r(NULL, "\n", &save)) {
|
||||
if (split(line, f, 6) < 5) continue;
|
||||
parse_line(&fresh[got++], f);
|
||||
}
|
||||
|
||||
LightLock_Lock(&lock);
|
||||
if (older) {
|
||||
// vorne anhängen, höchstens MAX_LINES (die neuesten fallen dann weg)
|
||||
int keep = lineCount;
|
||||
if (got + keep > MAX_LINES) keep = MAX_LINES - got;
|
||||
if (keep < 0) keep = 0;
|
||||
memmove(lines + got, lines, sizeof(Line) * keep);
|
||||
memcpy(lines, fresh, sizeof(Line) * (got > MAX_LINES ? MAX_LINES : got));
|
||||
lineCount = got + keep;
|
||||
cursor += got;
|
||||
top += got;
|
||||
} else {
|
||||
if (got > MAX_LINES) got = MAX_LINES;
|
||||
memcpy(lines, fresh, sizeof(Line) * got);
|
||||
lineCount = got;
|
||||
}
|
||||
LightLock_Unlock(&lock);
|
||||
free(fresh);
|
||||
snprintf(status, sizeof(status), "%s", "");
|
||||
lastLoad = osGetTime();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void worker(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char *buf = malloc(RESP_MAX);
|
||||
while (!quit) {
|
||||
LightEvent_Wait(&wake);
|
||||
if (quit) break;
|
||||
int j = job;
|
||||
job = JOB_NONE;
|
||||
if (j == JOB_NONE || !buf) continue;
|
||||
busy = true;
|
||||
if (j == JOB_LOGIN) {
|
||||
char form[160], enc[140];
|
||||
url_encode(enc, sizeof(enc), password);
|
||||
snprintf(form, sizeof(form), "password=%s", enc);
|
||||
memset(password, 0, sizeof(password));
|
||||
token[0] = 0;
|
||||
u32 len;
|
||||
u32 code = request("/mbmnet/logs/login", form, buf, 256, &len);
|
||||
if (code == 200 && len == 64) {
|
||||
memcpy(token, buf, 64);
|
||||
token[64] = 0;
|
||||
save_token(token);
|
||||
j = JOB_RELOAD;
|
||||
} else {
|
||||
if (code == 401) snprintf(status, sizeof(status), "Falsches Passwort.");
|
||||
else if (code == 429) snprintf(status, sizeof(status), "Zu viele Versuche, 15 Minuten warten.");
|
||||
else if (code) snprintf(status, sizeof(status), "Anmeldung fehlgeschlagen (HTTP %lu)", (unsigned long)code);
|
||||
// code 0: status sagt schon, warum keine Verbindung
|
||||
state = code == 401 ? LV_NEED_PASSWORD : LV_ERROR;
|
||||
busy = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
bool ok = j == JOB_OLDER ? load_lines(buf, true) : load_sources(buf) && load_lines(buf, false);
|
||||
if (!ok) { forget_token(); state = LV_NEED_PASSWORD; snprintf(status, sizeof(status), "Bitte anmelden."); }
|
||||
else state = LV_READY;
|
||||
busy = false;
|
||||
}
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void start_job(int j)
|
||||
{
|
||||
if (busy || job != JOB_NONE) return;
|
||||
job = j;
|
||||
if (state != LV_READY) state = LV_LOADING;
|
||||
LightEvent_Signal(&wake);
|
||||
}
|
||||
|
||||
void logview_open(const char *srv)
|
||||
{
|
||||
snprintf(server, sizeof(server), "%s", srv);
|
||||
if (!lines) lines = calloc(MAX_LINES, sizeof(Line));
|
||||
if (!thread) {
|
||||
LightLock_Init(&lock);
|
||||
LightEvent_Init(&wake, RESET_ONESHOT);
|
||||
s32 prio = 0x30;
|
||||
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||
thread = threadCreate(worker, NULL, 32 * 1024, prio + 1, -2, false);
|
||||
}
|
||||
detail = false;
|
||||
following = true;
|
||||
load_token();
|
||||
status[0] = 0;
|
||||
if (!token[0]) { state = LV_NEED_PASSWORD; return; }
|
||||
state = LV_LOADING;
|
||||
start_job(JOB_RELOAD);
|
||||
}
|
||||
|
||||
bool logview_finish(void)
|
||||
{
|
||||
if (!thread) return true;
|
||||
quit = true;
|
||||
LightEvent_Signal(&wake);
|
||||
if (R_FAILED(threadJoin(thread, 100ULL * 1000 * 1000))) return false;
|
||||
threadFree(thread);
|
||||
thread = NULL;
|
||||
free(lines);
|
||||
lines = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Eingaben --------------------------------------------------------------------------
|
||||
|
||||
static void select_source(int sel)
|
||||
{
|
||||
if (sel < 0) sel = sourceCount;
|
||||
if (sel > sourceCount) sel = 0;
|
||||
srcSel = sel;
|
||||
snprintf(filterKey, sizeof(filterKey), "%s", sel ? sources[sel - 1].key : "");
|
||||
if (srcSel < srcTop) srcTop = srcSel;
|
||||
if (srcSel >= srcTop + SRC_ROWS) srcTop = srcSel - SRC_ROWS + 1;
|
||||
following = true;
|
||||
start_job(JOB_RELOAD);
|
||||
}
|
||||
|
||||
static bool ask_password(void)
|
||||
{
|
||||
SwkbdState kb;
|
||||
char input[64] = "";
|
||||
swkbdInit(&kb, SWKBD_TYPE_NORMAL, 2, sizeof(input) - 1);
|
||||
swkbdSetPasswordMode(&kb, SWKBD_PASSWORD_HIDE_DELAY);
|
||||
swkbdSetHintText(&kb, "Passwort der Log-Seite");
|
||||
swkbdSetButton(&kb, SWKBD_BUTTON_LEFT, "Abbrechen", false);
|
||||
swkbdSetButton(&kb, SWKBD_BUTTON_RIGHT, "Anmelden", true);
|
||||
if (swkbdInputText(&kb, input, sizeof(input)) != SWKBD_BUTTON_RIGHT || !input[0]) return false;
|
||||
snprintf(password, sizeof(password), "%s", input);
|
||||
memset(input, 0, sizeof(input));
|
||||
state = LV_LOADING;
|
||||
snprintf(status, sizeof(status), "Melde an ...");
|
||||
job = JOB_LOGIN;
|
||||
LightEvent_Signal(&wake);
|
||||
return true;
|
||||
}
|
||||
|
||||
typedef struct { float x, y, w, h; } Rect;
|
||||
static const Rect btnFilter = { 8, 204, 100, 30 }, btnReload = { 112, 204, 96, 30 }, btnBack = { 212, 204, 100, 30 };
|
||||
static bool in(Rect r, touchPosition tp) { return tp.px >= r.x && tp.px < r.x + r.w && tp.py >= r.y && tp.py < r.y + r.h; }
|
||||
|
||||
bool logview_update(u32 down, u32 repeat, u32 held)
|
||||
{
|
||||
(void)held;
|
||||
if (state == LV_NEED_PASSWORD && !busy && job == JOB_NONE) {
|
||||
if (!ask_password()) return false; // abgebrochen: zurück zu den Werkzeugen
|
||||
}
|
||||
touchPosition tp = { 0 };
|
||||
if (down & KEY_TOUCH) hidTouchRead(&tp);
|
||||
|
||||
if (down & KEY_B || (down & KEY_TOUCH && in(btnBack, tp))) {
|
||||
if (detail) detail = false;
|
||||
else return false;
|
||||
}
|
||||
if (down & KEY_A) detail = !detail && lineCount > 0;
|
||||
if (down & KEY_X || (down & KEY_TOUCH && in(btnFilter, tp))) { errorsOnly = !errorsOnly; following = true; start_job(JOB_RELOAD); }
|
||||
if (down & KEY_Y || (down & KEY_TOUCH && in(btnReload, tp))) { following = true; start_job(JOB_RELOAD); }
|
||||
if (down & KEY_R) select_source(srcSel + 1);
|
||||
if (down & KEY_L) select_source(srcSel - 1);
|
||||
if (down & KEY_TOUCH && tp.py >= 24 && tp.py < 24 + SRC_ROWS * SRC_ROW_H) {
|
||||
int row = srcTop + (tp.py - 24) / SRC_ROW_H;
|
||||
if (row <= sourceCount) select_source(row);
|
||||
}
|
||||
|
||||
LightLock_Lock(&lock);
|
||||
int count = lineCount;
|
||||
if (!detail) {
|
||||
if (repeat & (KEY_UP | KEY_CPAD_UP)) {
|
||||
if (cursor > 0) cursor--;
|
||||
else if (count && state == LV_READY) start_job(JOB_OLDER); // ganz oben: ältere nachladen
|
||||
}
|
||||
if (repeat & (KEY_DOWN | KEY_CPAD_DOWN)) cursor++;
|
||||
if (repeat & KEY_LEFT) cursor -= ROWS;
|
||||
if (repeat & KEY_RIGHT) cursor += ROWS;
|
||||
}
|
||||
if (following) cursor = count - 1;
|
||||
if (cursor >= count) cursor = count - 1;
|
||||
if (cursor < 0) cursor = 0;
|
||||
following = count == 0 || cursor == count - 1;
|
||||
if (cursor < top) top = cursor;
|
||||
if (cursor >= top + ROWS) top = cursor - ROWS + 1;
|
||||
if (top > count - ROWS) top = count - ROWS;
|
||||
if (top < 0) top = 0;
|
||||
LightLock_Unlock(&lock);
|
||||
|
||||
// Live: unten angekommen, alle 5 Sekunden neu laden
|
||||
if (following && state == LV_READY && osGetTime() - lastLoad > 5000) start_job(JOB_RELOAD);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Zeichnen ---------------------------------------------------------------------------
|
||||
|
||||
static u32 level_color(char level) { return level == 'e' ? COL_RED : level == 'w' ? COL_AMBER : COL_CARD_HI; }
|
||||
|
||||
void logview_draw_top(void)
|
||||
{
|
||||
C2D_DrawRectSolid(0, 0, 0, TOP_W, SCR_H, COL_BG);
|
||||
C2D_DrawRectSolid(0, 0, 0.5f, TOP_W, 20, COL_BG2);
|
||||
char head[96], line[160];
|
||||
snprintf(head, sizeof(head), "Logs · %s%s", srcSel ? sources[srcSel - 1].name : "alle Quellen", errorsOnly ? " · nur Fehler" : "");
|
||||
ui_text(head, 6, 3, 0.45f, COL_WHITE, 0);
|
||||
const char *right = busy || state == LV_LOADING ? "lädt ..." : status[0] ? status : following ? "live" : "angehalten";
|
||||
ui_fit(line, sizeof(line), right, 0.38f, 180);
|
||||
ui_text(line, TOP_W - 6, 4, 0.38f, status[0] ? COL_AMBER : following ? COL_GREEN : COL_MUTED, C2D_AlignRight);
|
||||
|
||||
if (state == LV_NEED_PASSWORD) {
|
||||
ui_text_wrap(status[0] ? status : "Anmeldung mit dem Passwort der Log-Seite.", 20, 100, 0.5f, COL_MUTED, TOP_W - 40);
|
||||
return;
|
||||
}
|
||||
LightLock_Lock(&lock);
|
||||
if (!lineCount) {
|
||||
ui_text(state == LV_LOADING ? "Einen Moment ..." : "Keine Zeilen.", TOP_W / 2, 110, 0.5f, COL_MUTED, C2D_AlignCenter);
|
||||
LightLock_Unlock(&lock);
|
||||
return;
|
||||
}
|
||||
for (int r = 0; r < ROWS && top + r < lineCount; r++) {
|
||||
const Line *l = &lines[top + r];
|
||||
float y = 22 + r * ROW_H;
|
||||
if (top + r == cursor) C2D_DrawRectSolid(0, y, 0.5f, TOP_W, ROW_H, COL_CARD);
|
||||
time_t when = (time_t)(l->t / 1000 + timeOffset);
|
||||
struct tm *tm = gmtime(&when);
|
||||
char hhmm[12];
|
||||
snprintf(hhmm, sizeof(hhmm), "%02d:%02d:%02d", tm ? tm->tm_hour : 0, tm ? tm->tm_min : 0, tm ? tm->tm_sec : 0);
|
||||
ui_text(hhmm, 4, y + 1, 0.34f, COL_MUTED, 0);
|
||||
C2D_DrawRectSolid(46, y + 1, 0.5f, 3, ROW_H - 2, level_color(l->level));
|
||||
ui_fit(line, sizeof(line), l->src, 0.34f, 56);
|
||||
ui_text(line, 53, y + 1, 0.34f, COL_MUTED, 0);
|
||||
ui_fit(line, sizeof(line), l->msg, 0.36f, TOP_W - 116);
|
||||
ui_text(line, 112, y + 1, 0.36f, l->level == 'e' ? C2D_Color32(0xFF, 0x8A, 0x8A, 0xFF) : COL_WHITE, 0);
|
||||
}
|
||||
if (detail && cursor < lineCount) {
|
||||
const Line *l = &lines[cursor];
|
||||
ui_round_rect(14, 30, TOP_W - 28, 200, 10, COL_BG2);
|
||||
C2D_DrawRectSolid(14, 40, 0.5f, 4, 180, level_color(l->level));
|
||||
snprintf(head, sizeof(head), "%s · Zeile %lu", l->src, (unsigned long)l->id);
|
||||
ui_text(head, 26, 36, 0.42f, COL_MUTED, 0);
|
||||
ui_text_wrap(l->msg, 26, 56, 0.42f, COL_WHITE, TOP_W - 60);
|
||||
}
|
||||
LightLock_Unlock(&lock);
|
||||
}
|
||||
|
||||
static void button(Rect r, const char *label, bool on)
|
||||
{
|
||||
ui_round_rect(r.x, r.y, r.w, r.h, 10, on ? COL_RED : COL_CARD);
|
||||
ui_text(label, r.x + r.w / 2, r.y + 8, 0.42f, COL_WHITE, C2D_AlignCenter);
|
||||
}
|
||||
|
||||
void logview_draw_bottom(void)
|
||||
{
|
||||
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
|
||||
C2D_DrawRectSolid(0, 0, 0.5f, BOT_W, 22, COL_BG2);
|
||||
ui_text("Quellen", 6, 3, 0.45f, COL_WHITE, 0);
|
||||
ui_text("L/R wechseln · A Details · X Fehler", BOT_W - 6, 5, 0.34f, COL_MUTED, C2D_AlignRight);
|
||||
char line[96];
|
||||
LightLock_Lock(&lock);
|
||||
for (int r = 0; r < SRC_ROWS && srcTop + r <= sourceCount; r++) {
|
||||
int i = srcTop + r;
|
||||
float y = 24 + r * SRC_ROW_H;
|
||||
if (i == srcSel) {
|
||||
C2D_DrawRectSolid(0, y, 0.5f, BOT_W, SRC_ROW_H, COL_CARD_HI);
|
||||
C2D_DrawRectSolid(0, y + 4, 0.5f, 3, SRC_ROW_H - 8, COL_RED);
|
||||
}
|
||||
if (i == 0) { ui_text("Alle Quellen", 14, y + 5, 0.45f, COL_WHITE, 0); continue; }
|
||||
const Source *s = &sources[i - 1];
|
||||
C2D_DrawCircleSolid(14, y + SRC_ROW_H / 2, 0.5f, 4, s->online ? COL_GREEN : COL_MUTED);
|
||||
snprintf(line, sizeof(line), "%s · %s", s->name, s->host);
|
||||
char fitted[96];
|
||||
ui_fit(fitted, sizeof(fitted), line, 0.42f, s->errors ? 200 : 270);
|
||||
ui_text(fitted, 24, y + 5, 0.42f, COL_WHITE, 0);
|
||||
if (s->errors) {
|
||||
snprintf(line, sizeof(line), "%lu Fehler", (unsigned long)s->errors);
|
||||
float w = ui_text_width(line, 0.36f) + 12;
|
||||
ui_round_rect(BOT_W - 8 - w, y + 4, w, 16, 8, COL_RED);
|
||||
ui_text(line, BOT_W - 8 - w / 2, y + 5, 0.36f, COL_WHITE, C2D_AlignCenter);
|
||||
}
|
||||
}
|
||||
LightLock_Unlock(&lock);
|
||||
button(btnFilter, errorsOnly ? "Alle Stufen" : "Nur Fehler", errorsOnly);
|
||||
button(btnReload, "Neu laden", false);
|
||||
button(btnBack, detail ? "Schließen" : "Zurück", false);
|
||||
}
|
||||
Reference in New Issue
Block a user