Commit Graph
47 Commits
Author SHA1 Message Date
puttaalu 125b6b5241 README: note Android manage screen and tidy app descriptions
The Android app now edits/deletes posts, and the web upload is bulk; reflect
the Android manage capability in the Features and Apps sections.
2026-10-18 23:34:09 +02:00
puttaalu 003a4057bf Auto-suffix duplicate filenames in web bulk upload
The server derives a photo's slug from the uploaded filename, so two photos with
the same name in one batch would overwrite each other. Each card now gets a slug
that is unique among the current cards (second same-named photo becomes -2, -3,
…), computed with a client mirror of the server's sanitiseSlug and sent as the
multipart filename. The slug is assigned once when a photo is added, so it stays
stable across retries.

Kept client-side on purpose: a server-side "slug exists -> suffix" would turn
the apps' idempotent re-upload retries (same filename overwrites the same post)
into duplicates.
2026-10-18 23:31:05 +02:00
puttaalu c21ddc5b7e Add bulk (multi-photo) upload to the web form
The /upload page now takes several photos at once, mirroring the Android and Qt
apps: a card per photo with its own preview, EXIF-prefilled date, caption and
tag-autocompleting tags field, a remove button, and per-photo status. "Post
photos" uploads them sequentially with an x/N progress line; failures can be
retried by pressing Post again, successes link straight to the new photo page.

No server change — each photo is still a separate POST /upload, exactly as the
apps' per-photo queue does it, so slugs come from filenames as before.
2026-10-18 23:29:04 +02:00
puttaalu 225bdc9b4f Add an edit/delete manage screen to the Android app
A new Manage screen (reached from the Post screen) lists published posts with
thumbnails and lets you edit a post's caption, date and tags or delete it to the
server's trash, over the same /api/photos, /api/edit and /api/delete endpoints
the web /manage uses.

UploadClient gains listPhotos/editPost/deletePost/fetchThumbnail, parsing JSON
with the built-in org.json (no new dependency) and posting edits/deletes as
urlencoded forms. The app sends no Origin or Sec-Fetch-Site header, so it passes
the server's same-origin CSRF check as a non-browser client; Basic auth is
already attached on every request. UploadViewModel holds the manage state,
loads the list, fetches thumbnails in the background, and applies edits/deletes
optimistically.
2026-10-18 23:23:39 +02:00
puttaalu e67d25b191 Update README for current features
Bring the README in line with the gallery as it stands: add a Features section
(views and default-view, albums/tags, day pages, link previews, manage/trash,
tag autocomplete, app-side auth, the signed-in indicator, and the Android and
Qt desktop apps). Correct the auth story (bcrypt htpasswd in the app, nginx
only terminates TLS and proxies), move htpasswdFile to the top-level module
option, document defaultView/pageSize/tags, and add an Apps section.
2026-10-18 23:16:26 +02:00
puttaalu 94859fb0d6 Show management links and a signed-in indicator when logged in
Add GET /api/session, an unguarded probe that reports whether the request
carries valid credentials. It returns 200 with {"authenticated":bool} and never
sends WWW-Authenticate, so the static public pages can check login state without
triggering the browser's Basic-auth dialog. To keep it from being a
password-guessing oracle that bypasses the /upload limiter, a request with no
credentials is answered cheaply as false, while wrong credentials go through the
same failed-attempt limiter (429 once drained).

static/session.js, loaded by every page template, calls it and, only when
authenticated, builds a fixed toolbar: Upload / Manage links, an "Edit this"
link on photo pages (via <body data-slug>, deep-linking to /manage?edit=<slug>,
which scrolls to and focuses that row), a signed-in indicator, and a log-out
control. Logged-out visitors see nothing and get no dialog.

Log out is best-effort: HTTP Basic has no reliable sign-out (the browser caches
credentials until the window closes), so it overwrites the cached credential and
reloads, which works in some browsers but not all. A dependable logout would
need cookie sessions.
2026-10-18 22:52:59 +02:00
puttaalu c4aa345914 Autocomplete existing tags in the upload and manage forms
A small shared widget (static/tagcomplete.js) completes the tag after the last
comma from the existing tag list, keeping earlier tags intact. It is wired to
every comma-separated tag input: the upload form, and the manage page's per-row
edit field and bulk add/remove fields.

