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:
+12
-21
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user