summaryrefslogtreecommitdiff
path: root/bin
AgeCommit message (Collapse)Author
2026-06-25release 0.12.10.12.1Paul Buetow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25Keep 2x2 features off the page bottom; flush short final pagesPaul Buetow
Two layout-breaking cases remained after the fixed-column grid landed: 1. A 2x2 feature spans two rows, so one placed near the bottom of a page left an L-shaped gap grid-auto-flow: dense could not backfill (nothing follows it) -- a cut-off corner. append_preview_grid now only offers the feature layout while at least feature_tail_margin (16) photos remain, so a hero always sits in the upper rows with enough trailing 1-cell tiles to complete its rows at every breakpoint (and short pages get no hero). 2. A short final page (a leftover handful of photos) could be subdivided down below 12 cells, where it can't be aligned to a multiple of 12 and is ragged. Such pages now go through _build_final_page_tiles: plain singles, then merge down to a multiple of 12 (count >= 12 -> flush grid) or a full-row "fill" filmstrip (count < 12 -> one clean banner, or stacked banners). Longer/full pages keep the normal roll-and-align path. Verified hole-free at 2/3/4/6 columns with a grid-auto-flow:dense packing simulator across ~480 generated pages (regular + short final), 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25release 0.12.00.12.0Paul Buetow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25Fill the album's short final page with a full-row tilePaul Buetow
A short final preview page (e.g. one leftover photo when the photo count isn't a multiple of the page size) can't be aligned to a multiple of 12 cells, so it left an orphaned bottom-right corner. The album's LAST page now widens its leftover final single tile into a "fill" tile spanning the whole row (grid-column: 1 / -1) at any breakpoint, so the bottom edge is flush; object-fit: cover keeps the wider crop undistorted. append_preview_grid takes a fill_last flag: render_full_preview_page sets it only for the page with no "next" link; stats mini-albums pass 'no' so their small galleries are unaffected. build_tile_block gains a 'fill' layout and header.tmpl an a.fill-row rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25Add runtime GNU-tool guard; document Linux-only platform supportPaul Buetow
shuriken shells out to GNU-only features of the standard Unix tools (find -printf, stat -c, cp -a, sort -R). Add require_gnu_tools in a new src/lib/compat.source.sh, sourced early and invoked from main() before any action runs. On invocation it feature-probes each tool in a throwaway temp dir; if any probe fails it prints a clear error naming the offending tool and exits 1, so non-GNU (macOS/BSD) environments fail fast instead of producing broken output. README gains a Platform compatibility section and the requirements line now mentions GNU coreutils/findutils. Tests cover the find and stat rejection paths; the shared test helper that builds a coreutils-without-imagemagick PATH now includes cp and stat (which the guard probes).
2026-06-25Flush preview-grid rows: fixed breakpoint columns + multiple-of-12 pagesPaul Buetow
The overview grid used auto-fill columns (an unpredictable count at view time) while tiles per page were fixed at generation, so the last row was ragged -- an empty, cut-off bottom-right corner, made worse by a 2x2 feature tile. CSS (header.tmpl): replace auto-fill with a FIXED column count per width breakpoint -- 2 (phone) / 3 / 4 / 6 -- all divisors of 12. THUMBHEIGHT no longer drives the grid (it only sizes the thumbnail files), so its obsolete render-var is dropped (template.source.sh). Generator (album-tile-layout, album-thumbnail-html): append_preview_grid now decides a page's tiles, then snaps the grid-cell total onto a multiple of 12 before emitting, via two photo-preserving levers -- _grid_split_subdivides_to_add (round up: split subdivided tiles into singles; preferred, abundant) and _grid_merge_singles_to_remove (round down: merge adjacent singles). Because 2/3/4/6 all divide 12, a multiple-of-12 page tiles into a COMPLETE rectangle at every breakpoint: a flush last row at any window width, with no image distortion (object-fit: cover). Per-photo preview numbers and all navigation redirects are unchanged. Tiny pages (a short final page or small stats mini-album) are left as-is. Decrements use assignment, not bare "(( --k ))": under set -euo pipefail an arithmetic command evaluating to 0 returns status 1 and would abort generate. Helper namerefs are uniquely named and arrays are forwarded by name to avoid bash circular-nameref errors. Tests: add test_album_grid_cells_align_to_multiple_of_12 (both levers); update the two tests that pinned the old auto-fill CSS / render-var list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24release 0.11.40.11.4Paul Buetow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Unify escape/date helper API; fix current_date_text cachingPaul Buetow
Make the template.source.sh escape/date helper family follow one consistent shape: each escaper now has a nameref <name>_to form (hot path, writes a named variable) plus a thin printf wrapper <name> that delegates to it. The leading "_" now exclusively marks private helpers; the public escape API (called from sibling modules) is unprefixed. - current_date_text now delegates to current_date_text_to so both forms share the SHURIKEN_CURRENT_DATE_TEXT cache; the printf form no longer silently re-execs `date` on every direct call. Output unchanged. - Drop the misleading "_" prefix on the public escape API and update all callers: _html_escape->html_escape, _css_string_escape-> css_string_escape, _json_string->json_string, _json_bool->json_bool, _json_string_escape->json_string_escape. - Add the missing JSON nameref forms: json_string_escape_to, json_string_to, json_bool_to (printf wrappers delegate to them). - Add tests: JSON printf-vs-nameref parity and a current_date_text caching + nameref-parity check. No escaping/encoding or date output changes -- API-shape/perf only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Fix TARBALL_INCLUDE config default drift (7r0, scoped down)Paul Buetow
apply_config_defaults fell back to TARBALL_INCLUDE=no while the user-facing src/shuriken.default.conf documents (and --init writes) TARBALL_INCLUDE=yes. The two disagreed, so an album generated from a config that omitted the key silently dropped the tarball despite the documented default saying otherwise. 'yes' is the authoritative default: the original definition was `declare -r TARBALL_INCLUDE=yes` (tarball inclusion was on from the start); the ':-no' fallback was introduced later during a refactor and drifted from the documented intent. Align apply_config_defaults to ':-yes' and add a comment explaining the invariant and the history. Tests: add test_tarball_include_default_matches_init_config asserting the effective default agrees between a fresh --init config and apply_config_defaults (and is 'yes'); update the omitted-runtime-defaults --print-config expectation to the corrected 'yes'. bin/shuriken regenerated via just build. This is the scoped-down drift fix only. The full CONFIG_SPECS registry plus cfg_get/SHURIKEN_CFG access-layer rewrite proposed in 7r0 remains DEFERRED. just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Extract shared list_photos/pick_random_photo helper (br0)Paul Buetow
Collapse the duplicated "find photos, error on empty, random_index by ctx, return one" pattern into a single photo-list.source.sh exposing list_photos <dir> and pick_random_photo <dir> <ctx>. Consumers in album-photo-select, album-render, image-pipeline and stats-render now call the shared helpers; _stats_load_background_photos keeps its caching via a cached list_photos call rather than re-listing per filter page. Per-call-site random_index ctx strings are preserved, so selection semantics are unchanged. Sourced before its consumers in LIB_SOURCES (Justfile + src/shuriken.sh); bin/shuriken regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Split album-render.source.sh along its four seams (ar0)Paul Buetow
album-render.source.sh bundled four concerns that change for unrelated reasons. Extract three focused siblings, leaving album-render as the page orchestrator: - album-tile-layout.source.sh tile_layout_for, build_tile_block, build_subdivided_tile - album-thumbnail-html.source.sh build_preview_thumbnail, append_preview_grid - album-photo-select.source.sh album_photo_files, album_page_records, splash_photo_files, random_splash_photo, randomphoto album-render.source.sh keeps page assembly, the per-photo view/details pages, navigation redirects, index/splash, and the job_pool_* plumbing. Every function moved whole with no body/signature change. The album_view_page_for_photo accessor and its private ALBUM_VIEW_PAGE_BY_PHOTO map stay in album-render so the stats mini-album boundary is unchanged. LIB_SOURCES (Justfile + src/shuriken.sh) sources the three new modules before album-render. Regenerated bin/shuriken. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Encapsulate job pool behind a single handle (job_pool_*)Paul Buetow
Replace the four-parallel-nameref job-pool API (pids/statuses/labels/failed, each needing its own `shellcheck disable=SC2034` at every call site) with a single pool handle: a name prefix whose four backing variables (${pool}_pids/_statuses/_labels/_failed) are derived on demand by the helpers. Bash can't nest indexed arrays in an associative array, so a prefixed-handle with declare -g backing vars is the simplest pure-nameref encoding (no eval). New public API: job_pool_init <pool> job_pool_submit <pool> <label> <cmd...> job_pool_wait <pool> # returns 1 if any job failed Migrate all callers (scalephotos, create_all_photo_derivatives, render_album_pages, render_view_redirects, render_filter_pages) to the handle. queue_preview_page_render_job / queue_album_view_render_job / _album_record_view_photo / _stats_enqueue_filter_album now take one pool arg instead of four names. Drop the dead wrappers and unused parameterization: wait_for_image_job_slot, wait_for_template_render_job_slot and their _jobs variants only ever passed IMAGE_JOBS, so the max_jobs parameter is gone and throttling is fixed at IMAGE_JOBS inside the pool. Also removes wait_for_album_view_render_jobs (now just job_pool_wait). Throttling (max IMAGE_JOBS concurrent), failure detection and failed-job propagation are unchanged; the parallel-throttling and failure-logging tests pass unmodified. SC2034 disable-comments across the touched files drop 29->7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Split album-metadata.source.sh into focused modules (6r0)Paul Buetow
album-metadata.source.sh aggregated six unrelated concerns. Move each along its existing seam (pure mechanical move, no logic changes): - EXIF presentation (photo_exif_details_html, tooltip helpers, the _photo_exif_values_to wrapper) stays in album-metadata.source.sh, which is now EXIF-presentation only. - File counting (count_files, count_incoming_images, count_tree_files) -> image.source.sh, which already owns incoming_image_files; count_incoming_images is a direct wrapper of it. - Tarball naming (tarball_name_plan, generated_tarball_name) -> archive.source.sh, which already owns tarball()/resolve_tar_opts. - Generation metadata + JSON (_collect_generation_metadata, _generation_metadata_json, write_generation_metadata) -> new generation-metadata.source.sh. - Dry-run (dry_run, collect_dry_run_*, print_dry_run_plan) -> new dry-run.source.sh. - clear_exif_cache -> metadata-cache.source.sh, next to the cache primitive cached_photo_identify_output. LIB_SOURCES (Justfile + src/shuriken.sh): insert generation-metadata and dry-run right after album-metadata, before album-render/album. They depend on image, archive, template and metadata-cache (all earlier or runtime-only calls), and are consumed by the album coordinator and the dry-run CLI action, which come later. bin/shuriken regenerated via just build. File-header comments updated to reflect the new homes. just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Promote single canonical identify-stream EXIF parser into metadata-cache (8r0)Paul Buetow
The `identify -verbose` EXIF-line regex and its array-fill loop were duplicated in three places that had already drifted: album-metadata's photo_exif_details_html and _photo_exif_values_to (exif: only) and stats-aggregate's _stats_parse_identify_stream (exif: plus a native Geometry -> __geometry path). Promote one canonical parser, photo_exif_values_to, into metadata-cache.source.sh next to its sibling cache primitive cached_photo_identify_output. It reads an identify stream from stdin and fills a nameref associative array; it is a strict superset of all three former sites (bare exif: tag keys plus the synthetic __geometry key). - stats accumulate_photo_stats now calls photo_exif_values_to (stdin); _stats_parse_identify_stream is removed. - album _photo_exif_values_to is a thin wrapper that pipes cached_photo_identify_output through the canonical parser. - album photo_exif_details_html consumes the same parser and skips the __geometry key it does not display. metadata-cache is sourced before both consumers in LIB_SOURCES, so the canonical parser is available at use time. Regenerated bin/shuriken via `just build`. Added test_shared_identify_parser_returns_exif_and_geometry asserting one parse yields both an exif: key and __geometry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Trap-clean refresh_splash temp file; drop fragile errexit $- dancePaul Buetow
refresh_splash mktemp'd $tmp_html was only removed on the explicit failure-return paths, so a signal between mktemp and the final mv leaked a .index.html.XXXXXX file in DIST_DIR that --clean (which only sweeps .shuriken.* staging artifacts) would never reap. Register a cleanup trap right after mktemp, mirroring source_template_file: RETURN covers normal and error returns, INT/TERM/HUP cover signal termination, the handler clears all of these traps (including itself), and the success path clears the trap before the mv so the renamed file is not deleted on return. Also remove the fragile errexit save/restore that string-tested $- to remember whether errexit was on. refresh_splash always runs under the top-level set -euo pipefail, so a localized "set +e; ( set -e; ... ); status=$?; set -e" around each render subshell is sufficient and matches the project's canonical "localized set +e for expected failures" idiom. The bare standalone subshell is required: bash ignores an inner set -e when a compound command sits in an if/&&/|| context, which would let render_album_splash_page run past a failing photo=$(random_splash_photo) and silently produce a broken splash page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Fix: shell-quote rewritten TEMPLATE_DIR so spaced source paths source cleanlyPaul Buetow
init_config rewrote the TEMPLATE_DIR line via awk as a bare, unquoted literal when running --init from a source checkout. A source-root path containing a space (or any shell metacharacter) produced a config that aborts on source under "set -euo pipefail": the value was word-split, truncating the path and running its tail as a command (exit 127), leaving TEMPLATE_DIR empty and breaking every subsequent action. Emit the rewritten value as a single-quoted assignment, escaping any embedded single quote as '\'', so the generated shuriken.conf round-trips through sourcing regardless of spaces or single quotes. The non-rewrite path (installed /etc/default/shuriken) is unchanged. Regenerated bin/shuriken via just build. Tests: test_init and test_init_with_hash_in_source_path now assert the SOURCED TEMPLATE_DIR (the real contract) instead of the literal line, and a new test_init_with_space_in_source_path covers the spaced-path regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24release 0.11.30.11.3Paul Buetow
Mobile-friendly generated album: viewport meta tag, responsive 2-column phone grid, touch-friendly menu buttons, and a missing-alt fix on stats filter view pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24Make the generated album mobile friendlyPaul Buetow
Pure CSS/HTML, no JavaScript: - Add a <meta name="viewport"> tag to the two display heads (header.tmpl and splash.tmpl) so phones stop rendering at a zoomed-out desktop width. - Make the overview grid responsive: minmax(min(THUMBHEIGHT, 100%), 1fr) so a column never overflows a narrow screen, plus a max-width:700px media query that shows exactly two square columns on phones (2x2 feature tiles become full-width heroes). aspect-ratio keeps cells square at every width. - Add overflow-x:hidden to body so the slam/glitch entry animations (translateX +/-80vw) cannot make a phone scroll sideways. - Make the menus touch friendly on phones: the navigator/footer/splash links become large rounded buttons (~44px tap targets) and the "|" separators (now wrapped in <span class="nav-sep">) are hidden on mobile while still showing on desktop. - Fix a pre-existing W3C error: the stats filter view-page image was missing an alt attribute (and carried an obsolete border attribute). Verified with headless screenshots at phone width and the W3C Nu HTML checker + CSS validator (zero errors). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22release 0.11.20.11.2Paul Buetow
Thumbnail grid fills the full window width (no large left/right margins) and the spacing above the header / below the footer matches the inter- element gaps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22release 0.11.10.11.1Paul Buetow
Cap the big 2x2 feature tiles at two per page (main preview pages and stats mini-album galleries). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22Cap the 2x2 feature tiles at two per pagePaul Buetow
Each tile rolled for a feature independently, so a page (or stats gallery) could fill up with large 2x2 hero tiles. append_preview_grid now counts the features it has placed and stops offering the "feature" layout to tile_layout_for once two have been used, so any single grid gets at most two feature tiles; later tiles fall back to subdivided or single. The cap is per append_preview_grid call, so it applies to both the main preview pages and the stats mini-album galleries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22release 0.11.00.11.0Paul Buetow
Dynamic thumbnail tiles: subdivided tiles, 2x2 feature tiles, a CSS-grid overview, and the same effects in the stats mini-album galleries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22Reuse the album tile grid for stats mini-album galleries (DRY)Paul Buetow
The per-camera and per-filter stats mini-album galleries rendered their own plain img.thumb grid, so they did not get the dynamic tiles. Make them reuse the album's shared grid builder instead. Generalize append_preview_grid (and build_tile_block / build_subdivided_tile / build_preview_thumbnail) to take an href_prefix rather than a page number: the main album passes "<page_num>-" (view pages "<page>-<n>.html") and the stats mini-albums pass "" (view pages bare "<n>.html") -- the only difference between the two grids. The duplicate _stats_filter_thumbnail is removed; _stats_build_filter_thumbs now calls append_preview_grid, and camera.tmpl wraps the result in the same thumbs-grid container. Stats galleries now get the identical 2x2 feature tiles, subdivided sub-thumbnails, entry animations and hover effects as the main overview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22Add large 2x2 feature tiles and a CSS-grid overviewPaul Buetow
Allow a single photo to be blown up into a large "feature" tile that spans a 2x2 block of the album overview, controlled by a new THUMB_FEATURE_PERCENT (0-100, default 10; 0 disables). Each tile rolls for a feature first, then for a subdivision, otherwise stays a normal square. To pack mixed-size tiles (normal 1x1, subdivided 1x1, feature 2x2) without gaps, the overview is now a real CSS grid with grid-auto-flow: dense, so smaller tiles backfill the holes a 2x2 feature would leave. Tile spacing moved from per-image padding to the grid gap. Feature tiles reuse the img.thumb class and its dramatic hover. THUMB_FEATURE_PERCENT is wired through the same layers as THUMB_SUBDIVIDE_PERCENT: config defaults, 0..100 validation, --print-config, the --feature CLI flag, the usage text, and the shuriken.json / --dry-run metadata, with docs and tests updated. Generated HTML and CSS pass the W3C Nu HTML checker and CSS validator. Setting both percentages to 0 reproduces the previous all-1x1 grid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22Add dynamic subdivided thumbnail tilesPaul Buetow
Make the album preview grid livelier: with a configurable probability (THUMB_SUBDIVIDE_PERCENT, default 30%) a square thumbnail tile is subdivided into several smaller thumbnails packed into the same square footprint, chosen at random from: - quad: 2x2 squares (4 photos) - two-wide: two stacked full-width strips (2 photos) - squares+wide: two squares plus one full-width strip, strip on the top or the bottom (3 photos) Each sub-thumbnail stays its own clickable photo with its own view page; subdivision only groups consecutive photos visually, so preview numbering and the view/details/redirect pages are unchanged. No new images are generated (CSS object-fit crops the existing aspect-correct thumbs into squares or wide strips). Sub-thumbnails get the same random entry animation and the same dramatic hover (flip/scale/rotate/filter) as full thumbs. The layout choice reuses the seeded random_index, so builds stay reproducible under RANDOM_SEED. THUMB_SUBDIVIDE_PERCENT=0 reproduces the previous output byte-for-byte. The new option is wired through the config defaults, validation (0..100), --print-config, the --subdivide CLI flag, the usage text, and the shuriken.json / --dry-run metadata, with docs and tests updated. Generated HTML and CSS pass the W3C Nu HTML checker and CSS validator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21Fix W3C HTML validation errors in generated pages; release 0.10.10.10.1Paul Buetow
Every shuriken-generated page failed W3C validation. Fixed across the templates and the bash thumbnail builders so all page categories (splash, gallery, photo view, details, stats overview, stats filter mini-albums and redirect stubs) validate cleanly: - Add <!DOCTYPE html>, <html lang="en"> and <meta charset="utf-8"> to the header, splash and redirect templates. - Drop the obsolete type="text/css" on <style> and border='0' on <img>. - Fix invalid CSS "margin: 2 auto" -> "margin: 2px auto". - Add required alt attributes to every <img> (splash, thumbnails, views). - Replace the obsolete name attribute on <a> thumbnail anchors with id (in album-render.source.sh and stats-filter-album.source.sh, where the markup is actually built, plus preview.tmpl for consistency). - Give redirect stubs a <title>; drop trailing slashes on void elements. Updated the affected cli.sh assertions and rebuilt bin/shuriken. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19docs: split README into quick-start + reference pages under docs/Paul Buetow
Trim README.md to a quick-start guide (install, init, generate, sync, clean) with a main-flags table and feature-toggle summary, and move the detailed reference material into focused pages under docs/: installation, usage, configuration, generation, publishing, templates. Correctness fixes carried over during the split: - The 'Site generated ... with <URL>' source link lives in the page header bar (header.tmpl), not the footer (footer.tmpl only renders the tarball download). Fixed in docs and the SOURCE_URL code comment. - --sync is a config-backed action and accepts --config PATH / reads ./shuriken.conf; the --config action list now includes it. - --refresh-splash also re-copies the site favicon; documented. - State the Bash 5.1 requirement (enforced by the script) in the docs. - docs/stats-exif-audit.md: the EXIF cache moved to metadata-cache.source.sh and the native-field parser extension was implemented; add a status note and fix the stale module path. bin/shuriken regenerated from the config.source.sh comment change.
2026-06-18Default stats generation to off; release 0.10.00.10.0Paul Buetow
The EXIF stats site (stats/ overview plus per-camera and filter mini-albums) was generated by default. Flip the STATS_PAGE default to "no" so a plain album stays lean; enable it explicitly with STATS_PAGE=yes or --stats. Updated the bundled default config, the README ("off by default"), and the tests that relied on the old default (the stats-rendering and dry-run-override tests now pass --stats / set STATS_PAGE=yes; the print-config expectations now show STATS_PAGE=no). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17Add configurable SOURCE_URL footer link; release 0.9.00.9.0Paul Buetow
The footer "Site generated ... with <link>" was hardcoded to the shuriken.sh repository. Make it configurable via the SOURCE_URL config variable and the --source-url CLI flag (defaulting to the shuriken.sh repo, so existing sites are unchanged). The footer derives the displayed text from the URL by stripping its scheme. Plumbed through apply_config_defaults, CLI override targets/spec, --print-config, the verbose effective-config log, and the header template's new render_source_url_html (config_html) render var. Documented in shuriken.default.conf and README; added a generation test asserting a custom SOURCE_URL replaces the default footer link, and updated the print-config and header render-var-subset expectations. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17pn0 decouple stats from album internalsPaul Buetow
Stats reached directly into album-module internals: the private global ALBUM_VIEW_PAGE_BY_PHOTO and the EXIF cache reader cached_photo_identify_output. Introduce a clean boundary, behaviour and generated HTML byte-identical. - Promote the EXIF identify cache primitive (cached_photo_identify_output plus its private helpers photo_cache_signature and print_cached_photo_identify_output) out of album-metadata.source.sh into a new shared src/lib/metadata-cache.source.sh, sourced before both album and stats (right after metadata-label in LIB_SOURCES). It is a low-level metadata primitive used by both consumers, so it no longer belongs to album internals. Signature/behaviour unchanged. - Add album_view_page_for_photo accessor in album-render.source.sh as the documented public API; keep ALBUM_VIEW_PAGE_BY_PHOTO as the album's private backing store. stats-filter-album.source.sh now calls the accessor instead of indexing the global, so a change to album page naming/caching stays contained in the album module. - Add test_album_stats_decoupling_boundary asserting the accessor returns the backing-store value and that the assembled bin/shuriken keeps the cache primitive in the shared module and no longer indexes the global from the stats filter section. Existing stats/album tests unchanged. Verified: 3-image fixture (STATS_PAGE=yes, fixed seed) diff -r of stashed original vs new build is byte-identical across all 47 dist files (only the inherent generated_at timestamp normalized). just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17kn0 compute only required template render vars per templatePaul Buetow
prepare_template_render_vars previously computed and serialized every one of the 30+ render_* fields for each template invocation, even though e.g. the header template references only a handful. It now computes only the subset the target template actually needs. The needed set is driven by the required_templates (5th) field of each TEMPLATE_RENDER_FIELD_SPECS entry, corrected/completed so every spec's required_templates exactly matches that render_var's references in the .tmpl files (config_html and derived kinds previously left it empty). A new template_needed_render_vars_to builds the per-template set; prepare_template_render_vars takes the template name and skips non-needed fields; serialize_template_render_context emits only the computed keys into the BASH_ENV context file. Dependency closure: handlers read only from the input context array or config globals, never from another computed render_var, so the direct per-template set is the full closure (no transitive expansion needed). Side-effects: every handler is a pure value computation; none consume RANDOM/seed; current_date_html only primes the deterministic SHURIKEN_CURRENT_DATE_TEXT cache (idempotent), so subsetting is safe for all fields. render_html_dir_html (required_templates='*') is referenced by no template but kept always-computed as a documented cheap exception. Output is byte-identical: diff -r over a full generated dist/ (3-image, 2-camera fixture, STATS_PAGE=yes, fixed random seed) between the previous bin/shuriken and this build matches exactly across all 63 files (51 HTML album/details/splash/stats/per-camera/filter pages + tarball). Tests: add test_template_render_vars_subset_is_minimal_for_header (header computes only its needed vars, succeeds with unrelated config globals unset) and test_template_render_var_subsetting_matches_templates (spec needed-set equals each .tmpl's render_* references). Existing test_template_required_context_vars_come_from_render_specs is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17gn0: dispatch CLI actions via registration tablePaul Buetow
Adding a CLI action previously required editing two case statements (run_action and run_configured_action). Introduce ACTION_SPECS, a single '|'-delimited registry (flag|handler|requires_config|validation_fn| validation_arg) matching the CLI_OPTION_SPEC / STATS_CATEGORIES encoding, and replace both dispatchers with table lookups via action_spec_field. - run_action: looks up requires_config; routes non-config actions (--version/--init) through run_unconfigured_action (shared config/override/force precheck) and the rest through run_configured_action. - run_configured_action: keeps the force-generate guard and config load/log, then runs the entry's validation_fn (with optional arg, used by --dry-run) and handler via run_configured_action_body. - Extracted the --clean inline rm body into a clean_dist handler and added an action_print_version handler so every action is just a registry entry plus named functions. - Unknown/empty actions have no entry, so dispatch falls through to the same usage + exit 1 behavior as the old case "*)" arm. Adding an action is now one ACTION_SPECS entry plus its handler/ validation functions; neither dispatcher changes (Open/Closed). Added test_action_dispatch_is_registry_driven proving every parser action flag has a registry entry, all handlers/validators resolve, and unknown actions are rejected. All existing action/dispatcher tests pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17en0: make stats categories self-registering via STATS_CATEGORIESPaul Buetow
Adding an EXIF stats category previously required editing four places: the reset function, a new _stats_record_*, the body builder, and a new _stats_render_* section. Introduce a STATS_CATEGORIES registry (the single source of truth) and make the generic code iterate it instead. - STATS_CATEGORIES: ordered, pipe-delimited specs (count_array|prefix|heading|render_kind), declared -gra so it survives a function-scoped source. The array order IS the overview display order. - STATS_CATEGORY_BUCKETS: tab-delimited bucket ladders for the 'ordered' histogram kinds (apertures wide->narrow, etc.). - STATS_RECORD_FUNCTIONS: the per-photo recorder dispatch list. Collapsed touch-points: - reset_photo_exif_stats clears each registry count array via _stats_category_arrays. - accumulate_photo_stats dispatches recorders from STATS_RECORD_FUNCTIONS. - _stats_build_body iterates STATS_CATEGORIES, dispatching each spec through _stats_render_category (camera/ranked/ordered/month kinds). - _stats_render_ordered_section reads its ladder from STATS_CATEGORY_BUCKETS; the camera leaderboard is now ranked + a 'stats-leaderboard' list_class. Adding a category is now: append one STATS_CATEGORIES entry (plus a STATS_CATEGORY_BUCKETS row for an ordered ladder) and have a record function tally into its array. No edits to reset, the body builder, or a per-category render branch. Behaviour-preserving: category order, bucket order, headings, counts and links are unchanged. Verified byte-identical by diffing the stats/ output of the pre-change binary against the new one over the same fixture album. Added test_stats_categories_registry_is_single_source_of_truth (registered in main()) asserting reset, the body builder and the bucket ladders all derive from the registry; it fails if a category is added in only one place. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17ln0 make --clean remove leftover staging directoriesPaul Buetow
The --clean action deleted DIST_DIR but left behind the staging/backup directories the generation pipeline creates as siblings of DIST_DIR (.shuriken.<basename>.staging.* / .backup.*). Users expect --clean to remove all generation output, so extend it to also delete those. Cleanup runs after the validate_clean_dist_dir safety guard (8n0), so a dangerous DIST_DIR still aborts before any deletion. It only matches shuriken's own basename-specific staging/backup prefixes (the exact mktemp templates from config.staging.source.sh), uses nullglob so a missing match never expands to a literal pattern, and only removes directories. Unrelated dotfiles in the parent are never touched. Update test_clean to assert the staging/backup dirs are removed while unrelated entries survive, and document the behavior in the CLI usage text and README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17mn0 share camera Make+Model dedup helperPaul Buetow
The rule that joins a camera's EXIF Make + Model into one label while avoiding a duplicated manufacturer prefix (e.g. "Canon Canon EOS 5D" -> "Canon EOS 5D") was implemented independently in the album tooltip builder and the stats leaderboard tally. Extract it into a single shared helper camera_label_from_make_model in the new src/lib/metadata-label.source.sh, sourced before both callers. Both prior implementations were behavior-identical (empty model -> make, empty make -> model, exact/prefix dedup, case-sensitive), so this is a pure DRY refactor with no observable output change. Added a focused unit test covering dedup, plain concatenation and the empty-field edge cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17on0 remove run_action_body serialization runnerPaul Buetow
Shuriken is a single-process CLI, yet the action layer could serialize 30+ globals plus every function definition (declare -p / declare -f) and pipe them into a fresh "bash -euo pipefail" process to run an action. Production already forced the in-process run_action_body_direct via SHURIKEN_ACTION_BODY_RUNNER, so the serialized-subprocess path was dead in production and only added complexity (a hand-maintained variable list to keep in sync). Per KISS, drop it. - Remove run_action_body_context and the run_action_body dispatcher. - Collapse run_configured_action_body to call the action in-process directly and remove the SHURIKEN_ACTION_BODY_RUNNER indirection in main(). - Move the only genuinely needed isolation into a test-only shim (test::run_action_isolated in tests/helpers.sh) for the generate real-failure test, which must capture a failure status without the in-process errexit abort ending the caller (correct in production, where main runs under errexit). - Update the errexit/status-propagation tests to exercise the direct path. Template-engine serialization is unrelated and left untouched. just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17an0 reject scalar SYNC_DESTINATIONS instead of word-splitting itPaul Buetow
A scalar SYNC_DESTINATIONS containing spaces was word-split by the shared resolve_config_array helper, breaking a single destination into multiple broken arguments passed to rsync. A list of rsync destinations is inherently a list, and array syntax is the only spelling that preserves embedded spaces. resolve_sync_destinations now detects a scalar declaration via declare -p and fails with a clear config_error telling the user to use array syntax. The array path is unchanged, and resolve_config_array's scalar word-splitting is left intact for TAR_OPTS (where turning "-c -v" into separate options is desired). Adds focused tests proving the scalar case errors without invoking rsync and the array case preserves a space-containing destination as one argument. Updates src/shuriken.default.conf and README.md to document the requirement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17bn0 guard preview_num arithmetic against non-numeric inputPaul Buetow
The preview_num next/prev render handlers computed neighbour page numbers with $(( context_value +/- 1 )) but only guarded against an empty value. A non-numeric preview_num context value (e.g. a stray string) slipped past the [ -n ] check and triggered a bash arithmetic syntax error which, under set -e, aborted the whole script. Validate the context value is a non-negative integer ([[ value =~ ^[0-9]+$ ]]) before the arithmetic in both prepare_template_render_var__preview_num_next_html and __preview_num_prev_html. Invalid or missing values now default to an empty render value, matching the existing missing-neighbour behaviour; the valid-numeric path is unchanged. Add test_template_render_var_preview_num_guards_non_numeric in tests/cli.sh, which drives the handlers directly under bash -euo pipefail to prove a bad preview_num no longer crashes and a numeric one still yields the exact +1 / -1 neighbour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-168n0 validate DIST_DIR before --clean rm -rfPaul Buetow
The --clean action ran `rm -rf "$DIST_DIR"` after only an `[ -d ]` check, so a misconfigured DIST_DIR (empty, /, $HOME, system dirs, etc.) could recursively delete the wrong tree. Add validate_clean_dist_dir (and resolve_dist_dir_path) in config.validate.source.sh and call it in the --clean case before any deletion. The guard canonicalizes DIST_DIR with `pwd -P` (handling ./ trailing slashes, symlinks and relative paths; for a not-yet-existing dir it resolves the existing parent and re-attaches the basename) and refuses to clean when the resolved path is empty, the filesystem root, a well-known system directory, the resolved $HOME, or the current working directory. Rejection uses config_error with a clear message and a non-zero exit, so nothing is deleted. Normal DIST_DIRs still clean. Tests (tests/cli.sh, registered in main): a HOME-as-DIST_DIR case (uses a fake HOME under TEST_TMPDIR with a sentinel file, so a regression can only touch the throwaway temp dir) and an empty-DIST_DIR case both assert rejection and that nothing is removed. Note: leftover staging artifacts on --clean are out of scope (task ln0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-169n0 warn on identify failure instead of caching empty EXIFPaul Buetow
cached_photo_identify_output() swallowed all ImageMagick errors with 'imagemagick_identify ... || true', so a corrupt photo or identify failure left the cache with only the signature line and no EXIF. That signature-only file was a valid-looking cache hit, so the photo rendered with empty tooltip/stats and the failure was never retried or warned about again. Now capture identify's exit status; on failure, log_warning naming the photo and rm -f the cache file so the next run retries instead of reusing an empty result. The function still returns 0 so one unreadable photo does not abort generation (it runs in backgrounded render jobs under set -euo pipefail) -- the photo just renders without EXIF, now with a warning. Adds tests/cli.sh test_generate_warns_and_skips_cache_on_identify_failure and a TEST_IMAGEMAGICK_IDENTIFY_FAIL hook in the fake ImageMagick to drive a failing identify; asserts exit 0, a warning naming the photo, the photo still rendered, and that the failed photo's cache is absent while a successful photo's cache is present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16wn0 batch preview-thumbnail rendering per pagePaul Buetow
Each thumbnail on a preview page (page-N.html) used to be rendered by its own "template preview" call, paying the full source_template_file cost -- an "env -i bash" invocation -- per thumbnail. With MAXPREVIEWS thumbnails per page that was N template renders per page just for the grid. render_full_preview_page now builds the markup for ALL of a page's thumbnails in bash (build_preview_thumbnail / append_preview_thumbnail) and emits the whole grid in ONE render via a new previewpage.tmpl that takes the pre-built HTML through a context_raw "preview_thumbs" field -- the same pattern the stats filter galleries (camera.tmpl) already use. Per-thumbnail markup is byte-identical to the old preview.tmpl output: same <a name=... href=...><img class='thumb <anim>' .../></a> structure, order, HTML escaping and seeded "slow" animation class. Header and footer stay as their own template calls, so a page now costs ~1 previewpage render + header/footer instead of N + chrome. The parallel job-pool integration and failure contract are unchanged: each preview page is still one background render job. Added the render_preview_thumbs_html field spec (hn0 dispatch pattern, context_raw kind), registered previewpage in the validate_template_dir required templates and in the required-context-vars test expectations, and pointed the four generation template-failure tests at previewpage.tmpl (generation no longer renders preview.tmpl). The standalone "template preview" engine unit tests keep exercising preview.tmpl, which still ships. Verified byte-identical output: generated the fixture album (including a spaces/special-char filename) twice with RANDOM_SEED=42 using the parent commit's bin/shuriken vs the new bin; every .html file is identical. just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16jn0 split bootstrap.source.sh into logging/paths/startup modulesPaul Buetow
Separate the mixed concerns in bootstrap.source.sh per [SRP]: - logging.source.sh: output_is_quiet, output_is_verbose, log_info, log_verbose, log_warning (the output/logging concern). - paths.source.sh: resolve_default_rc_file, resolve_source_root, resolve_default_template_dir, resolve_default_asset_dir, template_dir_uses_default, apply_template_dir_default, init_config (install/source-root/default-dir + rc-file/template-dir resolution). - bootstrap.source.sh keeps the CLI usage text and the shared resolve_config_array parser used by the config modules. Pure code move, verified byte-identical function bodies. LIB_SOURCES now lists logging.source.sh first, then bootstrap, then paths; matching source directives added to src/shuriken.sh so --check-sourced sees the new modules. just test/shellcheck/check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16cn0 split stats.source.sh into aggregate/render/filter-album modulesPaul Buetow
Pure code move: split the ~1248-line stats.source.sh (SRP+SoC) into three cohesive modules, all functions and STATS_* constants/globals moved verbatim: - stats-aggregate.source.sh: EXIF parsing, bucketing, _stats_tally, reset/collect_photo_exif_stats and the STATS_* maps. - stats-render.source.sh: stats overview page (sections, bars, leaderboard, render_stats_page, _stats_build_body, _stats_filter_link, background pickers). - stats-filter-album.source.sh: per-filter mini-albums (render_filter_pages, gallery/view/thumbnail builders, _stats_enqueue_filter_album). Updated LIB_SOURCES accordingly. No logic change (verified: all 55 functions and top-level globals preserved; full suite, shellcheck, check-generated, diff all pass). The SoC point (HTML built in bash rather than templates) is left for a follow-up (nn0/qn0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16in0 split album.source.sh into cohesive modulesPaul Buetow
Refactor the ~1442-line album.source.sh ([SRP] violation) into three cohesive modules, leaving album.source.sh a thin coordinator. Pure code move, no behavior change -- whole functions and their doc comments moved verbatim; the assembled bin/shuriken contains the same function set. - image-pipeline.source.sh: ImageMagick derivative generation (create_photo_derivatives, create_all_photo_derivatives) and photo asset prep (prepare_generation_photo_assets). - album-metadata.source.sh: EXIF identify caching, exif tooltip/details helpers, file counts, shuriken.json metadata, EXIF cache clearing and the dry-run plan. - album-render.source.sh: preview/view/details/splash/redirect page rendering, the parallel render-job orchestration and the ALBUM_VIEW_PAGE_BY_PHOTO global. - album.source.sh: thin coordinator (generate, refresh_splash, generate_stats_pages, site-asset/archive/html-clean helpers). Add the three modules to LIB_SOURCES (Justfile) and to the SHURIKEN_LIB_SOURCES markers in src/shuriken.sh, ordered after random and before album.source.sh / stats.source.sh. just build, just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16hn0 make template render kind dispatch extensible (OCP)Paul Buetow
Replace the hardcoded `case "$kind"` block in prepare_template_render_vars with a name-based registration/dispatch pattern. Each render field kind is now implemented by one prepare_template_render_var__<kind> handler; the core loop resolves the handler by name (prepare_template_render_var__$kind), verifies it exists via `declare -F`, calls it with a uniform signature (out_nameref, context_name, source_name), and reports a config_error for an unknown kind (no matching handler) -- preserving the previous error behavior. Adding a new kind now means defining a new handler function only; the loop never changes. Handlers cover all existing kinds: context_css, context_html, context_raw, current_date_html, config_html (keeps its inner source_name dispatch for HEIGHT/MAXPREVIEWS/TITLE/etc. and the same :- defaults), original_basepath_is_set, preview_num_next_html, preview_num_prev_html, and tarball_include. Escaping and defaults are unchanged, so rendered output is byte-identical (verified by diff -r of a full --generate album, before vs after). Add test_template_render_var_dispatch_is_extensible proving every declared kind resolves to a handler and that a newly defined handler is dispatched without touching the core loop (OCP). just test, just shellcheck, just check-generated and git diff --check all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16