Suggestions come from /tags.json, which the generator already writes on every
build and nginx serves statically, so no server change is needed; the list is
fetched once, cached, and invalidated after an edit that may add a tag. Keyboard
navigation (up/down, enter/tab, escape) and click selection are supported.
2026-10-18 22:39:13 +02:00
puttaalu bf75c9fe8e Fix manage handlers ignoring multipart form bodies
The /manage page posts with FormData, i.e. multipart/form-data, but the edit,
delete, tags, restore and empty-trash handlers parsed with r.ParseForm, which
only reads urlencoded bodies. So every field came back empty: delete returned
200 with deleted:0 and moved nothing, edits saved nothing — while curl -d
(urlencoded) worked, which is why tests and manual checks missed it.

Parse with ParseMultipartForm (tolerating ErrNotMultipart so urlencoded still
works), via a shared parseForm helper used by all manage handlers. handleUpload
already did this. Also stop the UI reporting deleted:0 / updated:0 as success,
and add a multipart regression test.
2026-10-18 22:32:18 +02:00
puttaalu 25384d3f3b Rebuild on file renames so deletes take effect
The watcher only reacted to Create/Write/Remove. A /manage delete moves the
image and sidecar into content/.trash/, which on Linux arrives as a Rename
(inotify IN_MOVED_FROM), not a Remove — so the watcher never fired and the
delete left the published site unchanged (uploads worked because they Create
and Write). Watch Rename too, which also covers removing a photo with a manual
mv. The delete handler still triggers its own immediate rebuild; this makes the
watcher path agree with it.
2026-10-18 22:19:09 +02:00
puttaalu 8fa1aa4cd4 Add -default-view to choose timeline or grid as the site root
The view named by -default-view (timeline, the default, or grid) is published
at the site root; the other is served under /timeline/ or /grid/. viewBase and
pageURL derive every page path, pager link and the Timeline<->Grid cross-link
from the single setting, and Build removes both timeline/ and grid/ on each run
so flipping the default never leaves a stale root behind.

Reimplemented on trunk (which has albums and the /manage feature) rather than
cherry-picked, since the original was written against an earlier base. Credit
for the edit/delete and default-view ideas goes to Santhosh Raju, now noted in
a README Contributors section.

- main.go: -default-view flag, validated (fatal on anything but timeline/grid)
- module.nix: services.photogallery.defaultView enum option
- templates: timeline/grid/day/album/albums cross-links use TimelineURL/GridURL
- tests: timeline-root, grid-root, and switch-cleanup
2026-10-18 22:06:46 +02:00
puttaalu a7f2afc3ad Add a /manage page to edit, delete and restore posts
A new auth-gated web UI at /manage lists every post for inline editing of
caption, date and tags, with per-row and bulk delete and bulk tag add/remove.
It is backed by a small JSON API (all behind the same htpasswd Basic auth as
/upload, and unlinked from the public site):

  GET  /api/photos        list posts (newest first)
  POST /api/edit          rewrite one sidecar (caption, date, tags)
  POST /api/tags          bulk add/remove tags across posts
  POST /api/delete        move posts to content/.trash/ (reversible)
  GET  /api/trash         list trashed items
  POST /api/restore       move a trashed item back (409 on live-slug clash)
  POST /api/trash/empty   hard delete trashed items (id… or all=1)

Design notes:
- Editing only rewrites the .toml; the slug (and URL) never changes.
- Delete is reversible: image + sidecar move to content/.trash/ with a
  timestamp suffix, excluded from the site (loadPhotos skips dirs). The only
  hard delete is emptying the trash.
- Build now prunes: it drops the derived HTML trees (photo/ day/ album/ page/
  grid/page/) and regenerates them, and pruneRenditions removes orphaned
  images/thumbs/medium/og. Without this a deleted post would linger at its old
  URL and on stale day/album pages; this also cleans up after a hand-removed
  sidecar.
- Sidecars are written atomically (temp + rename) so a concurrent build never
  observes a half-written file.
- Slugs and trash ids are validated as bare filenames, closing path traversal
  and dotfile access.
- Every state-changing POST passes a sameOrigin (CSRF) check, since Basic-auth
  credentials are replayed by the browser on every request to the host. It
  honours Sec-Fetch-Site, falls back to Origin vs base-url/Host, and allows
  header-less non-browser clients. Ported from the independent /manage on
  plex/trunk-ng.

