# Shared Documents Dashboard Section Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add a "Documents Shared with You" section to the dashboard that lists documents the current user is a collaborator on. **Architecture:** Collaborator relationships are stored as a `collaborators []string` (DIDs) array inside each document record on the owner's PDS. There is no central index of "documents I collaborate on" — we need to persist that mapping in the local SQLite DB when a collaborator accepts an invite. The dashboard then queries that local table to get the list of (ownerDID, rkey) pairs, fetches each document title from the owner's PDS via XRPC, and renders them alongside the user's own documents. **Tech Stack:** Go 1.22, SQLite (WAL), ATProto XRPC client, `html/template`, gorilla/sessions --- ### Task 1: Add `collaborations` table via migration **Files:** - Create: `migrations/008_collaborations.sql` **Step 1: Write the migration** ```sql CREATE TABLE IF NOT EXISTS collaborations ( collaborator_did TEXT NOT NULL, owner_did TEXT NOT NULL, document_rkey TEXT NOT NULL, added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (collaborator_did, owner_did, document_rkey) ); ``` **Step 2: Verify migration runs** Start the server (`go run ./cmd/server`) and confirm in logs: `Applied migration: 008_collaborations.sql`. Stop server. **Step 3: Commit** ```bash git add migrations/008_collaborations.sql git commit -m "Add collaborations table to track shared documents" ``` --- ### Task 2: Add DB methods for collaborations **Files:** - Modify: `internal/db/db.go` **Step 1: Write the failing test** In `internal/db/db_test.go` (create if absent): ```go func TestCollaborations(t *testing.T) { d := openTestDB(t) const collabDID = "did:plc:collab" const ownerDID = "did:plc:owner" const rkey = "abc123" if err := d.AddCollaboration(collabDID, ownerDID, rkey); err != nil { t.Fatalf("AddCollaboration: %v", err) } // idempotent if err := d.AddCollaboration(collabDID, ownerDID, rkey); err != nil { t.Fatalf("AddCollaboration (dup): %v", err) } rows, err := d.GetCollaborations(collabDID) if err != nil { t.Fatalf("GetCollaborations: %v", err) } if len(rows) != 1 { t.Fatalf("want 1 row, got %d", len(rows)) } if rows[0].OwnerDID != ownerDID || rows[0].DocumentRKey != rkey { t.Errorf("unexpected row: %+v", rows[0]) } } ``` You'll need an `openTestDB` helper — look for one in the existing test files. If absent, create: ```go func openTestDB(t *testing.T) *DB { t.Helper() dir := t.TempDir() SetMigrationsDir("../../migrations") d, err := Open(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } if err := d.Migrate(); err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) return d } ``` **Step 2: Run test to confirm it fails** ```bash go test ./internal/db/... -run TestCollaborations -v ``` Expected: compile error — `AddCollaboration` and `GetCollaborations` undefined. **Step 3: Add `CollaborationRow` type and DB methods to `internal/db/db.go`** ```go type CollaborationRow struct { CollaboratorDID string OwnerDID string DocumentRKey string AddedAt time.Time } func (db *DB) AddCollaboration(collabDID, ownerDID, rkey string) error { _, err := db.Exec( `INSERT INTO collaborations (collaborator_did, owner_did, document_rkey, added_at) VALUES (?, ?, ?, ?) ON CONFLICT DO NOTHING`, collabDID, ownerDID, rkey, time.Now(), ) return err } func (db *DB) GetCollaborations(collabDID string) ([]CollaborationRow, error) { rows, err := db.Query( `SELECT collaborator_did, owner_did, document_rkey, added_at FROM collaborations WHERE collaborator_did = ? ORDER BY added_at DESC`, collabDID, ) if err != nil { return nil, err } defer rows.Close() var result []CollaborationRow for rows.Next() { var r CollaborationRow if err := rows.Scan(&r.CollaboratorDID, &r.OwnerDID, &r.DocumentRKey, &r.AddedAt); err != nil { return nil, err } result = append(result, r) } return result, rows.Err() } ``` **Step 4: Run test to confirm it passes** ```bash go test ./internal/db/... -run TestCollaborations -v ``` Expected: PASS. **Step 5: Commit** ```bash git add internal/db/db.go internal/db/db_test.go git commit -m "Add AddCollaboration and GetCollaborations DB methods" ``` --- ### Task 3: Record collaboration on invite acceptance **Files:** - Modify: `internal/handler/handler.go` — `AcceptInvite` function (~line 586) **Step 1: After `PutDocument` succeeds in `AcceptInvite`, insert the collaboration row** Find the block: ```go h.DB.DeleteInvite(invite.Token) // Redirect to owner-scoped document URL... http.Redirect(w, r, "/docs/"+invite.CreatedBy+"/"+rKey, http.StatusSeeOther) ``` Replace with: ```go h.DB.DeleteInvite(invite.Token) if err := h.DB.AddCollaboration(collabSession.DID, invite.CreatedBy, rKey); err != nil { log.Printf("AcceptInvite: record collaboration: %v", err) // Non-fatal: collaborator is already on the document; just redirect. } http.Redirect(w, r, "/docs/"+invite.CreatedBy+"/"+rKey, http.StatusSeeOther) ``` Also handle the "already a collaborator" early-return — add the same `AddCollaboration` call there (idempotent, safe to call again): ```go for _, c := range doc.Collaborators { if c == collabSession.DID { h.DB.AddCollaboration(collabSession.DID, invite.CreatedBy, rKey) // ensure row exists http.Redirect(w, r, "/docs/"+invite.CreatedBy+"/"+rKey, http.StatusSeeOther) return } } ``` **Step 2: Build** ```bash go build ./... ``` Expected: no errors. **Step 3: Commit** ```bash git add internal/handler/handler.go git commit -m "Record collaboration row when invite is accepted" ``` --- ### Task 4: Fetch shared documents in the Dashboard handler **Files:** - Modify: `internal/handler/handler.go` — `Dashboard` function (~line 134) **Step 1: Add `DashboardContent` struct to hold both document lists** Near the top of `handler.go`, alongside `PageData` and `DocumentEditData`: ```go type DashboardContent struct { OwnDocs []*model.Document SharedDocs []*SharedDocument } type SharedDocument struct { RKey string OwnerDID string Title string UpdatedAt string CreatedAt string } ``` **Step 2: Update `Dashboard` to populate both lists** Replace the existing `Dashboard` handler body (after the user/client setup) with logic that: 1. Lists the user's own records (existing logic, unchanged). 2. Calls `h.DB.GetCollaborations(session.DID)` to get shared doc references. 3. For each collaboration row, uses `h.xrpcClient(ownerUser.ID)` to call `GetRecord` on the owner's PDS and extract title/dates. 4. Passes a `DashboardContent` as `Content`. ```go func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { user, userHandle := h.currentUser(r) if user == nil { h.render(w, "landing.html", PageData{Title: "Diffdown"}) return } empty := DashboardContent{OwnDocs: []*model.Document{}, SharedDocs: []*SharedDocument{}} client, err := h.xrpcClient(user.ID) if err != nil { log.Printf("Dashboard: xrpc client: %v", err) h.render(w, "documents.html", PageData{Title: "Documents", User: user, UserHandle: userHandle, Content: empty}) return } // Own documents records, _, err := client.ListRecords(client.DID(), collectionDocument, 100, "") if err != nil { log.Printf("Dashboard: list records: %v", err) h.render(w, "documents.html", PageData{Title: "Documents", User: user, UserHandle: userHandle, Content: empty}) return } var ownDocs []*model.Document for _, rec := range records { doc := &model.Document{} if err := json.Unmarshal(rec.Value, doc); err != nil { continue } doc.URI = rec.URI doc.CID = rec.CID doc.RKey = model.RKeyFromURI(rec.URI) ownDocs = append(ownDocs, doc) } // Shared documents session, _ := h.DB.GetATProtoSession(user.ID) var sharedDocs []*SharedDocument if session != nil { collabs, err := h.DB.GetCollaborations(session.DID) if err != nil { log.Printf("Dashboard: get collaborations: %v", err) } for _, c := range collabs { ownerUser, err := h.DB.GetUserByDID(c.OwnerDID) if err != nil { log.Printf("Dashboard: owner not found %s: %v", c.OwnerDID, err) continue } ownerClient, err := h.xrpcClient(ownerUser.ID) if err != nil { log.Printf("Dashboard: owner xrpc client %s: %v", c.OwnerDID, err) continue } value, _, err := ownerClient.GetRecord(c.OwnerDID, collectionDocument, c.DocumentRKey) if err != nil { log.Printf("Dashboard: get shared record %s/%s: %v", c.OwnerDID, c.DocumentRKey, err) continue } var doc model.Document if err := json.Unmarshal(value, &doc); err != nil { continue } sharedDocs = append(sharedDocs, &SharedDocument{ RKey: c.DocumentRKey, OwnerDID: c.OwnerDID, Title: doc.Title, UpdatedAt: doc.UpdatedAt, CreatedAt: doc.CreatedAt, }) } } h.render(w, "documents.html", PageData{ Title: "Documents", User: user, UserHandle: userHandle, Content: DashboardContent{OwnDocs: ownDocs, SharedDocs: sharedDocs}, }) } ``` **Step 3: Build** ```bash go build ./... ``` Expected: no errors. (Template will need updating in the next task.) **Step 4: Commit** ```bash git add internal/handler/handler.go git commit -m "Fetch shared documents for dashboard" ``` --- ### Task 5: Update the documents template **Files:** - Modify: `templates/documents.html` **Step 1: Update the template to use `DashboardContent`** The template currently iterates over `.Content` directly. Change it to use `.Content.OwnDocs` and add a new section for `.Content.SharedDocs`. The shared doc link format is `/docs/{ownerDID}/{rkey}` (same as the collaborator view URL established in existing routes). ```html {{template "base" .}} {{define "content"}}

