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:
+138
-3
@@ -22,7 +22,7 @@ import (
|
||||
const (
|
||||
homeserverURL = "https://matrix-af2f3d.organic-machine.com"
|
||||
masIssuer = "https://auth-af2f3d.organic-machine.com/"
|
||||
masClientID = "VDC4XQ2ZKN2TJ0BYVJ54FK7M6Y"
|
||||
masClientID = "3DC4XQ2ZKN2TJ0BYVJ54FK7M6Y"
|
||||
loopbackPort = 8765
|
||||
keyringServiceName = "fn_registry.matrix_client_pc"
|
||||
oidcTimeoutSeconds = 300
|
||||
@@ -91,6 +91,8 @@ func (s *MatrixService) Login() (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
logInfo("Login start", "client_id", masClientID, "loopback", loopbackPort, "issuer", masIssuer)
|
||||
|
||||
cfg := infra.MasOidcLoopbackConfig{
|
||||
Issuer: masIssuer,
|
||||
ClientID: masClientID,
|
||||
@@ -101,14 +103,18 @@ func (s *MatrixService) Login() (string, error) {
|
||||
}
|
||||
res, err := infra.MasOidcLoopback(cfg)
|
||||
if err != nil {
|
||||
logError("oidc loopback failed", "err", err)
|
||||
return "", fmt.Errorf("oidc: %w", err)
|
||||
}
|
||||
logInfo("oidc loopback OK", "token_type", res.TokenType, "expires_in", res.ExpiresIn, "scope", res.Scope)
|
||||
|
||||
// Pre-fetch user_id by hitting /whoami directly (mautrix requires UserID at NewClient).
|
||||
userID, deviceID, err := whoami(s.ctx, homeserverURL, res.AccessToken)
|
||||
if err != nil {
|
||||
logError("whoami failed", "err", err, "homeserver", homeserverURL)
|
||||
return "", fmt.Errorf("whoami: %w", err)
|
||||
}
|
||||
logInfo("whoami OK", "user_id", userID, "device_id", deviceID)
|
||||
|
||||
clientCfg := infra.MatrixClientInitConfig{
|
||||
HomeserverURL: homeserverURL,
|
||||
@@ -135,11 +141,22 @@ func (s *MatrixService) Login() (string, error) {
|
||||
tok.ExpiresAt = time.Now().Add(time.Duration(res.ExpiresIn) * time.Second)
|
||||
}
|
||||
if err := s.store.Save(userID, tok); err != nil {
|
||||
logError("keyring save failed", "err", err, "user_id", userID)
|
||||
return "", fmt.Errorf("keyring save: %w", err)
|
||||
}
|
||||
if err := writeLastUser(userID); err != nil {
|
||||
logWarn("write last_user.txt failed (non-fatal)", "err", err)
|
||||
}
|
||||
logInfo("login complete + token persisted", "user_id", userID)
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// GetLastUserID returns the last-logged-in user ID persisted in <UserConfigDir>/matrix_client_pc/last_user.txt.
|
||||
// Empty string if never logged in or if file unreadable.
|
||||
func (s *MatrixService) GetLastUserID() string {
|
||||
return readLastUser()
|
||||
}
|
||||
|
||||
// GetSession returns the persisted session for the given user_id.
|
||||
func (s *MatrixService) GetSession(userID string) (*SessionView, error) {
|
||||
if userID == "" {
|
||||
@@ -178,9 +195,62 @@ func (s *MatrixService) Logout(userID string) error {
|
||||
s.client = nil
|
||||
s.crypto = nil
|
||||
s.userID = ""
|
||||
if err := clearLastUser(); err != nil {
|
||||
logWarn("clear last_user.txt failed (non-fatal)", "err", err)
|
||||
}
|
||||
return s.store.Delete(userID)
|
||||
}
|
||||
|
||||
// Diagnostics is a snapshot of the live Matrix service state, used by the frontend
|
||||
// "comprobar chats" panel. Safe to call any time (returns zero values if not started).
|
||||
type Diagnostics struct {
|
||||
Started bool `json:"started"`
|
||||
UserID string `json:"user_id"`
|
||||
HomeserverURL string `json:"homeserver_url"`
|
||||
ClientReady bool `json:"client_ready"`
|
||||
CryptoInitialized bool `json:"crypto_initialized"`
|
||||
SyncActive bool `json:"sync_active"`
|
||||
RoomsCount int `json:"rooms_count"`
|
||||
EncryptedRooms int `json:"encrypted_rooms"`
|
||||
DMsCount int `json:"dms_count"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// GetDiagnostics returns a live snapshot of service state + a fresh ListRooms count.
|
||||
func (s *MatrixService) GetDiagnostics() Diagnostics {
|
||||
s.mu.Lock()
|
||||
d := Diagnostics{
|
||||
Started: s.sync != nil,
|
||||
UserID: s.userID,
|
||||
HomeserverURL: homeserverURL,
|
||||
ClientReady: s.client != nil,
|
||||
CryptoInitialized: s.crypto != nil,
|
||||
SyncActive: s.sync != nil,
|
||||
}
|
||||
client := s.client
|
||||
s.mu.Unlock()
|
||||
|
||||
if client != nil {
|
||||
rooms, err := infra.MatrixRoomList(s.ctx, infra.MatrixRoomListConfig{Client: client})
|
||||
if err != nil {
|
||||
d.LastError = err.Error()
|
||||
logWarn("diagnostics: room list error", "err", err)
|
||||
} else {
|
||||
d.RoomsCount = len(rooms)
|
||||
for _, r := range rooms {
|
||||
if r.IsEncrypted {
|
||||
d.EncryptedRooms++
|
||||
}
|
||||
if r.IsDirect {
|
||||
d.DMsCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logInfo("GetDiagnostics", "rooms", d.RoomsCount, "encrypted", d.EncryptedRooms, "dms", d.DMsCount)
|
||||
return d
|
||||
}
|
||||
|
||||
// Stop shuts down the sync loop without deleting credentials. Safe to call multiple times.
|
||||
func (s *MatrixService) Stop() {
|
||||
s.mu.Lock()
|
||||
@@ -212,10 +282,22 @@ func (s *MatrixService) Start(userID string) error {
|
||||
s.sync = nil
|
||||
}
|
||||
|
||||
logInfo("Start invoked", "user_id", userID)
|
||||
|
||||
tok, err := s.store.Load(userID)
|
||||
if err != nil {
|
||||
logError("keyring load failed", "err", err, "user_id", userID)
|
||||
return fmt.Errorf("keyring load: %w", err)
|
||||
}
|
||||
logInfo("token loaded from keyring",
|
||||
"user_id", tok.UserID,
|
||||
"device_id", tok.DeviceID,
|
||||
"homeserver", tok.HomeserverURL,
|
||||
"client_id", tok.ClientID,
|
||||
"has_refresh", tok.RefreshToken != "",
|
||||
"expires_at", tok.ExpiresAt,
|
||||
"now", time.Now(),
|
||||
)
|
||||
|
||||
storeDir := userStoreDir(userID)
|
||||
clientCfg := infra.MatrixClientInitConfig{
|
||||
@@ -228,7 +310,35 @@ func (s *MatrixService) Start(userID string) error {
|
||||
}
|
||||
clientRes, err := infra.MatrixClientInit(clientCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("matrix init: %w", err)
|
||||
logError("matrix client init failed (token rejected by Synapse)",
|
||||
"err", err,
|
||||
"user_id", userID,
|
||||
"hint", "Token may be stale — call Logout(user_id) then Login() again",
|
||||
)
|
||||
return fmt.Errorf("matrix init: %w (token rejected — re-login required)", err)
|
||||
}
|
||||
logInfo("matrix client init OK", "store_dir", storeDir, "device_id", string(clientRes.Client.DeviceID))
|
||||
|
||||
// Defensive: if DeviceID still empty after init, retry whoami + persist back.
|
||||
// Happens when keyring has stale token (saved before whoami fixed) or when
|
||||
// MAS-issued token's whoami response omits device_id (some servers do this).
|
||||
if clientRes.Client.DeviceID == "" {
|
||||
logWarn("client.DeviceID empty after init — retrying whoami")
|
||||
uid, did, werr := whoami(s.ctx, tok.HomeserverURL, tok.AccessToken)
|
||||
if werr != nil {
|
||||
logError("whoami retry failed", "err", werr)
|
||||
return fmt.Errorf("whoami retry: %w", werr)
|
||||
}
|
||||
if did == "" {
|
||||
logError("Synapse whoami returned empty device_id — MAS session likely lacks device binding",
|
||||
"user_id", uid,
|
||||
)
|
||||
return fmt.Errorf("synapse whoami did not return device_id — re-login required to bind a device")
|
||||
}
|
||||
clientRes.Client.DeviceID = id.DeviceID(did)
|
||||
tok.DeviceID = did
|
||||
_ = s.store.Save(userID, *tok)
|
||||
logInfo("whoami retry OK + persisted", "device_id", did)
|
||||
}
|
||||
|
||||
// Pickle key: load from keyring (hex), or generate fresh and persist.
|
||||
@@ -249,15 +359,23 @@ func (s *MatrixService) Start(userID string) error {
|
||||
PickleKey: pickleKey,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("matrix crypto init (hang here = MAS rejected UIA, see memory feedback_agents_e2ee_unblock_pattern): %w", err)
|
||||
logError("crypto init failed",
|
||||
"err", err,
|
||||
"crypto_store", cryptoStorePath,
|
||||
"hint", "If hang: MAS rejected UIA. WIPE crypto.db + relogin.",
|
||||
)
|
||||
return fmt.Errorf("matrix crypto init: %w", err)
|
||||
}
|
||||
logInfo("crypto init OK", "store", cryptoStorePath)
|
||||
|
||||
syncRes, err := infra.MatrixSyncService(s.ctx, infra.MatrixSyncServiceConfig{
|
||||
Client: clientRes.Client,
|
||||
})
|
||||
if err != nil {
|
||||
logError("sync service start failed", "err", err)
|
||||
return fmt.Errorf("matrix sync: %w", err)
|
||||
}
|
||||
logInfo("sync service started")
|
||||
|
||||
s.client = clientRes.Client
|
||||
s.crypto = cryptoRes
|
||||
@@ -267,9 +385,26 @@ func (s *MatrixService) Start(userID string) error {
|
||||
// Fan events out via Wails runtime.
|
||||
go s.fanout()
|
||||
|
||||
logInfo("Start complete", "user_id", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLogTail returns the last n lines of the app log file for the diagnostics UI.
|
||||
func (s *MatrixService) GetLogTail(n int) ([]string, error) {
|
||||
if n <= 0 {
|
||||
n = 200
|
||||
}
|
||||
return TailLog(n)
|
||||
}
|
||||
|
||||
// GetLogPath returns the absolute path to the log file (for the diagnostics UI).
|
||||
func (s *MatrixService) GetLogPath() string {
|
||||
if globalLogger == nil {
|
||||
return ""
|
||||
}
|
||||
return globalLogger.Path()
|
||||
}
|
||||
|
||||
func (s *MatrixService) fanout() {
|
||||
if s.ctx == nil || s.sync == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user