module.nix proxies /manage and /api/ to the app (nginx otherwise serves them
as missing static files). The apps are unchanged; the API is there for them to
adopt edit/delete later.
2026-10-18 21:52:11 +02:00
puttaalu 434d2437bf Move upload auth from nginx into the app
Add HTTP Basic auth on /upload, reading an Apache htpasswd file (bcrypt
only, `htpasswd -B`) via a new -htpasswd flag. This lets nginx drop to a
plain TLS-terminating reverse proxy with no auth of its own; the existing
bcrypt htpasswd file is reused unchanged.

auth.go:
- LoadHtpasswd parses user:hash lines, keeps bcrypt entries ($2a/$2b/$2y,
  so Apache's $2y$ works), skips MD5/SHA/crypt with a warning.
- Unknown users still pay for one bcrypt comparison against a dummy hash,
  so response timing can't enumerate valid usernames.
- Failed attempts draw from a token-bucket limiter (x/time/rate, ~1/s,
  burst 10) returning 429 once drained; successful and bulk uploads never
  touch it.

server.go guards both /upload routes (nil authenticator = passthrough for
local --serve). /health stays open. Empty -htpasswd disables auth and logs
a warning.

module.nix: new top-level services.photogallery.htpasswdFile, delivered to
the service via systemd LoadCredential (so a root-owned 0400 secret works).
Removed nginx.htpasswdFile and basic_auth from the vhost.

Both uploader apps already send Basic credentials, so no client change.

Deps: golang.org/x/crypto/bcrypt, golang.org/x/time/rate (vendorHash bump).
2026-10-18 14:45:33 +02:00
puttaalu 3291ed67d5 shorten the text for RSS feed link 2026-10-18 13:35:10 +02:00
puttaalu faa1e01c59 Add a tags field to the Android and desktop uploaders
Each photo card gains a free-text tags input, sent as a raw
comma-separated `tags` form field on upload. The server normalizes it
(parseTags), so the clients stay dumb — no tag parsing on-device.

- Android: PickedItem.tags + setTags; threaded through UploadQueue →
  UploadWorker → UploadClient's multipart.
- Desktop: a tags QLineEdit per PhotoItemWidget; UploadClient.upload sends
  it.
2026-10-08 16:19:25 +02:00
puttaalu a42abfab48 Add albums: group photos by free-text tags
An album is a tag. Photos carry lowercase free-text tags in their .toml
sidecar (`tags = [...]`), parallel to the existing day grouping — no
database, all derived at build time.

- albums.go: parseTags (lowercase/trim/dedupe, slug via sanitiseSlug),
  groupByTag mirroring groupByDay, and rendering for album pages, the
  index, and the JSON API. Slug collisions between distinct names are
  suffixed and logged.
- Routes: /album/<slug>/ (paginated square grid per tag), /albums/ (index
  with counts), /tags.json (machine-readable tag list, served statically
  like feed.xml).
- POST /upload gains a `tags` field written into the sidecar; the web
  upload form gains a tags input.
- Tags shown on each photo page linking to their album; an Albums link in
  the timeline/grid header.
2026-10-08 16:14:09 +02:00
puttaalu f33bdd55b1 Relicense to the public domain (Unlicense)
Release the project into the public domain instead of MIT.
2026-09-26 14:06:20 +02:00
puttaalu 91fbf4f93b Name the desktop binary photog
The CMake target stays 'uploader' but the output binary is now 'photog',
matching the package and app, so 'nix run' and the desktop entry resolve
it (nix run looked for bin/photog and failed on bin/uploader). Also set
meta.mainProgram.
2026-09-26 10:51:39 +02:00
puttaalu 17222f7120 Make the desktop flake build on macOS
Wayland is Linux-only and refuses to evaluate on darwin, so qtwayland is
now added only on Linux (macOS uses the Cocoa QPA plugin in qtbase). The
package, dev shell, and plugin search path all build from one platform-
guarded module list. Evaluates on aarch64-darwin; Linux is unchanged.
2026-09-26 10:44:23 +02:00
puttaalu 591eaed6a3 Add app icons across web, Android, and the desktop app
- Web: favicon.ico + png icons under static/, served from the site root
  and linked from every page head.
- Android: the artwork becomes the launcher icon — the 432px adaptive
  foreground over the gruvbox background, replacing the placeholder vector
  drawables.
- Desktop: the icon is the Qt window/taskbar icon (embedded via app.qrc),
  and the app is now an installable package — a wrapped binary, a
  photog.desktop entry, and hicolor icons — so `nix profile install
  ./desktop` puts "Photog" in the application menu.
2026-09-26 10:33:22 +02:00
puttaalu a7783671f2 Add bulk multi-photo upload to the desktop app
Mirror the Android bulk feature in the Qt uploader: choose several photos
at once, each a card with its own caption, date, and upload status, and
post the batch.

- Per-photo cards (PhotoItemWidget) in a scrollable list; retry re-posts
  only the failures.
- One upload per photo with a per-item UUID slug suffix so a batch posted
  in the same second cannot collide.
- Image prep runs off the UI thread via QtConcurrent so the window stays
  responsive.
- A "Simultaneous uploads" setting (1–8, default 3) caps how many photos
  are prepared/uploaded at once; the rest queue and fill freed slots.
2026-09-26 10:17:18 +02:00
puttaalu 1d81ec4511 Add a source-code link to the gallery header
Next to the RSS feed link on the timeline and grid headers, pointing at
the public repo so visitors can find the source.
2026-09-26 10:17:18 +02:00
puttaalu dc47619937 Rename the Android app to Photog 2026-09-26 10:17:18 +02:00
puttaalu c404aac66c add icons 2026-09-26 10:16:17 +02:00
puttaalu 23e14f5aa8 Merge desktop-qt-uploader: Qt6 desktop uploader 2026-08-27 08:44:35 +02:00
puttaalu 6af0eb115c Add multi-photo bulk upload to the Android app
Pick or share several photos at once and post them in one batch, still
against the existing single-image /upload endpoint — no server change.

- Unify the post screen on a list model (PickedItem): a single-photo post
  or share is just a batch of one, so there is one code path.
- Multi-select via PickMultipleVisualMedia and multi-share via
  ACTION_SEND_MULTIPLE (manifest filter added).
- Per-photo caption and EXIF-prefilled date; each card shows its own
  upload status, with a summary and retry-only-failures on re-post.
- Enqueue one WorkManager worker per photo, reusing the durable retry
  queue. UploadQueue now appends a per-item suffix to the slug so a burst
  enqueued in the same second cannot collide and be dropped by
  enqueueUniqueWork.
2026-08-27 08:43:24 +02:00
puttaalu d07e3eb0dc Add Qt6 desktop uploader
A native Qt6/C++ desktop client for the gallery's POST /upload API, in
desktop/ with its own flake (kept separate from the Go server module the
same way android/ is). Single-photo parity with the Android app:

- multipart upload with preemptive HTTP Basic auth and retry-aware outcome
  classification (uploadclient)
- JPEG/PNG metadata stripped byte-for-byte to drop embedded GPS, EXIF
  orientation baked in, and a quality/scale valve for the 20M nginx cap;
  HEIC decodes via the qtimageformats plugin (imageprep)
- base URL / username in QSettings, password in the system keychain via
  qtkeychain; https required and a trailing /upload stripped (settings)
- settings and post screens in a QStackedWidget (mainwindow)

Native Wayland or xcb is selected at runtime. Bulk upload, EXIF
capture-date prefill, and moving image prep off the UI thread are left as
follow-ups.
2026-08-26 00:28:35 +02:00
puttaalu d4fec5b0ff Add Open Graph link previews to photo pages
Shared links now unfurl with an image in WhatsApp, Telegram and anything
else that reads Open Graph, instead of showing the bare URL.

A dedicated og/ rendition is generated per photo: 1200px longest side,
quality laddered down until it fits under ~250 KB. WhatsApp drops the
preview entirely once the image passes ~300 KB, and the medium rendition
can exceed that, so it is not reused. Dimensions for og:image:width and
og:image:height are read back from the encoded file with DecodeConfig, so
they are emitted even when the rendition itself was skipped as up to date.

The og:image and og:url values are absolute via BaseURL; both crawlers
ignore relative image paths. Previews therefore only work against the
deployed base URL, not http://localhost.
2026-08-24 13:59:49 +02:00
puttaalu eb6dba6eb0 Add single-day pages linked from timeline date headings
Each timeline date heading now links to /day/<YYYY-MM-DD>/, a page
holding just that day's photos with a back link to the timeline.
renderDayPages writes one per groupByDay run; the ISO label doubles
as the URL slug.
2026-08-20 00:13:49 +02:00
puttaalu 4ddf5adf24 Make the timeline the default view, paginated and configurable
The day-grouped timeline is now the site root (/, /page/N/); the flat
square grid moves to /grid/ (/grid/page/N/). Both are linked from the
profile header, separated by a divider.

Timeline pages pack whole day-groups until they reach the page size,
never splitting a day across a page boundary, so appended pages never
repeat a date heading — the infinite-scroll handler relies on this.

Page size is configurable via -page-size (default 60), threaded through
the Generator and exposed as services.photogallery.pageSize in the
NixOS module.
2026-08-20 00:11:08 +02:00
puttaalu 9b6fcb482b Add a timeline page grouping photos by day
New /timeline/ page renders photos grouped into consecutive same-day
runs (ISO date heading plus per-day count), reusing the square grid.
groupByDay collapses the newest-first photo list; the page is not
paginated. Linked from the profile header.
2026-08-19 23:46:28 +02:00
puttaalu c169c030d6 Update CLAUDE.md for pagination, medium images, swipe nav and the Android app
Also corrects the serif font, which is Crimson Pro rather than Vollkorn,
and notes that .gitignore patterns here have to be anchored or they eat
the Kotlin package directory.
2026-08-19 23:36:14 +02:00
puttaalu 15822ed227 Add swipe navigation to the photo detail page
Swipe left for older, right for newer, matching the existing arrow keys.
The image tracks the finger and slides out on commit; the exit animation
covers the page load, so this stays plain static-site navigation.

The keyboard handler moves out of the template into static/photo-nav.js,
which the existing //go:embed of static/ picks up with no generator
changes.

Swipes starting within 24px of a screen edge are left alone — that strip
is the browser's own back-gesture. Only touch pointers are handled, so
desktop drag-to-save still works, and the axis locks after 10px so
vertical drags stay scrolls. With no neighbour that way the image
rubber-bands instead of navigating.

Also prefetch the neighbour medium JPEGs, not just their HTML, or the
next page still blocks on a fresh image request.

The keyboard handler now bails on modifier keys; it previously hijacked
Alt+ArrowLeft, which is browser-back on Linux and Windows.
2026-08-19 23:24:42 +02:00
puttaalu 210ce69e18 Add an Android uploader app
Native single-photo uploader for POST /upload. Lives in android/ with its
own flake, so the root flake stays Go-only and NixOS consumers of
nixosModules.default do not pull the Android SDK into their lock.

The server is untouched, so the app closes every gap client side:

- The slug is the enqueue-time timestamp, persisted in the work input.
  server.go derives the stored slug from the uploaded filename, so reusing
  that filename across WorkManager retries overwrites the same post rather
  than creating a duplicate. That stands in for an idempotency key.
- EXIF is stripped before upload. The gallery publishes the uploaded file
  verbatim as its full-size download, and phone photos carry GPS. JPEG and
  PNG drop metadata at marker and chunk level with the compressed pixels
  untouched; HEIC is decoded to JPEG because the extension check rejects it.
- Photos flagged for rotation are rotated into the pixels instead of relying
  on the orientation tag, which cannot survive the strip and which the
  thumbnailer ignores regardless.
- Anything over 18 MiB steps quality, then resolution, to stay inside
  nginx's client_max_body_size 20M.
- Credentials are sealed with an AES-GCM key in the Android Keystore. The
  key deliberately does not require user authentication, or background
  retries could not read it. Basic auth is attached by an interceptor
  rather than an Authenticator, which reacts to a 401 by replaying a
  multipart body that is not reliably replayable.
- The connection test uses GET /upload. /health is unreachable from outside
  because the nginx vhost only proxies location /upload.

WorkManager's SystemForegroundService needs foregroundServiceType declared
on the service entry, not just as a permission, or setForeground throws on
Android 14+. Only lintVitalRelease catches that, never a debug build.

Also anchors the .gitignore patterns. Unanchored, "photogallery" matched the
Kotlin package directory ws/inflo/photogallery/ and silently swallowed every
source file on git add.
2026-08-13 22:01:20 +02:00
puttaalu 70d115e11c Split the photo nav so Back sits alone on the left
Move the Newer and Older links to a right-aligned group joined by a
slash so they stop crowding the Back link, and drop the aqua accent so
every link in the row shares the muted serif tone.
2026-07-03 23:11:11 +02:00
puttaalu 3639bd5577 Add newer/older navigation to the photo detail page
Replace the standalone Back link with a three-cell nav row so each
photo page links to its neighbours in the timeline. Prefetch the
neighbouring pages and bind the arrow keys for keyboard navigation.
2026-07-03 23:03:54 +02:00
puttaalu 856439f3fd Paginate the gallery grid with 60 photos per page
Emit /page/N/ pages alongside index.html and render a pager at the
bottom of each page. Ships as a plain link that navigates without JS,
and an IntersectionObserver enhancement that fetches the next page and
appends tiles for an infinite-scroll feel.
2026-07-02 22:20:28 +02:00
puttaalu 613906cd0c Colour the profile name and handle with Gruvbox green and blue 2026-07-02 11:38:56 +02:00
puttaalu 51b082ccf4 Make profile identity, bio and fonts configurable
The profile header, photo page byline and copyright footer previously
hardcoded the author name, handle, bio, and font choices. Move all of
these to CLI flags on the binary and matching options on the NixOS
module so the same build can serve any single-user gallery.

New surface, all with defaults matching the previous hardcoded values:
  --author       / services.photogallery.author
  --handle       / services.photogallery.handle
  --bio          / services.photogallery.bio
  --bio-alt      / services.photogallery.bioAlt   ("" to omit)
  --fonts-url    / services.photogallery.fontsURL
  --serif-family / services.photogallery.serifFamily
  --ml-family    / services.photogallery.mlFamily

Templates read the two font families from a small inline <style> that
overrides the :root variables, so the stylesheet stays fully static and
the font URL is loaded via the configured stylesheet link.

README updated to describe the new options, the medium/full-size photo
detail flow, and the EXIF-based date prefill on the upload page.
2026-07-02 02:30:35 +02:00
puttaalu ec42a65c5b Add copyright footer to gallery and photo pages
Renders a © 2000–<current-year> Ashik Salahudeen line under both the
index and each photo page. The year comes from time.Now().Year() at
render time so the range stays current without manual updates.
2026-07-02 02:18:32 +02:00
puttaalu b26bef0296 Allow reselecting the upload photo and prefill date from EXIF
Once a file was picked the preview <img> overlaid the file input and
blocked further clicks, so the drop zone became read-only. Making the
preview and the new 'Click to change' hint pointer-events:none lets
clicks pass through to the input again.

Also parses the JPEG APP1 EXIF segment inline (no dependency) to read
DateTimeOriginal and prefill the date field. Falls back to the file's
lastModified for PNGs or JPEGs without EXIF. The date field stays
editable in either case.
2026-07-02 02:09:54 +02:00
puttaalu fcedf7e06a Switch English serif from Vollkorn to Crimson Pro
Vollkorn read as too chunky next to the Malayalam bio in Manjari.
Crimson Pro is a calmer book-face serif that pairs more evenly.

Renames the CSS variable from --vk to --serif to reflect that the
value is no longer Vollkorn-specific, and bumps .ml to 17px/1.5
so the Malayalam subtitle sits at a comfortable reading size.
2026-07-02 02:02:12 +02:00
puttaalu cd19795e9e Break line before Malayalam bio subtitle 2026-07-02 01:56:58 +02:00
puttaalu d269c8b1b4 Aspect-preserved detail image with full-size download
Photo detail page previously showed a square crop of the source image. Now
generate an aspect-preserved medium JPEG (longest side 1600, quality 88)
and display that on the detail page instead. The original file is linked
from a click-through on the image and from a sidebar 'Full size' download
link.

Also skip regeneration of images/thumbs/medium when the destination is
already at least as new as the source, so a rebuild triggered by editing
one sidecar (or restarting the service) no longer redecodes and rescales
every photo.
2026-07-02 01:53:23 +02:00
puttaalu 7f7a51ceb5 Raise nginx upload size limit to 20M
Default client_max_body_size of 1M caused 413s on typical photo uploads.
2026-07-02 01:39:51 +02:00
puttaalu c51539cbb4 fix: change username 2026-07-02 01:26:06 +02:00
puttaalu 3a8a7ab429 Add MIT license 2026-07-02 00:57:49 +02:00
puttaalu fd2828f1e7 Initial commit 2026-07-02 00:53:17 +02:00