feat: persist last user + diagnostics + logging + icon + defensive whoami

Backend:
- last_user.go: writes/reads <UserConfigDir>/matrix_client_pc/last_user.txt.
  Login persists; Logout clears.
- GetLastUserID bind replaces fragile localStorage in App.tsx.
- GetDiagnostics bind: live snapshot (started, client_ready, crypto_init,
  sync_active, rooms_count, encrypted_rooms, dms_count, last_error).
- applog.go: slog to stderr + <UserConfigDir>/matrix_client_pc/app.log.
  GetLogTail + GetLogPath binds.
- matrix_service.go logs throughout Login/Start. After MatrixClientInit,
  if client.DeviceID empty -> retry whoami + persist back (defensive).
- main.go inits logger before wails.Run, OnShutdown logs close.

Frontend:
- App.tsx awaits GetLastUserID() instead of localStorage.
- HomeScreen.tsx Health modal (green stethoscope button) with HealthRow
  status dots — comprobar chats.
- Auto-relogin on token-rejected error in Start().

Icon:
- appicon.ico (Phosphor chat-circle + #7c3aed) generated via generate_app_icon.
- build/windows/icon.ico replaced (Wails embeds via windres).
- build/appicon.png regenerated from ico (256x256).

Refs: issues 0147 + 0148 + 0150 (partial). Fixes M_UNKNOWN_TOKEN auto-recovery.
This commit is contained in:
egutierrez
2026-05-25 17:20:52 +02:00
parent 1d3744f2d7
commit 23c933bfa2
9 changed files with 478 additions and 34 deletions
+12 -21
View File
@@ -2,36 +2,27 @@ import { useEffect, useState } from "react";
import { Box, LoadingOverlay } from "@mantine/core";
import LoginScreen from "./LoginScreen";
import HomeScreen from "./HomeScreen";
import { GetSession } from "../wailsjs/go/main/MatrixService";
const LAST_USER_KEY = "matrix_client_pc.last_user_id";
import { GetLastUserID, GetSession } from "../wailsjs/go/main/MatrixService";
export default function App() {
const [userID, setUserID] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const last = localStorage.getItem(LAST_USER_KEY);
if (!last) {
setLoading(false);
return;
}
GetSession(last)
.then((s) => {
(async () => {
try {
const last = await GetLastUserID();
if (!last) return;
const s = await GetSession(last);
if (s && s.has_token) setUserID(s.user_id);
})
.finally(() => setLoading(false));
} finally {
setLoading(false);
}
})();
}, []);
const handleLogin = (uid: string) => {
localStorage.setItem(LAST_USER_KEY, uid);
setUserID(uid);
};
const handleLogout = () => {
localStorage.removeItem(LAST_USER_KEY);
setUserID(null);
};
const handleLogin = (uid: string) => setUserID(uid);
const handleLogout = () => setUserID(null);
return (
<Box pos="relative" mih="100vh">
+151 -9
View File
@@ -6,8 +6,11 @@ import {
Burger,
Button,
Center,
Code,
Group,
Loader,
Modal,
ScrollArea,
Stack,
Text,
Title,
@@ -15,11 +18,16 @@ import {
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
import {
IconBug,
IconLock,
IconLogout,
IconStethoscope,
IconUserCircle,
} from "@tabler/icons-react";
import {
GetDiagnostics,
GetLogPath,
GetLogTail,
Logout,
SendMarkdown,
SendText,
@@ -35,6 +43,25 @@ import { useMatrixTimeline } from "./hooks/useMatrixTimeline";
const NAVBAR_WIDTH = 300;
function HealthRow({ label, value, ok }: { label: string; value: string; ok: boolean }) {
return (
<Group gap="sm" justify="space-between">
<Text size="sm" c="dimmed">{label}</Text>
<Group gap="xs">
<Box
w={8}
h={8}
style={{
borderRadius: "50%",
background: ok ? "var(--mantine-color-green-6)" : "var(--mantine-color-red-6)",
}}
/>
<Code>{value}</Code>
</Group>
</Group>
);
}
export default function HomeScreen({
userID,
onLogout,
@@ -46,6 +73,35 @@ export default function HomeScreen({
const [activeRoomID, setActiveRoomID] = useState<string | null>(null);
const [started, setStarted] = useState(false);
const [startError, setStartError] = useState<string | null>(null);
const [diagOpen, { open: openDiagnostics, close: closeDiagnostics }] = useDisclosure(false);
const [healthOpen, { open: openHealth, close: closeHealth }] = useDisclosure(false);
const [logLines, setLogLines] = useState<string[]>([]);
const [logPath, setLogPath] = useState<string>("");
const [health, setHealth] = useState<any | null>(null);
const [healthLoading, setHealthLoading] = useState(false);
useEffect(() => {
if (!diagOpen) return;
GetLogTail(200).then(setLogLines).catch(() => {});
GetLogPath().then(setLogPath).catch(() => {});
}, [diagOpen]);
async function refreshHealth() {
setHealthLoading(true);
try {
const h = await GetDiagnostics();
setHealth(h);
} catch (e) {
setHealth({ error: String(e) });
} finally {
setHealthLoading(false);
}
}
useEffect(() => {
if (!healthOpen) return;
refreshHealth();
}, [healthOpen]);
const { rooms } = useMatrixRooms(started);
const { events, loading: timelineLoading, error: timelineError } = useMatrixTimeline(
@@ -64,12 +120,18 @@ export default function HomeScreen({
if (!cancelled) {
const msg = String(e?.message ?? e);
setStartError(msg);
const stale = msg.includes("token rejected") || msg.includes("M_UNKNOWN_TOKEN");
notifications.show({
title: "Sync error",
title: stale ? "Token expired — re-login required" : "Sync error",
message: msg,
color: "red",
autoClose: false,
});
// Auto-logout on stale token so user lands back on LoginScreen.
if (stale) {
try { await Logout(userID); } catch {}
onLogout();
}
}
}
})();
@@ -145,14 +207,34 @@ export default function HomeScreen({
{userID}
</Text>
</Group>
<Button
variant="subtle"
color="gray"
leftSection={<IconLogout size={16} />}
onClick={handleLogout}
>
Logout
</Button>
<Group gap="xs">
<Button
variant="subtle"
color="green"
size="xs"
leftSection={<IconStethoscope size={14} />}
onClick={openHealth}
>
Health
</Button>
<Button
variant="subtle"
color="gray"
size="xs"
leftSection={<IconBug size={14} />}
onClick={openDiagnostics}
>
Logs
</Button>
<Button
variant="subtle"
color="gray"
leftSection={<IconLogout size={16} />}
onClick={handleLogout}
>
Logout
</Button>
</Group>
</Group>
</AppShell.Header>
@@ -241,6 +323,66 @@ export default function HomeScreen({
</>
)}
</AppShell.Main>
<Modal
opened={healthOpen}
onClose={closeHealth}
title="Health — comprobar chats"
size="lg"
>
<Stack gap="md">
<Group justify="space-between">
<Text size="xs" c="dimmed">
Snapshot del estado del MatrixService backend.
</Text>
<Button size="xs" variant="light" onClick={refreshHealth} loading={healthLoading}>
Refresh
</Button>
</Group>
{!health && <Text c="dimmed">No data yet.</Text>}
{health?.error && (
<Text c="red" size="sm">
{health.error}
</Text>
)}
{health && !health.error && (
<Stack gap={4}>
<HealthRow label="Started" value={String(health.started)} ok={health.started} />
<HealthRow label="Client ready" value={String(health.client_ready)} ok={health.client_ready} />
<HealthRow label="Crypto initialized" value={String(health.crypto_initialized)} ok={health.crypto_initialized} />
<HealthRow label="Sync active" value={String(health.sync_active)} ok={health.sync_active} />
<HealthRow label="User ID" value={health.user_id || "—"} ok={!!health.user_id} />
<HealthRow label="Homeserver" value={health.homeserver_url || "—"} ok />
<HealthRow label="Rooms total" value={String(health.rooms_count)} ok={health.rooms_count > 0} />
<HealthRow label="Encrypted rooms" value={String(health.encrypted_rooms)} ok />
<HealthRow label="Direct messages" value={String(health.dms_count)} ok />
{health.last_error && (
<Text c="red" size="xs" mt="sm">
Last error: {health.last_error}
</Text>
)}
</Stack>
)}
</Stack>
</Modal>
<Modal
opened={diagOpen}
onClose={closeDiagnostics}
title="Diagnostics — last 200 log lines"
size="xl"
>
<Stack gap="xs">
<Text size="xs" c="dimmed">
Log file: <Code>{logPath || "(unknown)"}</Code>
</Text>
<ScrollArea h={500} type="auto">
<pre style={{ fontSize: 11, lineHeight: 1.4, margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
{logLines.length === 0 ? "(empty)" : logLines.join("\n")}
</pre>
</ScrollArea>
</Stack>
</Modal>
</AppShell>
);
}