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
13parsing, #4 instance checker, #5 Makefile, #6 emote bug. They are mapped 13parsing, #4 instance checker, #5 Makefile, #6 emote bug. They are mapped
14below where they overlap. 14below where they overlap.
15 15
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
16## Ordering principle 23## Ordering principle
17 24
18Frontend proxies get blocked upstream. The existing throttle 25Frontend proxies get blocked upstream. The existing throttle
@@ -42,6 +49,8 @@ Concurrent requests for the same page each go upstream. No response carries
42 49
43### 0.1 CI on merge requests (S, #7) 50### 0.1 CI on merge requests (S, #7)
44 51
52Done in !6.
53
45No pipeline runs `go vet`, `go test` or `golangci-lint`. The only workflow 54No pipeline runs `go vet`, `go test` or `golangci-lint`. The only workflow
46is the GitHub release build; gitbay has zero builds. `.golangci.yml` exists 55is the GitHub release build; gitbay has zero builds. `.golangci.yml` exists
47but is never run. 56but is never run.
@@ -57,6 +66,8 @@ Verify: an MR with a failing test shows a failed build.
57 66
58### 1.1 Cache DeviantArt API responses (L, #8) 67### 1.1 Cache DeviantArt API responses (L, #8)
59 68
69Done in !8.
70
60Add an in-memory cache in front of every devianter call: DD, search, 71Add an in-memory cache in front of every devianter call: DD, search,
61deviation, gruser, gallery, favourites, comments. Keyed by endpoint plus 72deviation, gruser, gallery, favourites, comments. Keyed by endpoint plus
62arguments. Bounded by entry count or bytes. Singleflight so concurrent 73arguments. Bounded by entry count or bytes. Singleflight so concurrent
@@ -80,6 +91,8 @@ sequential requests for one page make one upstream call.
80 91
81### 1.2 Cache avatars and emotes, add Cache-Control (M, #9) 92### 1.2 Cache avatars and emotes, add Cache-Control (M, #9)
82 93
94Done in !9.
95
83`Emojitar` in `app/wrapper.go` fetches from a.deviantart.net or 96`Emojitar` in `app/wrapper.go` fetches from a.deviantart.net or
84e.deviantart.net on every request and never stores the result. Route it 97e.deviantart.net on every request and never stores the result. Route it
85through the same disk and memory cache path as `DownloadAndSendMedia`. 98through the same disk and memory cache path as `DownloadAndSendMedia`.
@@ -96,6 +109,8 @@ present in `curl -I` output.
96 109
97### 1.3 robots.txt and per-client rate limit (S, #10) 110### 1.3 robots.txt and per-client rate limit (S, #10)
98 111
112Done in !10.
113
99Serve `/robots.txt` disallowing `/search`, `/api`, `/group_user`, 114Serve `/robots.txt` disallowing `/search`, `/api`, `/group_user`,
100`/media` and any path with `?p=`. Add a per-client-IP token bucket ahead of 115`/media` and any path with `?p=`. Add a per-client-IP token bucket ahead of
101the upstream throttle so one crawler cannot consume the whole DA budget and 116the 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.
109 124
110### 1.4 Fewer calls per page (M, #11) 125### 1.4 Fewer calls per page (M, #11)
111 126
127Done in !11.
128
112Post view is two API calls because comments are fetched inline. Move 129Post view is two API calls because comments are fetched inline. Move
113comments behind a link (`/post/{author}/{name}/comments` or `?comments=1`) 130comments behind a link (`/post/{author}/{name}/comments` or `?comments=1`)
114so the default post view is one call. Same for the user about page. 131so the default post view is one call. Same for the user about page.
@@ -123,6 +140,8 @@ transport.
123 140
124### 1.5 Media cache on by default (S, #12) 141### 1.5 Media cache on by default (S, #12)
125 142
143Done in !12.
144
126A proxying instance with no cache re-fetches every image from wixmp on every 145A proxying instance with no cache re-fetches every image from wixmp on every
127view. Set `cache.enabled: true` in the built-in defaults in `app/config.go` 146view. Set `cache.enabled: true` in the built-in defaults in `app/config.go`
128and in `config.example.json`, with a sane `lifetime` and `max-size`. Keep 147and 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.
134 153
135### 2.1 Escape template output (M, #13) 154### 2.1 Escape template output (M, #13)
136 155
156Done in !7.
157
137`app/util.go` imports `text/template`. Nothing interpolated is escaped: the 158`app/util.go` imports `text/template`. Nothing interpolated is escaped: the
138search query in `static/html/search.htm` and `head.htm`, and every DA 159search query in `static/html/search.htm` and `head.htm`, and every DA
139username, title and description written by `DeviationList`, 160username, title and description written by `DeviationList`,
@@ -150,6 +171,8 @@ Land before other template work.
150 171
151### 2.2 Restore the user About branch (S, #14) 172### 2.2 Restore the user About branch (S, #14)
152 173
174Done in !13.
175
153`app/wrapper.go:35` has `else if false`, inherited from upstream commit 176`app/wrapper.go:35` has `else if false`, inherited from upstream commit
154048bb47. Registration date, interests, social links and bio never render for 177048bb47. Registration date, interests, social links and bio never render for
155users. Find out why it was disabled (likely a devianter struct change), 178users. Find out why it was disabled (likely a devianter struct change),
@@ -157,6 +180,8 @@ restore the branch, add a test with a fixture.
157 180
158### 2.3 Group search pagination (S, #15) 181### 2.3 Group search pagination (S, #15)
159 182
183Done in !13.
184
160`app/wrapper.go:274` increments the page and requests offset `10*page`, so 185`app/wrapper.go:274` increments the page and requests offset `10*page`, so
161page two starts at result 20 and results 10 to 19 are never shown. The nav 186page two starts at result 20 and results 10 to 19 are never shown. The nav
162bar also shows the incremented number. Use `10*(page-1)` and do not mutate 187bar 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
164 189
165### 2.4 Emojitar writes a body after 404 (S, #16) 190### 2.4 Emojitar writes a body after 404 (S, #16)
166 191
192Done in !9.
193
167`app/wrapper.go:344` lacks a `return` after `ReturnHTTPError(404)`. 194`app/wrapper.go:344` lacks a `return` after `ReturnHTTPError(404)`.
168 195
169### 2.5 Valid Atom feed (S, #17) 196### 2.5 Valid Atom feed (S, #17)
170 197
198Done in !15.
199
171`DeviationList` in `app/parsers.go` emits no feed-level `<id>` or 200`DeviationList` in `app/parsers.go` emits no feed-level `<id>` or
172`<updated>`, bare integer entry ids, RFC 1123 `<published>` instead of RFC 201`<updated>`, bare integer entry ids, RFC 1123 `<published>` instead of RFC
1733339, and `media:thumbinal`. Verified on the live feed. Fix all five and add 2023339, and `media:thumbinal`. Verified on the live feed. Fix all five and add
@@ -176,17 +205,23 @@ elements.
176 205
177### 2.6 `-c` bounds check (S, #18) 206### 2.6 `-c` bounds check (S, #18)
178 207
208Done in !13.
209
179`app/cli.go:29` checks `len(a) >= 2` instead of `n+1 < len(a)`; 210`app/cli.go:29` checks `len(a) >= 2` instead of `n+1 < len(a)`;
180`skunkyart -x -c` panics. 211`skunkyart -x -c` panics.
181 212
182### 2.7 Sanitize the 502 page (S, #19) 213### 2.7 Sanitize the 502 page (S, #19)
183 214
215Done in !7.
216
184`Error` in `app/util.go` writes the upstream error, including the full 217`Error` in `app/util.go` writes the upstream error, including the full
185CloudFront block page, into an `<h3>` unescaped. Truncate to one line and 218CloudFront block page, into an `<h3>` unescaped. Truncate to one line and
186escape. Folds into 2.1 if done together. 219escape. Folds into 2.1 if done together.
187 220
188### 2.8 Parse templates once (S, #20) 221### 2.8 Parse templates once (S, #20)
189 222
223Done in !13.
224
190`ExecuteTemplate` calls `ParseFS` on every request. Parse at startup; 225`ExecuteTemplate` calls `ParseFS` on every request. Parse at startup;
191supply the per-request `T` function through the data struct or a per-request 226supply the per-request `T` function through the data struct or a per-request
192`Funcs` clone. Template errors then fail at boot instead of as 500s. 227`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
195 230
196### 3.1 Config-less start and default alignment (S, #21) 231### 3.1 Config-less start and default alignment (S, #21)
197 232
233Done in !16.
234
198`ExecuteConfig` exits if `config.json` is missing even though defaults 235`ExecuteConfig` exits if `config.json` is missing even though defaults
199exist. Start with defaults when no `-c` is given and the default file is 236exist. Start with defaults when no `-c` is given and the default file is
200absent. Align the built-in `nsfw: true` with the example's `false`, or 237absent. Align the built-in `nsfw: true` with the example's `false`, or
@@ -202,18 +239,24 @@ document why they differ.
202 239
203### 3.2 Cache documentation (S, #22) 240### 3.2 Cache documentation (S, #22)
204 241
242Done in !16.
243
205`SETUP.md`: `update-interval` is in seconds (the example scans every 5s); 244`SETUP.md`: `update-interval` is in seconds (the example scans every 5s);
206the `d` unit works but is unlisted; `y` is 360 days; exceeding `max-size` 245the `d` unit works but is unlisted; `y` is 360 days; exceeding `max-size`
207deletes the whole cache directory; `lifetime: null` in the example. 246deletes the whole cache directory; `lifetime: null` in the example.
208 247
209### 3.3 API and search type docs (S, #23) 248### 3.3 API and search type docs (S, #23)
210 249
250Done in !16.
251
211`API.md` says `t` is text search; devianter defines it as tag. The 252`API.md` says `t` is text search; devianter defines it as tag. The
212"Folders" option in `static/html/gruser.htm` maps to `f`, which is 253"Folders" option in `static/html/gruser.htm` maps to `f`, which is
213favourites. Fix the doc and rename or remove the option. 254favourites. Fix the doc and rename or remove the option.
214 255
215### 3.4 i18n coverage (M, #24) 256### 3.4 i18n coverage (M, #24)
216 257
258Done in !17.
259
217Go-built HTML hardcodes English: comment headers, "In reply to", 260Go-built HTML hardcodes English: comment headers, "In reply to",
218pagination, folder and content headings, "No results", "[ TEXT ]". 261pagination, folder and content headings, "No results", "[ TEXT ]".
219`gruser.htm` section headings and the index blurb are untranslated. Every 262`gruser.htm` section headings and the index blurb are untranslated. Every
@@ -223,6 +266,8 @@ language, and either use or remove `Languages()`.
223 266
224### 3.5 systemd unit (S, #25) 267### 3.5 systemd unit (S, #25)
225 268
269Done in !18.
270
226`services/skunkyart.example.service` uses `Directory=` (not a valid key), 271`services/skunkyart.example.service` uses `Directory=` (not a valid key),
227placeholder paths, and says it was never tested. Write a working unit with 272placeholder paths, and says it was never tested. Write a working unit with
228`WorkingDirectory`, `User`, `DynamicUser` or a dedicated user, 273`WorkingDirectory`, `User`, `DynamicUser` or a dedicated user,
@@ -230,11 +275,15 @@ placeholder paths, and says it was never tested. Write a working unit with
230 275
231### 3.6 SETUP.md structure (S, #26) 276### 3.6 SETUP.md structure (S, #26)
232 277
278Done in !16.
279
233The nginx section sits between config keys; `theme` and `language` come 280The nginx section sits between config keys; `theme` and `language` come
234after it. Reorder: config keys, units, reverse proxy. 281after it. Reorder: config keys, units, reverse proxy.
235 282
236### 3.7 README (S, #27) 283### 3.7 README (S, #27)
237 284
285Done in !18.
286
238Add: endpoints and what they do, running the binary without Docker with 287Add: endpoints and what they do, running the binary without Docker with
239the service files, what `REDIRECTS.md` is for (redirector rules), and a 288the service files, what `REDIRECTS.md` is for (redirector rules), and a
240screenshot. 289screenshot.
@@ -243,6 +292,8 @@ screenshot.
243 292
244### 4.1 Viewport and mobile CSS (S, #28) 293### 4.1 Viewport and mobile CSS (S, #28)
245 294
295Done in !19.
296
246`static/html/head.htm` and `index.htm` use `initial-scale=0.4` and 297`static/html/head.htm` and `index.htm` use `initial-scale=0.4` and
247`height=device-height`; `skunky.css` then compensates with 298`height=device-height`; `skunky.css` then compensates with
248`* { font-size: 120% }` in portrait. Use `width=device-width, 299`* { font-size: 120% }` in portrait. Use `width=device-width,
@@ -251,12 +302,16 @@ width before and after.
251 302
252### 4.2 Accessibility (S, #29) 303### 4.2 Accessibility (S, #29)
253 304
305Done in !19.
306
254Listing and avatar images in `DeviationList`, `ParseComments` and 307Listing and avatar images in `DeviationList`, `ParseComments` and
255`BuildUserPlate` have no `alt`. The post page has no heading element for 308`BuildUserPlate` have no `alt`. The post page has no heading element for
256the title. Add both. 309the title. Add both.
257 310
258### 4.3 Index stylesheet (S, #30) 311### 4.3 Index stylesheet (S, #30)
259 312
313Done in !19.
314
260`static/html/index.htm` carries an inline stylesheet duplicating layout 315`static/html/index.htm` carries an inline stylesheet duplicating layout
261rules. Move it into `skunky.css`. 316rules. Move it into `skunky.css`.
262 317
@@ -264,6 +319,8 @@ rules. Move it into `skunky.css`.
264 319
265### 5.1 One canonical forge (S, #31) 320### 5.1 One canonical forge (S, #31)
266 321
322Done in !20.
323
267Origin and issues are on gitbay; releases, the image, Dependabot, the 324Origin and issues are on gitbay; releases, the image, Dependabot, the
268instances.json fetch at `app/util.go:64`, the About page "Report an issue" 325instances.json fetch at `app/util.go:64`, the About page "Report an issue"
269link, the index source link, and the `--add-instance` exit message all 326link, the index source link, and the `--add-instance` exit message all
@@ -274,17 +331,23 @@ README and leave the links.
274 331
275### 5.2 Instance checker (M, issue #4, #32) 332### 5.2 Instance checker (M, issue #4, #32)
276 333
334Done in !21.
335
277A scheduled job that fetches each instance's `/api/instance` and marks dead 336A scheduled job that fetches each instance's `/api/instance` and marks dead
278ones in `INSTANCES.md`, or a CI job that fails when one is down. 337ones in `INSTANCES.md`, or a CI job that fails when one is down.
279 338
280### 5.3 LibRedirect listing (S, #33) 339### 5.3 LibRedirect listing (S, #33)
281 340
341Open. The two upstream pull requests are written up on #33.
342
282`REDIRECTS.md` already describes the URL mapping. Check whether LibRedirect 343`REDIRECTS.md` already describes the URL mapping. Check whether LibRedirect
283lists SkunkyArt with the dead upstream instances and submit the fork and 344lists SkunkyArt with the dead upstream instances and submit the fork and
284art.krz.sh. This is the cheapest way to get users. 345art.krz.sh. This is the cheapest way to get users.
285 346
286### 5.4 Makefile and binary releases (S, issue #5, #34) 347### 5.4 Makefile and binary releases (S, issue #5, #34)
287 348
349Done in !22.
350
288Targets for build with the embed tag and version stamp, test, lint. 351Targets for build with the embed tag and version stamp, test, lint.
289Publish binaries alongside the image on release tags. 352Publish binaries alongside the image on release tags.
290 353
@@ -299,25 +362,17 @@ Publish binaries alongside the image on release tags.
299- #6 emote bug: the `a.Val[8:9] == "e"` and `[37:len-4]` offsets in the 362- #6 emote bug: the `a.Val[8:9] == "e"` and `[37:len-4]` offsets in the
300 HTML branch of `ParseDescription`. Parse the URL instead of slicing. 363 HTML branch of `ParseDescription`. Parse the URL instead of slicing.
301 364
302## Stacked MR order 365## Merge record
303 366
304Each MR branches from the previous one's tip and is merged in order. 367Merged into main in this order on 2026-09-11, each stacked on the one
305 368before: !6 (0.1), !7 (2.1, 2.7), !8 (1.1), !9 (1.2, 2.4), !10 (1.3),
3061. `ci/pipeline` (0.1) 369!11 (1.4), !12 (1.5), !13 (2.2, 2.3, 2.6, 2.8), !15 (2.5), !16 (3.1,
3072. `fix/escape-templates` (2.1 + 2.7) 3703.2, 3.3, 3.6), !17 (3.4), !18 (3.5, 3.7), !19 (4.1, 4.2, 4.3), !20
3083. `feat/api-cache` (1.1, after its spec is approved) 371(5.1), !21 (5.2), !22 (5.4). Then !23 (release string), !24 (Go 1.26 in
3094. `feat/avatar-cache-headers` (1.2) 372the image and binaries builds) and !25 (no VCS stamping) for the
3105. `feat/robots-ratelimit` (1.3) 373release itself.
3116. `feat/fewer-calls` (1.4) 374
3127. `chore/cache-default-on` (1.5) 375Two things the stack taught: lint on macOS never compiles the Linux-only
3138. `fix/small-bugs` (2.2, 2.3, 2.4, 2.6, 2.8; one MR, one commit each) 376files, so run `GOOS=linux golangci-lint run` before pushing; and `go get`
3149. `fix/atom-feed` (2.5) 377can raise the go directive in go.mod, so check the Dockerfile and
31510. `docs/config-and-setup` (3.1, 3.2, 3.3, 3.6) 378workflow images still match it.
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.