chore: file cleanup 75142cc3
Steve Simkins · 2026-05-17 14:57 2 file(s) · +1 −181
.gitignore +1 −2
27 27
apps/og-go/og-go
28 28
apps/posts-go/posts-go
29 29
apps/shrink-go/shrink-go
30 -
apps/sipp-go/sipp-cli
31 -
apps/sipp-go/sipp-server
30 +
apps/sipp-go/sipp-go
gaps-plan.md (deleted) +0 −179
1 -
# Plan: bring Go ports to parity with Rust originals
2 -
3 -
## Context
4 -
5 -
Repo `/home/stevedylandev/Developer/andromeda` has paired Rust + Go versions of 10 apps. Audit (Phase 1) found gaps in 4 Go apps. User wants full parity. Deep-dive (Phase 2) corrected the initial gap list — some "missing" items in jotts-go are already implemented, and the posts EXIF strip referenced earlier does **not** exist in the Rust source either.
6 -
7 -
This plan covers only the real, confirmed gaps.
8 -
9 -
---
10 -
11 -
## Apps at parity (no work)
12 -
13 -
bookmarks, cellar, easel, feeds, library, og.
14 -
15 -
---
16 -
17 -
## Gap 1 — `posts-go`: R2/S3 storage backend
18 -
19 -
**Rust source to mirror:**
20 -
- `apps/posts/src/storage.rs` — `R2Config { bucket, creds, public_url, http }`, methods `from_env`, `put_object`, `delete_object`, `public_url_for`. Uses `rusty_s3 = "0.9"` + reqwest.
21 -
- `apps/posts/src/server/mod.rs:552` — init at startup, store `Option<R2Config>` in `AppState`.
22 -
- `apps/posts/src/server/handlers/admin.rs:439-522` — `admin_upload_file` routes via `if let Some(r2) = &state.r2`, sets `storage_backend` to `"r2"` or `"local"`, rolls back on failure.
23 -
- `apps/posts/src/server/handlers/public.rs:165-199` — `serve_uploaded_file` redirects to `r2.public_url_for(filename)` when backend is r2.
24 -
25 -
**EXIF strip is NOT in Rust** — drop from scope.
26 -
27 -
**Go changes (apps/posts-go/):**
28 -
- New package `storage/` with interface:
29 -
  ```go
30 -
  type Backend interface {
31 -
      Put(ctx context.Context, key, contentType string, data []byte) error
32 -
      Delete(ctx context.Context, key string) error
33 -
      PublicURL(key string) string
34 -
      Name() string // "local" | "r2"
35 -
  }
36 -
  ```
37 -
- `storage/local.go` — wrap existing FS funcs from `storage.go`.
38 -
- `storage/r2.go` — use `github.com/aws/aws-sdk-go-v2/service/s3` with custom endpoint resolver for R2 (`https://<account>.r2.cloudflarestorage.com`). Or `github.com/minio/minio-go/v7` for less ceremony.
39 -
- `app.go` — add `Storage storage.Backend` field.
40 -
- `main.go` — `if os.Getenv("R2_BUCKET") != "" { storage.NewR2(...) } else { storage.NewLocal(uploadsDir) }`.
41 -
- `handlers_admin.go` (~line 301 `adminUploadFile`) — call `a.Storage.Put(...)`, capture `a.Storage.Name()` for DB insert, delete on rollback.
42 -
- `handlers_public.go` (~line 156 `serveUploadedFile`) — if `f.StorageBackend == "r2"`, `http.Redirect(... StatusTemporaryRedirect)`.
43 -
- `db.go` (~line 395 `createFile`) — pass backend string; column already exists.
44 -
- `.env.example` — add `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_PUBLIC_URL`.
45 -
46 -
---
47 -
48 -
## Gap 2 — `shrink-go`: EXIF preserve + GPS strip
49 -
50 -
**Rust source to mirror:** `apps/shrink/src/server.rs:103-207` (`strip_gps_from_exif`). Crates: `img-parts = "0.3"` + `image = "0.25"`. Algorithm:
51 -
1. `Jpeg::from_bytes(orig)` → `j.exif().to_vec()` (raw APP1 payload).
52 -
2. Re-encode resized JPEG (loses EXIF).
53 -
3. Parse raw EXIF: TIFF header (II/MM), read IFD0 offset, walk entries (12 bytes each) for tag `0x8825` (GPS IFD pointer), zero the GPS IFD's entry count (2 bytes at the pointed offset).
54 -
4. `out_jpeg.set_exif(Some(exif.into()))`, write final bytes.
55 -
56 -
**Go changes (apps/shrink-go/):**
57 -
- Add deps: `github.com/dsoprea/go-exif/v3` and `github.com/dsoprea/go-jpeg-image-structure/v2`.
58 -
- New `exif.go`:
59 -
  - `extractExif(orig []byte) []byte` — use go-jpeg-image-structure segment list, return APP1 bytes (skip first 6 `Exif\0\0` prefix).
