Commit 13e911dba3
Verified · cmc ci/build: success ci/lint: success ci/test: success
Layout: unified · split
ROADMAP.md +77 −22
| @@ -13,6 +13,13 @@ Existing gitbay issues: #1 cleanup, #2 search filters, #3 description | |||
| 13 | parsing, #4 instance checker, #5 Makefile, #6 emote bug. They are mapped | 13 | parsing, #4 instance checker, #5 Makefile, #6 emote bug. They are mapped |
| 14 | below where they overlap. | 14 | below where they overlap. |
| 15 | 15 | ||
| 16 | ## Status | ||
| 17 | |||
| 18 | Everything below except 5.3 and 5.5 shipped in v1.5.0 (2026-09-11), with | ||
| 19 | v1.5.1 fixing the release build. Merge requests !6 through !25 on gitbay. | ||
| 20 | Still open: #33 (LibRedirect submission), #1, #2, #3 and #6 from the | ||
| 21 | original list, which need real DeviantArt payloads to work from. | ||
| 22 | |||
| 16 | ## Ordering principle | 23 | ## Ordering principle |
| 17 | 24 | ||
| 18 | Frontend proxies get blocked upstream. The existing throttle | 25 | Frontend 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 | ||
| 52 | Done in !6. | ||
| 53 | |||
| 45 | No pipeline runs `go vet`, `go test` or `golangci-lint`. The only workflow | 54 | No pipeline runs `go vet`, `go test` or `golangci-lint`. The only workflow |
| 46 | is the GitHub release build; gitbay has zero builds. `.golangci.yml` exists | 55 | is the GitHub release build; gitbay has zero builds. `.golangci.yml` exists |
| 47 | but is never run. | 56 | but 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 | ||
| 69 | Done in !8. | ||
| 70 | |||
| 60 | Add an in-memory cache in front of every devianter call: DD, search, | 71 | Add an in-memory cache in front of every devianter call: DD, search, |
| 61 | deviation, gruser, gallery, favourites, comments. Keyed by endpoint plus | 72 | deviation, gruser, gallery, favourites, comments. Keyed by endpoint plus |
| 62 | arguments. Bounded by entry count or bytes. Singleflight so concurrent | 73 | arguments. 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 | ||
| 94 | Done 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 |
| 84 | e.deviantart.net on every request and never stores the result. Route it | 97 | e.deviantart.net on every request and never stores the result. Route it |
| 85 | through the same disk and memory cache path as `DownloadAndSendMedia`. | 98 | through 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 | ||
| 112 | Done in !10. | ||
| 113 | |||
| 99 | Serve `/robots.txt` disallowing `/search`, `/api`, `/group_user`, | 114 | Serve `/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 |
| 101 | the upstream throttle so one crawler cannot consume the whole DA budget and | 116 | the 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 | ||
| 127 | Done in !11. | ||
| 128 | |||
| 112 | Post view is two API calls because comments are fetched inline. Move | 129 | Post view is two API calls because comments are fetched inline. Move |
| 113 | comments behind a link (`/post/{author}/{name}/comments` or `?comments=1`) | 130 | comments behind a link (`/post/{author}/{name}/comments` or `?comments=1`) |
| 114 | so the default post view is one call. Same for the user about page. | 131 | so 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 | ||
| 143 | Done in !12. | ||
| 144 | |||
| 126 | A proxying instance with no cache re-fetches every image from wixmp on every | 145 | A proxying instance with no cache re-fetches every image from wixmp on every |
| 127 | view. Set `cache.enabled: true` in the built-in defaults in `app/config.go` | 146 | view. Set `cache.enabled: true` in the built-in defaults in `app/config.go` |
| 128 | and in `config.example.json`, with a sane `lifetime` and `max-size`. Keep | 147 | and 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 | ||
| 156 | Done 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 |
| 138 | search query in `static/html/search.htm` and `head.htm`, and every DA | 159 | search query in `static/html/search.htm` and `head.htm`, and every DA |
| 139 | username, title and description written by `DeviationList`, | 160 | username, 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 | ||
| 174 | Done 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 |
| 154 | 048bb47. Registration date, interests, social links and bio never render for | 177 | 048bb47. Registration date, interests, social links and bio never render for |
| 155 | users. Find out why it was disabled (likely a devianter struct change), | 178 | users. 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 | ||
| 183 | Done 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 |
| 161 | page two starts at result 20 and results 10 to 19 are never shown. The nav | 186 | page two starts at result 20 and results 10 to 19 are never shown. The nav |
| 162 | bar also shows the incremented number. Use `10*(page-1)` and do not mutate | 187 | bar 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 | ||
| 192 | Done 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 | ||
| 198 | Done 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 |
| 173 | 3339, and `media:thumbinal`. Verified on the live feed. Fix all five and add | 202 | 3339, 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 | ||
| 208 | Done 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 | ||
| 215 | Done 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 |
| 185 | CloudFront block page, into an `<h3>` unescaped. Truncate to one line and | 218 | CloudFront block page, into an `<h3>` unescaped. Truncate to one line and |
| 186 | escape. Folds into 2.1 if done together. | 219 | escape. 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 | ||
| 223 | Done in !13. | ||
| 224 | |||
| 190 | `ExecuteTemplate` calls `ParseFS` on every request. Parse at startup; | 225 | `ExecuteTemplate` calls `ParseFS` on every request. Parse at startup; |
| 191 | supply the per-request `T` function through the data struct or a per-request | 226 | supply 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 | ||
| 233 | Done 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 |
| 199 | exist. Start with defaults when no `-c` is given and the default file is | 236 | exist. Start with defaults when no `-c` is given and the default file is |
| 200 | absent. Align the built-in `nsfw: true` with the example's `false`, or | 237 | absent. 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 | ||
| 242 | Done 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); |
| 206 | the `d` unit works but is unlisted; `y` is 360 days; exceeding `max-size` | 245 | the `d` unit works but is unlisted; `y` is 360 days; exceeding `max-size` |
| 207 | deletes the whole cache directory; `lifetime: null` in the example. | 246 | deletes 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 | ||
| 250 | Done 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 |
| 213 | favourites. Fix the doc and rename or remove the option. | 254 | favourites. 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 | ||
| 258 | Done in !17. | ||
| 259 | |||
| 217 | Go-built HTML hardcodes English: comment headers, "In reply to", | 260 | Go-built HTML hardcodes English: comment headers, "In reply to", |
| 218 | pagination, folder and content headings, "No results", "[ TEXT ]". | 261 | pagination, 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 | ||
| 269 | Done 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), |
| 227 | placeholder paths, and says it was never tested. Write a working unit with | 272 | placeholder 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 | ||
| 278 | Done in !16. | ||
| 279 | |||
| 233 | The nginx section sits between config keys; `theme` and `language` come | 280 | The nginx section sits between config keys; `theme` and `language` come |
| 234 | after it. Reorder: config keys, units, reverse proxy. | 281 | after it. Reorder: config keys, units, reverse proxy. |
| 235 | 282 | ||
| 236 | ### 3.7 README (S, #27) | 283 | ### 3.7 README (S, #27) |
| 237 | 284 | ||
| 285 | Done in !18. | ||
| 286 | |||
| 238 | Add: endpoints and what they do, running the binary without Docker with | 287 | Add: endpoints and what they do, running the binary without Docker with |
| 239 | the service files, what `REDIRECTS.md` is for (redirector rules), and a | 288 | the service files, what `REDIRECTS.md` is for (redirector rules), and a |
| 240 | screenshot. | 289 | screenshot. |
| @@ -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 | ||
| 295 | Done 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 | ||
| 305 | Done in !19. | ||
| 306 | |||
| 254 | Listing and avatar images in `DeviationList`, `ParseComments` and | 307 | Listing 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 |
| 256 | the title. Add both. | 309 | the title. Add both. |
| 257 | 310 | ||
| 258 | ### 4.3 Index stylesheet (S, #30) | 311 | ### 4.3 Index stylesheet (S, #30) |
| 259 | 312 | ||
| 313 | Done 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 |
| 261 | rules. Move it into `skunky.css`. | 316 | rules. 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 | ||
| 322 | Done in !20. | ||
| 323 | |||
| 267 | Origin and issues are on gitbay; releases, the image, Dependabot, the | 324 | Origin and issues are on gitbay; releases, the image, Dependabot, the |
| 268 | instances.json fetch at `app/util.go:64`, the About page "Report an issue" | 325 | instances.json fetch at `app/util.go:64`, the About page "Report an issue" |
| 269 | link, the index source link, and the `--add-instance` exit message all | 326 | link, 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 | ||
| 334 | Done in !21. | ||
| 335 | |||
| 277 | A scheduled job that fetches each instance's `/api/instance` and marks dead | 336 | A scheduled job that fetches each instance's `/api/instance` and marks dead |
| 278 | ones in `INSTANCES.md`, or a CI job that fails when one is down. | 337 | ones 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 | ||
| 341 | Open. 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 |
| 283 | lists SkunkyArt with the dead upstream instances and submit the fork and | 344 | lists SkunkyArt with the dead upstream instances and submit the fork and |
| 284 | art.krz.sh. This is the cheapest way to get users. | 345 | art.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 | ||
| 349 | Done in !22. | ||
| 350 | |||
| 288 | Targets for build with the embed tag and version stamp, test, lint. | 351 | Targets for build with the embed tag and version stamp, test, lint. |
| 289 | Publish binaries alongside the image on release tags. | 352 | Publish 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 | ||
| 304 | Each MR branches from the previous one's tip and is merged in order. | 367 | Merged into main in this order on 2026-09-11, each stacked on the one |
| 305 | 368 | before: !6 (0.1), !7 (2.1, 2.7), !8 (1.1), !9 (1.2, 2.4), !10 (1.3), | |
| 306 | 1. `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, |
| 307 | 2. `fix/escape-templates` (2.1 + 2.7) | 370 | 3.2, 3.3, 3.6), !17 (3.4), !18 (3.5, 3.7), !19 (4.1, 4.2, 4.3), !20 |
| 308 | 3. `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 |
| 309 | 4. `feat/avatar-cache-headers` (1.2) | 372 | the image and binaries builds) and !25 (no VCS stamping) for the |
| 310 | 5. `feat/robots-ratelimit` (1.3) | 373 | release itself. |
| 311 | 6. `feat/fewer-calls` (1.4) | 374 | |
| 312 | 7. `chore/cache-default-on` (1.5) | 375 | Two things the stack taught: lint on macOS never compiles the Linux-only |
| 313 | 8. `fix/small-bugs` (2.2, 2.3, 2.4, 2.6, 2.8; one MR, one commit each) | 376 | files, so run `GOOS=linux golangci-lint run` before pushing; and `go get` |
| 314 | 9. `fix/atom-feed` (2.5) | 377 | can raise the go directive in go.mod, so check the Dockerfile and |
| 315 | 10. `docs/config-and-setup` (3.1, 3.2, 3.3, 3.6) | 378 | workflow images still match it. |
| 316 | 11. `feat/i18n-coverage` (3.4) | ||
| 317 | 12. `chore/services-readme` (3.5, 3.7) | ||
| 318 | 13. `ui/viewport-a11y` (4.1, 4.2, 4.3) | ||
| 319 | 14. `chore/canonical-forge` (5.1) | ||
| 320 | 15. 5.2 through 5.5 as independent MRs off `main` | ||
| 321 | |||
| 322 | Items 8 through 15 do not depend on the cache stack and can be reordered or | ||
| 323 | interleaved when the cache work stalls on design. | ||