🐾 Handlr ?

Guide β€Ί Playing

Photos, stamps and recap cards

The gallery, the five stamped-overlay layouts, the session recap card, what the overlay says (and when it says it), and what is stripped on the way out.

Every photo you put into Handlr β€” a check-in, a verification shot for training β€” lands in one content-addressed store under your data folder and shows in the Gallery. Voice memos live in the same store but are not gallery tiles: audio has no pixels.

/gallery shows every photo, newest first, with the task it verified if it was one. Each tile has a stamp-layout picker, a download of the stamped version, a πŸ“€ share button and a 🧳 toggle (below).

Stamped photos

A stamped photo is your picture with an overlay of stats, re-encoded as a fresh JPEG. Five layouts, plus auto, which picks by aspect ratio:

Layout Name in the app Shape
bar Hairline one thin line of facts along an edge
hero Collar a bold band with the headline number
badge Tag a compact corner tag
dossier Case File a grid of labelled values
rail Strip a footer strip, suits wide photos

Sizes: 1080, 1600 (default) or 2048 px on the long edge. The default layout and which stats appear are yours to set β€” see Customise β†’ Stamps for the full token list (streak, day N, time locked, tasks per week, rank, and about forty others).

The overlay is as-of the shutter

Every value on a stamped photo is computed from the log at the photo's timestamp: the clock as it stood, the streak as it was, the training count up to that moment. A photo from three weeks ago does not wear today's streak. One deliberate exception: an event undone after the photo was taken still appears on that photo's overlay β€” otherwise every undo would silently recaption every picture already in your gallery.

EXIF and GPS are stripped by construction. The stamped file is rendered from pixels and saved as a new JPEG; there is no metadata to copy. The original stays in your store untouched.

Hidden timer: while a hidden lock is running, no overlay prints the sentence or the time remaining β€” either one plus the start time would give the end away. Once the lock ends, they return.

Recap cards

Every finished (or in-progress) session has a recap card at /session/<id>/recap.jpg, linked from the Last lock card and the Sessions list. Two shapes: story (1080 Γ— 1920, for a phone screen) and square. The card shows the verdict (clean / early / in progress), the headline time, a grid of stats, your rank and level, and a strip of the session's photos.

Which stats sit in the grid is a token list (Settings β†’ Stamps β†’ Session recap card). The session's private note is not printed unless you add ?note=1 to the URL β€” private prose on a share object has to be opt-in.

Share cards

A recap card is about one finished lock. A share card is whatever you point it at β€” at Share in the nav, or /share.

Pick a kind, a layout, a theme, a shape, and optionally a photo; then download it or hand it to the share sheet, with the post text already written. Every choice lives in the link, so the picture you are looking at, the file you download and the words that go with it are the same card at the same moment β€” the page pins its clock into all three.

A bare Share page opens on your β˜… default preset, or on the built-in Daily starter (the Dense layout) when you have none. A link anybody made before 0.14 still means what it says and draws the card it drew β€” the same pixels outside an event, save where 0.13 cut a value short or the card's moment fell inside a lock that ended later; during an event the stamp is added.

On a phone the page is built for one hand: the preview and the πŸ“€ Share button stay pinned at the top while the panels β€” Stats, Post, Look, Photo β€” sit underneath, and a tap on a stat, a theme or a hashtag changes the card in place, without reloading and without the screen jumping. The address bar follows along, so a reload or a bookmark lands on the same card. With JavaScript off every control is still a plain link that lands back on its own panel.

Three kinds

Is about Its clock
Right now the live state β€” locked or not, how long, the streak this moment
The last stretch a span: 7, 14, 30 or 90 days, and what happened in it the two ends of the span
One badge one thing you earned, and where things stood then the badge's own moment

The last stretch prints the span's actual dates rather than calling itself "this week". That is deliberate: a card headed THIS WEEK on a Wednesday is a claim about three days wearing the name of a week, which is the same thing badges refuse to do when they wait for a month to end before deciding its winner.

One badge is a record, not a snapshot with a trophy on it. Every number on it is as it stood when the badge was earned β€” so a card about something you did in August shows August's streak, not today's. The date in the corner is that moment, and the whole card agrees with it. It appears as an option once you have earned a badge.

Themes