60 -
  - `stripGPS(exif []byte) []byte` — mirror Rust byte-level walk (don't use go-exif builder; cheaper to keep parity).
61 -
  - `injectExif(jpeg, exif []byte) []byte` — splice modified APP1 after SOI marker.
62 -
- `image.go` / `handlers.go` — wrap compress flow: extract before resize, strip GPS, inject after re-encode.
63 -
64 -
---
65 -
66 -
## Gap 3 — `sipp-go`: content wrap toggle (TUI theme is out of scope)
67 -
68 -
**Already done in Go** (verified via `apps/sipp-go/tui/`): syntect→chroma highlight, clipboard auto-copy on select (`update.go:244-261`), line numbers (`ShowLineNumbers = true`), vim keybindings (`keys.go`).
69 -
70 -
**Remaining:** Ctrl+W content wrap toggle (Rust `wrap_content: bool`).
71 -
72 -
**Go changes (apps/sipp-go/tui/):**
73 -
- `model.go` — add `wrapContent bool` field.
74 -
- `keys.go` — add `WrapToggle` binding (`ctrl+w`).
75 -
- `update.go` — when focus is content view/edit and `WrapToggle` matches, flip flag, reset scroll, emit status message.
76 -
- `view.go` — when rendering content viewport/textarea, branch on `wrapContent`.
77 -
78 -
**Custom .tmTheme loading:** Chroma has no native TextMate XML loader. Defer — README already documents the trade-off. Re-open as a follow-up if user wants it; will require either a tmTheme→chroma converter or hand-porting the two themes to chroma `chroma.MustNewStyle`.
79 -
80 -
---
81 -
82 -
## Gap 4 — `jotts-go`: complete the TUI editor
83 -
84 -
**Already done in Go** (verified):
85 -
- `cmd_auth.go` — interactive auth + `~/.config/jotts/config.toml` (BurntSushi/toml).
86 -
- `cmd_upload.go` — file → note + clipboard.
87 -
- `cmd_server.go:29` — startup `sessions.PruneExpired()` (in `crates-go/auth/auth.go`).
88 -
89 -
**Remaining:** interactive TUI editor. Mirror `apps/jotts/src/tui/{app,events,render}.rs` + `apps/jotts/src/tui.rs`.
90 -
91 -
**Go changes (apps/jotts-go/tui/):**
92 -
- `model.go` — `Focus` enum: List, Content, CreateTitle, CreateContent, EditTitle, EditContent, Search.
93 -
- `keys.go` — vim bindings: `hjkl`, `y` (copy content), `Y` (copy share link), `d` (delete with confirm), `c` (new), `e` (edit), `E` (external editor), `o` (open in browser), `/` (search), `?` (help), `q`/`esc` (quit/back).
94 -
- `update.go` — mode FSM driving Focus transitions; copy triggers via `atotto/clipboard.WriteAll`; status-line messages.
95 -
- `editor.go` (new) — external editor: read `$EDITOR`, write content to `os.CreateTemp`, `exec.Command(editor, path).Run()` with stdio attached to current TTY, re-read file on exit.
96 -
- `browser.go` (new) — open `<remote_url>/notes/<short_id>` via `github.com/pkg/browser` (cross-platform).
97 -
- `view.go` — two-pane layout (lipgloss `JoinHorizontal`, 30/70), borders/title styles, help line at bottom.
98 -
- Markdown render: keep existing `glamour` (acceptable substitute for syntect — agent confirmed parity in behavior).
99 -
- `backend.go` — already abstracts local/remote; reuse.
100 -
101 -
**Deps to add:** `github.com/pkg/browser` (everything else already in go.mod).
102 -
103 -
---
104 -
105 -
## Critical files (modified or created)
106 -
107 -
| App | Path | Action |
108 -
|-----|------|--------|
109 -
| posts-go | `storage/{interface,local,r2}.go` | new |
110 -
| posts-go | `app.go`, `main.go`, `handlers_admin.go`, `handlers_public.go`, `db.go`, `.env.example` | edit |
111 -
| shrink-go | `exif.go` | new |
112 -
| shrink-go | `image.go`, `handlers.go`, `go.mod` | edit |
113 -
| sipp-go | `tui/{model,keys,update,view}.go` | edit |
114 -
| jotts-go | `tui/{editor,browser}.go` | new |
115 -
| jotts-go | `tui/{model,keys,update,view}.go`, `go.mod` | edit |
116 -
117 -
---
118 -
119 -
## Reused existing utilities
120 -
121 -
- `crates-go/auth/auth.go` — sessions, bearer/session middleware (no changes).
122 -
- `crates-go/web` — embedded handler, render helpers (no changes).
123 -
- `crates-go/sqlite`, `crates-go/config`, `crates-go/darkmatter` — reused as-is.
124 -
- posts-go local FS funcs already in `storage.go` — wrap into `LocalStorage`.
125 -
- sipp-go clipboard + chroma flow already wired — only add wrap toggle.
126 -
- jotts-go `tui/backend.go` (local + remote impls) — reused.
127 -
128 -
---
129 -
130 -
## Execution order (suggested)
131 -
132 -
1. **shrink-go EXIF** — smallest, self-contained, no schema changes.
133 -
2. **sipp-go wrap toggle** — ~20 LoC, fast win.
134 -
3. **posts-go R2** — biggest data-path change; needs R2 creds for end-to-end test.
135 -
4. **jotts-go TUI** — largest, mostly UI iteration; do last so other parity work isn't blocked.
136 -
137 -
---
138 -
139 -
## Verification
140 -
141 -
Per app:
142 -
143 -
**shrink-go**
144 -
```bash
145 -
cd apps/shrink-go && go build ./... && go run .
146 -
# upload a JPEG with GPS via the form, download result, run:
147 -
exiftool result.jpg | rg -i 'gps|orientation|make|model'
148 -
# expect: GPS fields absent, other EXIF present
149 -
```
150 -
151 -
**sipp-go**
152 -
```bash
153 -
cd apps/sipp-go && go run ./cmd/server
154 -
# open snippet, press Ctrl+W, confirm wrap behavior toggles, scroll resets
155 -
```
156 -
157 -
**posts-go**
158 -
```bash
159 -
cd apps/posts-go && go build ./... && go test ./...
160 -
# unset R2 vars: upload via admin → file lands in uploads/, GET /uploads/<f> returns 200
161 -
# set R2 vars (test bucket): upload → check bucket contains object, GET returns 307 to R2 public URL
162 -
sqlite3 posts.sqlite "select storage_backend from files order by id desc limit 5;"
163 -
```
164 -
165 -
**jotts-go**
166 -
```bash
167 -
cd apps/jotts-go && go run . server &
168 -
go run . tui --remote http://localhost:3000 --api-key $KEY
169 -
# walk: create note, edit, y copies content, Y copies link, E opens $EDITOR, o opens browser, d deletes
170 -
```
171 -
172 -
Cross-cutting: `go vet ./...` and `go build ./...` from repo root after each app's changes.
173 -
174 -
---
175 -
176 -
## Out of scope (recorded for follow-up)
177 -
178 -
- sipp-go custom `.tmTheme` loading — needs chroma converter or hand-port; defer per README's existing call-out.
179 -
- posts-go EXIF strip on upload — not in Rust either; only do if user asks separately.