···11+# Project Context
22+33+## Project Structure
44+- **Root:**
55+ - `backend/`: Go backend service.
66+ - `frontend/`: Astro + React frontend.
77+ - `Makefile`: Build commands.
88+ - `docker-compose.yml`: Container orchestration.
99+1010+## Backend (`/backend`)
1111+- **Language:** Go
1212+- **Framework:** Chi (Router)
1313+- **Database:** SQLite (with `modernc.org/sqlite` driver)
1414+- **Authentication:** Goth (GitHub OAuth)
1515+- **Key Directories:**
1616+ - `internal/api/`: Handlers and routing.
1717+ - `router.go`: Defines API routes.
1818+ - `handlers/`: Contains logic for Auth, Repos, etc.
1919+ - `internal/database/`: Database connection, models, and queries.
2020+ - `models.go`: Struct definitions (`User`, `AuthToken`, `BranchState`, `DraftContent`).
2121+ - `queries.go`: SQL queries.
2222+ - `migrations/`: SQL migration files.
2323+ - `internal/connectors/`: External API integrations (GitHub).
2424+ - `github.go`: Handles GitHub API calls (listing repos, files).
2525+2626+## Frontend (`/frontend`)
2727+- **Framework:** Astro (Static Site Generation / Server Side Rendering) with React islands.
2828+- **Styling:** Tailwind CSS.
2929+- **State Management:** React Query (TanStack Query).
3030+- **Key Directories:**
3131+ - `src/pages/`:
3232+ - `index.astro`: Landing page.
3333+ - `dashboard.astro`: Main application entry point.
3434+ - `src/components/dashboard/`:
3535+ - `DashboardApp.tsx`: Main React component for the dashboard.
3636+ - `SetupWizard.tsx`: Component for selecting repository and folder (To be refactored).
3737+ - `FileTree.tsx`: Displays repository file structure.
3838+ - `src/lib/api/`: API client and endpoints.
3939+ - `repos.ts`: Calls to backend repo endpoints.
4040+4141+## Current Functionality
4242+1. **Auth:** GitHub OAuth flow via `/api/auth/github/login`.
4343+2. **Repo Selection:** Users select a repo and optional folder path via `SetupWizard`.
4444+3. **File Listing:** Fetches file tree from GitHub via Backend. Currently shows all folders, even empty ones.
4545+4. **Editing:** Markdown editing (implied, likely using TipTap or similar based on `node_modules`).
4646+4747+## Proposed Changes Context
4848+- **Last Repo Persistence:** Currently, the app doesn't remember the selected repo across sessions. We need to add `last_repo` to the `User` model.
4949+- **Folder Selection:** The user finds the folder selection step unnecessary. We will enforce root (`""`) as the default.
5050+- **Empty Folders:** The file tree currently is "dumb" and shows the full structure. We need to make the backend filtering "smart" to only return relevant paths.
5151+- **Navigation:** The dashboard currently handles both setup and editing in one view. Splitting this into `/select-repo` and `/dashboard` will improve UX and browser history management.
+54
.opencode/plans/plan.md
···11+# Implementation Plan
22+33+## 1. Save & Load Last Repo (Backend & Database)
44+**Objective:** Store the user's last accessed repository in the database and return it upon login.
55+66+### Database Schema
77+- [ ] Create a new migration `backend/internal/database/migrations/002_add_last_repo.sql`:
88+ - Add `last_repo` column (TEXT) to the `users` table.
99+- [ ] Update `backend/internal/database/migrations.go` to ensure the new migration file is embedded and executed.
1010+1111+### Backend Logic
1212+- [ ] Update `backend/internal/database/models.go`:
1313+ - Add `LastRepo string` to the `User` struct.
1414+- [ ] Update `backend/internal/database/queries.go`:
1515+ - Update `GetUserByID` query and scan.
1616+ - Update `GetUserByGithubID` query and scan.
1717+ - Update `CreateUser` query and scan.
1818+ - Add `UpdateUserRepo(userID int, repoName string)` function.
1919+- [ ] Update `backend/internal/api/handlers/auth.go`:
2020+ - In `GetCurrentUser`, include `last_repo` in the response.
2121+ - Create `UpdateUserRepo` handler to handle `POST /api/user/repo`.
2222+- [ ] Update `backend/internal/api/router.go`:
2323+ - Register the new route `POST /api/user/repo`.
2424+2525+## 2. Remove Folder Selection Step (Frontend)
2626+**Objective:** Simplify the setup wizard to skip the folder selection step and default to root.
2727+2828+### Frontend Logic
2929+- [ ] Modify `frontend/src/components/dashboard/SetupWizard.tsx`:
3030+ - Remove the "Step 2" (Folder Configuration) UI and logic.
3131+ - Change "Next" button in Step 1 to "Complete Setup".
3232+ - On submission, default the folder path to `""` (root).
3333+3434+## 3. Filter Empty Folders (Backend)
3535+**Objective:** Only show folders that actually contain markdown files (recursively).
3636+3737+### Backend Logic
3838+- [ ] Modify `backend/internal/connectors/github.go`:
3939+ - In `ListFiles`:
4040+ - Implement logic to check if a directory or its subdirectories contain matching files (based on extensions).
4141+ - If a directory is empty (contains no matching files), exclude it from the returned `FileNode` structure.
4242+4343+## 4. Separate Repo Selection URL (Frontend Routing)
4444+**Objective:** Create a distinct URL for repo selection to support browser navigation.
4545+4646+### Frontend Architecture
4747+- [ ] Create `frontend/src/pages/select-repo.astro`:
4848+ - Migrate `SetupWizard` component usage here.
4949+- [ ] Update `frontend/src/pages/dashboard.astro` & `DashboardApp.tsx`:
5050+ - Check for `last_repo` in user data or URL params.
5151+ - If no repo is active, redirect to `/select-repo`.
5252+ - If repo is active, show the `EditorContainer`.
5353+- [ ] Update Sidebar Navigation:
5454+ - "Change repository" button should link to `/select-repo`.
+30
backend/internal/api/handlers/auth.go
···242242}
243243244244// Logout logs out the current user
245245+func (h *AuthHandler) UpdateLastRepo(w http.ResponseWriter, r *http.Request) {
246246+ var payload struct {
247247+ LastRepo string `json:"last_repo"`
248248+ }
249249+250250+ if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
251251+ http.Error(w, "Invalid request payload", http.StatusBadRequest)
252252+ return
253253+ }
254254+255255+ session, err := auth.GetSession(r)
256256+ if err != nil {
257257+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
258258+ return
259259+ }
260260+261261+ userID, ok := auth.GetUserID(session)
262262+ if !ok {
263263+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
264264+ return
265265+ }
266266+267267+ if err := h.db.UpdateUserRepo(userID, payload.LastRepo); err != nil {
268268+ http.Error(w, "Failed to update last repository", http.StatusInternalServerError)
269269+ return
270270+ }
271271+272272+ w.WriteHeader(http.StatusNoContent)
273273+}
274274+245275func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
246276 session, err := auth.GetSession(r)
247277 if err != nil {
···11+-- Create table 'user_repos' to manage repositories for each user
22+CREATE TABLE user_repos (
33+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44+ user_id INTEGER NOT NULL,
55+ repo_name TEXT NOT NULL,
66+ repo_link TEXT NOT NULL,
77+ last_used_at DATETIME NOT NULL,
88+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
99+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1010+1111+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
1212+);
+12
backend/internal/database/models.go
···1111 AvatarURL string `json:"avatar_url"`
1212 CreatedAt time.Time `json:"created_at"`
1313 UpdatedAt time.Time `json:"updated_at"`
1414+ LastRepo string `json:"last_repo"`
1515+}
1616+1717+// UserRepo represents a repository associated with a user
1818+type UserRepo struct {
1919+ ID int `json:"id"`
2020+ UserID int `json:"user_id"`
2121+ RepoName string `json:"repo_name"`
2222+ RepoLink string `json:"repo_link"`
2323+ LastUsedAt time.Time `json:"last_used_at"`
2424+ CreatedAt time.Time `json:"created_at"`
2525+ UpdatedAt time.Time `json:"updated_at"`
1426}
15271628// AuthToken represents an OAuth token for a provider
+81-3
backend/internal/database/queries.go
···33import (
44 "database/sql"
55 "fmt"
66+ "time"
67)
7889// CreateUser creates a new user or updates if exists
910func (db *DB) CreateUser(user *User) error {
1011 query := `
1111- INSERT INTO users (github_id, username, email, avatar_url, updated_at)
1212+ INSERT INTO users (github_id, username, email, avatar_url, lastRepo, updated_at)
1213 VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
1314 ON CONFLICT(github_id) DO UPDATE SET
1415 username = excluded.username,
1516 email = excluded.email,
1617 avatar_url = excluded.avatar_url,
1818+ lastRepo = excluded.lastRepo,
1719 updated_at = CURRENT_TIMESTAMP
1820 RETURNING id, created_at, updated_at
1921 `
···2628func (db *DB) GetUserByGithubID(githubID int) (*User, error) {
2729 user := &User{}
2830 query := `
2929- SELECT id, github_id, username, email, avatar_url, created_at, updated_at
3131+ SELECT id, github_id, username, email, avatar_url, lastRepo, created_at, updated_at
3032 FROM users
3133 WHERE github_id = ?
3234 `
···5557func (db *DB) GetUserByID(id int) (*User, error) {
5658 user := &User{}
5759 query := `
5858- SELECT id, github_id, username, email, avatar_url, created_at, updated_at
6060+ SELECT id, github_id, username, email, avatar_url, lastRepo, created_at, updated_at
5961 FROM users
6062 WHERE id = ?
6163 `
···254256}
255257256258// DeleteBranchState deletes branch state for a repository
259259+// UpdateUserRepo updates the last repository selected by a user
260260+func (db *DB) UpdateUserRepo(userID int, lastRepo string) error {
261261+ query := `
262262+ UPDATE users
263263+ SET lastRepo = ?, updated_at = CURRENT_TIMESTAMP
264264+ WHERE id = ?
265265+ `
266266+267267+ _, err := db.Exec(query, lastRepo, userID)
268268+ if err != nil {
269269+ return fmt.Errorf("failed to update user's last repository: %w", err)
270270+ }
271271+272272+ return nil
273273+}
274274+275275+// InsertUserRepo adds a new repository to the user_repos table
276276+func (db *DB) InsertUserRepo(userID int, repoName, repoLink string, lastUsedAt time.Time) error {
277277+ query := `
278278+ INSERT INTO user_repos (user_id, repo_name, repo_link, last_used_at)
279279+ VALUES (?, ?, ?, ?)
280280+ `
281281+282282+ _, err := db.Exec(query, userID, repoName, repoLink, lastUsedAt)
283283+ if err != nil {
284284+ return fmt.Errorf("failed to insert user repo: %w", err)
285285+ }
286286+287287+ return nil
288288+}
289289+290290+// UpdateLastUsedAt updates the last_used_at for a specific repo
291291+func (db *DB) UpdateLastUsedAt(userID int, repoName string, lastUsedAt time.Time) error {
292292+ query := `
293293+ UPDATE user_repos
294294+ SET last_used_at = ?, updated_at = CURRENT_TIMESTAMP
295295+ WHERE user_id = ? AND repo_name = ?
296296+ `
297297+298298+ _, err := db.Exec(query, lastUsedAt, userID, repoName)
299299+ if err != nil {
300300+ return fmt.Errorf("failed to update last_used_at: %w", err)
301301+ }
302302+303303+ return nil
304304+}
305305+306306+// GetUserRepos retrieves all repositories for a user
307307+func (db *DB) GetUserRepos(userID int) ([]UserRepo, error) {
308308+ query := `
309309+ SELECT id, user_id, repo_name, repo_link, last_used_at, created_at, updated_at
310310+ FROM user_repos
311311+ WHERE user_id = ?
312312+ `
313313+314314+ rows, err := db.Query(query, userID)
315315+ if err != nil {
316316+ return nil, fmt.Errorf("failed to get user repos: %w", err)
317317+ }
318318+ defer rows.Close()
319319+320320+ var repos []UserRepo
321321+ for rows.Next() {
322322+ var repo UserRepo
323323+ if err := rows.Scan(
324324+ &repo.ID, &repo.UserID, &repo.RepoName, &repo.RepoLink,
325325+ &repo.LastUsedAt, &repo.CreatedAt, &repo.UpdatedAt,
326326+ ); err != nil {
327327+ return nil, fmt.Errorf("failed to scan row: %w", err)
328328+ }
329329+ repos = append(repos, repo)
330330+ }
331331+332332+ return repos, nil
333333+}
334334+257335func (db *DB) DeleteBranchState(userID int, repoFullName string) error {
258336 query := `
259337 DELETE FROM branch_states