package main import ( "context" "fmt" "net" "net/http" "os" "path/filepath" "time" "git.quad4.io/Quad4-Software/webnews/internal/api" "git.quad4.io/Quad4-Software/webnews/internal/storage" ) // App struct type App struct { ctx context.Context am *api.AuthManager db *storage.SQLiteDB port int debug bool } // NewApp creates a new App struct func NewApp(debug bool) *App { // Initialize SQLite in the user's home directory home, err := os.UserHomeDir() dbPath := "webnews.db" if err == nil { dbPath = filepath.Join(home, ".config", "webnews", "library.db") } if debug { fmt.Printf("[debug] using database path: %s\n", dbPath) } db, err := storage.NewSQLiteDB(dbPath) if err != nil { fmt.Printf("Warning: Failed to initialize SQLite, falling back to IndexedDB: %v\n", err) } else if debug { fmt.Printf("[debug] SQLite initialized\n") } return &App{ am: api.NewAuthManager("none", "", "", false), db: db, debug: debug, } } func (a *App) logDebug(format string, args ...any) { if a != nil && a.debug { fmt.Printf("[debug] "+format+"\n", args...) } } // logHandler wraps HTTP handlers to log requests when debug is enabled. func (a *App) logHandler(next http.Handler) http.Handler { if !a.debug { return next } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) fmt.Printf("[debug] http %s %s %dms\n", r.Method, r.URL.Path, time.Since(start).Milliseconds()) }) } // GetDBStats returns database statistics func (a *App) GetDBStats() (storage.DBStats, error) { a.logDebug("GetDBStats") if a.db == nil { return storage.DBStats{}, fmt.Errorf("SQLite not initialized") } return a.db.GetStats() } // VacuumDB runs the VACUUM command func (a *App) VacuumDB() error { a.logDebug("VacuumDB") if a.db == nil { return fmt.Errorf("SQLite not initialized") } return a.db.Vacuum() } // CheckDBIntegrity runs a PRAGMA integrity_check func (a *App) CheckDBIntegrity() (string, error) { a.logDebug("CheckDBIntegrity") if a.db == nil { return "", fmt.Errorf("SQLite not initialized") } return a.db.IntegrityCheck() } // Storage methods for Wails to call func (a *App) SaveSettings(settings string) error { a.logDebug("SaveSettings") if a.db == nil { return nil } return a.db.SaveSettings(settings) } func (a *App) GetSettings() (string, error) { a.logDebug("GetSettings") if a.db == nil { return "{}", nil } return a.db.GetSettings() } func (a *App) SaveCategories(cats string) error { a.logDebug("SaveCategories") if a.db == nil { return nil } return a.db.SaveCategories(cats) } func (a *App) GetCategories() (string, error) { a.logDebug("GetCategories") if a.db == nil { return "[]", nil } return a.db.GetCategories() } func (a *App) SaveFeeds(feeds string) error { a.logDebug("SaveFeeds") if a.db == nil { return nil } return a.db.SaveFeeds(feeds) } func (a *App) GetFeeds() (string, error) { a.logDebug("GetFeeds") if a.db == nil { return "[]", nil } return a.db.GetFeeds() } func (a *App) SaveArticles(articles string) error { a.logDebug("SaveArticles") if a.db == nil { return nil } return a.db.SaveArticles(articles) } func (a *App) GetArticles(feedId string, offset, limit int, categoryId string) (string, error) { a.logDebug("GetArticles feedId=%s categoryId=%s offset=%d limit=%d", feedId, categoryId, offset, limit) if a.db == nil { return "[]", nil } return a.db.GetArticles(feedId, offset, limit, categoryId) } func (a *App) SearchArticles(query string, limit int) (string, error) { a.logDebug("SearchArticles query=%s limit=%d", query, limit) if a.db == nil { return "[]", nil } return a.db.SearchArticles(query, limit) } func (a *App) UpdateArticle(article string) error { a.logDebug("UpdateArticle") if a.db == nil { return nil } return a.db.UpdateArticle(article) } func (a *App) MarkAsRead(id string) error { a.logDebug("MarkAsRead id=%s", id) if a.db == nil { return nil } return a.db.MarkAsRead(id) } func (a *App) DeleteFeed(feedId string) error { a.logDebug("DeleteFeed feedId=%s", feedId) if a.db == nil { return nil } return a.db.DeleteFeed(feedId) } func (a *App) PurgeOldContent(days int) (int64, error) { a.logDebug("PurgeOldContent days=%d", days) if a.db == nil { return 0, nil } return a.db.PurgeOldContent(days) } func (a *App) ClearAll() error { a.logDebug("ClearAll") if a.db == nil { return nil } return a.db.ClearAll() } func (a *App) GetReadingHistory(days int) (string, error) { a.logDebug("GetReadingHistory days=%d", days) if a.db == nil { return "[]", nil } return a.db.GetReadingHistory(days) } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx a.logDebug("startup begin") // Start local API server on a random port listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { fmt.Printf("Error starting local server: %v\n", err) return } a.port = listener.Addr().(*net.TCPAddr).Port a.logDebug("local API listener bound on %s", listener.Addr().String()) mux := http.NewServeMux() // CORS middleware for local desktop API cors := func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Account-Number") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) } } // Register handlers from our common api package mux.HandleFunc("/api/feed", cors(api.HandleFeedProxy)) mux.HandleFunc("/api/proxy", cors(api.HandleProxy)) mux.HandleFunc("/api/fulltext", cors(api.HandleFullText)) mux.HandleFunc("/api/ping", cors(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok","auth":{"required":false,"mode":"none","canReg":false}}`) })) server := &http.Server{ Addr: listener.Addr().String(), Handler: a.logHandler(mux), ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } go func() { if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { fmt.Printf("Error serving desktop API: %v\n", err) } }() fmt.Printf("Desktop API server started on port %d\n", a.port) a.logDebug("startup complete") } // GetAPIPort returns the port the local server is running on // This can be used by the frontend to know where to send requests func (a *App) GetAPIPort() int { a.logDebug("GetAPIPort -> %d", a.port) return a.port } // LogFrontend allows the frontend to log to the terminal func (a *App) LogFrontend(message string) { fmt.Printf("[frontend] %s\n", message) }