Your Documents

New Document
{{range .Content.OwnDocs}}

{{.Title}}

{{if .UpdatedAt}} {{else}} {{end}}
{{else}}

No documents yet.

Create your first document
{{end}}
{{if .Content.SharedDocs}}

Documents Shared with You

{{range .Content.SharedDocs}}

{{.Title}}

{{if .UpdatedAt}} {{else}} {{end}}
{{end}}
{{end}}
{{end}} {{define "scripts"}} {{end}} ``` **Step 2: Build and run** ```bash go build ./... && go run ./cmd/server ``` Visit `http://localhost:8080` while logged in. Verify "Your Documents" section still renders. If you have an accepted collaboration, verify it appears under "Documents Shared with You". **Step 3: Commit** ```bash git add templates/documents.html git commit -m "Add 'Documents Shared with You' section to dashboard" ``` --- ### Task 6: Check existing routes cover the collaborator view URL **Files:** - Read: `cmd/server/main.go` The collaborator view URL `/docs/{ownerDID}/{rkey}` must be registered. Confirm `CollaboratorDocumentView` and `CollaboratorDocumentEdit` are wired up. If not, add them. No code change expected here — this was built in a prior sprint — but verify before closing out. ```bash grep -n "Collaborator" cmd/server/main.go ``` Expected: entries for `CollaboratorDocumentView` and `CollaboratorDocumentEdit`. --- ## Notes - **No backfill needed for existing collaborators** — the `collaborations` table will be empty until invites are accepted post-deploy. This is acceptable; collaborators can re-use an existing invite link or the owner can send a new one. - **PDS fetch errors are non-fatal** — if a shared document's owner PDS is unreachable, that card is silently skipped. Add a log line if debugging is needed. - **`fmtdate` template func** — already registered in `cmd/server/main.go`; no changes needed. - **List/card view toggle** — updated in Task 5 to apply to both lists so both respond to the toggle buttons.