| Age | Commit message (Collapse) | Author |
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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.
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
Only per-photo view/detail pages were backgrounded; preview pages
(page-*.html) and navigation redirects rendered serially in the main
loop, serializing album generation.
- render_full_preview_page assembles a complete preview page (header,
optional prev link, ordered thumbnails, next/footer) in one call so
the per-page appends stay sequential; queue_preview_page_render_job
backgrounds each whole page via the shared template render pool,
throttled to IMAGE_JOBS.
- render_album_pages now materializes the full deterministic page
layout up front (album_page_records) and enqueues both view jobs and
one preview-page job per page into the same pool; per-photo
bookkeeping moved to _album_record_view_photo.
- render_view_redirects enqueues one job per view page
(render_page_view_redirects) and blocks until all redirects exist,
preserving its synchronous contract while parallelizing the work.
- Removed obsolete advance_album_preview_page and
render_album_page_thumbnail.
Parallelism only affects timing: seeded random choices are
context-keyed, so output is byte-identical across runs. A failed
preview or redirect job flips render_failed and makes generation exit
non-zero, matching the view-job failure contract. The IMAGE_JOBS cap
still bounds concurrent renders.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
source_template_file previously built the BASH_ENV context file by piping
"declare -p ...; declare -f; serialize_template_render_context ...;" into a
fresh "bash -euo pipefail" subprocess for every rendered page. That dumped all
~5000 lines of shuriken functions and spawned a subprocess per page just to run
the serializer - a large per-page cost for albums with hundreds-to-thousands of
pages.
Now serialize_template_render_context runs in the current shell with stdout
redirected straight into the context tempfile, followed by an appended
"unset BASH_ENV". The serializer returns its own non-zero status explicitly so
the failure is detected via an "if" status-test (which returns normally through
the RETURN trap and cleans up the partial context file), robust even when
source_template_file runs inside a status-tested "if template ..." call chain
where bash would otherwise suppress an inner errexit abort.
The trap-based cleanup (RETURN plus INT/TERM/HUP re-raising to $BASHPID) is
preserved unchanged. Two serializer test mocks that relied on the removed
"| bash" errexit now return non-zero explicitly. Rendered HTML output is
unchanged; just test, just shellcheck, just check-generated and
git diff --check all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The generated pages link a favicon.ico that was always the bundled shuriken
favicon (copy_site_favicon hard-copied share/shuriken/assets/favicon.ico). Add a
FAVICON config variable and a --favicon PATH CLI flag: when set, that file is
published as favicon.ico instead of the bundled default; when empty, the bundled
favicon is used as before.
Plumbed through apply_config_defaults, CLI_OPTION_SPEC + override allowlist,
usage, print_config, the action config list and effective-setting log, and
validated (a non-empty FAVICON must be a readable file) before generation.
shuriken.default.conf and the README document it; a test covers a custom favicon,
its appearance in --print-config, and rejection of a missing file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The filter mini-album pages all drew their blurred background from the whole
album, so a filter page's background did not relate to its category. Mirror the
main album instead:
- A filter gallery's background is now a random photo from THAT filter's own set
(like the album preview pages pick a random album photo).
- A filter view page's background is the photo it shows (like the album view
pages).
- The stats overview still uses a random album-wide photo.
Adds _stats_pick_background (random pick from a filter's photo list) and drops
the now-unused album-wide background preload from render_filter_pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The dist directory is the swapped-in staging dir, which mktemp -d creates mode
0700. The published album root therefore stayed 0700, so the first `shuriken
--sync` created the remote album directory 0700 and the web server (daemon) could
not read it -- requiring a manual chmod 755 on each mirror.
Relax the staging dir to the umask-default directory mode right after mktemp -d
(what mkdir would have produced), so the dist root matches its subdirectories and
is served/synced with sane permissions. Add a regression test asserting the dist
root mode equals its photos/ subdirectory mode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The filter mini-albums put thousands of HTML files directly in the album root.
Reorganise so only the main album lives in DIST_DIR and all stats content goes
under stats/:
stats/index.html - the stats overview (was stats.html)
stats/<pagebase>/index.html - each filter gallery (was <pagebase>.html)
stats/<pagebase>/<index>.html - each filter view page (was <pagebase>--N.html)
Each mini-album is its own directory, so no single directory holds more than one
filter's pages. Links updated accordingly: the overview links to <pagebase>/, the
gallery thumbnails link to sibling <index>.html, view prev/next/gallery are
same-dir, and assets/album-details/back-to-stats resolve via the ../.. backhref.
The header Stats link and camera.tmpl back link point at stats/index.html.
Mini-albums still reuse the shared photos/thumbs/blurs assets and render in
parallel. Tests and the dry-run plan updated for the new layout; README documents
the stats/ subdirectory structure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The per-photo `identify -verbose` cache lived in $DIST_DIR/.shuriken-cache, which
is inside the throwaway staging dir and the published dist: a fresh/cleared dist
lost it (forcing a full, slow re-identify of every original) and it got deployed
to web servers.
Move it to a volatile ./cache/exif directory parallel to ./dist (dirname of
DIST_DIR resolves to the working dir in both the staging and direct contexts).
Now an unchanged photo skips identify on every regenerate, the cache survives a
cleared dist, and it is never deployed. --force clears ./cache/exif once up front
(clear_exif_cache) and the run repopulates it, so force still does exactly one
identify per photo. Dropped .shuriken-cache from the staging-copy loop.
Tests and the audit doc updated for the new cache location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
_stats_random_background scanned the photos directory once per page; with a
filter mini-album per stat bucket that is thousands of scans. Load the sorted
photo list once into a global (STATS_BG_PHOTOS) that render_filter_pages
populates before forking the render jobs, so each background subshell inherits
the cached array and just indexes into it. The seeded pick is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Generalises the per-camera mini-albums to every stats category: clicking any
row on the stats page (camera, lens, year, month, aperture, shutter, ISO, focal
length, megapixels, aspect, orientation, format, exposure program, metering,
white balance, flash) now opens a filter mini-album of just the matching photos,
with view pages whose prev/next cycle within the filter.
Aggregation: a single _stats_tally now records, for every tallied bucket, the
bucket's photo list and a unique filename-safe "pagebase" (camera-canon-eos-r5,
iso-400, year-2023, ...) with collision resolution, exposed via STATS_FILTER_*
maps. The old camera-only STATS_CAMERA_SLUGS/PHOTOS/SLUG_OWNERS are gone.
Render: every bar/leaderboard row is wrapped in a link to its bucket's pagebase
via _stats_filter_link. render_filter_pages replaces render_camera_pages and
renders ALL filter mini-albums (gallery + per-photo view pages) reusing
camera.tmpl/cameraview.tmpl. The galleries/view pages reuse the album's shared
photos/thumbs/blurs assets -- only the HTML differs.
Parallelism: filter pages render through the shared job pool, throttled to
IMAGE_JOBS (the same pool the album view pages use). The album render warms every
photo's EXIF cache (via details pages) before the parallel filter render, so the
filter jobs only read the cache.
Tests updated for the new STATS_FILTER_* structures and extended to assert
non-camera filter mini-albums (orientation, ISO) and their stats-page links.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Two fixes to the per-camera pages:
- The camera thumbnail grid is now full-width and reflows with the window like
the main album. Previously it sat inside the 900px-wide stats column, so wide
thumbnails only fit ~2 per row. camera.tmpl now puts the grid in a full-width
sibling block (centered by the body, like the album's direct thumbnail
children); the heading/back-link stay in the narrow stats column.
- Each camera is now a self-contained mini album. Thumbnails link to per-camera
view pages (camera-<slug>--<index>.html) whose prev/next cycle only through
that camera's photos, instead of linking into the shared album view pages
(where next/prev walked the whole album and left the camera). The view pages
reuse the album's look (blurred background, EXIF tooltip, animation) and offer
Gallery / Details (to the album details page) / Direct-link nav. The "--"
index separator can never collide with a gallery name since slugs never
contain "--".
Adds cameraview.tmpl + its render field, _stats_render_camera_{gallery,views}
and the cameraview body builders. Tests updated for the new thumbnail links and
extended to assert the per-camera view pages and their camera-scoped navigation;
required-context-vars test now also covers the camera and cameraview templates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Three refinements to the stats site:
- Stats and per-camera pages now get a random blurred background image (same
seeded pick the album preview pages use) instead of a plain black page. Adds
_stats_random_background + STATS_BLURS_DIR; falls back to black when no photos
exist (e.g. isolated unit tests).
- Per-camera thumbnails now behave like the main album: they link to each
photo's album view page (navigation, details, EXIF tooltip) and carry the same
seeded animation class, instead of linking to the raw image. render_album_pages
records a photo -> "<page>-<preview>" map (ALBUM_VIEW_PAGE_BY_PHOTO) that the
camera pages read; photos with no recorded view page fall back to the image.
- Spacing: the camera leaderboard rows get padding + a separator line so the long
wrapping camera names are distinguishable, and the stats sections, headings,
bar rows, back-link, and camera grid get moderate extra spacing so nothing
looks cramped.
Tests updated for the new camera-thumbnail markup and extended to assert the
stats background and the album view-page links; camera.tmpl comment corrected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The details view already showed a title="" hover summary (camera, aperture,
ISO, shutter, date) on its image; the normal image view did not. Wire the
same tooltip into view.tmpl:
- render_view_page now computes the EXIF tooltip via photo_exif_tooltip_text
(reusing the shared identify cache, so no extra ImageMagick work) and passes
it as the exif_tooltip context var.
- exif_tooltip is now a required render field for the view template too.
- view.tmpl adds the title="" attribute to its <img> when the summary is
non-empty, mirroring details.tmpl (no title when there is no EXIF).
- Tests: extend the EXIF render test to assert the view image carries the same
tooltip, and update the required-context-vars expectation for view.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Review follow-up: collect_dry_run_plan stored plan["stats_page"] but
print_dry_run_plan never rendered it, so --dry-run gave no signal about
the STATS_PAGE toggle or the stats/camera pages generate() would write
(dead assignment, breaking parity with splash_page).
Print a "Stats page: <yes|no>" line and, when enabled, list stats.html
and camera-*.html under "Planned generated files" (camera pages are a
wildcard since dry-run does not run EXIF aggregation). Add a --dry-run
--no-stats test asserting the stats plan is omitted, and extend the
existing dry-run test to assert the stats lines when enabled.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Final integration of the stats-site feature:
- Add STATS_PAGE config (default yes) mirroring SPLASH_PAGE through every
layer: shuriken.default.conf, apply_config_defaults, CLI_OPTION_SPEC
(--stats/--no-stats), the CLI_CONFIG_OVERRIDE_TARGETS allowlist (without
which the flag was silently ignored), usage text, yes/no validation,
print_config, the action config list + effective-setting log, the
generation metadata, and the dry-run plan.
- generate() now calls a gated generate_stats_pages helper after the album
pages (and before archiving, so the stats/camera pages are tarballed):
collect_photo_exif_stats + render_stats_page . . + render_camera_pages . .
- Emit "stats_page" in shuriken.json next to "splash_page".
- Gate the header Stats nav link behind STATS_PAGE via a new
render_stats_page_html config_html field, so it is hidden (no 404) when
stats are disabled.
- Document STATS_PAGE and --stats/--no-stats in the README.
- Tests: full --generate produces stats.html + camera-<slug>.html + the nav
link and stats_page=true; --no-stats suppresses all three and sets
stats_page=false; print-config and metadata expectations updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Implement render_camera_pages <html_dir> <backhref> in stats.source.sh: one
camera-<slug>.html per camera in STATS_CAMERA_SLUGS, reusing the aggregation's
collision-resolved slugs and per-camera photo lists so filenames match pm0's
leaderboard links. Each page reuses the shared header/footer chrome plus a new
camera.tmpl that renders an HTML-escaped camera heading, a pre-built thumbnail
grid, and a back-to-stats link.
Thumbnails reuse preview.tmpl's markup style (thumb image from thumbs/) but link
to the full-size image under photos/ rather than a per-photo view page, since the
view-page name is not derivable outside the album pagination loop. The photos and
thumbs dir names are fixed to match generate()'s render_album_pages call.
Cameras are walked in LC_ALL=C label order for reproducible output, and cameras
with no recorded photos are skipped. Adds the camera_name/camera_thumbs render
field specs and a tests/cli.sh case covering per-camera files, slug collisions,
HTML escaping, thumbnail link targets, and determinism.
This provides the function + template + tests only; wiring into generate() and
the STATS_PAGE toggle remain for rm0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Review follow-up: _stats_keys_by_count_desc sorted the count tie-break
under the caller's locale, so the generated stats page could differ
byte-for-byte across locales/machines. Pin the sort with LC_ALL=C to keep
the static output reproducible, consistent with the project's seeded /
deterministic generation behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Render the aggregated STATS_* counters from the om0 module into a static
stats.html. render_stats_page builds the variable-length body (camera
leaderboard, temporal/exposure/dimension histograms, format and enum
breakdowns) as HTML and hands it to the new stats.tmpl through a raw
context field, wrapping it with the shared header/footer the way
view/details pages do. Bars are pure CSS so the page stays JavaScript-free.
- new template share/templates/default/stats.tmpl (page chrome + body)
- render_stats_page <html_dir> <backhref> [page_name] plus small section
builders in src/lib/stats.source.sh; EXIF-derived labels are HTML-escaped
- new TEMPLATE_RENDER_FIELD_SPECS field render_stats_body_html (context_raw)
and 'stats' added to render_backhref_html's required list
- camera leaderboard entries link to camera-<slug>.html (um0 owns those)
- Stats nav link wired into the shared header bar
- tests cover leaderboard links, counts/percentages, a histogram section,
&/< escaping, and omission of empty categories
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Review follow-up on the stats aggregation module:
- Distinct camera labels that sanitized to the same slug (e.g. two models
differing only in punctuation) shared one STATS_CAMERA_PHOTOS list and
one camera-<slug>.html link, so um0 would render a merged/ambiguous
per-camera page. Add _stats_resolve_camera_slug, which uniquifies
colliding slugs with a numeric suffix and tracks ownership in a new
STATS_SLUG_OWNERS reverse map. It returns via a nameref (not command
substitution) so the global mutation persists in the caller's shell.
- Align the stats ISO fallback order with album.source.sh's tooltip
builder (ISOSpeedRatings -> PhotographicSensitivity -> ISO) so a photo
with multiple ISO tags buckets the same value it displays.
- Document that the format breakdown trusts the file extension.
- Add test_stats_distinct_cameras_get_unique_slugs covering the col |