diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..aa0d161 --- /dev/null +++ b/TODO.md @@ -0,0 +1,69 @@ +# Wails Desktop App — Roadmap + +## Milestone 1 — Scaffold & Tooling + +- [x] Install Tailwind v4 (`tailwindcss`, `@tailwindcss/vite`) and wire `vite.config.ts` +- [x] Install Fontsource packages (`@fontsource/jetbrains-mono`, `@fontsource-variable/geist`, `@fontsource-variable/lora`) +- [x] Create `frontend/src/index.css` with `@import "tailwindcss"` and `@theme` tokens (palette, fonts) +- [x] Put font CSS imports in `App.svelte` +- [x] Set up `Taskfile.yml` for build tasks +- [x] Verify `wails dev` hot-reloads a "Hello World" page with correct fonts and theme + +## Milestone 2 — Database Layer + +- [x] Implement `database.go` — `Open()`, `Close()`, embedded migrations via `//go:embed` +- [x] Copy existing migrations from CLI (`000`–`003`) and add `004_add_facets_column.sql` (`ALTER TABLE posts ADD COLUMN facets TEXT`) +- [x] Enable WAL mode (`PRAGMA journal_mode=WAL`) on connection open +- [x] Implement `models.go` — `Post`, `Auth`, `SearchResult` structs (add `Facets` field to `Post`) +- [x] Implement `PostExists`, `InsertPost`, `UpsertAuth`, `GetAuth`, `SearchPosts`, `CountPosts` +- [x] Verify the desktop app and CLI can read/write the same database concurrently + +## Milestone 3 — Authentication + +- [ ] Implement `AuthService` struct with Wails service binding +- [ ] `Login(handle)` — loopback OAuth via `oauth.NewLocalhostConfig`, persist tokens to shared DB +- [ ] `Whoami(force)` — load auth from DB, optionally resolve handle from DID +- [ ] `IsAuthenticated()` — check for valid auth row +- [ ] Automatic token refresh on `OnStartup` lifecycle hook +- [ ] Frontend: login view with handle input, "Login" button, and status display + +## Milestone 4 — Indexing & Progress + +- [ ] Implement `IndexService` struct with Wails service binding +- [ ] `Refresh(limit)` — concurrent bookmark + like fetch, batch insert (port `RefreshAndIndex` logic) +- [ ] Populate `facets` column from `FeedPost.Facets` during `convertPostView` +- [ ] `IsIndexing()` — thread-safe boolean guard to prevent concurrent refreshes +- [ ] Emit Wails events: `index:started`, `index:progress`, `index:done` +- [ ] Frontend: "Refresh" button in header, optional limit input +- [ ] Frontend: bottom-pinned progress bar component driven by `index:*` events + +## Milestone 5 — Search & Data Table + +- [ ] Implement `SearchService` struct with Wails service binding +- [ ] `Search(query, source)` — FTS5 query with BM25 ranking and source filter +- [ ] `CountPosts()` — total indexed post count +- [ ] Frontend: search bar with query input and source filter (All / Saved / Liked segmented control) +- [ ] Frontend: tabbed data table component (Saved / Liked / All tabs) +- [ ] Columns: Author Handle, Text (truncated), Created At, ♥ Likes, 🔁 Reposts, 💬 Replies, Source +- [ ] Client-side column sorting (click header to toggle asc/desc) +- [ ] Row click → open post URL in default browser via `runtime.BrowserOpenURL` + +## Milestone 6 — Facets & Log Viewer + +- [ ] Frontend: facet parser — convert UTF-8 byte offsets to JS string indices +- [ ] Frontend: facet renderer — links (``), mentions (`@handle`), hashtags (`#tag`) +- [ ] Integrate rendered facets into post text in data table rows +- [ ] Implement `LogService` — custom `io.Writer` that emits `log:line` events +- [ ] Wire `LogService` writer into `log.NewWithOptions` alongside file writer +- [ ] Frontend: log viewer panel with terminal-style dark background, monospace text +- [ ] Auto-scroll to bottom with scroll-lock toggle +- [ ] Level filter buttons: Debug, Info, Warn, Error + +## Milestone 7 — Polish + +- [ ] Empty state: show "No posts indexed" with prompt to refresh +- [ ] Error handling: toast/notification for network failures, auth expiry +- [ ] Keyboard shortcuts: `Cmd+K` focus search, `Cmd+R` refresh, `Cmd+L` toggle log viewer +- [ ] Window title and app icon (`build/appicon.png`) +- [ ] Production build verification (`wails3 build` → macOS `.app` bundle) +- [ ] README with build instructions, screenshots, and usage diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..e936563 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,32 @@ +version: '3' + +tasks: + dev: + desc: Run the application in development mode with hot reload + cmds: + - wails dev + + build: + desc: Build the application for production + cmds: + - wails build + + build:clean: + desc: Clean and build the application for production + cmds: + - wails build -clean + + generate: + desc: Generate Wails bindings + cmds: + - wails generate module + + init: + desc: Initialize the project (install dependencies) + cmds: + - cd frontend && npm install + + check: + desc: Check TypeScript and Svelte + cmds: + - cd frontend && npm run check diff --git a/app.go b/app.go index af53038..22448bd 100644 --- a/app.go +++ b/app.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "os" + "path/filepath" ) // App struct @@ -15,10 +17,38 @@ func NewApp() *App { return &App{} } -// startup is called when the app starts. The context is saved -// so we can call the runtime methods +// 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 + + dbPath := getDBPath() + if err := Open(dbPath); err != nil { + fmt.Printf("failed to open database: %v\n", err) + } +} + +// shutdown is called when the app shuts down +func (a *App) shutdown(ctx context.Context) { + if err := Close(); err != nil { + fmt.Printf("failed to close database: %v\n", err) + } +} + +// getDBPath returns the path to the shared database +func getDBPath() string { + if dir := os.Getenv("BSKY_BROWSER_DATA"); dir != "" { + return filepath.Join(dir, "bsky-browser.db") + } + + configDir := os.Getenv("XDG_CONFIG_HOME") + if configDir == "" { + home, _ := os.UserHomeDir() + configDir = filepath.Join(home, ".config") + } + + return filepath.Join(configDir, "bsky-browser", "bsky-browser.db") } // Greet returns a greeting for the given name diff --git a/database.go b/database.go new file mode 100644 index 0000000..bfd8b5b --- /dev/null +++ b/database.go @@ -0,0 +1,323 @@ +package main + +import ( + "database/sql" + "embed" + "fmt" + "os" + "path/filepath" + "time" + + _ "modernc.org/sqlite" +) + +var db *sql.DB + +//go:embed migrations/*.sql +var migrationsFS embed.FS + +// Open opens the database connection and runs migrations +func Open(dbPath string) error { + fmt.Printf("opening database: %s\n", dbPath) + + dir := filepath.Dir(dbPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create database directory: %w", err) + } + + var err error + db, err = sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)") + if err != nil { + return fmt.Errorf("failed to open database: %w", err) + } + + if err := db.Ping(); err != nil { + return fmt.Errorf("failed to ping database: %w", err) + } + + _, err = db.Exec("PRAGMA journal_mode=WAL") + if err != nil { + return fmt.Errorf("failed to enable WAL mode: %w", err) + } + + fmt.Println("database connection established with WAL mode") + + if err := runMigrations(); err != nil { + return fmt.Errorf("failed to run migrations: %w", err) + } + + fmt.Println("database migrations completed successfully") + return nil +} + +func runMigrations() error { + content, err := migrationsFS.ReadFile("migrations/000_initial_schema.sql") + if err != nil { + return fmt.Errorf("failed to read migration: %w", err) + } + + if _, err := db.Exec(string(content)); err != nil { + return fmt.Errorf("failed to execute migration: %w", err) + } + + return nil +} + +// Close closes the database connection +func Close() error { + fmt.Println("closing database connection") + if db != nil { + err := db.Close() + if err != nil { + fmt.Printf("failed to close database: %v\n", err) + return err + } + fmt.Println("database connection closed") + } + return nil +} + +// PostExists checks if a post with the given URI already exists in the database +func PostExists(uri string) (bool, error) { + var exists bool + err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM posts WHERE uri = ?)", uri).Scan(&exists) + if err != nil { + return false, err + } + return exists, nil +} + +// InsertPost inserts a post into the database +func InsertPost(post *Post) error { + fmt.Printf("inserting post: %s by %s\n", post.URI, post.AuthorHandle) + + exists, err := PostExists(post.URI) + if err != nil { + fmt.Printf("failed to check if post exists: %s, error: %v\n", post.URI, err) + return err + } + + if exists { + fmt.Printf("skipping already indexed post: %s\n", post.URI) + return nil + } + + query := ` + INSERT INTO posts (uri, cid, author_did, author_handle, text, created_at, like_count, repost_count, reply_count, source, facets) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(uri) DO UPDATE SET + cid = excluded.cid, + author_did = excluded.author_did, + author_handle = excluded.author_handle, + text = excluded.text, + created_at = excluded.created_at, + like_count = excluded.like_count, + repost_count = excluded.repost_count, + reply_count = excluded.reply_count, + source = excluded.source, + facets = excluded.facets, + indexed_at = CURRENT_TIMESTAMP + ` + + _, err = db.Exec(query, + post.URI, + post.CID, + post.AuthorDID, + post.AuthorHandle, + post.Text, + post.CreatedAt, + post.LikeCount, + post.RepostCount, + post.ReplyCount, + post.Source, + post.Facets, + ) + + if err != nil { + fmt.Printf("failed to insert post: %s, error: %v\n", post.URI, err) + } + + return err +} + +// UpsertAuth inserts or updates auth information +func UpsertAuth(auth *Auth) error { + fmt.Printf("upserting auth: %s (%s)\n", auth.DID, auth.Handle) + + query := ` + INSERT INTO auth (did, handle, access_jwt, refresh_jwt, pds_url, session_id, + auth_server_url, auth_server_token_endpoint, auth_server_revocation_endpoint, + dpop_auth_nonce, dpop_host_nonce, dpop_private_key, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(did) DO UPDATE SET + handle = excluded.handle, + access_jwt = excluded.access_jwt, + refresh_jwt = excluded.refresh_jwt, + pds_url = excluded.pds_url, + session_id = excluded.session_id, + auth_server_url = excluded.auth_server_url, + auth_server_token_endpoint = excluded.auth_server_token_endpoint, + auth_server_revocation_endpoint = excluded.auth_server_revocation_endpoint, + dpop_auth_nonce = excluded.dpop_auth_nonce, + dpop_host_nonce = excluded.dpop_host_nonce, + dpop_private_key = excluded.dpop_private_key, + updated_at = CURRENT_TIMESTAMP + ` + + _, err := db.Exec(query, + auth.DID, + auth.Handle, + auth.AccessJWT, + auth.RefreshJWT, + auth.PDSURL, + auth.SessionID, + auth.AuthServerURL, + auth.AuthServerTokenEndpoint, + auth.AuthServerRevocationEndpoint, + auth.DPoPAuthNonce, + auth.DPoPHostNonce, + auth.DPoPPrivateKey, + ) + + if err != nil { + fmt.Printf("failed to upsert auth: %s, error: %v\n", auth.DID, err) + } + + return err +} + +// GetAuth loads the auth record from the database +func GetAuth() (*Auth, error) { + fmt.Println("loading auth from database") + + query := `SELECT did, handle, access_jwt, refresh_jwt, pds_url, session_id, + auth_server_url, auth_server_token_endpoint, auth_server_revocation_endpoint, + dpop_auth_nonce, dpop_host_nonce, dpop_private_key, updated_at + FROM auth LIMIT 1` + + var auth Auth + var updatedAt string + + var sessionID, authServerURL, authServerTokenEndpoint, authServerRevocationEndpoint, dpopAuthNonce, dpopHostNonce, dpopPrivateKey sql.NullString + + err := db.QueryRow(query).Scan( + &auth.DID, + &auth.Handle, + &auth.AccessJWT, + &auth.RefreshJWT, + &auth.PDSURL, + &sessionID, + &authServerURL, + &authServerTokenEndpoint, + &authServerRevocationEndpoint, + &dpopAuthNonce, + &dpopHostNonce, + &dpopPrivateKey, + &updatedAt, + ) + + if sessionID.Valid { + auth.SessionID = sessionID.String + } + if authServerURL.Valid { + auth.AuthServerURL = authServerURL.String + } + if authServerTokenEndpoint.Valid { + auth.AuthServerTokenEndpoint = authServerTokenEndpoint.String + } + if authServerRevocationEndpoint.Valid { + auth.AuthServerRevocationEndpoint = authServerRevocationEndpoint.String + } + if dpopAuthNonce.Valid { + auth.DPoPAuthNonce = dpopAuthNonce.String + } + if dpopHostNonce.Valid { + auth.DPoPHostNonce = dpopHostNonce.String + } + if dpopPrivateKey.Valid { + auth.DPoPPrivateKey = dpopPrivateKey.String + } + + if err == sql.ErrNoRows { + fmt.Println("no auth record found in database") + return nil, nil + } + if err != nil { + fmt.Printf("failed to load auth: %v\n", err) + return nil, err + } + + auth.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt) + fmt.Printf("auth loaded successfully: %s (%s)\n", auth.DID, auth.Handle) + return &auth, nil +} + +// SearchPosts searches posts using FTS5 +func SearchPosts(query string, source string) ([]SearchResult, error) { + fmt.Printf("searching posts: query=%s, source=%s\n", query, source) + + sqlQuery := ` + SELECT p.uri, p.cid, p.author_did, p.author_handle, p.text, p.created_at, + p.like_count, p.repost_count, p.reply_count, p.source, p.indexed_at, + bm25(posts_fts, 5.0, 1.0) AS rank + FROM posts_fts + JOIN posts p ON posts_fts.rowid = p.rowid + WHERE posts_fts MATCH ? + AND (? = '' OR p.source = ?) + ORDER BY rank + LIMIT 25 + ` + + rows, err := db.Query(sqlQuery, query, source, source) + if err != nil { + fmt.Printf("failed to execute search query: %v\n", err) + return nil, err + } + defer rows.Close() + + var results []SearchResult + for rows.Next() { + var r SearchResult + var createdAt, indexedAt string + + err := rows.Scan( + &r.URI, + &r.CID, + &r.AuthorDID, + &r.AuthorHandle, + &r.Text, + &createdAt, + &r.LikeCount, + &r.RepostCount, + &r.ReplyCount, + &r.Source, + &indexedAt, + &r.Rank, + ) + if err != nil { + return nil, err + } + + r.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt) + r.IndexedAt, _ = time.Parse("2006-01-02 15:04:05", indexedAt) + results = append(results, r) + } + + fmt.Printf("search completed: %d results\n", len(results)) + return results, rows.Err() +} + +// CountPosts returns the total number of posts in the database +func CountPosts() (int, error) { + fmt.Println("counting posts in database") + + var count int + err := db.QueryRow("SELECT COUNT(*) FROM posts").Scan(&count) + if err != nil { + fmt.Printf("failed to count posts: %v\n", err) + return 0, err + } + + fmt.Printf("post count: %d\n", count) + return count, nil +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6bf6b44..62bebd6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,13 @@ "": { "name": "frontend", "version": "0.0.0", + "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource-variable/lora": "^5.2.8", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "svelte": "^5.53.12", @@ -23,7 +30,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -40,7 +46,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -57,7 +62,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -74,7 +78,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -91,7 +94,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -108,7 +110,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -125,7 +126,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -142,7 +142,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -159,7 +158,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -176,7 +174,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -193,7 +190,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -210,7 +206,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -227,7 +222,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -244,7 +238,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -261,7 +254,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -278,7 +270,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -295,7 +286,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -312,7 +302,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -329,7 +318,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -346,7 +334,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -363,7 +350,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -380,7 +366,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -397,7 +382,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -414,7 +398,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -431,7 +414,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -441,11 +423,37 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/geist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.8.tgz", + "integrity": "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-WBA9elru6Jdp5df2mES55wuOO0WIrn3kpXnI4+W2ek5u3ZgLS9XS4gmIlcQhiZOWEKl95meYdvK7xI+ETLCq/Q==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/lora": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/lora/-/lora-5.2.8.tgz", + "integrity": "sha512-cxjTJ9BbOWIzusewR4UMBLVePvTSWV6dtNaNsCkF/oKoyA68fJGWfaYCILOOP1BObE4dmjfZ3xo6m9hdHhtYhg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -456,7 +464,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -467,7 +474,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -477,14 +483,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -498,7 +502,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -512,7 +515,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -526,7 +528,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -540,7 +541,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -554,7 +554,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -568,7 +567,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -582,7 +580,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "glibc" ], @@ -599,7 +596,6 @@ "cpu": [ "arm" ], - "dev": true, "libc": [ "musl" ], @@ -616,7 +612,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "glibc" ], @@ -633,7 +628,6 @@ "cpu": [ "arm64" ], - "dev": true, "libc": [ "musl" ], @@ -650,7 +644,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "glibc" ], @@ -667,7 +660,6 @@ "cpu": [ "loong64" ], - "dev": true, "libc": [ "musl" ], @@ -684,7 +676,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "glibc" ], @@ -701,7 +692,6 @@ "cpu": [ "ppc64" ], - "dev": true, "libc": [ "musl" ], @@ -718,7 +708,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "glibc" ], @@ -735,7 +724,6 @@ "cpu": [ "riscv64" ], - "dev": true, "libc": [ "musl" ], @@ -752,7 +740,6 @@ "cpu": [ "s390x" ], - "dev": true, "libc": [ "glibc" ], @@ -769,7 +756,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "glibc" ], @@ -786,7 +772,6 @@ "cpu": [ "x64" ], - "dev": true, "libc": [ "musl" ], @@ -803,7 +788,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -817,7 +801,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -831,7 +814,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -845,7 +827,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -859,7 +840,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -873,7 +853,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -929,11 +908,279 @@ "vite": "^6.3.0 || ^7.0.0" } }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/trusted-types": { @@ -1026,6 +1273,15 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devalue": { "version": "5.6.4", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", @@ -1033,11 +1289,23 @@ "dev": true, "license": "MIT" }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1076,7 +1344,6 @@ } }, "node_modules/esbuild/node_modules/@esbuild/netbsd-arm64": { - "dev": true, "optional": true }, "node_modules/esm-env": { @@ -1101,7 +1368,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1119,7 +1385,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1130,6 +1395,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1140,6 +1411,276 @@ "@types/estree": "^1.0.6" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1151,7 +1692,6 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -1171,7 +1711,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -1201,14 +1740,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1221,7 +1758,6 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1264,7 +1800,6 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -1322,7 +1857,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -1380,11 +1914,29 @@ "typescript": ">=5.0.0" } }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -1401,7 +1953,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, + "devOptional": true, "license": "0BSD" }, "node_modules/typescript": { @@ -1422,7 +1974,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.27.0", diff --git a/frontend/package.json b/frontend/package.json index f609870..3a75efa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,5 +16,12 @@ "tslib": "^2.8.1", "typescript": "^5.9.3", "vite": "^7.2.6" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@fontsource-variable/lora": "^5.2.8", + "@tailwindcss/vite": "^4.2.1", + "tailwindcss": "^4.2.1" } } diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 6e4fec1..11bd378 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,83 +1,13 @@ -
- -
{resultText}
-
- - +
+
+

