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