Commit 13e911dba3

13e911dba3b7667bf9f31b6f40334b2babf98bc5

parent: e6031c4c60

Verified · cmc ci/build: success ci/lint: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-11 16:39 UTC

Record what the roadmap shipped in v1.5

Ref #33

Layout: unified · split

ROADMAP.md +77 −22
@@ -13,6 +13,13 @@ Existing gitbay issues: #1 cleanup, #2 search filters, #3 description
1313parsing, #4 instance checker, #5 Makefile, #6 emote bug. They are mapped
1414below where they overlap.
1515
16## Status
17
18Everything below except 5.3 and 5.5 shipped in v1.5.0 (2026-09-11), with
19v1.5.1 fixing the release build. Merge requests !6 through !25 on gitbay.
20Still open: #33 (LibRedirect submission), #1, #2, #3 and #6 from the
21original list, which need real DeviantArt payloads to work from.
22
1623## Ordering principle
1724
1825Frontend proxies get blocked upstream. The existing throttle
@@ -42,6 +49,8 @@ Concurrent requests for the same page each go upstream. No response carries
4249
4350### 0.1 CI on merge requests (S, #7)
4451
52Done in !6.
53
4554No pipeline runs `go vet`, `go test` or `golangci-lint`. The only workflow
4655is the GitHub release build; gitbay has zero builds. `.golangci.yml` exists
4756but is never run.
@@ -57,6 +66,8 @@ Verify: an MR with a failing test shows a failed build.
5766
5867### 1.1 Cache DeviantArt API responses (L, #8)
5968
69Done in !8.
70
6071Add an in-memory cache in front of every devianter call: DD, search,
6172deviation, gruser, gallery, favourites, comments. Keyed by endpoint plus
6273arguments. Bounded by entry count or bytes. Singleflight so concurrent
@@ -80,6 +91,8 @@ sequential requests for one page make one upstream call.
8091
8192### 1.2 Cache avatars and emotes, add Cache-Control (M, #9)
8293
94Done in !9.
95
8396`Emojitar` in `app/wrapper.go` fetches from a.deviantart.net or
8497e.deviantart.net on every request and never stores the result. Route it
8598through the same disk and memory cache path as `DownloadAndSendMedia`.
@@ -96,6 +109,8 @@ present in `curl -I` output.
96109
97110### 1.3 robots.txt and per-client rate limit (S, #10)
98111
112Done in !10.
113
99114Serve `/robots.txt` disallowing `/search`, `/api`, `/group_user`,
100115`/media` and any path with `?p=`. Add a per-client-IP token bucket ahead of
101116the upstream throttle so one crawler cannot consume the whole DA budget and
@@ -109,6 +124,8 @@ Verify: test that N+1 requests from one address within the window get 429.
109124
110125### 1.4 Fewer calls per page (M, #11)
111126
127Done in !11.
128
112129Post view is two API calls because comments are fetched inline. Move
113130comments behind a link (`/post/{author}/{name}/comments` or `?comments=1`)
114131so the default post view is one call. Same for the user about page.
@@ -123,6 +140,8 @@ transport.
123140
124141### 1.5 Media cache on by default (S, #12)
125142
143Done in !12.
144
126145A proxying instance with no cache re-fetches every image from wixmp on every
127146view. Set `cache.enabled: true` in the built-in defaults in `app/config.go`
128147and in `config.example.json`, with a sane `lifetime` and `max-size`. Keep
@@ -134,6 +153,8 @@ Verify: fresh start with no config writes to the cache directory.
134153
135154### 2.1 Escape template output (M, #13)
136155
156Done in !7.
157
137158`app/util.go` imports `text/template`. Nothing interpolated is escaped: the
138159search query in `static/html/search.htm` and `head.htm`, and every DA
139160username, title and description written by `DeviationList`,
@@ -150,6 +171,8 @@ Land before other template work.
150171
151172### 2.2 Restore the user About branch (S, #14)
152173
174Done in !13.
175
153176`app/wrapper.go:35` has `else if false`, inherited from upstream commit
154177048bb47. Registration date, interests, social links and bio never render for
155178users. Find out why it was disabled (likely a devianter struct change),
@@ -157,6 +180,8 @@ restore the branch, add a test with a fixture.
157180
158181### 2.3 Group search pagination (S, #15)
159182
183Done in !13.
184
160185`app/wrapper.go:274` increments the page and requests offset `10*page`, so
161186page two starts at result 20 and results 10 to 19 are never shown. The nav
162187bar also shows the incremented number. Use `10*(page-1)` and do not mutate
@@ -164,10 +189,14 @@ bar also shows the incremented number. Use `10*(page-1)` and do not mutate
164189
165190### 2.4 Emojitar writes a body after 404 (S, #16)
166191
192Done in !9.
193
167194`app/wrapper.go:344` lacks a `return` after `ReturnHTTPError(404)`.
168195
169196### 2.5 Valid Atom feed (S, #17)
170197
198Done in !15.
199
171200`DeviationList` in `app/parsers.go` emits no feed-level `<id>` or
172201`<updated>`, bare integer entry ids, RFC 1123 `<published>` instead of RFC
1732023339, and `media:thumbinal`. Verified on the live feed. Fix all five and add
@@ -176,17 +205,23 @@ elements.
176205
177206### 2.6 `-c` bounds check (S, #18)
178207
208Done in !13.
209
179210`app/cli.go:29` checks `len(a) >= 2` instead of `n+1 < len(a)`;
180211`skunkyart -x -c` panics.
181212
182213### 2.7 Sanitize the 502 page (S, #19)
183214
215Done in !7.
216
184217`Error` in `app/util.go` writes the upstream error, including the full
185218CloudFront block page, into an `<h3>` unescaped. Truncate to one line and
186219escape. Folds into 2.1 if done together.
187220
188221### 2.8 Parse templates once (S, #20)
189222
223Done in !13.
224
190225`ExecuteTemplate` calls `ParseFS` on every request. Parse at startup;
191226supply the per-request `T` function through the data struct or a per-request
192227`Funcs` clone. Template errors then fail at boot instead of as 500s.
@@ -195,6 +230,8 @@ supply the per-request `T` function through the data struct or a per-request
195230
196231### 3.1 Config-less start and default alignment (S, #21)
197232
233Done in !16.
234
198235`ExecuteConfig` exits if `config.json` is missing even though defaults
199236exist. Start with defaults when no `-c` is given and the default file is
200237absent. Align the built-in `nsfw: true` with the example's `false`, or
@@ -202,18 +239,24 @@ document why they differ.
202239
203240### 3.2 Cache documentation (S, #22)
204241
242Done in !16.
243
205244`SETUP.md`: `update-interval` is in seconds (the example scans every 5s);
206245the `d` unit works but is unlisted; `y` is 360 days; exceeding `max-size`
207246deletes the whole cache directory; `lifetime: null` in the example.
208247
209248### 3.3 API and search type docs (S, #23)
210249
250Done in !16.
251
211252`API.md` says `t` is text search; devianter defines it as tag. The
212253"Folders" option in `static/html/gruser.htm` maps to `f`, which is
213254favourites. Fix the doc and rename or remove the option.
214255
215256### 3.4 i18n coverage (M, #24)
216257
258Done in !17.
259
217260Go-built HTML hardcodes English: comment headers, "In reply to",
218261pagination, folder and content headings, "No results", "[ TEXT ]".
219262`gruser.htm` section headings and the index blurb are untranslated. Every
@@ -223,6 +266,8 @@ language, and either use or remove `Languages()`.
223266
224267### 3.5 systemd unit (S, #25)
225268
269Done in !18.
270
226271`services/skunkyart.example.service` uses `Directory=` (not a valid key),
227272placeholder paths, and says it was never tested. Write a working unit with
228273`WorkingDirectory`, `User`, `DynamicUser` or a dedicated user,
@@ -230,11 +275,15 @@ placeholder paths, and says it was never tested. Write a working unit with
230275
231276### 3.6 SETUP.md structure (S, #26)
232277
278Done in !16.
279
233280The nginx section sits between config keys; `theme` and `language` come
234281after it. Reorder: config keys, units, reverse proxy.
235282
236283### 3.7 README (S, #27)
237284
285Done in !18.
286
238287Add: endpoints and what they do, running the binary without Docker with
239288the service files, what `REDIRECTS.md` is for (redirector rules), and a
240289screenshot.
@@ -243,6 +292,8 @@ screenshot.
243292
244293### 4.1 Viewport and mobile CSS (S, #28)
245294
295Done in !19.
296
246297`static/html/head.htm` and `index.htm` use `initial-scale=0.4` and
247298`height=device-height`; `skunky.css` then compensates with
248299`* { font-size: 120% }` in portrait. Use `width=device-width,
@@ -251,12 +302,16 @@ width before and after.
251302
252303### 4.2 Accessibility (S, #29)
253304
305Done in !19.
306
254307Listing and avatar images in `DeviationList`, `ParseComments` and
255308`BuildUserPlate` have no `alt`. The post page has no heading element for
256309the title. Add both.
257310
258311### 4.3 Index stylesheet (S, #30)
259312
313Done in !19.
314
260315`static/html/index.htm` carries an inline stylesheet duplicating layout
261316rules. Move it into `skunky.css`.
262317
@@ -264,6 +319,8 @@ rules. Move it into `skunky.css`.
264319
265320### 5.1 One canonical forge (S, #31)
266321
322Done in !20.
323
267324Origin and issues are on gitbay; releases, the image, Dependabot, the
268325instances.json fetch at `app/util.go:64`, the About page "Report an issue"
269326link, the index source link, and the `--add-instance` exit message all
@@ -274,17 +331,23 @@ README and leave the links.
274331
275332### 5.2 Instance checker (M, issue #4, #32)
276333
334Done in !21.
335
277336A scheduled job that fetches each instance's `/api/instance` and marks dead
278337ones in `INSTANCES.md`, or a CI job that fails when one is down.
279338
280339### 5.3 LibRedirect listing (S, #33)
281340
341Open. The two upstream pull requests are written up on #33.
342
282343`REDIRECTS.md` already describes the URL mapping. Check whether LibRedirect
283344lists SkunkyArt with the dead upstream instances and submit the fork and
284345art.krz.sh. This is the cheapest way to get users.
285346
286347### 5.4 Makefile and binary releases (S, issue #5, #34)
287348
349Done in !22.
350
288351Targets for build with the embed tag and version stamp, test, lint.
289352Publish binaries alongside the image on release tags.
290353
@@ -299,25 +362,17 @@ Publish binaries alongside the image on release tags.
299362- #6 emote bug: the `a.Val[8:9] == "e"` and `[37:len-4]` offsets in the
300363 HTML branch of `ParseDescription`. Parse the URL instead of slicing.
301364
302## Stacked MR order
303
304Each MR branches from the previous one's tip and is merged in order.
305
3061. `ci/pipeline` (0.1)
3072. `fix/escape-templates` (2.1 + 2.7)
3083. `feat/api-cache` (1.1, after its spec is approved)
3094. `feat/avatar-cache-headers` (1.2)
3105. `feat/robots-ratelimit` (1.3)
3116. `feat/fewer-calls` (1.4)
3127. `chore/cache-default-on` (1.5)
3138. `fix/small-bugs` (2.2, 2.3, 2.4, 2.6, 2.8; one MR, one commit each)
3149. `fix/atom-feed` (2.5)
31510. `docs/config-and-setup` (3.1, 3.2, 3.3, 3.6)
31611. `feat/i18n-coverage` (3.4)
31712. `chore/services-readme` (3.5, 3.7)
31813. `ui/viewport-a11y` (4.1, 4.2, 4.3)
31914. `chore/canonical-forge` (5.1)
32015. 5.2 through 5.5 as independent MRs off `main`
321
322Items 8 through 15 do not depend on the cache stack and can be reordered or
323interleaved when the cache work stalls on design.
365## Merge record
366
367Merged into main in this order on 2026-09-11, each stacked on the one
368before: !6 (0.1), !7 (2.1, 2.7), !8 (1.1), !9 (1.2, 2.4), !10 (1.3),
369!11 (1.4), !12 (1.5), !13 (2.2, 2.3, 2.6, 2.8), !15 (2.5), !16 (3.1,
3703.2, 3.3, 3.6), !17 (3.4), !18 (3.5, 3.7), !19 (4.1, 4.2, 4.3), !20
371(5.1), !21 (5.2), !22 (5.4). Then !23 (release string), !24 (Go 1.26 in
372the image and binaries builds) and !25 (no VCS stamping) for the
373release itself.
374
375Two things the stack taught: lint on macOS never compiles the Linux-only
376files, so run `GOOS=linux golangci-lint run` before pushing; and `go get`
377can raise the go directive in go.mod, so check the Dockerfile and
378workflow images still match it.