Hello World

+

bsky-browser-desktop

+

Built with Wails v3 + Tailwind v4

- - diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..25326af --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,21 @@ +@import "tailwindcss"; + +@theme { + --font-mono: "JetBrains Mono Variable", monospace; + --font-sans: "Geist Variable", sans-serif; + --font-serif: "Lora Variable", serif; + --color-surface: #0a0a0a; + --color-border: #1a1a1a; +} + +html { + background-color: #000000; + color: #e5e5e5; +} + +body { + margin: 0; + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 8e0aea1..98d7911 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,4 +1,4 @@ -import './style.css' +import './index.css' import App from './App.svelte' import { mount } from 'svelte' diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d37616f..8c03d85 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,8 @@ import {defineConfig} from 'vite' import {svelte} from '@sveltejs/vite-plugin-svelte' +import tailwindcss from '@tailwindcss/vite' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [svelte()] + plugins: [tailwindcss(), svelte()] }) diff --git a/go.mod b/go.mod index 5baecfe..c850ecb 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ module bsky-browser-gui -go 1.23 +go 1.24.0 + +toolchain go1.24.11 require github.com/wailsapp/wails/v2 v2.11.0 require ( github.com/bep/debounce v1.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -19,8 +22,10 @@ require ( github.com/leaanthony/u v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/samber/lo v1.49.1 // indirect github.com/tkrajina/go-reflector v0.5.8 // indirect @@ -29,9 +34,14 @@ require ( github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.37.0 // indirect golang.org/x/text v0.22.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.46.1 // indirect ) // replace github.com/wailsapp/wails/v2 v2.11.0 => /Users/owais/go/pkg/mod diff --git a/go.sum b/go.sum index e3658ec..02da27f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -34,12 +36,16 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -61,6 +67,8 @@ github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSB github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= @@ -72,6 +80,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= @@ -79,3 +89,11 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= diff --git a/main.go b/main.go index 2ef94de..502e163 100644 --- a/main.go +++ b/main.go @@ -12,22 +12,16 @@ import ( var assets embed.FS func main() { - // Create an instance of the app structure app := NewApp() - - // Create application with options err := wails.Run(&options.App{ - Title: "bsky-browser-gui", - Width: 1024, - Height: 768, - AssetServer: &assetserver.Options{ - Assets: assets, - }, + Title: "bsky-browser-gui", + Width: 1024, + Height: 768, + AssetServer: &assetserver.Options{Assets: assets}, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, - Bind: []interface{}{ - app, - }, + OnShutdown: app.shutdown, + Bind: []any{app}, }) if err != nil { diff --git a/migrations/000_initial_schema.sql b/migrations/000_initial_schema.sql new file mode 100644 index 0000000..da86bdc --- /dev/null +++ b/migrations/000_initial_schema.sql @@ -0,0 +1,62 @@ +-- Combined initial schema for bsky-browser-desktop +-- Includes all migrations: auth, posts, FTS5, OAuth fields, and facets + +-- Auth table with all OAuth fields +CREATE TABLE IF NOT EXISTS auth ( + did TEXT PRIMARY KEY, + handle TEXT NOT NULL, + access_jwt TEXT NOT NULL, + refresh_jwt TEXT NOT NULL, + pds_url TEXT NOT NULL, + session_id TEXT, + auth_server_url TEXT, + auth_server_token_endpoint TEXT, + auth_server_revocation_endpoint TEXT, + dpop_auth_nonce TEXT, + dpop_host_nonce TEXT, + dpop_private_key TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Posts table with facets support +CREATE TABLE IF NOT EXISTS posts ( + uri TEXT PRIMARY KEY, + cid TEXT NOT NULL, + author_did TEXT NOT NULL, + author_handle TEXT NOT NULL, + text TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL, + like_count INTEGER DEFAULT 0, + repost_count INTEGER DEFAULT 0, + reply_count INTEGER DEFAULT 0, + source TEXT NOT NULL CHECK(source IN ('saved', 'liked')), + facets TEXT, + indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- FTS5 virtual table for full-text search +CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5( + text, + author_handle, + content='posts', + content_rowid='rowid', + tokenize='unicode61' +); + +-- Triggers to keep FTS5 index in sync with posts table +CREATE TRIGGER IF NOT EXISTS posts_ai AFTER INSERT ON posts BEGIN + INSERT INTO posts_fts(rowid, text, author_handle) + VALUES (new.rowid, new.text, new.author_handle); +END; + +CREATE TRIGGER IF NOT EXISTS posts_ad AFTER DELETE ON posts BEGIN + INSERT INTO posts_fts(posts_fts, rowid, text, author_handle) + VALUES ('delete', old.rowid, old.text, old.author_handle); +END; + +CREATE TRIGGER IF NOT EXISTS posts_au AFTER UPDATE ON posts BEGIN + INSERT INTO posts_fts(posts_fts, rowid, text, author_handle) + VALUES ('delete', old.rowid, old.text, old.author_handle); + INSERT INTO posts_fts(rowid, text, author_handle) + VALUES (new.rowid, new.text, new.author_handle); +END; diff --git a/models.go b/models.go new file mode 100644 index 0000000..38a12d0 --- /dev/null +++ b/models.go @@ -0,0 +1,44 @@ +package main + +import ( + "time" +) + +// Post represents a Bluesky post in the database +type Post struct { + URI string `json:"uri"` + CID string `json:"cid"` + AuthorDID string `json:"author_did"` + AuthorHandle string `json:"author_handle"` + Text string `json:"text"` + CreatedAt time.Time `json:"created_at"` + LikeCount int `json:"like_count"` + RepostCount int `json:"repost_count"` + ReplyCount int `json:"reply_count"` + Source string `json:"source"` // 'saved' or 'liked' + Facets string `json:"facets"` // JSON-encoded facets + IndexedAt time.Time `json:"indexed_at"` +} + +// Auth represents OAuth authentication information +type Auth struct { + DID string `json:"did"` + Handle string `json:"handle"` + AccessJWT string `json:"access_jwt"` + RefreshJWT string `json:"refresh_jwt"` + PDSURL string `json:"pds_url"` + SessionID string `json:"session_id"` + AuthServerURL string `json:"auth_server_url"` + AuthServerTokenEndpoint string `json:"auth_server_token_endpoint"` + AuthServerRevocationEndpoint string `json:"auth_server_revocation_endpoint"` + DPoPAuthNonce string `json:"dpop_auth_nonce"` + DPoPHostNonce string `json:"dpop_host_nonce"` + DPoPPrivateKey string `json:"dpop_private_key"` + UpdatedAt time.Time `json:"updated_at"` +} + +// SearchResult represents a search result with BM25 ranking +type SearchResult struct { + Post + Rank float64 `json:"rank"` +}