Seven palettes, and six of them are the badge metals: Steel, Moss, Sapphire, Amethyst and Gold are the five rarities the badge art uses, and Plate is the cream plate a badge sits on. Midnight is the app's own colours β€” what every card looked like before themes existed, and still the default.

Badges drawn on a card wear their own metal, so a legendary badge is gold on any theme.

Layouts

Three layouts, and they differ in how much room the clock gets β€” never in what is true.

Layout What it is Story Square Wide
Poster the big timer β€” what every card looked like before 0.14 6 stats (3 with a photo) 6 (3) 3
Dense a small timer and the room given to the numbers 12 (9 beside a photo) 9 (6) 6
Check-in the photo fills the card; a glass panel holds the stats 12 8 β€”

Check-in needs a right now card with a photo, in story or square. Ask for it anywhere else and the card quietly draws as Dense, and the page says so. The Check-in starter (the dashboard's πŸ“€ button uses it) picks today's photo for you.

A Check-in card has one clock: the shutter. Every number on it β€” the timer, the streak, the event day β€” is as it stood when the photo was taken (clamped to when it reached the box, so a camera with a fast clock cannot date a picture into the future), and that moment is printed on the photo itself. Drawn again next week, it is the same card, byte for byte.

Stats

How many fit depends on the layout and the shape (the table above); the page says fits N for the card on screen, from the same arithmetic that draws it. The Stats panel shows twelve slots in the card's own order: tap a slot, then move it earlier or later, or remove it; tap a stat in the catalogue below β€” each one shows its value right now β€” to add or drop it. β†Ί default list goes back to the list in Settings β†’ Stamps & recap; clear means no stats at all. The choice travels in the link and changes no setting; a preset keeps it.

A stat with nothing to say at this moment is left out rather than printed as a zero, so the card lays out what resolved, never what you asked for. A value too long for its cell takes two cells instead of being cut short β€” "LOCKTOBER Β· DAY 2/31" reads in full. A slot drawn dashed won't fit on this card; a dim one is already on the card β€” on Dense and Check-in the elapsed time is the big number, the level and rank are in the footer, and a stamp on the card already names the event and the day β€” so none of those is drawn twice.

What fills the lower half

Never nothing. In order: the photo if you picked one, then the badges β€” the ones earned in the span on a week card, the rest of your collection on a milestone card, or all of them on a right now card if you ask β€” and otherwise the events that moved the clock most, which is the same log the numbers above it come from.

A badge's icon is never printed, because the icons are emoji and the card's font has no emoji in it. A badge is drawn instead β€” the same hexagon-and-metal-ring shape the badge art uses.

The card has two moments in it, and says so

This is the one thing worth understanding about a share card that carries a photograph. The stats describe the card's own moment. The photograph describes whenever its shutter fired β€” possibly weeks ago. So the card keeps them visibly apart rather than blending them:

  • the card's own date and time sit in the top-right corner;
  • the photo's own date and time are burned into the bottom of the picture itself.

The caption is not optional, and it lives inside the image rather than in the card's margin β€” a card gets cropped, re-shared and screenshotted, and a date that lives in the surrounding chrome survives none of that.

A Check-in card is the exception that proves the rule: it has one moment, the shutter, and prints it on the photo. Nothing on it is from now, so there is nothing to keep apart β€” which is the only reason it is allowed to fill the card with the picture.

Why a milestone card takes no photograph

Its numbers are already as-of the day the badge was earned. Adding a picture would put a third moment on one card β€” the badge's, the shutter's, and the reader's assumption that a card they were just handed is about today. Two is already the most anyone can keep straight, so that kind refuses a photo and says so on the page.

The event stamp

When you are in an event β€” Locktober, or any playset with an [event] β€” the card carries a drawn postage stamp in the event's own colour: a perforated rim, a padlock, the event's name, the day in big numerals with /31 beside it, and the year. Nothing to set up and nothing for the playset to ship: it is drawn from the event's name, dates and accent, so every event playset gets one.

It sits where its moment is. On a framed photo it is on the photo, and dated by the shutter β€” a photo from the 2nd says DAY 2 forever, however late you share it. On a Check-in card it is in the header (the shutter is that card's only clock); on a card without a photo it is in the header, as of the card's own moment. No event at that moment means no stamp β€” never a fallback to "the event running now".

Turn it off for one card under Look β†’ Event stamp, or with seal=0 in the link.

The stamp reads the playset as it is now

A card re-drawn later asks the playset what the event was. Switch the playset off, remove it, or install a version with different dates, and old cards drawn again lose their stamp or change it. A picture you already shared is a file and does not change.

Post text

Under the card, the Post panel has the words ready: built here, on the box, from the same card β€” the same moment, and the stats the card actually drew, in its order. No model writes it and nothing is sent anywhere.

Three styles. Pup (the default) has three phrasings that take turns by event day β€” one of them quotes the playset's own line, so on Locktober's second day it opens Day 2 of 31. It's Locktober, pup. β€” Stats is the numbers, and Minimal is just the day and the tags. Custom ✎ opens the template, which you can edit:

  • a field in braces fills in from the card: {name}, {sign}, {day}, {days}, {left}, {year}, {line}, {locked}, {stats} (up to four of the card's own stats), {tags}, {photo_note}, {badge} β€” and any stat id from the catalogue, like {streak_current};
  • a field with nothing to say drops its own segment: a line is split at Β· and only the pieces whose field is empty go, so πŸ”’ Locked {locked} Β· {stats} on an unlocked pup is just the stats, never "Locked None"; a line that empties is dropped;
  • it never prints what a hidden timer hides β€” the time to go and the sentence are simply not there β€” and never a location.

Hashtags. An event gives two by default β€” #Locktober #Locktober2026 β€” and offers more (#LocktoberDay2, #chastity, #locked, #keyholder, #chastitycage, #pupplay, #petplay) that are never on until you tap them. Add your own; {day}, {days} and {year} inside one fill in from the event. A playset's catalogue tags are never turned into hashtags.

Alt text describes the card for a screen reader (it is written for you and cannot be edited into saying something the picture does not). Content warning is chastity Β· Locktober day 2/31 by default; set your own in the panel. The counters under the text track X (β‰ˆ280, emoji count double), Bluesky (300) and Mastodon (500, counting the CW).

Sharing it. On an iPhone, πŸ“€ Share puts the post text on your clipboard and opens the share sheet with the picture, in one tap β€” pick the app, then paste. (iOS drops the picture if text rides along in the share sheet, which is why the text goes by clipboard.) The picture is prepared before you tap; if you tap before it is ready the page tells you to wait a moment rather than downloading. ⧉ Copy text, Copy alt and Copy CW copy one thing each.

Presets

A preset is a whole look with a name: the stats, the layout, the theme, the shape, the event stamp, the post style, the hashtags, the template and the CW. The row of chips under the preview holds them.

  • οΌ‹ Save keeps what the page shows under a name.
  • A chip applies that preset to the photo you are on β€” a preset never stores a photo or a location, which are chosen per card.
  • Change anything and the active chip shows ●: tap it to update the preset or save as new.
  • Look β†’ Manage presets renames, stars and deletes them. The β˜… default is what a bare Share page and the dashboard's πŸ“€ button open on. An update or a delete can be undone from the message that confirms it.

Two built-in starters, Daily and Check-in, sit after your own presets, with a dashed outline; they cannot be renamed, starred or deleted. Presets live in this box's database and ride the full backup. Saving one needs JavaScript; applying one is a link.

Location on photos

Handlr can record roughly where a photo was taken and print it on a card or a stamped export. Two separate switches, because they are two different decisions:

Setting Default
Record a coarse location with new photos photo.place_capture on
Print it on an image nothing β€” every export asks explicitly off

The rounding happens when the photo is saved, not when it is printed. The exact coordinate is never written to disk, so it is not in the database, not in your backups and not in the export bundle β€” and a card cannot be sharpened back later, by you or by anyone who gets a copy of your data.

How coarse is photo.place_dp:

Grid Reads as
0 ~111 km region
1 ~11 km city
2 ~1.1 km neighbourhood β€” the default
3 ~110 m street

Where it comes from, in order:

  1. The photo's own EXIF β€” what the camera recorded, so it stays correct however late you log the photo.
  2. Your browser, but only for a photo you are logging now. A photo backdated to last Tuesday will not take today's position: that would put where you are standing onto a picture taken somewhere else.
  3. A label you type ("the park", "hotel, Chicago"). A typed label replaces the coordinate entirely rather than appearing next to it.

A photo whose camera recorded nothing simply has no location, and the Share page says so instead of offering a switch that would do nothing.

What rounding does and does not buy you

A 1 km cell is a neighbourhood, not a doorway. But the grid is fixed, so every photo taken at home lands in the same cell β€” and over a long log, the cell you appear in most is home, to within a kilometre. Coarsening limits the blast radius of one image; it is not a disguise. If that matters for a particular card, use a typed label, or leave the location off.

Carrying photos to a new box

🧳 means travels with my pup to my next box β€” not shown to any pack. It is off by default, and it is about one thing only: which photos go into your pup.json when you arrive on your own new box. What a pack sees is a different choice, made on Consent item by item; ticking 🧳 shows a photo to nobody.

  • Toggle it on a gallery tile, or all in one place under Pup ID β†’ What travels.
  • A photo that travels is re-encoded: rotated upright, at most 1600 px, JPEG, with no EXIF, no colour profile and no comment β€” no location, no camera details. Its caption and its place stay home. So do voice memos, always.
  • At most 60 photos go in one pup.json, newest first; a photo past that says stays β€” over the 60-photo limit. A photo whose check-in or task you undid stays home and says stays β€” undone. The chip and the count on the Pup ID page say exactly what the file will carry (a re-encoded photo over 1 MB, or past 24 MB in all, is also left out when the file is made).
  • A photo that arrived that way is a carried photo on the new box: it shows on its kit item and under What travels, it is counted by no statistic and no card (it was not taken there), and it can travel again.

Sharing

The πŸ“€ button uses the phone's share sheet (the Web Share API). It needs a secure origin β€” https://, through your reverse proxy β€” and a browser that supports it. Anywhere else it downloads the JPEG instead. Nothing in Handlr posts anywhere; sharing is you handing a file to another app.

On an iPhone the share sheet gets the picture only, and the share card's post text is already on your clipboard β€” paste it into the post. (The file in Save to Files is named for the event day, like locktober-2026-day02-20261002-1205-story.jpg.)

For 12 hours after a photo check-in, the dashboard shows πŸ“€ Share Day N right under the big buttons: one tap opens the Share page with that photo, the Check-in card, the stamp and the text ready. The gallery's πŸƒ card link does the same for any photo.

Limits and formats

  • Photos: JPEG, PNG, WebP, up to 40 MB. The check is applied to the upload stream, so an oversize file is refused before it costs memory.
  • HEIC is converted by your browser on the way in β€” automatically, on a phone. The server still has no HEIF decoder, so a desktop browser that cannot decode it sends the file as-is and gets a refusal explaining the fix. See Troubleshooting β†’ HEIC for what conversion costs you.
  • Only unreadable formats are converted. A JPEG, PNG, WebP or GIF is uploaded byte for byte β€” a canvas round-trip would strip the EXIF, and EXIF is where the capture time and the location come from.
  • Video is not stored, but picking one offers a use a frame from it button β€” a still pulled out of the clip, one tap. Offered, not automatic: a still is not the clip.
  • Voice memos: up to 12 MB / ten minutes; the recorder stops itself at ten.
  • Thumbnails are generated on upload and rebuilt on demand if one goes missing after a restore.

Picking one

Two doors, deliberately different:

  • πŸ“Έ Photo check-in on the dashboard is the fast one β€” one tap, camera, logged. It goes straight to the camera and submits itself, so there is nothing to confirm. If it catches the ceiling, undo it from the Log.
  • Everywhere else β€” the fold under the check-in button, and the verification photo on Training β€” opens your normal picker, so the camera roll is available and a photo you already took is fine. You then get a thumbnail of the file that is actually going to be uploaded, its size, and ↻ replace.

That the thumbnail is of the uploaded file matters more than it sounds: a HEIC is converted and a clip becomes a single frame before anything is sent, and the preview shows the result of that, not the thing you tapped. If a preview appears with no picture in it, the file is being sent as-is because this browser could not decode it β€” the line underneath says so, and the server's refusal will name the real format.

Where the bytes are

<data dir>/media/YYYY/MM/<sha256>.<ext> with thumbnails under media/thumbs/. The export bundle carries originals and thumbnails (Export). Media is not in the SQLite file, so a database-only backup is not a full backup.

This page is docs/photos.md in the repo. Spotted a mistake? Tell us how to report it β†’