| Age | Commit message (Collapse) | Author |
|
Regression from or0 (STATS_* accessors): stats_category_max compared
values via `(( _stats_counts_ref[key] > max ))`. For a nameref to an
ASSOCIATIVE array, an arithmetic subscript is itself arithmetic-evaluated,
so a string key (e.g. "sony") resolves to an unset variable -> 0, i.e. it
always read index 0 and returned max 0. Every bar then scaled to
width:0% (counts/percentages were unaffected, so only the bars looked
broken). Read the value through a quoted ${assoc[$key]} expansion first,
then compare.
The byte-identical double-render test missed this because both renders
were equally broken; added explicit non-zero bar-width assertions
(100% / 50%) to test_render_stats_page_renders_sections_and_escapes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Pure, behavior-preserving refactor: extract _-prefixed helpers from six
functions that exceeded the project's 50-line threshold, leaving each
original as a thin orchestrator. Generated HTML, dry-run output,
shuriken.json, EXIF cache behavior and the flush-grid layout are all
byte-identical (full test suite green).
Refactored:
- _generation_metadata_json -> _generation_metadata_json_head +
_generation_metadata_json_settings
- print_dry_run_plan -> _print_dry_run_settings + _print_dry_run_files
- _photo_exif_tooltip_text_from_values -> _collect_exif_tooltip_parts +
_emit_exif_tooltip_parts
- cached_photo_identify_output -> _rebuild_photo_identify_cache
- render_album_pages -> _render_album_page (one page record)
- append_preview_grid -> _roll_and_align_page_tiles + _emit_page_tiles
(uniquely-named namerefs to avoid circular-nameref)
Left intact (delicate errexit/trap management that must stay in one
function scope, where a split would change semantics):
source_template_file, refresh_splash, generate_staged,
replace_dist_with_staging.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Consumers 5 (log_configured_action) and 6 (the --dry-run plan) are
consciously left bespoke: both are human-facing prose that interleaves a
curated subset of config fields (each with its own label and per-field
decoration) with non-config values (resolved rc_file path,
SHURIKEN_FORCE_GENERATE, computed image/page/redirect counts, planned
tarball name, and whole non-config 'Planned directories/files' sections).
Driving them from CONFIG_SPECS would require per-line label+format+marker
facets that contort the schema for no DRY benefit, since each string
appears exactly once.
They already read the canonical registry-driven globals, so CONFIG_SPECS
remains the single source of truth for the config schema; only the
presentation stays hand-written. Added comments to both explaining the
decision. No behavior change -- output stays byte-identical (asserted by
the effective-config log and dry-run plan tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Introduce src/lib/config.spec.source.sh: CONFIG_SPECS, a single '|'-delimited
config-field registry (name|default|has_default|cli_overridable|validation|
print_kind), using the same spec idiom as ACTION_SPECS / TEMPLATE_RENDER_FIELD_SPECS.
This replaces the parallel, hand-maintained config-knowledge lists that caused the
TARBALL_INCLUDE default-drift bug (fixed in 7r0).
Derived consumers (behaviour byte-identical):
- apply_config_defaults: loops the registry applying VAR="${VAR:-default}" for
has_default=yes scalars; arrays keep their declare -p guards.
- CLI_CONFIG_OVERRIDE_TARGETS: built from cli_overridable=yes (verified to match
CLI_OPTION_SPEC's config= targets exactly).
- print_config: emits in registry order dispatching on print_kind.
- validate_common_config: required set + per-field rule come from the registry via
config_spec_validation + validate_config_field_required/_kind; two-phase order and
historical reporting order preserved.
log_configured_action and the dry-run plan are not yet converted (bespoke prose /
intermixed computed values). shellcheck: TARBALL_SUFFIX lost its visible literal
assignment, annotated at the archive use site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Move the safety-critical rm -rf guard (resolve_dist_dir_path and
validate_clean_dist_dir, including the dangerous-path blocklist) out of
config.validate.source.sh into a dedicated module so the policy that gates
an unconditional rm -rf lives in one isolated place with its own test
surface. This is a pure move refactor: the guard logic, the forbidden
list, and the error messages are byte-identical.
Register the new module in both LIB_SOURCES lists (Justfile and the
src/shuriken.sh marker block) at the same position, right before
config.validate.source.sh, keeping the anti-drift invariant green and
regenerating bin/shuriken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The EXIF cache dir ($(dirname "$DIST_DIR")/cache/exif) was recomputed inline,
byte for byte, in both cached_photo_identify_output (read/write) and
clear_exif_cache (--force/--clean removal). The plain parent of DIST_DIR
(dirname "$DIST_DIR") was likewise recomputed in metadata-cache and in
action.source.sh's staging-artifact cleanup.
Extract two helpers computed once from DIST_DIR:
- working_dir() in config.source.sh (next to existing_parent_dir, the other
DIST_DIR-parent resolver): plain `dirname "$DIST_DIR"`.
- exif_cache_dir() in metadata-cache.source.sh (owns the EXIF cache):
`working_dir()/cache/exif`.
Route cached_photo_identify_output, clear_exif_cache, and
clean_generation_staging_artifacts through them so the cache reader and the
cleaner can never drift to different directories. Paths are byte-identical to
the prior inline code (dirname semantics, cache/exif suffix, trailing-slash and
relative/absolute handling all preserved). Stale "recompute the cache dir"
comments removed; helpers document the path once. Add a unit test asserting the
helpers agree and resolve beside dist for several DIST_DIR shapes.
Broader DIST_DIR parameterization of leaf pipeline helpers was intentionally
left out of scope (only the duplicated path computation is centralized here).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
collect_dry_run_page_plan re-derived page_count via the ceil formula and
redirect_count via a magic "*4+2", duplicating logic owned by
album_page_records (pagination) and render_page_view_redirects (redirect
files). The preview could silently drift from a real --generate.
Single source of truth:
- album_page_count_for_image_count (album-photo-select.source.sh) owns the
MAXPREVIEWS-per-page grouping count that album_page_records realises.
- ALBUM_REDIRECTS_PER_PAGE=4 / ALBUM_REDIRECTS_LAST_PAGE_EXTRA=2 +
album_redirect_count_for_page_count (album-render.source.sh) own the
per-page (4) plus last-page-extra (2) redirect tally that
render_page_view_redirects actually emits.
dry-run now predicts both counts through these helpers (no dist files
touched, side-effect free). Confirmed the real redirect count is
page_count*4+2, so output is byte-identical: partial-final-page (3 preview
pages / 14 navigation redirects) and empty album (0 / 0) unchanged.
Regenerated bin/shuriken; just test/shellcheck/check-generated all pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
The stats reader modules (stats-render.source.sh, stats-filter-album.source.sh)
indexed stats-aggregate.source.sh's private STATS_* associative arrays directly,
so a key-convention change in the aggregator would silently break both readers.
Add a read API owned by stats-aggregate.source.sh (the data owner), mirroring
album-render's ALBUM_VIEW_PAGE_BY_PHOTO / album_view_page_for_photo split:
stats_total_photos - STATS_TOTALS[photos] denominator
stats_filter_pagebase - (prefix,label) -> pagebase, hiding the
STATS_FILTER_KEYSEP catkey encoding
stats_filter_title - STATS_FILTER_TITLE[pagebase]
stats_filter_photos - STATS_FILTER_PHOTOS[pagebase] list
stats_filter_count - number of filter mini-albums
stats_filter_pagebases - pagebases, LC_ALL=C-sorted (order owned here)
stats_category_count/size/max/keys_by_count_desc
- per-category count-array reads
Route every cross-module STATS_* read through these accessors. The render
module's _stats_max_count / _stats_keys_by_count_desc were duplicates of the new
stats_category_max / stats_category_keys_by_count_desc, so they are removed.
Behavior preserved exactly: missing-key semantics, iteration order (ordered
ladders, calendar months, count-desc with LC_ALL=C tie-break, sorted pagebase
enqueue) and generated HTML are byte-identical. Header docs updated to describe
the accessor boundary. The arrays stay the backing store; only the cross-module
READ path is encapsulated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
_stats_render_category dispatched on render_kind via a four-arm case
(camera|ranked|ordered|month), a modification magnet inconsistent with
the codebase's other data-driven dispatch (the STATS_RECORD_FUNCTIONS
registry and template.source.sh's prepare_template_render_var__<kind>
declare -F lookup).
Resolve the per-kind renderer by name instead: dispatch to
_stats_render_section__<render_kind> via `declare -F`, guarded so a
missing handler fails loudly (and so set -euo pipefail's errexit does
not trip on declare -F's non-zero "absent" status). Rename the three
section renderers to the _stats_render_section__{ranked,ordered,month}
convention.
Fold the camera kind into ranked: the camera leaderboard differed from
an ordinary ranked section only by the extra 'stats-leaderboard' <ul>
class -- data, not logic -- so it becomes a 'ranked' entry carrying that
class in a new optional 5th spec field (list_class), eliminating the
camera special case. Stats body output is byte-identical before/after
for every render kind (verified via snapshot diff); the existing stats
tests stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Replace the hardcoded per-name case in
prepare_template_render_var__config_html (HEIGHT/MAXPREVIEWS/
ORIGINAL_BASEPATH/SOURCE_URL/STATS_PAGE/THUMBHEIGHT/TITLE) with a single
indirect read of the spec's source_name: context_value=${!source_name-}.
The handler is now fully data-driven and never needs editing when a new
config render-var is added (Open/Closed), matching the module's
spec-driven dispatch. source_name is already passed to every handler by
prepare_template_render_vars, so no dispatch contract changed.
Output is byte-identical for all reachable states: apply_config_defaults
always runs before render (HEIGHT/ORIGINAL_BASEPATH/SOURCE_URL/STATS_PAGE/
TITLE always set, so the old defensive :- fallbacks like STATS_PAGE :-no
were unreachable), and the unset-safe '-' default reproduces the empty
result for refresh-only MAXPREVIEWS/THUMBHEIGHT.
Dropping the literal ${TITLE:-} re-surfaced a previously-suppressed
SC2153 on $TITLE in generation-metadata.source.sh (lowercase 'title'
locals in the stats modules); add a targeted shellcheck disable there.
Regenerated bin/shuriken. just test/shellcheck/check-generated all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Under "set -euo pipefail" the pattern
cmd
status=$?
if (( status != 0 )); then return "$status"; fi
is redundant for these positions: errexit already aborts on cmd's failure
with cmd's exact exit code before the status check could run. Replace it
with bare calls, dropping the now-pointless "local -i status=0" and the
status-capture boilerplate (and the stale comments describing it).
Deliberately NOT using "cmd || return $?" here: main -> run_action ->
run_configured_action -> generate_staged relies on errexit staying ACTIVE
so generate_staged's internal "set -e" parallel-job failure detection
fires. Putting the call in a "||" list suppresses inner errexit (the gotcha
documented in album.source.sh's splash-render note) and lets a failing job
sail past. Bare calls preserve the exact exit codes and step ordering.
bin/shuriken regenerated from src/ via "just build".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Wrap each rsync in sync_dist in run_with_timeout (new SYNC_TIMEOUT config,
default 300s) so a hung or unreachable mirror cannot block the whole sync,
matching every other external call. Make destinations isolated: under
set -euo pipefail a single failing destination used to abort the loop and
silently skip the rest. Now each destination runs under a localized set +e
(the project's refresh_splash idiom), results are collected per destination,
a clear pass/fail summary is logged, and sync returns non-zero if any
destination failed while still attempting all of them.
SYNC_TIMEOUT is plumbed like TAR_TIMEOUT: shuriken.default.conf,
apply_config_defaults, print_config, verbose config log, and positive-integer
validation in both validate_config (generate path) and validate_sync_config
(sync path). No CLI flag, matching TAR_TIMEOUT.
Tests: a sync where one destination fails still attempts the others and exits
non-zero with the summary; SYNC_TIMEOUT=0 is rejected as a positive integer.
Adds install_rsync_spy_failing_one helper and updates the print_config
expected blocks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
Running `bash src/shuriken.sh --generate ...` directly from a source
checkout printed "command not found" for camera_label_from_make_model,
photo_exif_values_to and cached_photo_identify_output for every photo,
silently emptying EXIF tooltips/details and, with STATS_PAGE=yes,
omitting the whole stats/ tree. The hand-maintained source list inside
the SHURIKEN_LIB_SOURCES_BEGIN/END marker block had drifted from the
authoritative Justfile LIB_SOURCES, missing metadata-label,
metadata-cache, stats-aggregate, stats-render and stats-filter-album.
`just build` replaces the marker block with LIB_SOURCES when generating
bin/shuriken, so the installed binary and the bin-based test suite never
noticed; only direct src execution was affected.
- Add the 5 missing `source` lines to the marker block in the same order
as Justfile LIB_SOURCES, so the two lists now match exactly.
- Add tests/cli.sh case test_lib_sources_match_justfile_lib_sources that
extracts the marker-block module names and asserts they equal the
Justfile LIB_SOURCES (same set and order) to prevent future drift.
- shellcheck --check-sourced now follows the 5 newly-sourced libs;
suppress the cross-module nameref false positives (SC2178/SC2128/
SC2154) with explained directives and genuinely fix SC2004
(counts_ref[$key] -> [key]) and quote the TITLE default (${TITLE:-}).
bin/shuriken changes only by these propagated lib edits; the marker-block
source list it generates is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
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>
|
|
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>
|
|
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).
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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, |