starting new job...
→ running command: cd "/etc/byggsteg/job-clone//byggsteg/trunk" && git reset --hard && git pull
HEAD is now at a3d0a22 chore: Update guile-veritas
From https://codeberg.org/jjba23/byggsteg
a3d0a22..7b86109 trunk -> origin/trunk
Updating a3d0a22..7b86109
Fast-forward
manifest.scm | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && uname -a
Linux wolk-jjba 7.2.11-gnu #1 SMP PREEMPT_DYNAMIC 1 x86_64 GNU/Linux
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && git log --name-status HEAD^..HEAD
commit 7b86109a5903d67208e40752578a63a3a9360a8d
Author: Josep Bigorra <jjbigorra@gmail.com>
Date: Mon Oct 26 13:24:31 2026 +0100
chore: Update guile-scriba
The pin moves to v0.4.5, a maintenance release: scriba's log-level suite called
(apply test ...), which Veritas cannot expand, so its test file stopped
compiling. Nothing in the library's API, behaviour or documentation changed,
so the shell this manifest describes is the same one with a test suite that
runs again.
🤖 Generated with [ECA](https://eca.dev) (deepseek/deepseek-flash)
Co-Authored-By: eca-agent <git@eca.dev>
M manifest.scm
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && maak test
substitute: looking for substitutes on 'https://bordeaux.guix.gnu.org'... 100.0%
substitute:
substitute: looking for substitutes on 'https://bordeaux.guix.gnu.org'... 0.0%
substitute: looking for substitutes on 'https://bordeaux.guix.gnu.org'... 50.0%
substitute: looking for substitutes on 'https://ci.guix.gnu.org'... 100.0%
substitute:
substitute: looking for substitutes on 'https://ci.guix.gnu.org'... 0.0%
substitute: looking for substitutes on 'https://ci.guix.gnu.org'... 50.0%
The following derivations will be built:
/gnu/store/nxcnlywhfv099pcmm5v43a962s0jky9q-guile-scriba-0.4.5-checkout.drv
/gnu/store/bmv89pn0kqcpi3f5irqsi41nzbl1fjj4-guile-scriba-0.4.5.drv
/gnu/store/11zqn76i4jkr0vj9zkhsc8nbz0acqmdq-profile.drv
building /gnu/store/nxcnlywhfv099pcmm5v43a962s0jky9q-guile-scriba-0.4.5-checkout.drv...
building /gnu/store/bmv89pn0kqcpi3f5irqsi41nzbl1fjj4-guile-scriba-0.4.5.drv...
100% ▕█████████████████████████████████████████████████████████████████████████▏building CA certificate bundle...
12% ▕█████████▏ ▏
25% ▕██████████████████▎ ▏
38% ▕███████████████████████████▍ ▏
50% ▕████████████████████████████████████▌ ▏
62% ▕█████████████████████████████████████████████▋ ▏
75% ▕██████████████████████████████████████████████████████▊ ▏
88% ▕███████████████████████████████████████████████████████████████▉ ▏
100% ▕█████████████████████████████████████████████████████████████████████████▏
listing Emacs sub-directories...
building fonts directory...
building directory of Info manuals...
building profile with 21 packages...
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; compiling ./test/veritas/unit/analytics-spec.scm
;;; compiling ./test/veritas/support/deployment.scm
;;; compiling ./lib/byggsteg/preferences.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/preferences.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/support/deployment.scm.go
;;; compiling ./lib/byggsteg/html.scm
;;; compiling ./lib/byggsteg/permissions.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/permissions.scm.go
;;; compiling ./lib/byggsteg/version.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/version.scm.go
;;; compiling ./lib/byggsteg/job/pipeline.scm
;;; compiling ./lib/byggsteg/artifacts.scm
;;; compiling ./lib/byggsteg/upkeep.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/upkeep.scm.go
;;; compiling ./lib/byggsteg/domain/artifact.scm
;;; compiling ./lib/byggsteg/domain/repo.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/repo.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/artifact.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/artifacts.scm.go
;;; compiling ./lib/byggsteg/job/steps.scm
;;; compiling ./lib/byggsteg/container.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/container.scm.go
;;; compiling ./lib/byggsteg/job/runners.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/runners.scm.go
;;; compiling ./lib/byggsteg/job/streaming.scm
;;; compiling ./lib/byggsteg/domain/log.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/log.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/streaming.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/steps.scm.go
;;; compiling ./lib/byggsteg/job/tasks.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/tasks.scm.go
;;; compiling ./lib/byggsteg/domain/job.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/job.scm.go
;;; compiling ./lib/byggsteg/sqlite/job.scm
;;; compiling ./lib/byggsteg/sqlite/util.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/util.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/job.scm.go
;;; compiling ./lib/byggsteg/sqlite/log.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/log.scm.go
;;; compiling ./lib/byggsteg/sqlite/artifact.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/artifact.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/pipeline.scm.go
;;; compiling ./lib/byggsteg/faces.scm
;;; compiling ./lib/byggsteg/ansi.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/ansi.scm.go
WARNING: (hygguile hygguile): imported module (hygguile components select) overrides core binding `select'
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/faces.scm.go
WARNING: (byggsteg html): `get-short-commit-sha' imported from both (hygguile hygguile) and (kracht prelude)
WARNING:
scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 359279794 (replay with VERITAS_SEED=359279794)
found unit tests: (analytics-spec.scm job-search-spec.scm log-poll-spec.scm form-width-spec.scm artifact-spec.scm ansi-spec.scm container-spec.scm error-pages-spec.scm logs-page-spec.scm job-pipelines-spec.scm sqlite-spec.scm password-reset-spec.scm job-pipeline-spec.scm retention-spec.scm terminal-glass-spec.scm log-streaming-spec.scm deployment-identity-spec.scm runners-spec.scm logs-live-spec.scm password-strength-spec.scm api-v2-spec.scm console-resources-spec.scm about-page-spec.scm tasks-spec.scm footer-version-spec.scm heatmap-spec.scm user-bootstrap-spec.scm retry-spec.scm)
running unit tests from file: analytics-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the page-view script a deployment may put on its pages
[🧪 test] a deployment that keeps its files elsewhere names that host
[⚙️ assert] the script moves to the asset host with everything else
✅ got: src="https://assets.example.org/js/symbanta.js"
[🧪 test] a deployment that reports nowhere puts the script on no page
[⚙️ assert] no page names the service
✅ got: symbanta
[🧪 test] and the deployment has the script it names
[⚙️ assert] pub/js/symbanta.js is there to be served
✅ got: #t
[🧪 test] a deployment with a service of its own reports there
[⚙️ assert] the page names that service
✅ got: data-endpoint="https://events.example.org/v1/events/register"
[⚙️ assert] at the rate that deployment asked for
✅ got: data-interval="9000"
[⚙️ assert] and not to the service this project names
✅ got: api.symbanta.jointhefreeworld.org
[🧪 test] a page reports itself to the service this project names
[⚙️ assert] the page carries a page-view script
✅ got: #t
[⚙️ assert] and it is the file this deployment serves
✅ got: src="/js/symbanta.js"
[⚙️ assert] which names the service it reports to
✅ got: data-endpoint="https://api.symbanta.jointhefreeworld.org/v1/events/register"
[⚙️ assert] and how often, in milliseconds
✅ got: data-interval="4000"
ℹ️ info - ran 1 tasks in: 0 seconds, 41010 microseconds
running unit tests from file: job-search-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] search
[🧪 test] a question is answered from the jobs, the logs and the profiles
[⚙️ assert] a word found in one log and one job title
✅ got: ((jobs (search-job-b)) (logs (search-log-a)) (profiles ()))
[⚙️ assert] a found job is handed to the log its output is in
✅ got: search-log-b
[⚙️ assert] the same word written in capitals finds the same things
✅ got: ((jobs (search-job-b)) (logs (search-log-a)) (profiles ()))
[⚙️ assert] a word found in a profile's data is answered too
✅ got: search-profile-a
[🧪 test] the wildcards a reader types are searched for, not applied
[⚙️ assert] a percent sign finds the row that has one, not every row
✅ got: ((jobs (search-job-c)) (logs (search-log-b)) (profiles ()))
[⚙️ assert] an underscore finds the row that has one, not any character
✅ got: ((jobs (search-job-d)) (logs (search-log-c)) (profiles ()))
[🧪 test] a blank question asks for nothing rather than for everything
[⚙️ assert] an empty query, a query of spaces, and no query at all
✅ got: ((() () ()) (() () ()) (() () ()))
[🧪 test] each group of an answer is read up to the limit
[⚙️ assert] a letter most rows hold, answered twice over
✅ got: ((jobs 2) (logs 2) (profiles 1))
[⚙️ assert] and without a limit, every row that holds it
✅ got: ((jobs 4) (logs 3) (profiles 1))
[🧪 test] an excerpt is the part of a log the reader came for
[⚙️ assert] the window, its cuts, its one line, and where it starts
✅ got: ((holds-the-match #t) (marks-the-cut-before #t) (marks-the-cut-after #t) (is-one-line #t) (short-enough-to-read #t) (a-match-at-the-start-is-not-cut #t) (a-text-nothing-matches-begins-at-its-start #t))
ℹ️ info - ran 1 tasks in: 1 seconds, -975249 microseconds
running unit tests from file: log-poll-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] what a page following a job is answered with
[🧪 test] a job that is still writing is answered with its output and its state
[⚙️ assert] the answer carries the status, the mark of work in progress, and the output
✅ got: (#t #t #t #t #t)
[⚙️ assert] and it is closed where its place ends, with nothing left open
✅ got: />
[🧪 test] a job that is over is answered with the run's own readings
[⚙️ assert] the answer says the job stopped, and how it went
✅ got: (#t #t #t)
[⚙️ assert] and the mark of work in progress is left empty, and hidden
✅ got: (#t #f)
[⚙️ assert] and the output is not sent again to say it has not changed
✅ got: (#f #f)
[⚙️ assert] a job that failed says so where the page reads its status
✅ got: job failed
[🧪 test] the retry a finished job allows arrives with the answer
[⚙️ assert] a reader who may submit a retry is drawn one
✅ got: (#t #t)
[⚙️ assert] a reader who may not is drawn an empty place rather than a form
✅ got: (#t #f #t)
ℹ️ info - ran 1 tasks in: 0 seconds, 3590 microseconds
running unit tests from file: form-width-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the width of a field
[🧪 test] no page of the console forces a width
[⚙️ assert] the run-job form holds no minimum width
✅ got: ()
[⚙️ assert] the profile form holds no minimum width
✅ got: ()
[⚙️ assert] the search form holds no minimum width
✅ got: ()
[⚙️ assert] the locale chooser holds no minimum width
✅ got: ()
[🧪 test] every field is allowed to be as narrow as the screen
[⚙️ assert] every control of the run-job form may shrink
✅ got: ()
[⚙️ assert] every control of the profile form may shrink
✅ got: ()
[⚙️ assert] every control of the search form may shrink
✅ got: ()
[⚙️ assert] every control of the locale chooser may shrink
✅ got: ()
[🧪 test] every field is dressed in the console's own faces
[⚙️ assert] the job field is a field of this console
✅ got: ()
[⚙️ assert] so is the profile field
✅ got: ()
[⚙️ assert] so is the search field
✅ got: ()
[⚙️ assert] so is the locale chooser
✅ got: ()
[🧪 test] the field a job is written in is the console's own, not a copy
[⚙️ assert] the job field carries the face's own height
✅ got: #t
[⚙️ assert] and is as wide as the room it is given
✅ got: #t
[⚙️ assert] and no wider than a reading line
✅ got: #t
[⚙️ assert] with no width of its own written at the call site
✅ got: max-w-196
[⚙️ assert] the address the form leads to is held by a box that may break it
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 88829 microseconds
running unit tests from file: artifact-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] artifacts
[🧪 test] the table a collection writes to is the table the migrations make
[⚙️ assert] the artifact migration adds the artifact table
✅ got: (id job_id title artifact_path artifact_size created_at updated_at)
[🧪 test] a collected tarball is recorded, and the job it was built by finds it
[⚙️ assert] an artifact is read back by its id
✅ got: #t
[⚙️ assert] the artifact keeps the job that collected it
✅ got: spec-artifact-job
[⚙️ assert] the artifact keeps the size it was written with
✅ got: 2048
[⚙️ assert] both collections of one job are its artifacts
✅ got: (((id . spec-artifact-1) (job-id . spec-artifact-job) (title . byggsteg-job.tar.gz) (artifact-path . /tmp/byggsteg-artifact-spec-14717/spec-artifact-job.tar.gz) (artifact-size . 2048) (created-at . 1793017538) (updated-at . #f)) ((id . spec-artifact-2) (job-id . spec-artifact-job) (title . byggsteg-job-2.tar.gz) (artifact-path . /tmp/byggsteg-artifact-spec-14717/spec-artifact-job-2.tar.gz) (artifact-size . 4096) (created-at . 1793017538) (updated-at . #f)))
[⚙️ assert] a job nothing was collected for draws no artifacts
✅ got: ()
[🧪 test] only a file inside the artifact directory may be downloaded
[⚙️ assert] no row outside the artifact directory is servable
✅ got: ()
[⚙️ assert] a recorded tarball inside it is servable
✅ got: #t
[⚙️ assert] a row with no path is not servable
✅ got: #f
[🧪 test] a job asks for one tarball of bare paths, or a bundle per name
[⚙️ assert] a list of paths is one unnamed bundle, in the order written
✅ got: ((#f dist/wikimusic-api docs/manual))
[⚙️ assert] each named bundle keeps its name and its paths
✅ got: ((binaries dist bin) (docs docs/manual))
[⚙️ assert] bare paths and bundles mix, and keep the order they were written in
✅ got: ((#f README.org) (binaries dist) (docs docs/manual))
[⚙️ assert] a bundle written as one list is a name and the paths behind it
✅ got: ((binaries dist bin))
[⚙️ assert] a name written as a symbol is read as the name it spells
✅ got: ((binaries dist))
[⚙️ assert] one path written without its list is still one path
✅ got: ((#f dist/app))
[⚙️ assert] a field of nothing asks for nothing
✅ got: ()
[⚙️ assert] a bundle that names no path is dropped rather than packed empty
✅ got: ()
[🧪 test] forgetting a job's artifacts takes the tarballs and the rows
[⚙️ assert] both go, and a row naming a file outside the directory leaves it alone
✅ got: ((forgotten 3) (rows-left 0) (tarballs-gone (#f #f)) (a-file-outside-the-directory-was-left-alone #t))
[🧪 test] a sweep removes the tarballs no row points at, and nothing else
[⚙️ assert] the old orphan goes; the fresh one, the named one, a name byggsteg never writes and a directory stay
✅ got: ((removed (/tmp/byggsteg-artifact-spec-14717/orphan-old.tar.gz)) (the-old-orphan-is-gone #f) (the-fresh-one-is-kept #t) (one-a-row-still-names-is-kept #t) (a-name-byggsteg-never-writes-is-left #t) (a-directory-is-left #t))
[🧪 test] a download is offered under a name a header can carry
[⚙️ assert] a title becomes a plain file name
✅ got: my-build--trunk-..-x.tar.gz
[⚙️ assert] the second tarball of one job is numbered
✅ got: byggsteg-job-2.tar.gz
[⚙️ assert] a named bundle is offered under its own name
✅ got: (binaries.tar.gz binaries-2.tar.gz docs-manual.tar.gz)
[⚙️ assert] a size is read in the unit a reader can hold in mind
✅ got: 2.0 KiB
ℹ️ info - ran 1 tasks in: 0 seconds, 25717 microseconds
running unit tests from file: ansi-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] ANSI console output
[🧪 test] the escapes a terminal would act on are read, and text is not
[⚙️ assert] plain text and its newlines are left whole
✅ got: (hello
world)
[⚙️ assert] an output that ends on a newline ends on that newline
✅ got: (a
)
[⚙️ assert] the text of a run is escaped for the serializer
✅ got: (a <b> & c)
[⚙️ assert] no escape byte survives into the markup
✅ got: [0m
[⚙️ assert] a title wrapped in an OSC is dropped, its text kept
✅ got: (abc)
[🧪 test] a line is drawn where the cursor is, not appended to
[⚙️ assert] a carriage return overwrites the line it is on
✅ got: (cd)
[⚙️ assert] a second frame shorter than the first leaves its tail
✅ got: (xycdef)
[⚙️ assert] a backspace steps the cursor back over what it wrote
✅ got: (bacspace)
[⚙️ assert] a tab advances to the next stop, eight columns on
✅ got: (tab end)
[🧪 test] what a progress line erases, it erases
[⚙️ assert] erase-in-line clears from the cursor to the end
✅ got: (done)
[⚙️ assert] clear-line clears the whole line
✅ got: (def)
[⚙️ assert] a line drawn again is the frame left on the screen
✅ got: (substitute: looking for substitutes... 100.0%)
[🧪 test] colour is asked for, changed and put back
[⚙️ assert] a weight is a class and a colour is this console's own
✅ got: ((span (@ (class font-bold)) bold) and (span (@ (class dark:text-asparagus-300 text-asparagus-700)) green))
[⚙️ assert] a reset puts the weight and the colour back
✅ got: ((span (@ (class font-bold)) loud) quiet)
[⚙️ assert] a bright colour takes the bright half of the palette
✅ got: ((span (@ (class dark:text-redwood-100 text-redwood-500)) red))
[⚙️ assert] the decorations a class can carry are carried for both themes
✅ got: ((span (@ (class italic line-through underline)) deco))
[🧪 test] the light end of the palette is the palette's own
[⚙️ assert] white ink is the pale end of the stone ramp
✅ got: ((span (@ (class dark:text-stone-500 text-onyx-500)) white))
[⚙️ assert] and its bright half the step above it, not the sheet's white
✅ got: ((span (@ (class dark:text-stone-300 text-onyx-300)) bright))
[⚙️ assert] a white ground is the console's panel paint, its bright half the paper
✅ got: ((span (@ (class bg-stone-300)) white) (span (@ (class bg-stone-100)) bright))
[⚙️ assert] and no colour of the sixteen is the browser's white or black
✅ got: bg-white
[⚙️ assert] nor the ink it draws its own text in
✅ got: text-white
[⚙️ assert] and nothing at all is grounded in the sheet's black
✅ got: bg-black
[⚙️ assert] nor drawn in it
✅ got: text-black
[🧪 test] a colour a class cannot name is written where it is exact
[⚙️ assert] a 256-colour value is the cube's own red
✅ got: ((span (@ (style color:#ff0000)) red))
[⚙️ assert] a grey of the ramp is the grey it names
✅ got: ((span (@ (style background-color:#303030)) grey))
[⚙️ assert] true colour is the colour it says, to the byte
✅ got: ((span (@ (style color:#123456)) true))
[⚙️ assert] a colour and a ground are both written
✅ got: ((span (@ (style color:#5fd7ff;background-color:#303030)) both))
[🧪 test] a line of output read as prose carries no escape and no earlier frame
[⚙️ assert] the escapes are dropped and what they coloured is kept
✅ got: red
[⚙️ assert] a line painted again comes back as the reading it ended in
✅ got: second
[⚙️ assert] and text an escape does not touch is left as it stands
✅ got: hello
world
[🧪 test] the bytes a command sent are the reading that spells them out
[⚙️ assert] an escape is spelled the way a terminal would print it
✅ got: ^[[31mred^[[0m
[⚙️ assert] a carriage return is shown rather than obeyed
✅ got: first^Msecond
[⚙️ assert] and the line break is still the line break
✅ got: a
b
[⚙️ assert] and a character outside ASCII is a character, not a byte
✅ got: café ▮ 日本語
[🧪 test] a pane reads its output the way the page is answered
[⚙️ assert] the pane draws a coloured run as a span
✅ got: #t
[⚙️ assert] the pane carries no escape into the markup
✅ got: [0m
[⚙️ assert] the pane redraws a progress line rather than keeping both
✅ got: #t
[⚙️ assert] and what the reader is shown is the reading it ended in
✅ got: >second<span class="ml-2
[⚙️ assert] while the reading it was painted over is folded out of sight
✅ got: >first<
[⚙️ assert] and the pane's own control is what unfolds it
✅ got: data-frames-toggle
ℹ️ info - ran 1 tasks in: 0 seconds, 24437 microseconds
running unit tests from file: container-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] Container Suite
[🧪 test] A podman build is the caller's, rootless
[⚙️ assert] keep-id maps the container's user to the caller
✅ got: #t
[⚙️ assert] the build is not root
✅ got: #f
[🧪 test] The job's environment reaches the container
[⚙️ assert] variables are passed with --env
✅ got: #t
[⚙️ assert] the job's own variables are passed as written
✅ got: #t
[⚙️ assert] a locale is named when the job named none
✅ got: #t
[🧪 test] A socket that exists is the one a client is told about
[⚙️ assert] DOCKER_HOST wins when the deployment set it
✅ got: /tmp/byggsteg-container-spec-14717.sock
[⚙️ assert] the job is handed one DOCKER_HOST
✅ got: #t
[⚙️ assert] the reaper is off unless a deployment wants it
✅ got: #t
[⚙️ assert] the reaper is told which socket is mounted
✅ got: #t
[🧪 test] The deployment says which runtime, and none is a real answer
[⚙️ assert] podman is taken at its word
✅ got: podman
[⚙️ assert] so is docker
✅ got: docker
[⚙️ assert] none is a runtime a deployment may name
✅ got: none
[🧪 test] A socket that is not there is not handed out
[⚙️ assert] A DOCKER_HOST that points at nothing is worse than none
✅ got: ()
[🧪 test] The image is the word before the shell's arguments
[⚙️ assert] the image is what -lc runs in
✅ got: example/build:1
[⚙️ assert] the shell is named as the entrypoint
✅ got: --entrypoint
[🧪 test] A deployment that names a runner runs the runtime as it
[⚙️ assert] the runtime is started as the other account
✅ got: #t
[⚙️ assert] and the account is named
✅ got: #t
[⚙️ assert] HOME is passed through sudo
✅ got: #t
[⚙️ assert] and so is the runtime directory of that account
✅ got: #t
[⚙️ assert] and the PATH the daemon was started with
✅ got: #t
[🧪 test] A locale the job names is left as it is
[⚙️ assert] the job's locale is passed
✅ got: #t
[⚙️ assert] and not overruled by the default
✅ got: #f
[🧪 test] An executable is found in PATH by its full path
[⚙️ assert] a program that is installed is found
✅ got: #t
[⚙️ assert] a program that is not is not
✅ got: #f
[⚙️ assert] an absolute name is not looked up in PATH
✅ got: #f
[🧪 test] A job that names its own runtime keeps it
[⚙️ assert] the job's own DOCKER_HOST is what the command sees
✅ got: #t
[⚙️ assert] and byggsteg does not add a second one
✅ got: #f
[🧪 test] The clone is mounted where it already is
[⚙️ assert] The workdir is named
✅ got: #t
[⚙️ assert] and mounted at the same path
✅ got: #t
[⚙️ assert] the shell is the entrypoint
✅ got: #t
[⚙️ assert] the command is a login shell's -c
✅ got: #t
[🧪 test] A job that names no image runs on the machine
[⚙️ assert] Nothing is wrapped when nothing asked for it
✅ got: maak test
[⚙️ assert] No image, no runtime
✅ got: maak test
[⚙️ assert] A runtime of none leaves the command alone
✅ got: maak test
[🧪 test] A docker build names the user to write as
[⚙️ assert] docker has no keep-id, so the identity is named
✅ got: #t
[⚙️ assert] and the podman-only flag is not passed
✅ got: #f
[🧪 test] The runtime an image is built with can be named per deployment
[⚙️ assert] BYGGSTEG_CONTAINER_IMAGE is the default image
✅ got: alpine
[⚙️ assert] and empty means no default image
✅ got: #f
ℹ️ info - ran 1 tasks in: 0 seconds, 28159 microseconds
running unit tests from file: error-pages-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] error pages
[🧪 test] no page closes an element on itself
[⚙️ assert] no element of a status page swallows what follows it
✅ got: ()
[🧪 test] Artanis is handed a page for each status
[⚙️ assert] every status has a handler that answers with a string
✅ got: ()
[⚙️ assert] a status the table does not name still gets a page
✅ got: #t
[⚙️ assert] and it names the status it was asked for
✅ got: 418 unexpected status
[🧪 test] each status answers with a page of this deployment's console
[⚙️ assert] the 400 page carries the chrome, the code and its words
✅ got: ()
[⚙️ assert] the 401 page carries the chrome, the code and its words
✅ got: ()
[⚙️ assert] the 404 page carries the chrome, the code and its words
✅ got: ()
[⚙️ assert] the 500 page carries the chrome, the code and its words
✅ got: ()
[🧪 test] a page a reader can act on
[⚙️ assert] a missing credential is answered with the way to sign in
✅ got: /login
[⚙️ assert] a path that answers nothing still leads to the job queue
✅ got: /logs
[⚙️ assert] a server-side failure wears the red hairline
✅ got: border-redwood-700
[⚙️ assert] a client error keeps the phosphor hairline
✅ got: border-redwood-700
[🧪 test] a page is built by the components rather than describing them
[⚙️ assert] no page carries a component call instead of its element
✅ got: ()
[⚙️ assert] the 404 page heads with the code and traces it in a pre
✅ got: ()
ℹ️ info - ran 1 tasks in: 0 seconds, 52746 microseconds
running unit tests from file: logs-page-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] reading a run of a list of runs
[🧪 test] a row of a list of runs is the run's
[⚙️ assert] the row leads to the page its output is read on
✅ got: /logs/spec-log-id
[⚙️ assert] and carries the log's own name
✅ got: spec run
[⚙️ assert] and the moment the run was taken, which is the job's
✅ got: Thu Jan 1 01:00:00 1970
[⚙️ assert] and how the run went, which is the job's too
✅ got: job succeeded
[🧪 test] a log whose job is gone is drawn as no row
[⚙️ assert] there is no moment, no status and no page for it to say, so it says nothing
✅ got:
[🧪 test] the form that removes a run posts the run it is about
[⚙️ assert] the run's page offers the form
✅ got: #t
[⚙️ assert] and what it posts is the run's own id, and nothing else
✅ got: ((job-id . spec-job-id))
[⚙️ assert] so the handler reads the run out of the submission
✅ got: spec-job-id
ℹ️ info - ran 1 tasks in: 0 seconds, 5591 microseconds
running unit tests from file: job-pipelines-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] job pipelines
[🧪 test] the jobs a request asks for are read from its data
[⚙️ assert] a list of jobs and a single job are read alike
✅ got: ((a-list-of-two 2) (a-list-of-one 1) (the-first-project-names-the-job byggsteg) (a-pipeline-of-unnamed-jobs pipeline) (a-pipeline-of-bare-commands trivial))
[🧪 test] every job of a pipeline is run, in the order it is written
succeeded?: #t
[⚙️ assert] both stages ran, the first before the second
✅ got: ((the-first-job-ran #t) (the-second-job-ran #t) (in-the-order-they-are-written #t) (outcome (1 0 0)))
[🧪 test] a job that fails stops the pipeline
succeeded?: #f
[⚙️ assert] the job after the failure did not run, and the pipeline failed
✅ got: ((the-later-job-ran #f) (outcome (0 0 1)))
[🧪 test] a task that fails stops the tasks behind it in the same job
succeeded?: #f
[⚙️ assert] the task behind the failure did not run, and the job failed
✅ got: ((the-failing-task-ran #t) (the-task-behind-it-ran #f) (outcome (0 0 1)))
[🧪 test] a command's output outside ASCII reaches the log it wrote
succeeded?: #t
[⚙️ assert] the accented word, the block and the Japanese word are all there
✅ got: ((the-line-is-what-the-command-printed #t) (a-replacement-character #f) (outcome (1 0 0)))
[🧪 test] a job's commands read text in a locale of their own
succeeded?: #t
succeeded?: #t
[⚙️ assert] a command with no locale counts characters, and one a job named counts bytes
✅ got: ((the-characters-of-a-word #t) (the-bytes-of-it #f) (a-locale-the-job-names-itself #t) (outcome (1 0 0)))
[⚙️ assert] a locale a deployment names, or an empty naming, is read as it is
✅ got: ((a-deployment-that-names-none #t) (a-deployment-that-names-its-own LANG=de_DE.UTF-8) (an-empty-naming #t) (a-name-both-of-them-set PATH=/usr/bin LC_ALL=C) (the-environment-a-command-is-handed PATH=/usr/bin LC_ALL=C.UTF-8))
[🧪 test] every task of a job is run, and each is a step of its own
succeeded?: #t
[⚙️ assert] a job that asks for two tasks ran two, and the job passed
✅ got: ((how-many-tasks-ran 2) (outcome (1 0 0)))
[🧪 test] a pipeline of one job is a job
succeeded?: #t
[⚙️ assert] a single job's commands ran and its row says so
✅ got: ((the-job-ran #t) (outcome (1 0 0)))
[🧪 test] a stage that names several bundles collects a tarball for each
succeeded?: #t
[⚙️ assert] the bare path's tarball keeps the title, and each bundle its name
✅ got: ((how-many 3) (named-as-asked (spec-spec-collecting.tar.gz binaries-2.tar.gz docs-3.tar.gz)) (the-tarballs-are-there (#t #t #t)) (outcome (1 0 0)))
ℹ️ info - ran 1 tasks in: 0 seconds, 297360 microseconds
running unit tests from file: sqlite-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] SQLite Suite
[🧪 test] A row written is a row read, after the collector has run
[⚙️ assert] the twelve jobs are there, and each is the one that was written
✅ got: (spec job 12 spec job 11 spec job 10 spec job 9 spec job 8 spec job 7 spec job 6 spec job 5 spec job 4 spec job 3 spec job 2 spec job 1)
[🧪 test] The same statement answers again and again
[⚙️ assert] twenty readings of one statement all answer the same row
✅ got: (spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1 spec job 1)
[⚙️ assert] and a statement nothing has bound yet still answers with its placeholders taken
✅ got: ()
[⚙️ assert] a write after twenty reads still writes
✅ got: spec job 1, updated
[🧪 test] A statement is reused after it has done its work
[⚙️ assert] the jobs a filter finds are the jobs it found the time before
✅ got: 0
[⚙️ assert] and a job that is in progress is the one the filter finds
✅ got: (spec job 2)
[⚙️ assert] a row removed is gone, and the rest are still there
✅ got: (sqlite-spec-1 sqlite-spec-10 sqlite-spec-11 sqlite-spec-12 sqlite-spec-2 sqlite-spec-4 sqlite-spec-5 sqlite-spec-6 sqlite-spec-7 sqlite-spec-8 sqlite-spec-9)
ℹ️ info - ran 1 tasks in: 0 seconds, 418086 microseconds
running unit tests from file: password-reset-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] a password-reset link
[🧪 test] a link is worth following for the week after it was made
[⚙️ assert] yesterday is usable, the last instant of the week is usable, the instant after is not
✅ got: ((made-a-minute-ago #t) (made-yesterday #t) (made-inside-the-week #t) (made-exactly-a-week-ago #f) (made-a-week-and-a-day-ago #f))
[🧪 test] a link this deployment cannot date is not followed
[⚙️ assert] no row, no date and a date that is not a number all answer the same as an old link
✅ got: ((no-such-token #f) (a-row-without-a-date #f) (a-row-whose-date-is-not-an-instant #f))
ℹ️ info - ran 1 tasks in: 0 seconds, 17263 microseconds
running unit tests from file: job-pipeline-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] job pipeline
[🧪 test] job data that is not a thunk is refused, not applied
[⚙️ assert] job data that is not a procedure raises
✅ raised an error as expected
[⚙️ assert] a refused job is left for the sweep to fail
✅ got: ()
[🧪 test] the sweep fails what it cannot run, and runs a job with no log
could not run job spec-unparseable: read-error (#f "#<unknown port>:1:21: unexpected end of input while searching for: ~A" (#\)) #f)
could not run job spec-empty-job-data: misc-error (#f "byggsteg: job data did not evaluate to a procedure ~S" ("spec-empty-job-data") #f)
could not run job spec-queued-not-a-thunk: misc-error (#f "byggsteg: job data did not evaluate to a procedure ~S" ("spec-queued-not-a-thunk") #f)
succeeded?: #t
succeeded?: #t
[⚙️ assert] every job ended as it should, and the logs are right
✅ got: ()
ℹ️ info - ran 1 tasks in: 3 seconds, 77149 microseconds
running unit tests from file: retention-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] job retention
[🧪 test] a run that has outlived the retention is removed whole
[⚙️ assert] the tarball, its row, the log and the job all go, and the younger runs stay
✅ got: ((due-for-pruning (spec-old-run)) (pruned (spec-old-run)) (the-old-run-is-gone #f) (its-log-went-with-it #f) (its-tarball-row-went-with-it 0) (its-tarball-file-went-with-it #f) (the-run-from-yesterday-is-kept #t) (its-log-is-kept #t) (its-tarball-is-kept #t) (the-run-still-going-is-kept #t) (its-tarball-is-kept-too #t))
[🧪 test] a pass removes no more than the batch it was given
[⚙️ assert] the oldest goes first, the run behind it waits for the next pass
✅ got: ((due-asked-for-one (spec-oldest-run)) (due-asked-for-two (spec-oldest-run spec-older-run)) (a-pass-bounded-to-one (spec-oldest-run)) (the-run-behind-it-is-still-there #t) (and-the-next-pass-takes-it (spec-older-run)))
[🧪 test] a retention of 0 days keeps every run
[⚙️ assert] nothing is due, and a prune asked for anyway removes nothing
✅ got: ((nothing-is-due ()) (and-nothing-is-pruned 0))
[🧪 test] a run that is removed is removed whole
[⚙️ assert] the job, its log, its tarball's row and its tarball all go, and the run beside it stays
✅ got: ((the-removal-answers spec-removable-run) (the-job-is-gone #f) (its-log-went-with-it #f) (its-tarball-row-went-with-it 0) (its-tarball-file-went-with-it #f) (the-run-beside-it-is-still-there #t))
[🧪 test] a removal that raises part way leaves the run as it was
[⚙️ assert] the halves deleted before it raised are put back, down to the tarball
✅ got: ((the-removal-raised #t) (the-job-is-still-there #t) (its-log-is-still-there #t) (its-tarball-row-is-still-there 1) (its-tarball-file-is-still-there #t))
[🧪 test] a log whose job is gone is swept away
[⚙️ assert] the orphan goes, and the logs whose run is still there are left alone
✅ got: ((the-logs-whose-job-is-gone (log-spec-orphaned)) (the-orphan-is-gone #f) (the-logs-that-have-a-job-are-kept #t) (and-no-run-was-touched #t))
[🧪 test] a sweep removes no more than the batch it was given
[⚙️ assert] the oldest orphan goes first, the one behind it waits for the next sweep
✅ got: ((a-sweep-bounded-to-one (log-spec-elder-orphaned)) (the-one-behind-it-is-still-there #t) (and-the-next-sweep-takes-it (log-spec-younger-orphaned)))
(byggsteg html): imported module (hygguile hygguile) overrides core binding `select'
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/html.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/analytics-spec.scm.go
WARNING: (byggsteg html): `get-short-commit-sha' imported from both (hygguile hygguile) and (kracht prelude)
;;; compiling ./test/veritas/unit/job-search-spec.scm
;;; compiling ./lib/byggsteg/search.scm
;;; compiling ./lib/byggsteg/domain/profile.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/profile.scm.go
;;; compiling ./lib/byggsteg/sqlite/profile.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/profile.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/search.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/job-search-spec.scm.go
;;; compiling ./test/veritas/unit/log-poll-spec.scm
;;; compiling ./app/views/logs.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/app/views/logs.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/log-poll-spec.scm.go
;;; compiling ./test/veritas/unit/form-width-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/form-width-spec.scm.go
WARNING: (byggsteg html): imported module (hygguile hygguile) overrides core binding `select'
;;; compiling ./test/veritas/unit/artifact-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/artifact-spec.scm.go
;;; compiling ./test/veritas/unit/ansi-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/ansi-spec.scm.go
;;; compiling ./test/veritas/unit/container-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/container-spec.scm.go
;;; compiling ./test/veritas/unit/error-pages-spec.scm
;;; compiling ./lib/byggsteg/errors.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/errors.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/error-pages-spec.scm.go
;;; compiling ./test/veritas/unit/logs-page-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/logs-page-spec.scm.go
;;; compiling ./test/veritas/unit/job-pipelines-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/job-pipelines-spec.scm.go
;;; compiling ./test/veritas/unit/sqlite-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/sqlite-spec.scm.go
;;; compiling ./test/veritas/unit/password-reset-spec.scm
;;; compiling ./lib/byggsteg/domain/password-reset.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/password-reset.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/password-reset-spec.scm.go
;;; compiling ./test/veritas/unit/job-pipeline-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/job-pipeline-spec.scm.go
;;; compiling ./test/veritas/unit/retention-spec.scm
;;; compiling ./lib/byggsteg/job/retention.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/job/retention.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/retention-spec.scm.go
ℹ️ info - ran 1 tasks in: 0 seconds, 118855 microseconds
running unit tests from file: terminal-glass-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] terminal glass
[🧪 test] a pane closes its decorations and keeps its content outside them
[⚙️ assert] no element of a pane is closed on itself
✅ got: />
[⚙️ assert] the content sits two elements deep: the pane and its layer
✅ got: 2
ℹ️ info - ran 1 tasks in: 0 seconds, 3551 microseconds
running unit tests from file: log-streaming-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] job log streaming
[🧪 test] output reaches the log a batch at a time, whole
[⚙️ assert] the line that completes the batch is the one that writes
✅ got: ((calls-before-the-batch-completes 0) (text-held one
two
) (written-by-the-batch (one
two
three
)) (written-at-the-end (one
two
three
)) (calls-at-the-end 1))
[🧪 test] a buffer writes only when it has something new to say
[⚙️ assert] nothing gathered, and nothing gathered again, are not writes
✅ got: ((calls-of-an-empty-buffer 0) (first-flush (one
)) (calls-after-two-flushes 1))
[🧪 test] a command is announced the way a finished log announces it
[⚙️ assert] the banner names the command and starts its own line
✅ got: ((names-the-command #t) (starts-its-own-line #t))
[🧪 test] a running job's log row is refreshed while it runs
[⚙️ assert] two lines are held, and the batch that completes three is written
✅ got: ((before-the-batch ) (the-batch-is-in-the-row #t) (the-whole-log-is-in-the-row #t) (the-end-is-in-the-row #t))
[🧪 test] one job's log is the log that is written
[⚙️ assert] streaming into a job writes its log and no other
✅ got: ((the-job-log-is-written #t) (the-other-log-is-untouched ))
ℹ️ info - ran 1 tasks in: 0 seconds, 32936 microseconds
running unit tests from file: deployment-identity-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] what a deployment calls itself
[🧪 test] the name a deployment chose is the name it is read by
[⚙️ assert] the wordmark says it
✅ got: >buildbot
[⚙️ assert] the card a shared link is drawn as says it
✅ got: og:site_name" content="buildbot"
[⚙️ assert] and the JSON-LD a crawler reads says it
✅ got: "name":"buildbot"
[⚙️ assert] with no byggsteg left in the card of a renamed console
✅ got: og:site_name" content="byggsteg"
[⚙️ assert] nor in the crawler's reading of it
✅ got: "name":"byggsteg"
[🧪 test] the mark and the sentence are the deployment's own as well
[⚙️ assert] another deployment shows another mark
✅ got: https://example.org/mark.png
[⚙️ assert] and is announced with its own sentence
✅ got: a build server of our own
[🧪 test] an unnamed deployment is this project's own
[⚙️ assert] the project's name is the name by default
✅ got: byggsteg
[⚙️ assert] and its sentence of description is there to be announced
✅ got: #t
[⚙️ assert] the footer leads to the project's own source
✅ got: https://codeberg.org/jjba23/byggsteg
[⚙️ assert] and the landing page shows the project's own mark
✅ got: src="/img/byggsteg-logo.png"
[🧪 test] the places a page sends a reader are the deployment's own
[⚙️ assert] the footer's source link follows the deployment
✅ got: https://example.org/src
[⚙️ assert] the job form's guide follows it too
✅ got: https://example.org/src
[⚙️ assert] the footer's manual link follows it
✅ got: https://example.org/src/manual
[⚙️ assert] and so do the links the account of it ends with
✅ got: https://example.org/src
[⚙️ assert] and a renamed console sends nobody to codeberg
✅ got: codeberg.org
[⚙️ assert] nor in the crawler's reading of where to get it
✅ got: codeRepository":"https://codeberg.org
ℹ️ info - ran 1 tasks in: 0 seconds, 138412 microseconds
running unit tests from file: runners-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] Runners Suite
[🧪 test] Every runner in the table is named once, and names a program
[⚙️ assert] No runner is named twice
✅ got: 12
[⚙️ assert] A runner's name is one word, since it is a command
✅ got: #t
[🧪 test] A name may carry the runner it wants
[⚙️ assert] a bare name asks the checkout
✅ got: (#f . test)
[⚙️ assert] just- asks Just
✅ got: (just . test)
[⚙️ assert] npm- asks npm
✅ got: (npm . test)
[⚙️ assert] a target that itself begins with a runner's name is still reachable
✅ got: (make . just-deploy)
[⚙️ assert] a machine task is not split
✅ got: (#f . pull-and-restart)
[🧪 test] The order is the table's, and Make is the fallback
[⚙️ assert] a justfile wins over a Makefile
✅ got: just
[⚙️ assert] a maak.scm wins over a justfile
✅ got: maak
[⚙️ assert] a Makefile wins over Cargo.toml, which wraps it
✅ got: make
[⚙️ assert] a lockfile names the package manager
✅ got: pnpm
[⚙️ assert] a clone with no marker is a Make checkout
✅ got: make
[⚙️ assert] and so is one that is not there
✅ got: make
[🧪 test] A runner answers with the words that run a target
[⚙️ assert] Make takes the target alone
✅ got: (make test)
[⚙️ assert] so does Just
✅ got: (just test)
[⚙️ assert] npm runs a script, so `run' stays
✅ got: (npm run test)
[⚙️ assert] and so does pnpm
✅ got: (pnpm run build)
[⚙️ assert] Deno's tasks are a subcommand
✅ got: (deno task test)
[⚙️ assert] Dune's targets are subcommands of their own
✅ got: (dune test)
[🧪 test] The checkout's files say which runner it is
[⚙️ assert] a maak.scm is a Maak checkout
✅ got: maak
[⚙️ assert] a justfile is a Just checkout
✅ got: just
[⚙️ assert] a Taskfile.yml is a go-task checkout
✅ got: task
[⚙️ assert] a maskfile.md is a Mask checkout
✅ got: mask
[⚙️ assert] a Rakefile is a Rake checkout
✅ got: rake
[⚙️ assert] a Makefile is a Make checkout
✅ got: make
[⚙️ assert] a dune-project is a Dune checkout
✅ got: dune
[⚙️ assert] a Cargo.toml is a Cargo checkout
✅ got: cargo
[⚙️ assert] a deno.json is a Deno checkout
✅ got: deno
[⚙️ assert] a pnpm-lock.yaml is a pnpm checkout
✅ got: pnpm
[⚙️ assert] a yarn.lock is a yarn checkout
✅ got: yarn
[⚙️ assert] a package.json is an npm checkout
✅ got: npm
ℹ️ info - ran 1 tasks in: 0 seconds, 23398 microseconds
running unit tests from file: logs-live-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] a list of runs while its runs are going
[🧪 test] a row says which job it is about, and whether that job can still change
[⚙️ assert] a job that is running is a row worth asking about
✅ got: (#t #t #t)
[⚙️ assert] a job that has stopped is not asked about again
✅ got: data-log-running="false"
[⚙️ assert] and neither is one that has been taken and not started
✅ got: data-log-running="true"
[🧪 test] the status is read in a place the answer can draw over
[⚙️ assert] a running row draws its status, and the mark of work in progress inside it
✅ got: (#t #t #t)
[⚙️ assert] a finished row draws the status without it
✅ got: (#t #f)
[🧪 test] the answer is the rows that were asked about
[⚙️ assert] each row of the answer names its job, its state and the place its status is read in
✅ got: (#t #t #t #t)
[⚙️ assert] and carries no output, which is a log's page business and not a list's
✅ got: spec data
[🧪 test] a run that fails while the list is open arrives with the retry for it
[⚙️ assert] the row offers the retry the list would have drawn
✅ got: (#t #t)
[⚙️ assert] and so does the answer that keeps the row current
✅ got: (#t #t)
[⚙️ assert] a reader who may not send one is drawn an empty place rather than a form
✅ got: (#t #f)
[🧪 test] a row that has just been taken is read as a run that is still to come
[⚙️ assert] its status says it is queued, and the answer keeps asking about it
✅ got: (#t #t)
[🧪 test] a run taken while the list is open arrives as a row of the list
[⚙️ assert] the run is drawn whole: its row, its state, its name and its page
✅ got: (#t #t #t #t)
[⚙️ assert] and with the controls its row would have been drawn with
✅ got: data-log-actions=""
[🧪 test] a run that is both asked about and newest is drawn once
[⚙️ assert] the row the list already has is answered with its readings, and is not drawn again
✅ got: 1
ℹ️ info - ran 1 tasks in: 0 seconds, 21025 microseconds
running unit tests from file: password-strength-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the strength of a new password
[🧪 test] the deployment keeps the passwords it says it keeps, and no others
[⚙️ assert] short, dull and repeated passwords are refused; a passphrase and a varied password are not
✅ got: (( empty () #f) (short weak (length variety) #f) (aaaaaaaaaaaaaaaa weak (variety repetition) #f) (twelveletters weak (variety) #f) (twelveletters1 fair (variety) #f) (TwelveLetters1 good () #t) (TwelveLetters1! strong () #t) (correct horse battery staple good () #t) (give me the long passphrase please good () #t))
[🧪 test] the form lists the rules the endpoint keeps
[⚙️ assert] every rule of the deployment is on the page
✅ got: #t
[🧪 test] the form's meter counts by the deployment's own numbers
[⚙️ assert] the length, the kinds and the passphrase length are the page's values rather than copies
✅ got: #t
[🧪 test] the form shows what it is reading
[⚙️ assert] the reading has an element of its own, and there is one bar to a reading
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 30740 microseconds
running unit tests from file: api-v2-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the JSON API under /api/v2
[🧪 test] every route the API registers is a call this deployment can answer
[⚙️ assert] every route of the API goes through `api-route', which is what answers its failures in its own shape
✅ got: ((POST . /api/v2/login) (POST . /api/v2/logout) (GET . /api/v2/me) (GET . /api/v2/permissions) (GET . /api/v2/deployment) (GET . /api/v2/tasks) (GET . /api/v2/jobs) (POST . /api/v2/jobs) (GET . /api/v2/jobs/{id}) (DELETE . /api/v2/jobs/{id}) (POST . /api/v2/jobs/{id}/retry) (GET . /api/v2/logs) (GET . /api/v2/logs/{id}) (GET . /api/v2/artifacts) (GET . /api/v2/artifacts/{id}) (GET . /api/v2/artifacts/{id}/download) (GET . /api/v2/profiles) (POST . /api/v2/profiles) (GET . /api/v2/profiles/{id}) (DELETE . /api/v2/profiles/{id}) (GET . /api/v2/users) (POST . /api/v2/users) (GET . /api/v2/users/{id}) (PATCH . /api/v2/users/{id}) (DELETE . /api/v2/users/{id}) (POST . /api/v2/password/resets) (GET . /api/v2/password/resets/{token}) (POST . /api/v2/password/resets/{token}) (POST . /api/v2/password/strength) (GET . /api/v2/search) (GET . /api/v2/activity) (POST . /api/v2/maintenance/sweep-artifacts) (POST . /api/v2/maintenance/prune-runs) (POST . /api/v2/maintenance/sweep-logs) (POST . /api/v2/preferences/locale))
[⚙️ assert] every method a route is registered in is one `server.allowedmethods' names
✅ got: ()
[⚙️ assert] the API answers the two verbs the console does not, so a removal and a change can be asked for
✅ got: (DELETE PATCH)
[🧪 test] a run is read as the page that draws it reads it
[⚙️ assert] the three flags a row carries are the four statuses a client is told
✅ got: ((finished-well succeeded) (failed failed) (still-going running) (taken-and-not-started queued))
[⚙️ assert] a run is its id, its title, its status, its two instants and the log it wrote
✅ got: ((id . spec-run) (title . spec run & its output) (status . succeeded) (created-at . 1791922811) (updated-at . 1791922999) (log-id . spec-log))
[⚙️ assert] a run whose log row is gone says so with a null rather than by leaving the field out
✅ got: ((id . spec-run) (title . spec run & its output) (status . succeeded) (created-at . 1791922811) (updated-at . 1791922999) (log-id . null))
[🧪 test] a log is read with the run behind it, or as a log of a run that is gone
[⚙️ assert] a log is its id, its title, the run it belongs to, its two instants and the size of its output
✅ got: ((id . spec-log) (title . spec run & its output) (run-id . spec-run) (created-at . 1791922811) (updated-at . null) (length . 61))
[⚙️ assert] the length of a log is the size of the output a pane polls with
✅ got: (61 0)
[⚙️ assert] the log of a run is found by the row that names it, and a run no log names has none
✅ got: (spec-log #f)
[⚙️ assert] a list of logs carries the run behind each of them
✅ got: (((log (id . spec-log) (title . spec run & its output) (run-id . spec-run) (created-at . 1791922811) (updated-at . null) (length . 61)) (run (id . spec-run) (title . spec run) (status . queued) (created-at . null) (updated-at . null) (log-id . spec-log))))
[⚙️ assert] a log whose run is gone is answered with a null for the run rather than dropped
✅ got: (null)
[🧪 test] the output of a run is read the two ways the console's two tabs read it
[⚙️ assert] the rendered reading is the output a terminal would have left, and the raw one is what the command wrote
✅ got: (green a needle in the output
second line ^[[32mgreen^[[0m a needle in the output
second line)
[🧪 test] a tarball and a saved profile are read as the pages that list them read them
[⚙️ assert] a tarball names the run it belongs to, its title, its size and a path on this deployment to fetch it
✅ got: ((id . spec-artifact) (run-id . spec-run) (title . spec run.tar.gz) (size . 224) (created-at . 1791922811) (download-url . /api/v2/artifacts/spec-artifact/download))
[⚙️ assert] a profile is its id, its title, the Lisp it holds and its two instants
✅ got: ((id . spec-profile) (title . spec profile) (data . (lambda () (list (cons (quote task) "needle")))) (created-at . 1791922811) (updated-at . null))
[🧪 test] an account is read without the two columns that make it sign-in-able
[⚙️ assert] an account is its id, its name, its address and the permissions the ACL knows
✅ got: ((id . spec-account) (username . spec) (email . spec@example.org) (permissions . #(byggsteg:job-create)))
[⚙️ assert] no answer of this API carries the hash of a password or the salt it was taken with
✅ got: (email id permissions username)
[⚙️ assert] the session says what the token was minted with, and when it stops being read
✅ got: ((permissions . #(byggsteg:job-create byggsteg:job-delete)) (expires-at . 1792692817))
[⚙️ assert] a token that carries no permissions is read as a token that holds none
✅ got: ((permissions . #()) (expires-at . null))
[🧪 test] a request's fields are read the same way whatever sent them
[⚙️ assert] a string, a number and a boolean are read as the text they carry, a field written twice as the first of them, and a field that is not there as nothing
✅ got: ((a-string a@b.c) (a-number 1) (yes true) (no #f) (written-twice first) (not-there #f))
[🧪 test] a list field is read as a list, and a field that said nothing as nothing
[⚙️ assert] an array, one value, a field written twice, and the difference between an empty list and a request that said nothing
✅ got: ((an-array (a b)) (one-value (a)) (written-twice (a b)) (empty-array ()) (not-there #f))
[🧪 test] a page is bounded rather than refused
[⚙️ assert] nothing, a limit of none, an ordinary page, the most a page may be, more than that, and a limit that is not a number
✅ got: ((nothing-at-all 24) (a-limit-of-none 1) (an-ordinary-page 5) (the-most-a-page-may-be 200) (more-than-that 200) (a-number-that-is-not-one 7))
[🧪 test] the guard asks what a token is missing, and is told it in the order it asked
[⚙️ assert] an endpoint that takes nothing, a token that holds it all, one that holds none, and one that holds half
✅ got: ((an-endpoint-that-takes-nothing ()) (a-token-that-holds-it-all ()) (a-token-that-holds-none (byggsteg:job-create byggsteg:job-delete)) (a-token-that-holds-one-of-two (byggsteg:job-create)))
[🧪 test] a deployment says what it is, what it speaks and what it may be asked for
[⚙️ assert] the names and addresses of a deployment are the ones this source ships, and the address it named is the one it is read by
✅ got: ((name . byggsteg) (description . byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host.) (logo-url . /img/byggsteg-logo.png) (source-url . https://codeberg.org/jjba23/byggsteg) (manual-url . https://codeberg.org/jjba23/byggsteg/src/branch/trunk/README.org) (documentation-url . https://jointhefreeworld.org/manuals/byggsteg/) (public-url . https://byggsteg.example.org) (asset-base-url . ))
[⚙️ assert] the shape of its API is the version, the address and the document that describes it
✅ got: ((version . 2) (base . /api/v2) (spec . /openapi.yaml) (documentation . https://codeberg.org/jjba23/byggsteg/src/branch/trunk/README.org))
[⚙️ assert] every reading this API has is named in the capabilities it answers with
✅ got: (activity artifacts deployment jobs logs maintenance password-resets permissions preferences profiles search tasks users)
[⚙️ assert] the four languages the console speaks are the four the chooser offers
✅ got: ((en_US . English (US)) (nl_NL . Nederlands) (es_ES . Español) (ja_JP . 日本語))
[⚙️ assert] a language is spoken when it is one of those, and unknown when it is not
✅ got: ((#t #t #t #t) (#f #f #f #f))
[⚙️ assert] the settings a deployment runs with are the ones it answers with
✅ got: ((days . 7) (interval-seconds . 3600) (batch . 300) (concurrency . 3) (result-limit . 11))
[🧪 test] the tasks a job may name are the ones the worker dispatches
[⚙️ assert] every task a client may write is answered with the name it is dispatched by
✅ got: (build test lint check fmt docs package migrate clean coverage benchmark smoke e2e audit maak-build maak-test maak-lint maak-check maak-fmt maak-docs maak-package maak-migrate maak-clean maak-coverage maak-benchmark maak-smoke maak-e2e maak-audit just-build just-test just-lint just-check just-fmt just-docs just-package just-migrate just-clean just-coverage just-benchmark just-smoke just-e2e just-audit make-build make-test make-lint make-check make-fmt make-docs make-package make-migrate make-clean make-coverage make-benchmark make-smoke make-e2e make-audit deploy maak-deploy make-deploy restart pull-and-deploy pull-and-restart byggsteg-version trivial)
[⚙️ assert] every one of them is described, since a client has to be told what it may ask for
✅ got: #t
[🧪 test] one question is read over the three tables it is asked of
[⚙️ assert] the word, the groups it was found in, and what a window of each reading was read from
✅ got: ((the-word needle) (runs-that-carried-it 0) (logs-that-carried-it (spec-log)) (profiles-that-carried-it (spec-profile)) (what-the-log-excerpt-is output) (the-excerpt-itself green a needle in the output second line) (the-profile-excerpt-is-read-from data))
[⚙️ assert] a query of nothing is three empty groups rather than the whole deployment
✅ got: ((jobs ()) (logs ()) (profiles ()))
[🧪 test] a year of runs is read as the grid that draws it
[⚙️ assert] the weeks, the months, the total, the busiest day, the columns of seven days, and a day that has not happened
✅ got: ((weeks 50) (months 12) (total 1) (busiest 1) (columns 50) (days 350) (the-run-is-drawn 1) (a-day-with-no-runs-is-a-day #t) (a-day-that-has-not-happened #t))
ℹ️ info - ran 1 tasks in: 0 seconds, 56975 microseconds
running unit tests from file: console-resources-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the console's own appearance, served by the deployment itself
[🧪 test] and serves every sheet it names itself
[⚙️ assert] the page names the console's own sheets, and no others
✅ got: (/css/fonts.css /css/lucida.css /css/phosphor.css /css/byggsteg.olive.min.css)
[⚙️ assert] and serves /css/fonts.css, which it has
✅ got: #t
[⚙️ assert] and serves /css/lucida.css, which it has
✅ got: #t
[⚙️ assert] and serves /css/phosphor.css, which it has
✅ got: #t
[⚙️ assert] and serves /css/byggsteg.olive.min.css, which it has
✅ got: #t
[⚙️ assert] and serves /img/byggsteg-logo.png, which it has
✅ got: #t
[🧪 test] and the Lucida sheet names faces the deployment has
[⚙️ assert] the sheet declares the Lucida faces
✅ got: #t
[⚙️ assert] and the deployment has LSANS.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LSANSI.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LSANSD.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LSANSDI.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LTYPE.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LTYPEO.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LTYPEB.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has LTYPEBO.woff to serve
✅ got: #t
[⚙️ assert] and the deployment has l_10646.woff to serve
✅ got: #t
[🧪 test] the Lucida in front of the stack is the deployment's to serve
[⚙️ assert] a page asks for the Lucida sheet by default
✅ got: /css/lucida.css
[⚙️ assert] a deployment that may not serve those faces asks for none
✅ got: /css/lucida.css
[⚙️ assert] and one with a sheet of its own is asked for that
✅ got: https://example.org/lucida.css
[⚙️ assert] which is asked for as it stands and not moved to an asset host
✅ got: https://assets.example.org/https://example.org
[🧪 test] no page of the console asks anybody else for it
[⚙️ assert] the page asks nobody at fonts.googleapis.com
✅ got: fonts.googleapis.com
[⚙️ assert] the page asks nobody at fonts.gstatic.com
✅ got: fonts.gstatic.com
[⚙️ assert] the page asks nobody at cdn.jsdelivr.net
✅ got: cdn.jsdelivr.net
[⚙️ assert] the page asks nobody at unpkg.com
✅ got: unpkg.com
[⚙️ assert] the page asks nobody at cdnjs.cloudflare.com
✅ got: cdnjs.cloudflare.com
[⚙️ assert] the page asks nobody at /static-assets/
✅ got: /static-assets/
[🧪 test] a deployment that keeps its assets elsewhere names that host
[⚙️ assert] every sheet moves to the asset host, together
✅ got: (https://assets.example.org/css/fonts.css https://assets.example.org/css/lucida.css https://assets.example.org/css/phosphor.css https://assets.example.org/css/byggsteg.olive.min.css)
[⚙️ assert] and none of them is left at the deployment's own path
✅ got: <!DOCTYPE html><html lang="en" data-theme="dark"><head><title>console resources spec</title><script type="text/javascript">
(function () {
var root = document.documentElement
var choice = null
try { choice = localStorage.getItem('data-theme') } catch (error) { choice = null }
var scheme = function () {
root.style.colorScheme = root.getAttribute('data-theme') === 'light' ? 'light' : 'dark'
}
root.setAttribute('data-theme', choice === 'light' ? 'light' : 'dark')
scheme()
if (typeof MutationObserver !== 'undefined') {
new MutationObserver(scheme).observe(root, {attributes: true, attributeFilter: ['data-theme']})
}
})()
</script><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta charset="UTF-8" /><meta name="description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta name="robots" content="index, follow" /><link rel="canonical" href="http://127.0.0.1:3000/" /><meta property="og:type" content="website" /><meta property="og:site_name" content="byggsteg" /><meta property="og:title" content="console resources spec" /><meta property="og:description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta property="og:url" content="http://127.0.0.1:3000/" /><meta property="og:image" content="https://assets.example.org/img/byggsteg-logo.png" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:title" content="console resources spec" /><meta name="twitter:description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta name="twitter:image" content="https://assets.example.org/img/byggsteg-logo.png" /><link rel="stylesheet" href="https://assets.example.org/css/fonts.css" /><link rel="stylesheet" href="https://assets.example.org/css/lucida.css" /><link rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'" href="https://assets.example.org/css/phosphor.css" /><noscript><link rel="stylesheet" href="https://assets.example.org/css/phosphor.css" /></noscript><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/* Flyout menu / dropdown */
.flyout-submenu-button[aria-expanded="true"]::after {
content: "↑";
padding-left: 0.8em;
}
.flyout-submenu-button[aria-expanded="false"]::after {
content: "↓";
padding-left: 0.8em;
}
.flyout-submenu[hidden] {
display: none;
}
</style><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Companion stylesheet for hygguile.
Olive CSS intentionally only ships utility classes. A handful of
components need a little structural CSS that cannot be expressed with
single utilities (the styling of native <progress> and <details>, the
CSS-only tabs, switch, modal and tooltip widgets). Everything here is
plain, dependency-free CSS and works without JavaScript.
Wherever Olive already provides the feature we lean on its utilities
instead of reimplementing it: screen-reader-only text uses `sr-only`,
loading animations use `animate-spin` and `animate-pulse`, and focus
rings use the `focus-visible:outline-*` variants.
*/
/* ===== A couple of missing intrinsic-size utilities ===== */
.h-auto {
height: auto;
}
.w-auto {
width: auto;
}
/* ===== Spinner =====
Only the ring visual lives here; the rotation is Olive's
`animate-spin` utility, applied by the component itself. */
.hg-spinner {
border-radius: 50%;
border: 0.25rem solid rgba(120, 113, 108, 0.35);
border-top-color: #57534e;
}
[data-theme="dark"] .hg-spinner {
border-color: rgba(214, 211, 209, 0.25);
border-top-color: #d6d3d1;
}
/* ===== A readable default ink =====
Components carry their own colour classes, but text a page does not
decorate must still be readable, and Olive ships no base ink of its own.
Both rules land before olive.min.css in the document, so a `text-*`
utility anywhere in the markup still wins over them. */
body {
color: #1c1917;
}
[data-theme="dark"] body {
color: #ededee;
}
/* ===== Native progress bar ===== */
.hg-progress {
width: 100%;
height: 1rem;
border: 0;
border-radius: 81.92rem;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
background: rgba(120, 113, 108, 0.25);
}
.hg-progress::-webkit-progress-bar {
background: rgba(120, 113, 108, 0.25);
border-radius: 81.92rem;
}
.hg-progress::-webkit-progress-value {
background: #57534e;
border-radius: 81.92rem;
}
.hg-progress::-moz-progress-bar {
background: #57534e;
border-radius: 81.92rem;
}
[data-theme="dark"] .hg-progress,
[data-theme="dark"] .hg-progress::-webkit-progress-bar {
background: rgba(214, 211, 209, 0.2);
}
[data-theme="dark"] .hg-progress::-webkit-progress-value,
[data-theme="dark"] .hg-progress::-moz-progress-bar {
background: #d6d3d1;
}
/* ===== CSS-only tabs (no JavaScript) ===== */
.hg-tabs {
display: flex;
flex-direction: column;
}
.hg-tab-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hg-tab-labels {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
border-bottom: 2px solid rgba(120, 113, 108, 0.35);
}
.hg-tab-label {
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 0.75rem 0.75rem 0 0;
font-weight: 500;
/* 7.4:1 on the panel, not the 4.6:1 of a lighter grey: an unselected
label is still text and still has to be read */
color: #57534e;
transition: color 0.15s ease, background-color 0.15s ease;
}
[data-theme="dark"] .hg-tab-label {
color: #d6d3d1;
}
.hg-tab-panels {
padding-top: 1rem;
}
.hg-tab-panel {
display: none;
}
/* active label highlight */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-labels > .hg-tab-label:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-labels > .hg-tab-label:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-labels > .hg-tab-label:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-labels > .hg-tab-label:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-labels > .hg-tab-label:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-labels > .hg-tab-label:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-labels > .hg-tab-label:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
background: rgba(120, 113, 108, 0.25);
color: #1c1917;
}
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-labels > .hg-tab-label:nth-child(1),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-labels > .hg-tab-label:nth-child(2),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-labels > .hg-tab-label:nth-child(3),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-labels > .hg-tab-label:nth-child(4),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-labels > .hg-tab-label:nth-child(5),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-labels > .hg-tab-label:nth-child(6),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-labels > .hg-tab-label:nth-child(7),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
background: rgba(214, 211, 209, 0.2);
color: #f5f5f4;
}
/* active panel */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(8) {
display: block;
}
/* keyboard focus */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* ===== Switch backed by a native checkbox ===== */
.hg-switch-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hg-switch {
position: relative;
display: inline-block;
flex: none;
width: 2.75rem;
height: 1.5rem;
border-radius: 81.92rem;
background: #a8a29e;
cursor: pointer;
transition: background 0.2s ease;
}
.hg-switch::after {
content: "";
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 81.92rem;
background: #ffffff;
transition: transform 0.2s ease;
}
.hg-switch-input:checked ~ .hg-switch {
background: #57534e;
}
[data-theme="dark"] .hg-switch-input:checked ~ .hg-switch {
background: #d6d3d1;
}
.hg-switch-input:checked ~ .hg-switch::after {
transform: translateX(1.25rem);
}
.hg-switch-input:focus-visible ~ .hg-switch {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* ===== CSS-only modal via :target ===== */
.hg-modal {
display: none;
}
.hg-modal:target {
display: grid;
place-items: center;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 64;
}
.hg-modal-backdrop {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(12, 10, 9, 0.6);
}
.hg-modal-content {
position: relative;
z-index: 1;
width: 90%;
max-width: 34rem;
margin: 1rem;
padding: 1.25rem;
border-radius: 1rem;
box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.35);
background: #f5f5f4;
color: #1c1917;
}
[data-theme="dark"] .hg-modal-content {
background: #262626;
color: #f5f5f4;
}
.hg-modal-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.hg-modal-close {
text-decoration: none;
font-size: 1.5rem;
line-height: 1;
color: inherit;
}
.hg-modal-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.hg-modal-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
/* ===== CSS-only tooltips ===== */
.hg-tooltip {
position: relative;
display: inline-flex;
align-items: center;
}
.hg-tooltip::after {
content: attr(data-hg-tooltip);
/* Hidden really means hidden: an absolutely positioned bubble still
occupies layout space, so a tooltip near the right edge would widen
the page and leave a phone layout that can be panned sideways.
`display: none` removes the box entirely, which is how the modal and
the fly-out menu already hide themselves. */
display: none;
position: absolute;
z-index: 64;
width: max-content;
max-width: 16rem;
padding: 0.35rem 0.6rem;
border-radius: 0.6rem;
background: #1c1917;
color: #f5f5f4;
font-size: 0.8rem;
line-height: 1.25;
pointer-events: none;
}
.hg-tooltip:hover::after,
.hg-tooltip:focus-visible::after {
display: block;
/* display cannot be transitioned, so the fade-in is an animation */
animation: hg-tooltip-in 0.15s ease;
}
@keyframes hg-tooltip-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.hg-tooltip-top::after {
bottom: 100%;
left: 50%;
transform: translate(-50%, -0.4rem);
}
.hg-tooltip-bottom::after {
top: 100%;
left: 50%;
transform: translate(-50%, 0.4rem);
}
.hg-tooltip-left::after {
right: 100%;
top: 50%;
transform: translate(-0.4rem, -50%);
}
.hg-tooltip-right::after {
left: 100%;
top: 50%;
transform: translate(0.4rem, -50%);
}
/* On a narrow viewport a wide bubble cannot always be centred on its
trigger without leaving the screen, so the vertical variants are
capped in width and aligned to the trigger's end edge instead. In
left-to-right writing, overflow to the left never makes a page pan
sideways, while overflow to the right does. */
@media (max-width: 40rem) {
.hg-tooltip-top::after,
.hg-tooltip-bottom::after {
left: auto;
right: 0;
max-width: calc(100vw - 3rem);
}
.hg-tooltip-top::after {
transform: translate(0, -0.4rem);
}
.hg-tooltip-bottom::after {
transform: translate(0, 0.4rem);
}
}
/* ===== Drifting colour washes =====
The wallpaper trick: a few oversized, heavily blurred discs of colour
behind a frosted panel. `.hg-blob` only adds the slow drift, so the
size, position, colour and blur stay in the markup where Olive already
expresses them (`blur-3xl`, `w-96`, `bg-naples-yellow-500/50`, ...).
Stagger the discs with an inline `animation-delay` so they never move
in lockstep.
How fast a disc drifts and how far it travels are the page's business,
not this sheet's: `glass-wash` writes `--hg-blob-duration`, `--hg-blob-x`
and `--hg-blob-y` as inline custom properties -- and an inline
`animation-delay` to stagger the discs -- so the values below are only
the defaults for a wash that says nothing. A keyframe resolves its
`var()`s per element, so one panel can breathe for a minute while the
next crosses its own corner in a couple of rems. */
.hg-blob {
animation: hg-blob-drift var(--hg-blob-duration, 26s) ease-in-out infinite alternate;
}
@keyframes hg-blob-drift {
from {
transform: translate3d(0, 0, 0) scale(1);
}
to {
transform: translate3d(var(--hg-blob-x, 1.5rem),
var(--hg-blob-y, -1.25rem),
0)
scale(var(--hg-blob-scale, 1.08));
}
}
/* ===== The layer the discs are painted in =====
A pane rounds the layer holding its washes and clips it, and neither the
rounding nor the clip stops a disc: a child with a `filter` is painted
with the blur that filter asks for, and the browser draws that blur into
the square box it reaches -- past the layer's rounded edge, into the
corner the radius curves away from. What is left on the page is a faint
square of wash beside each corner, a percent or two of ink: measured
against a mid-grey page, a corner square that should have held the
page's own 128 read 130. `overflow: clip`, `contain: paint`,
`isolation: isolate` and promoting the layer to one of its own all leave
it in place (each measured on its own); an explicit `clip-path` is what
clips it. And a clip has to name a radius -- it cannot inherit one the
way `border-radius: inherit` can.
So this class names it -- and names it off the very rounding the markup
gave the layer, since the layer wears the pane's own rounding classes.
The radius has to be restated as a length, so these rules restate
Olive's named ladder; they are the one place here that has to agree with
another sheet, and a spec pins the tokens the framework rounds its own
glass with to a rule apiece, so a rounding cannot lose its clip without
a test going red.
A radius given outright in `--hg-glass-clip' still wins, on the layer or
on any ancestor, which is what a caller rounding with one of Olive's
numeric steps (`rounded-24', say) reaches for, as does a pane whose
radius is not one of the named ones at all.
The default in each rule is `rounded-3xl', the rounding a pane wears
when a caller leaves `#:rounding' alone: a layer wearing something the
table below does not name still keeps its washes in. */
.hg-glass-layer {
clip-path: inset(0 round var(--hg-glass-clip, 1.28rem));
}
.hg-glass-layer[class~="rounded-none"] {
clip-path: inset(0 round var(--hg-glass-clip, 0));
}
.hg-glass-layer[class~="rounded-2xs"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.16rem));
}
.hg-glass-layer[class~="rounded-xs"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.32rem));
}
.hg-glass-layer[class~="rounded-sm"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.48rem));
}
.hg-glass-layer[class~="rounded-md"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.64rem));
}
.hg-glass-layer[class~="rounded-lg"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.80rem));
}
.hg-glass-layer[class~="rounded-xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.96rem));
}
.hg-glass-layer[class~="rounded-2xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.12rem));
}
.hg-glass-layer[class~="rounded-3xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.28rem));
}
.hg-glass-layer[class~="rounded-4xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.44rem));
}
.hg-glass-layer[class~="rounded-5xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.60rem));
}
.hg-glass-layer[class~="rounded-full"] {
clip-path: inset(0 round var(--hg-glass-clip, 9999px));
}
/* ===== Frost that survives a rounded corner =====
A backdrop-filter asks the browser to blur and colour whatever sits
behind a pane. Chromium paints that filtered backdrop in the pane's
square border box and ignores its `border-radius`, so a rounded pane
leaks a square patch of frost wherever its radius curves away from the
corner (Chromium issue 41212594, still open). A radius on that element
does not stop it, and a `clip-path` on it does not either. The one
thing that does is clipping the frosted box itself: an element that
carries `overflow: hidden` alongside its radius gets its frost rounded
off, because the browser then has a shape to clip the filtered backdrop
to.
The frost therefore lives one level in, on the pane's own `::before`:
a layer that rounds itself to the pane's radius with `border-radius:
inherit` -- so it follows whatever radius the markup or a caller gave
the pane -- clips itself, and carries the blur. Being a decoration
rather than a container, it leaves the pane's own children painted
above it and free to escape, which is what a bar holding a dropdown
needs.
How strong the frost is stays a knob: `--hg-frost-blur` is the blur
radius, 4rem unless something says otherwise (the same value Olive's
`backdrop-blur-md` utility sets). A custom property cannot be a utility
class on a pseudo-element, so it is the one setting that is passed as a
declaration rather than a class: `glass-panel' writes it from Scheme
for the panes it builds (`#:frost'), markup this sheet frosts by hand
passes the declaration itself, and a pane with neither reaches for
`hg-frost-soft' below instead.
`position: relative` gives the layer something to be absolute against
even when a pane places itself with layout classes alone. A pane that
sets a position utility of its own takes over, since olive.min.css is
inlined after this sheet.
`z-index: 0' on the pane and `-1' on the layer are what keep the frost
*under* the pane's content. An absolutely placed layer paints above
in-flow text and blocks, so a frosted pane whose content was never
wrapped in a layer of its own -- a dropdown's list is one -- would
otherwise have its contents blurred away behind the frost. A negative
stack level puts the layer in the first step of the pane's stacking
order instead, right after the pane's own background and border, which
is where the frost belongs. The pane was a stacking context anyway
while it carried the blur itself, so nothing about the order of what
is *outside* the pane changes, and the layer's own corner can no
longer paint over the pane's hairline. */
.hg-frost {
position: relative;
z-index: 0;
}
.hg-frost::before {
content: "";
position: absolute;
z-index: -1;
inset: 0;
border-radius: inherit;
overflow: hidden;
-webkit-backdrop-filter: blur(var(--hg-frost-blur, 4rem));
backdrop-filter: blur(var(--hg-frost-blur, 4rem));
}
/* A gentler frost, for the small panes the framework frosts with a class
rather than a declaration: the carousel's arrow discs, a couple of rems
across, would be all blur at a panel's strength. Olive's blur ladder
cannot reach a pseudo-element, so this sheet names the one step below
the default that something actually wears. */
.hg-frost-soft {
--hg-frost-blur: 3rem;
}
/* The smallest step, for the pills and labels a page frosts by hand: a
badge two rems tall is all blur at a pane's strength, and the label on a
colour swatch wants a hint of it and no more. Naming the step is what
keeps those pills on the same recipe as the panes -- the frost paints in
a rounded layer of its own, so the square patch of blur Olive's
`backdrop-blur-*' utilities leave at a rounded corner never appears. */
.hg-frost-pill {
--hg-frost-blur: 1rem;
}
/* ===== The heading type scale =====
Heading line-heights are ratios rather than lengths, because a heading is
sized by a utility the page may move up or down the scale at will: a
length would have to be re-picked whenever the font-size changed, and a
ratio follows on its own. The steps loosen as the size shrinks (1.3 for
the display sizes, 1.35 for a section title, 1.4 for the smaller
headings), which is the usual shape of a type scale, and every one of
them is loose enough that a line's glyphs -- descenders, accents, the lot
-- stay inside the box the line occupies.
That is a requirement, not a preference. Gradient ink is a background
clipped to the letters, so it can only paint where the heading's own box
reaches: a `g` whose descender falls below it is not cropped but drawn
transparent. The ratios here are measured against the two faces the
framework ships, of which Plus Jakarta Sans asks for the most room at
1.24 of the line box at worst; 1.3 is that with margin to spare. Body
copy sits looser still, at 1.5. */
.hg-leading-display {
line-height: 1.3;
}
.hg-leading-title {
line-height: 1.35;
}
.hg-leading-subtitle {
line-height: 1.4;
}
/* ===== Gradient text =====
One gradient per theme, since a deep sweep that sings on the light
background turns to mud on the dark one.
The compound `h1.`/`h2.` selectors are deliberate: Olive's text-colour
utilities are single classes, and this sheet is inlined before
olive.min.css, so a bare `.hg-gradient-text` would lose the cascade
and the heading would keep its solid colour.
Nothing here has to widen the box the ink is painted in, and that is the
heading scale's doing rather than this rule's: every heading it matches
carries a leaded line-height from `hg-leading-*' above, so the descenders
of a `g` sit inside the heading's own box and have gradient to paint.
A page that overrides the leading with a tighter one -- `leading-none' or
a `leading-*' of its own, which lands after this sheet -- takes the clip
back into its own hands. */
h1.hg-gradient-text,
h2.hg-gradient-text {
background-image: linear-gradient(100deg,
oklch(0.7404 0.1399 91.55 / 1),
oklch(0.6183 0.1557 45.26 / 1) 45%,
oklch(0.4929 0.161 279.98 / 1));
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
[data-theme="dark"] h1.hg-gradient-text,
[data-theme="dark"] h2.hg-gradient-text {
background-image: linear-gradient(100deg,
oklch(0.8855 0.1307 92.19 / 1),
oklch(0.7404 0.1656 39.8 / 1) 45%,
oklch(0.7439 0.1301 288.16 / 1));
}
/* ===== Expandable cards =====
An expandable card is a native <details>, so it opens and hides with no
script at all. The only structural help it asks for here is to drop the
browser's own summary marker -- the chevron in the summary is the
affordance, and it turns as the card opens -- and, because a card is also
what a section rail links to, to keep a header a link jumped to clear of
that rail. */
.hg-expandable-summary {
list-style: none;
/* deep enough for a card header to clear a sticky section rail above it */
scroll-margin-top: 7rem;
}
.hg-expandable-summary::-webkit-details-marker {
display: none;
}
details[open] > .hg-expandable-summary .hg-expandable-chevron {
transform: rotate(180deg);
}
/* ===== Showcase sort control =====
Three buttons offer to reorder the tray, and the chosen one is the one the
page's script has marked `aria-pressed="true"'. Olive tints every button
with a mid surface -- legible, but all three of them the same shape and
shade -- and a control that says which order you are reading has to be
unmistakable at a glance, so the pressed one is filled from the opposite
end of the ramp instead: the near-black of the ink in a light theme, the
near-white of a card in a dark theme, each carrying the ink that surface is
already known to carry. The fill is only the darkest or lightest step of
the palette the page is already built from; the contrast of every pair
below is the framework's own, and the gate checks it. */
.hg-sort-button[aria-pressed="true"] {
background-color: oklch(0.3588 0.0044 247.95 / 1);
color: oklch(0.987 0.0013 286.37);
border-color: oklch(0.987 0.0013 286.37 / 0.4);
}
[data-theme="dark"] .hg-sort-button[aria-pressed="true"] {
background-color: oklch(0.947 0.0013 286.37 / 1);
color: oklch(0.3588 0.0044 247.95);
border-color: oklch(0.3588 0.0044 247.95 / 0.4);
}
/* ===== Code samples =====
The panel that holds a specimen's source folds away until it is asked for,
and the `hidden' attribute is what folds it. The attribute is enough on its
own for an element with no display utility -- the browser's own rule hides
it -- but a panel lays itself out with `flex', and an author rule outbids
the user agent's `[hidden]' however low it sits in the cascade, which would
leave every specimen's source open on every page. So the fold is stated
here as the framework's own rule, at the strength it has to be stated at.
With scripting off the noscript stylesheet reveals the panel instead and
hides the control, so a reader who cannot fold the source still reads it. */
.hg-code-sample-panel[hidden] {
display: none !important;
}
/* ===== Respect reduced-motion preferences ===== */
@media (prefers-reduced-motion: reduce) {
.hg-switch,
.hg-switch::after,
.hg-tooltip::after,
.hg-tab-label,
.hg-blob {
transition: none;
animation: none;
}
}
</style><noscript><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Progressive-enhancement rules that only apply when scripting is
disabled, because hygguile loads this file inside a <noscript> element
(see hygguile-recommended-resources).
The contract is simple:
- Anything that only works with JavaScript carries the .js-only class
and is hidden here.
- Components that can be made to work without JavaScript (the fly-out
submenus, the carousel) are progressively revealed instead of being
hidden.
*/
/* Interactive controls that depend on JavaScript do nothing without it. */
.js-only {
display: none !important;
}
/* Fly-out menus: reveal every submenu and drop the meaningless arrow. */
.flyout-submenu[hidden] {
display: block !important;
}
/* A code sample: the fold that hides the source needs scripting, and the
control that would lift it is a .js-only element, so without scripting
every specimen simply shows its own source rather than a dead button. */
.hg-code-sample-panel[hidden] {
display: flex !important;
}
.flyout-submenu {
position: static !important;
box-shadow: none !important;
background: transparent !important;
padding-left: 1rem !important;
}
.flyout-submenu-button::after {
content: "" !important;
padding-left: 0 !important;
}
</style></noscript><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function hygguileSaveDarkThemePreference(preference) {
document.documentElement.setAttribute('data-theme', preference)
localStorage.setItem('data-theme', preference)
}
function hygguileToggleDarkTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme')
if (currentTheme == null) {
//
// by default set dark since hygguile has a light theme by default
//
hygguileSaveDarkThemePreference('dark')
} else {
if (currentTheme == 'dark') {
hygguileSaveDarkThemePreference('light')
} else {
hygguileSaveDarkThemePreference('dark')
}
}
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function hygguileCopyPreInnerText(elementId) {
const id = elementId + '-pre'
console.log('copying inner text of element: ' + id)
var el = document.getElementById(id)
navigator.clipboard.writeText(el.innerText)
console.log(el.innerText)
}
</script><script>
document.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(window.location.search);
if(params.get('notify-body') == null && params.get('notify-title') == null){
return
}
const notificationTitle = params.get('notify-title') ?? ""
const notificationBody = params.get('notify-body') ?? ""
const notificationDurationMillis = params.get('notify-duration-millis') ?? 3000
const notificationBodyFontFamily = params.get('notify-body-font-family') ?? 'font-sans'
const notificationTray = document.getElementById('notification-tray')
notificationTray.innerHTML = "<div class=\"bg-stone-300/90 border-2 border-stone-500/25 dark:bg-onyx-700/75 dark:border-onyx-100/25 dark:text-stone-300 dark:text-stone-700 flex flex-col font-sans gap-4 hg-frost max-w-full max-w-prose px-5 py-5 relative rounded-xl text-onyx-100 text-onyx-300\"><div class=\"flex flex-row flex-wrap gap-10 items-center justify-between\"><h4 id=\"notification-title\" class=\"dark:text-asparagus-100 text-asparagus-900 text-xl\"></h4><i onclick=\"document.getElementById('notification-tray').innerHTML = ''\" class=\"cursor-pointer dark:text-asparagus-100 ph ph-x text-asparagus-900 text-xl\"></i></div><p id=\"notification-body\"></p></div>"
const notificationTitleNode = document.getElementById('notification-title')
const notificationBodyNode = document.getElementById('notification-body')
if(notificationTitleNode){
notificationTitleNode.textContent = notificationTitle
}
if(notificationBodyNode){
notificationBodyNode.textContent = notificationBody
notificationBodyNode.classList.toggle(notificationBodyFontFamily)
}
setTimeout(() => { notificationTray.innerHTML = '' }, notificationDurationMillis);
})
</script><script type="text/javascript" defer="" src="https://assets.example.org/js/byggsteg-busy.js"></script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
const carouselItems = (list) => Array.from(list.querySelectorAll('.carousel-list-item'))
const carouselCentres = (list, items) => {
const listCentre = list.getBoundingClientRect().left + list.clientWidth / 2
const centres = items.map((item) => {
const rect = item.getBoundingClientRect()
return rect.left + rect.width / 2
})
const nearest = centres.reduce((best, itemCentre, index) => {
if (Math.abs(itemCentre - listCentre) < Math.abs(centres[best] - listCentre)) {
return index
}
return best
}, 0)
return { listCentre, centres, nearest }
}
// a real function declaration, so the inline onclick attributes emitted by
// the component always find it on the global scope
function handleCarouselClick(direction, id) {
const list = document.getElementById(id)
if (!list) {
return
}
const items = carouselItems(list)
if (!items.length) {
return
}
// the items are centred snap points, so page one item at a time by
// measuring where each one actually sits instead of guessing a step;
// this keeps the gap between items (which changes at the responsive
// breakpoints) and any width difference honest
const { listCentre, centres, nearest } = carouselCentres(list, items)
const step = direction === 'previous' ? -1 : 1
const target = Math.min(Math.max(nearest + step, 0), items.length - 1)
list.scrollBy({ left: centres[target] - listCentre, behavior: 'smooth' })
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
document.addEventListener('DOMContentLoaded', () => {
const themePreference = localStorage.getItem('data-theme')
if (themePreference === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark')
} else if (themePreference === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
}
})
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function cycleBgClass(currentClass) {
const parts = currentClass.split('-')
const currentNumber = parseInt(parts.toReversed()[0], 10)
const nextNumber = currentNumber >= 900 ? 100 : currentNumber + 200
const startParts = parts.slice(0, parts.length - 1).join("-")
return `${startParts}-${nextNumber}`
}
function cycleColorShow(el) {
const currentClass = [...el.classList].find(cls => cls.startsWith('bg-'))
const newClass = cycleBgClass(currentClass)
el.classList.toggle(currentClass)
el.classList.toggle(newClass)
el.children[0].innerText = newClass.replace("bg-", "")
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
const closeMenu = (button, menu) => {
button.setAttribute('aria-expanded', 'false')
menu.hidden = true
}
const openMenu = (button, menu) => {
button.setAttribute('aria-expanded', 'true')
menu.hidden = false
}
const closeAllOpenMenus = () => {
const menuContainers = document.querySelectorAll('.flyout-menu-container')
menuContainers.forEach(menuContainer => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
if (menuButton) {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (isExpanded) {
menuButton.click()
}
}
})
}
const HOVER_DELAY = 2000
document.addEventListener('DOMContentLoaded', () => {
const menuContainers = document.querySelectorAll('.flyout-menu-container')
menuContainers.forEach(menuContainer => {
let hoverTimer
menuContainer.addEventListener('mouseenter', () => {
hoverTimer = setTimeout(() => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
if (menuButton) {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (!isExpanded) {
closeAllOpenMenus()
menuButton.click()
}
}
}, HOVER_DELAY)
})
menuContainer.addEventListener('mouseleave', () => {
clearTimeout(hoverTimer)
})
})
menuContainers.forEach(menuContainer => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
const dropdownMenu = menuContainer.querySelector('.flyout-submenu')
if (!menuButton || !dropdownMenu) {
return
}
const menuItems = dropdownMenu.querySelectorAll('a[href], button:not([disabled])')
const firstMenuItem = menuItems[0]
const lastMenuItem = menuItems[menuItems.length - 1]
menuButton.addEventListener('click', (event) => {
event.preventDefault()
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (isExpanded) {
closeMenu(menuButton, dropdownMenu)
} else {
closeAllOpenMenus()
openMenu(menuButton, dropdownMenu)
}
})
menuButton.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
openMenu(menuButton, dropdownMenu)
if (firstMenuItem) {
firstMenuItem.focus()
}
} else if (event.key === 'Escape') {
closeMenu(menuButton, dropdownMenu)
menuButton.focus()
}
})
dropdownMenu.addEventListener('keydown', (event) => {
const { key } = event
const activeElement = document.activeElement
if (key === 'Escape') {
closeMenu(menuButton, dropdownMenu)
menuButton.focus()
return
}
if (key === 'ArrowUp') {
event.preventDefault()
if (activeElement === firstMenuItem) {
lastMenuItem.focus()
} else {
const currentIndex = Array.from(menuItems).indexOf(activeElement)
if (currentIndex > 0) {
menuItems[currentIndex - 1].focus()
}
}
} else if (key === 'ArrowDown') {
event.preventDefault()
if (activeElement === lastMenuItem) {
firstMenuItem.focus()
} else {
const currentIndex = Array.from(menuItems).indexOf(activeElement)
if (currentIndex < menuItems.length - 1) {
menuItems[currentIndex + 1].focus()
}
}
}
})
})
})
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
The fold on a `code-sample': the source of a specimen is hidden until a
reader asks for it.
The component says everything this needs. The control carries an
`aria-controls' naming the panel and an `aria-expanded' saying whether the
panel is showing, and the panel carries the `hidden' attribute the component
wrote; the whole state the script keeps is those two attributes, so a page
that renders its own markup without the component gains nothing by loading
this file.
Without scripting the panel is revealed by the `noscript' stylesheet and the
control is hidden as a `.js-only' element, so a reader who cannot fold the
source still reads it.
*/
function hygguileToggleCodeSample(button) {
const id = button.getAttribute('aria-controls')
const panel = id && document.getElementById(id)
if (!panel) return
panel.hidden = !panel.hidden
button.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true')
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
The tray controls: the two things a page asks of a whole tray of cards at
once, which order it is in and whether all of it is open. Both are declared
in the markup by `sort-control' and `disclosure-controls', so the only
JavaScript a page carries is the call each button already makes.
Ordering is data, not code. A button reaches this file as
hygguileSortTray(this, 'title')
and the tray it orders is whichever container the button's group names in
its `data-hg-sort-target'. The items are ordered by their `data-title'
attribute -- numerically when both values are numbers, so data-order="10"
follows data-order="9" instead of sorting in front of it, and
alphabetically otherwise. The sort is stable, so items that compare equal
keep the order they are already in. A mode that no item carries is the
tray's own order: the items go back to the order the page wrote them in,
which is what a "Page order" button is for and why it needs no attribute
anywhere.
*/
(function () {
/* The order each tray was written in, read once the document is parsed:
by the time a button has been clicked it is too late to ask. */
const written = new Map()
const writtenOrder = (container) => written.get(container)
|| Array.prototype.slice.call(container.children)
const asNumber = (text) => {
const trimmed = text.trim()
return trimmed !== '' && !isNaN(Number(trimmed)) ? Number(trimmed) : null
}
const compare = (mode) => (a, b) => {
const left = a.getAttribute('data-' + mode) || ''
const right = b.getAttribute('data-' + mode) || ''
const leftNumber = asNumber(left)
const rightNumber = asNumber(right)
if (leftNumber !== null && rightNumber !== null) {
return leftNumber - rightNumber
}
return left.localeCompare(right)
}
const groupOf = (button) => button.closest('[data-hg-sort-target]')
/* One button of the group is the order in force; the stylesheet draws that
as the pressed one, and a screen reader reads it out. */
const mark = (button, group) => {
Array.prototype.forEach.call(group.querySelectorAll('button'),
(other) => {
other.setAttribute('aria-pressed',
other === button
? 'true' : 'false')
})
}
window.hygguileSortTray = (button, mode) => {
const group = groupOf(button)
const container = group
&& document.getElementById(group.getAttribute('data-hg-sort-target'))
if (!container) {
return
}
const items = Array.prototype.slice.call(container.children)
const carries = items.some((item) => item.hasAttribute('data-' + mode))
const ordered = carries
? items.sort(compare(mode))
: writtenOrder(container).slice()
ordered.forEach((item) => container.appendChild(item))
mark(button, group)
}
window.hygguileToggleTray = (open, selector) => {
Array.prototype.forEach.call(document.querySelectorAll(selector),
(details) => {
details.open = open
})
}
/* The tray is in the order the page wrote it in, and the first button of a
group is the order it opens in, so a page states its own order by writing
that mode first. */
document.addEventListener('DOMContentLoaded', () => {
Array.prototype.forEach.call(
document.querySelectorAll('[data-hg-sort-target]'),
(group) => {
const container = document.getElementById(
group.getAttribute('data-hg-sort-target'))
if (container) {
written.set(container, Array.prototype.slice.call(container.children))
}
const first = group.querySelector('button')
if (first) {
mark(first, group)
}
})
})
})()
</script><link rel="stylesheet" href="https://assets.example.org/css/byggsteg.olive.min.css"></link><script type="text/javascript" defer="" src="https://assets.example.org/js/symbanta.js" data-endpoint="https://api.symbanta.jointhefreeworld.org/v1/events/register" data-interval="4000"></script><script type="speculationrules">
{
"prerender": [
{
"where": {
"selector_matches": "a"
}
}
]
}
</script><script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"byggsteg","alternateName":"byggsteg CI/CD","applicationCategory":"DeveloperApplication","operatingSystem":"GNU/Linux","programmingLanguage":"Scheme","description":"byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host.","isAccessibleForFree":true,"license":"https://www.gnu.org/licenses/agpl-3.0.html","codeRepository":"https://codeberg.org/jjba23/byggsteg","downloadUrl":"https://codeberg.org/jjba23/byggsteg","author":{"@type":"Person","name":"Josep Bigorra"},"offers":{"@type":"Offer","price":"0","priceCurrency":"EUR"},"url":"http://127.0.0.1:3000/"}</script></head><body class="bg-stone-100 dark:bg-onyx-900 dark:text-stone-700 font-mono rounded-none text-onyx-100"><div id="notification-tray" role="status" aria-live="polite" aria-atomic="true" class="fixed right-8 top-8 z-32"></div><div class="dark:text-stone-700 flex flex-col justify-between min-h-screen text-onyx-100"><div><header style="--hg-frost-blur: 3rem" class="bg-stone-100/75 border-asparagus-700 border-b-2 dark:bg-onyx-900/75 dark:border-asparagus-900 hg-frost md:px-5 px-2 py-1 shadow-md sm:px-3 sticky top-0 w-full z-10"><div class="pb-1 pt-1 px-2 sm:pb-2 sm:px-3 w-full"><a href="/" class="flex flex-row gap-2 items-center no-underline"><span class="dark:text-asparagus-100 font-mono text-asparagus-900">$</span><h3 class="dark:hover:text-olive-300 dark:text-asparagus-100 font-bold font-sans hg-leading-display hover:text-asparagus-700 lg:text-xl sm:text-lg text-asparagus-900 text-md">byggsteg</h3><span class="animate-pulse dark:text-asparagus-100 font-mono text-asparagus-900">▮</span></a></div><div class="align-center flex flex-row flex-wrap gap-1 items-center sm:gap-2 sm:justify-between"><div class="contents gap-1 sm:flex sm:flex-row sm:flex-wrap sm:gap-2 sm:justify-start"><div><a href="/logs"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-scroll select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>logs</span></div></button></a></div><div><a href="/activity"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-chart-bar select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>activity</span></div></button></a></div><div><a href="/search"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-magnifying-glass select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>search</span></div></button></a></div><div><a href="/profiles"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-folder select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>profiles</span></div></button></a></div><div><a href="/about"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-info select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>about</span></div></button></a></div><div><a class="dark:text-asparagus-100 flex flex-row font-medium font-sans gap-2 items-center lowercase no-underline text-asparagus-900" href="/login"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-sign-in select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>login</span></div></button></a></div></div><div class="contents gap-1 sm:flex sm:flex-row sm:flex-wrap sm:gap-2 sm:justify-start"><a href="/jobs/request"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-circuitry select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>run job</span></div></button></a><button onclick="hygguileToggleDarkTheme()" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-light font-mono hover:bg-stone-900/90 hover:text-asparagus-700 js-only max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors"><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph- select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span><div><i class="dark:text-asparagus-100 md:text-md ph ph-sun select-none shrink-0 text-asparagus-900 text-sm"></i><i class="dark:text-asparagus-100 md:text-md ph ph-moon select-none shrink-0 text-asparagus-900 text-sm"></i></div></span></div></button></div></div></header><div class="container mx-auto my-4"><div class="bg-stone-300 border-2 border-asparagus-700 dark:bg-onyx-700 dark:border-asparagus-900 rounded-none shadow-md p-4"><img loading="lazy" class="h-auto w-48" src="https://assets.example.org/img/byggsteg-logo.png" /></div></div></div><footer class="bg-stone-300 border-asparagus-700 border-t-2 dark:bg-onyx-900 dark:border-asparagus-900 dark:text-stone-700 flex flex-col font-mono gap-1 items-start justify-start md:px-10 px-3 py-2 sm:px-6 sm:py-3 text-onyx-100"><div class="align-center flex flex-row flex-wrap gap-x-4 gap-y-1 items-center"><a href="https://codeberg.org/jjba23/byggsteg" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">source</a><a href="https://codeberg.org/jjba23/byggsteg/src/branch/trunk/README.org" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">manual</a><a href="https://jointhefreeworld.org/manuals/byggsteg/" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">documentation</a><a href="/about" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">about</a><a href="javascript:void(0)" class="cursor-pointer dark:text-asparagus-100 font-medium font-mono no-underline text-asparagus-900 text-sm" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })">back to the top</a></div><div class="align-center flex flex-row flex-wrap gap-x-3 gap-y-1 items-center"><em class="dark:text-stone-900 font-light font-mono italic leading-5 text-onyx-300 text-sm">© Josep Bigorra & NalaGinrut -- AGPL-3.0-or-later</em><em class="dark:text-stone-900 font-light font-mono italic leading-5 text-onyx-300 text-sm">version: 1.19.5 (7b86109)</em></div></footer></div></body></html>
[⚙️ assert] the mark the landing page shows moves with them
✅ got: <!DOCTYPE html><html lang="en" data-theme="dark"><head><title>console resources spec</title><script type="text/javascript">
(function () {
var root = document.documentElement
var choice = null
try { choice = localStorage.getItem('data-theme') } catch (error) { choice = null }
var scheme = function () {
root.style.colorScheme = root.getAttribute('data-theme') === 'light' ? 'light' : 'dark'
}
root.setAttribute('data-theme', choice === 'light' ? 'light' : 'dark')
scheme()
if (typeof MutationObserver !== 'undefined') {
new MutationObserver(scheme).observe(root, {attributes: true, attributeFilter: ['data-theme']})
}
})()
</script><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta charset="UTF-8" /><meta name="description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta name="robots" content="index, follow" /><link rel="canonical" href="http://127.0.0.1:3000/" /><meta property="og:type" content="website" /><meta property="og:site_name" content="byggsteg" /><meta property="og:title" content="console resources spec" /><meta property="og:description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta property="og:url" content="http://127.0.0.1:3000/" /><meta property="og:image" content="https://assets.example.org/img/byggsteg-logo.png" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:title" content="console resources spec" /><meta name="twitter:description" content="byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host." /><meta name="twitter:image" content="https://assets.example.org/img/byggsteg-logo.png" /><link rel="stylesheet" href="https://assets.example.org/css/fonts.css" /><link rel="stylesheet" href="https://assets.example.org/css/lucida.css" /><link rel="preload" as="style" onload="this.onload=null;this.rel='stylesheet'" href="https://assets.example.org/css/phosphor.css" /><noscript><link rel="stylesheet" href="https://assets.example.org/css/phosphor.css" /></noscript><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/* Flyout menu / dropdown */
.flyout-submenu-button[aria-expanded="true"]::after {
content: "↑";
padding-left: 0.8em;
}
.flyout-submenu-button[aria-expanded="false"]::after {
content: "↓";
padding-left: 0.8em;
}
.flyout-submenu[hidden] {
display: none;
}
</style><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Companion stylesheet for hygguile.
Olive CSS intentionally only ships utility classes. A handful of
components need a little structural CSS that cannot be expressed with
single utilities (the styling of native <progress> and <details>, the
CSS-only tabs, switch, modal and tooltip widgets). Everything here is
plain, dependency-free CSS and works without JavaScript.
Wherever Olive already provides the feature we lean on its utilities
instead of reimplementing it: screen-reader-only text uses `sr-only`,
loading animations use `animate-spin` and `animate-pulse`, and focus
rings use the `focus-visible:outline-*` variants.
*/
/* ===== A couple of missing intrinsic-size utilities ===== */
.h-auto {
height: auto;
}
.w-auto {
width: auto;
}
/* ===== Spinner =====
Only the ring visual lives here; the rotation is Olive's
`animate-spin` utility, applied by the component itself. */
.hg-spinner {
border-radius: 50%;
border: 0.25rem solid rgba(120, 113, 108, 0.35);
border-top-color: #57534e;
}
[data-theme="dark"] .hg-spinner {
border-color: rgba(214, 211, 209, 0.25);
border-top-color: #d6d3d1;
}
/* ===== A readable default ink =====
Components carry their own colour classes, but text a page does not
decorate must still be readable, and Olive ships no base ink of its own.
Both rules land before olive.min.css in the document, so a `text-*`
utility anywhere in the markup still wins over them. */
body {
color: #1c1917;
}
[data-theme="dark"] body {
color: #ededee;
}
/* ===== Native progress bar ===== */
.hg-progress {
width: 100%;
height: 1rem;
border: 0;
border-radius: 81.92rem;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
background: rgba(120, 113, 108, 0.25);
}
.hg-progress::-webkit-progress-bar {
background: rgba(120, 113, 108, 0.25);
border-radius: 81.92rem;
}
.hg-progress::-webkit-progress-value {
background: #57534e;
border-radius: 81.92rem;
}
.hg-progress::-moz-progress-bar {
background: #57534e;
border-radius: 81.92rem;
}
[data-theme="dark"] .hg-progress,
[data-theme="dark"] .hg-progress::-webkit-progress-bar {
background: rgba(214, 211, 209, 0.2);
}
[data-theme="dark"] .hg-progress::-webkit-progress-value,
[data-theme="dark"] .hg-progress::-moz-progress-bar {
background: #d6d3d1;
}
/* ===== CSS-only tabs (no JavaScript) ===== */
.hg-tabs {
display: flex;
flex-direction: column;
}
.hg-tab-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hg-tab-labels {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
border-bottom: 2px solid rgba(120, 113, 108, 0.35);
}
.hg-tab-label {
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 0.75rem 0.75rem 0 0;
font-weight: 500;
/* 7.4:1 on the panel, not the 4.6:1 of a lighter grey: an unselected
label is still text and still has to be read */
color: #57534e;
transition: color 0.15s ease, background-color 0.15s ease;
}
[data-theme="dark"] .hg-tab-label {
color: #d6d3d1;
}
.hg-tab-panels {
padding-top: 1rem;
}
.hg-tab-panel {
display: none;
}
/* active label highlight */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-labels > .hg-tab-label:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-labels > .hg-tab-label:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-labels > .hg-tab-label:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-labels > .hg-tab-label:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-labels > .hg-tab-label:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-labels > .hg-tab-label:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-labels > .hg-tab-label:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
background: rgba(120, 113, 108, 0.25);
color: #1c1917;
}
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-labels > .hg-tab-label:nth-child(1),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-labels > .hg-tab-label:nth-child(2),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-labels > .hg-tab-label:nth-child(3),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-labels > .hg-tab-label:nth-child(4),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-labels > .hg-tab-label:nth-child(5),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-labels > .hg-tab-label:nth-child(6),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-labels > .hg-tab-label:nth-child(7),
[data-theme="dark"] .hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
background: rgba(214, 211, 209, 0.2);
color: #f5f5f4;
}
/* active panel */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):checked) > .hg-tab-panels > .hg-tab-panel:nth-child(8) {
display: block;
}
/* keyboard focus */
.hg-tabs:has(> .hg-tab-input:nth-of-type(1):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(1),
.hg-tabs:has(> .hg-tab-input:nth-of-type(2):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(2),
.hg-tabs:has(> .hg-tab-input:nth-of-type(3):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(3),
.hg-tabs:has(> .hg-tab-input:nth-of-type(4):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(4),
.hg-tabs:has(> .hg-tab-input:nth-of-type(5):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(5),
.hg-tabs:has(> .hg-tab-input:nth-of-type(6):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(6),
.hg-tabs:has(> .hg-tab-input:nth-of-type(7):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(7),
.hg-tabs:has(> .hg-tab-input:nth-of-type(8):focus-visible) > .hg-tab-labels > .hg-tab-label:nth-child(8) {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* ===== Switch backed by a native checkbox ===== */
.hg-switch-input {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hg-switch {
position: relative;
display: inline-block;
flex: none;
width: 2.75rem;
height: 1.5rem;
border-radius: 81.92rem;
background: #a8a29e;
cursor: pointer;
transition: background 0.2s ease;
}
.hg-switch::after {
content: "";
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 81.92rem;
background: #ffffff;
transition: transform 0.2s ease;
}
.hg-switch-input:checked ~ .hg-switch {
background: #57534e;
}
[data-theme="dark"] .hg-switch-input:checked ~ .hg-switch {
background: #d6d3d1;
}
.hg-switch-input:checked ~ .hg-switch::after {
transform: translateX(1.25rem);
}
.hg-switch-input:focus-visible ~ .hg-switch {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* ===== CSS-only modal via :target ===== */
.hg-modal {
display: none;
}
.hg-modal:target {
display: grid;
place-items: center;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 64;
}
.hg-modal-backdrop {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(12, 10, 9, 0.6);
}
.hg-modal-content {
position: relative;
z-index: 1;
width: 90%;
max-width: 34rem;
margin: 1rem;
padding: 1.25rem;
border-radius: 1rem;
box-shadow: 0 1.25rem 3rem rgba(0, 0, 0, 0.35);
background: #f5f5f4;
color: #1c1917;
}
[data-theme="dark"] .hg-modal-content {
background: #262626;
color: #f5f5f4;
}
.hg-modal-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.hg-modal-close {
text-decoration: none;
font-size: 1.5rem;
line-height: 1;
color: inherit;
}
.hg-modal-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.hg-modal-footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
/* ===== CSS-only tooltips ===== */
.hg-tooltip {
position: relative;
display: inline-flex;
align-items: center;
}
.hg-tooltip::after {
content: attr(data-hg-tooltip);
/* Hidden really means hidden: an absolutely positioned bubble still
occupies layout space, so a tooltip near the right edge would widen
the page and leave a phone layout that can be panned sideways.
`display: none` removes the box entirely, which is how the modal and
the fly-out menu already hide themselves. */
display: none;
position: absolute;
z-index: 64;
width: max-content;
max-width: 16rem;
padding: 0.35rem 0.6rem;
border-radius: 0.6rem;
background: #1c1917;
color: #f5f5f4;
font-size: 0.8rem;
line-height: 1.25;
pointer-events: none;
}
.hg-tooltip:hover::after,
.hg-tooltip:focus-visible::after {
display: block;
/* display cannot be transitioned, so the fade-in is an animation */
animation: hg-tooltip-in 0.15s ease;
}
@keyframes hg-tooltip-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.hg-tooltip-top::after {
bottom: 100%;
left: 50%;
transform: translate(-50%, -0.4rem);
}
.hg-tooltip-bottom::after {
top: 100%;
left: 50%;
transform: translate(-50%, 0.4rem);
}
.hg-tooltip-left::after {
right: 100%;
top: 50%;
transform: translate(-0.4rem, -50%);
}
.hg-tooltip-right::after {
left: 100%;
top: 50%;
transform: translate(0.4rem, -50%);
}
/* On a narrow viewport a wide bubble cannot always be centred on its
trigger without leaving the screen, so the vertical variants are
capped in width and aligned to the trigger's end edge instead. In
left-to-right writing, overflow to the left never makes a page pan
sideways, while overflow to the right does. */
@media (max-width: 40rem) {
.hg-tooltip-top::after,
.hg-tooltip-bottom::after {
left: auto;
right: 0;
max-width: calc(100vw - 3rem);
}
.hg-tooltip-top::after {
transform: translate(0, -0.4rem);
}
.hg-tooltip-bottom::after {
transform: translate(0, 0.4rem);
}
}
/* ===== Drifting colour washes =====
The wallpaper trick: a few oversized, heavily blurred discs of colour
behind a frosted panel. `.hg-blob` only adds the slow drift, so the
size, position, colour and blur stay in the markup where Olive already
expresses them (`blur-3xl`, `w-96`, `bg-naples-yellow-500/50`, ...).
Stagger the discs with an inline `animation-delay` so they never move
in lockstep.
How fast a disc drifts and how far it travels are the page's business,
not this sheet's: `glass-wash` writes `--hg-blob-duration`, `--hg-blob-x`
and `--hg-blob-y` as inline custom properties -- and an inline
`animation-delay` to stagger the discs -- so the values below are only
the defaults for a wash that says nothing. A keyframe resolves its
`var()`s per element, so one panel can breathe for a minute while the
next crosses its own corner in a couple of rems. */
.hg-blob {
animation: hg-blob-drift var(--hg-blob-duration, 26s) ease-in-out infinite alternate;
}
@keyframes hg-blob-drift {
from {
transform: translate3d(0, 0, 0) scale(1);
}
to {
transform: translate3d(var(--hg-blob-x, 1.5rem),
var(--hg-blob-y, -1.25rem),
0)
scale(var(--hg-blob-scale, 1.08));
}
}
/* ===== The layer the discs are painted in =====
A pane rounds the layer holding its washes and clips it, and neither the
rounding nor the clip stops a disc: a child with a `filter` is painted
with the blur that filter asks for, and the browser draws that blur into
the square box it reaches -- past the layer's rounded edge, into the
corner the radius curves away from. What is left on the page is a faint
square of wash beside each corner, a percent or two of ink: measured
against a mid-grey page, a corner square that should have held the
page's own 128 read 130. `overflow: clip`, `contain: paint`,
`isolation: isolate` and promoting the layer to one of its own all leave
it in place (each measured on its own); an explicit `clip-path` is what
clips it. And a clip has to name a radius -- it cannot inherit one the
way `border-radius: inherit` can.
So this class names it -- and names it off the very rounding the markup
gave the layer, since the layer wears the pane's own rounding classes.
The radius has to be restated as a length, so these rules restate
Olive's named ladder; they are the one place here that has to agree with
another sheet, and a spec pins the tokens the framework rounds its own
glass with to a rule apiece, so a rounding cannot lose its clip without
a test going red.
A radius given outright in `--hg-glass-clip' still wins, on the layer or
on any ancestor, which is what a caller rounding with one of Olive's
numeric steps (`rounded-24', say) reaches for, as does a pane whose
radius is not one of the named ones at all.
The default in each rule is `rounded-3xl', the rounding a pane wears
when a caller leaves `#:rounding' alone: a layer wearing something the
table below does not name still keeps its washes in. */
.hg-glass-layer {
clip-path: inset(0 round var(--hg-glass-clip, 1.28rem));
}
.hg-glass-layer[class~="rounded-none"] {
clip-path: inset(0 round var(--hg-glass-clip, 0));
}
.hg-glass-layer[class~="rounded-2xs"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.16rem));
}
.hg-glass-layer[class~="rounded-xs"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.32rem));
}
.hg-glass-layer[class~="rounded-sm"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.48rem));
}
.hg-glass-layer[class~="rounded-md"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.64rem));
}
.hg-glass-layer[class~="rounded-lg"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.80rem));
}
.hg-glass-layer[class~="rounded-xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 0.96rem));
}
.hg-glass-layer[class~="rounded-2xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.12rem));
}
.hg-glass-layer[class~="rounded-3xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.28rem));
}
.hg-glass-layer[class~="rounded-4xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.44rem));
}
.hg-glass-layer[class~="rounded-5xl"] {
clip-path: inset(0 round var(--hg-glass-clip, 1.60rem));
}
.hg-glass-layer[class~="rounded-full"] {
clip-path: inset(0 round var(--hg-glass-clip, 9999px));
}
/* ===== Frost that survives a rounded corner =====
A backdrop-filter asks the browser to blur and colour whatever sits
behind a pane. Chromium paints that filtered backdrop in the pane's
square border box and ignores its `border-radius`, so a rounded pane
leaks a square patch of frost wherever its radius curves away from the
corner (Chromium issue 41212594, still open). A radius on that element
does not stop it, and a `clip-path` on it does not either. The one
thing that does is clipping the frosted box itself: an element that
carries `overflow: hidden` alongside its radius gets its frost rounded
off, because the browser then has a shape to clip the filtered backdrop
to.
The frost therefore lives one level in, on the pane's own `::before`:
a layer that rounds itself to the pane's radius with `border-radius:
inherit` -- so it follows whatever radius the markup or a caller gave
the pane -- clips itself, and carries the blur. Being a decoration
rather than a container, it leaves the pane's own children painted
above it and free to escape, which is what a bar holding a dropdown
needs.
How strong the frost is stays a knob: `--hg-frost-blur` is the blur
radius, 4rem unless something says otherwise (the same value Olive's
`backdrop-blur-md` utility sets). A custom property cannot be a utility
class on a pseudo-element, so it is the one setting that is passed as a
declaration rather than a class: `glass-panel' writes it from Scheme
for the panes it builds (`#:frost'), markup this sheet frosts by hand
passes the declaration itself, and a pane with neither reaches for
`hg-frost-soft' below instead.
`position: relative` gives the layer something to be absolute against
even when a pane places itself with layout classes alone. A pane that
sets a position utility of its own takes over, since olive.min.css is
inlined after this sheet.
`z-index: 0' on the pane and `-1' on the layer are what keep the frost
*under* the pane's content. An absolutely placed layer paints above
in-flow text and blocks, so a frosted pane whose content was never
wrapped in a layer of its own -- a dropdown's list is one -- would
otherwise have its contents blurred away behind the frost. A negative
stack level puts the layer in the first step of the pane's stacking
order instead, right after the pane's own background and border, which
is where the frost belongs. The pane was a stacking context anyway
while it carried the blur itself, so nothing about the order of what
is *outside* the pane changes, and the layer's own corner can no
longer paint over the pane's hairline. */
.hg-frost {
position: relative;
z-index: 0;
}
.hg-frost::before {
content: "";
position: absolute;
z-index: -1;
inset: 0;
border-radius: inherit;
overflow: hidden;
-webkit-backdrop-filter: blur(var(--hg-frost-blur, 4rem));
backdrop-filter: blur(var(--hg-frost-blur, 4rem));
}
/* A gentler frost, for the small panes the framework frosts with a class
rather than a declaration: the carousel's arrow discs, a couple of rems
across, would be all blur at a panel's strength. Olive's blur ladder
cannot reach a pseudo-element, so this sheet names the one step below
the default that something actually wears. */
.hg-frost-soft {
--hg-frost-blur: 3rem;
}
/* The smallest step, for the pills and labels a page frosts by hand: a
badge two rems tall is all blur at a pane's strength, and the label on a
colour swatch wants a hint of it and no more. Naming the step is what
keeps those pills on the same recipe as the panes -- the frost paints in
a rounded layer of its own, so the square patch of blur Olive's
`backdrop-blur-*' utilities leave at a rounded corner never appears. */
.hg-frost-pill {
--hg-frost-blur: 1rem;
}
/* ===== The heading type scale =====
Heading line-heights are ratios rather than lengths, because a heading is
sized by a utility the page may move up or down the scale at will: a
length would have to be re-picked whenever the font-size changed, and a
ratio follows on its own. The steps loosen as the size shrinks (1.3 for
the display sizes, 1.35 for a section title, 1.4 for the smaller
headings), which is the usual shape of a type scale, and every one of
them is loose enough that a line's glyphs -- descenders, accents, the lot
-- stay inside the box the line occupies.
That is a requirement, not a preference. Gradient ink is a background
clipped to the letters, so it can only paint where the heading's own box
reaches: a `g` whose descender falls below it is not cropped but drawn
transparent. The ratios here are measured against the two faces the
framework ships, of which Plus Jakarta Sans asks for the most room at
1.24 of the line box at worst; 1.3 is that with margin to spare. Body
copy sits looser still, at 1.5. */
.hg-leading-display {
line-height: 1.3;
}
.hg-leading-title {
line-height: 1.35;
}
.hg-leading-subtitle {
line-height: 1.4;
}
/* ===== Gradient text =====
One gradient per theme, since a deep sweep that sings on the light
background turns to mud on the dark one.
The compound `h1.`/`h2.` selectors are deliberate: Olive's text-colour
utilities are single classes, and this sheet is inlined before
olive.min.css, so a bare `.hg-gradient-text` would lose the cascade
and the heading would keep its solid colour.
Nothing here has to widen the box the ink is painted in, and that is the
heading scale's doing rather than this rule's: every heading it matches
carries a leaded line-height from `hg-leading-*' above, so the descenders
of a `g` sit inside the heading's own box and have gradient to paint.
A page that overrides the leading with a tighter one -- `leading-none' or
a `leading-*' of its own, which lands after this sheet -- takes the clip
back into its own hands. */
h1.hg-gradient-text,
h2.hg-gradient-text {
background-image: linear-gradient(100deg,
oklch(0.7404 0.1399 91.55 / 1),
oklch(0.6183 0.1557 45.26 / 1) 45%,
oklch(0.4929 0.161 279.98 / 1));
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
[data-theme="dark"] h1.hg-gradient-text,
[data-theme="dark"] h2.hg-gradient-text {
background-image: linear-gradient(100deg,
oklch(0.8855 0.1307 92.19 / 1),
oklch(0.7404 0.1656 39.8 / 1) 45%,
oklch(0.7439 0.1301 288.16 / 1));
}
/* ===== Expandable cards =====
An expandable card is a native <details>, so it opens and hides with no
script at all. The only structural help it asks for here is to drop the
browser's own summary marker -- the chevron in the summary is the
affordance, and it turns as the card opens -- and, because a card is also
what a section rail links to, to keep a header a link jumped to clear of
that rail. */
.hg-expandable-summary {
list-style: none;
/* deep enough for a card header to clear a sticky section rail above it */
scroll-margin-top: 7rem;
}
.hg-expandable-summary::-webkit-details-marker {
display: none;
}
details[open] > .hg-expandable-summary .hg-expandable-chevron {
transform: rotate(180deg);
}
/* ===== Showcase sort control =====
Three buttons offer to reorder the tray, and the chosen one is the one the
page's script has marked `aria-pressed="true"'. Olive tints every button
with a mid surface -- legible, but all three of them the same shape and
shade -- and a control that says which order you are reading has to be
unmistakable at a glance, so the pressed one is filled from the opposite
end of the ramp instead: the near-black of the ink in a light theme, the
near-white of a card in a dark theme, each carrying the ink that surface is
already known to carry. The fill is only the darkest or lightest step of
the palette the page is already built from; the contrast of every pair
below is the framework's own, and the gate checks it. */
.hg-sort-button[aria-pressed="true"] {
background-color: oklch(0.3588 0.0044 247.95 / 1);
color: oklch(0.987 0.0013 286.37);
border-color: oklch(0.987 0.0013 286.37 / 0.4);
}
[data-theme="dark"] .hg-sort-button[aria-pressed="true"] {
background-color: oklch(0.947 0.0013 286.37 / 1);
color: oklch(0.3588 0.0044 247.95);
border-color: oklch(0.3588 0.0044 247.95 / 0.4);
}
/* ===== Code samples =====
The panel that holds a specimen's source folds away until it is asked for,
and the `hidden' attribute is what folds it. The attribute is enough on its
own for an element with no display utility -- the browser's own rule hides
it -- but a panel lays itself out with `flex', and an author rule outbids
the user agent's `[hidden]' however low it sits in the cascade, which would
leave every specimen's source open on every page. So the fold is stated
here as the framework's own rule, at the strength it has to be stated at.
With scripting off the noscript stylesheet reveals the panel instead and
hides the control, so a reader who cannot fold the source still reads it. */
.hg-code-sample-panel[hidden] {
display: none !important;
}
/* ===== Respect reduced-motion preferences ===== */
@media (prefers-reduced-motion: reduce) {
.hg-switch,
.hg-switch::after,
.hg-tooltip::after,
.hg-tab-label,
.hg-blob {
transition: none;
animation: none;
}
}
</style><noscript><style type="text/css">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Progressive-enhancement rules that only apply when scripting is
disabled, because hygguile loads this file inside a <noscript> element
(see hygguile-recommended-resources).
The contract is simple:
- Anything that only works with JavaScript carries the .js-only class
and is hidden here.
- Components that can be made to work without JavaScript (the fly-out
submenus, the carousel) are progressively revealed instead of being
hidden.
*/
/* Interactive controls that depend on JavaScript do nothing without it. */
.js-only {
display: none !important;
}
/* Fly-out menus: reveal every submenu and drop the meaningless arrow. */
.flyout-submenu[hidden] {
display: block !important;
}
/* A code sample: the fold that hides the source needs scripting, and the
control that would lift it is a .js-only element, so without scripting
every specimen simply shows its own source rather than a dead button. */
.hg-code-sample-panel[hidden] {
display: flex !important;
}
.flyout-submenu {
position: static !important;
box-shadow: none !important;
background: transparent !important;
padding-left: 1rem !important;
}
.flyout-submenu-button::after {
content: "" !important;
padding-left: 0 !important;
}
</style></noscript><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function hygguileSaveDarkThemePreference(preference) {
document.documentElement.setAttribute('data-theme', preference)
localStorage.setItem('data-theme', preference)
}
function hygguileToggleDarkTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme')
if (currentTheme == null) {
//
// by default set dark since hygguile has a light theme by default
//
hygguileSaveDarkThemePreference('dark')
} else {
if (currentTheme == 'dark') {
hygguileSaveDarkThemePreference('light')
} else {
hygguileSaveDarkThemePreference('dark')
}
}
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function hygguileCopyPreInnerText(elementId) {
const id = elementId + '-pre'
console.log('copying inner text of element: ' + id)
var el = document.getElementById(id)
navigator.clipboard.writeText(el.innerText)
console.log(el.innerText)
}
</script><script>
document.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(window.location.search);
if(params.get('notify-body') == null && params.get('notify-title') == null){
return
}
const notificationTitle = params.get('notify-title') ?? ""
const notificationBody = params.get('notify-body') ?? ""
const notificationDurationMillis = params.get('notify-duration-millis') ?? 3000
const notificationBodyFontFamily = params.get('notify-body-font-family') ?? 'font-sans'
const notificationTray = document.getElementById('notification-tray')
notificationTray.innerHTML = "<div class=\"bg-stone-300/90 border-2 border-stone-500/25 dark:bg-onyx-700/75 dark:border-onyx-100/25 dark:text-stone-300 dark:text-stone-700 flex flex-col font-sans gap-4 hg-frost max-w-full max-w-prose px-5 py-5 relative rounded-xl text-onyx-100 text-onyx-300\"><div class=\"flex flex-row flex-wrap gap-10 items-center justify-between\"><h4 id=\"notification-title\" class=\"dark:text-asparagus-100 text-asparagus-900 text-xl\"></h4><i onclick=\"document.getElementById('notification-tray').innerHTML = ''\" class=\"cursor-pointer dark:text-asparagus-100 ph ph-x text-asparagus-900 text-xl\"></i></div><p id=\"notification-body\"></p></div>"
const notificationTitleNode = document.getElementById('notification-title')
const notificationBodyNode = document.getElementById('notification-body')
if(notificationTitleNode){
notificationTitleNode.textContent = notificationTitle
}
if(notificationBodyNode){
notificationBodyNode.textContent = notificationBody
notificationBodyNode.classList.toggle(notificationBodyFontFamily)
}
setTimeout(() => { notificationTray.innerHTML = '' }, notificationDurationMillis);
})
</script><script type="text/javascript" defer="" src="https://assets.example.org/js/byggsteg-busy.js"></script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
const carouselItems = (list) => Array.from(list.querySelectorAll('.carousel-list-item'))
const carouselCentres = (list, items) => {
const listCentre = list.getBoundingClientRect().left + list.clientWidth / 2
const centres = items.map((item) => {
const rect = item.getBoundingClientRect()
return rect.left + rect.width / 2
})
const nearest = centres.reduce((best, itemCentre, index) => {
if (Math.abs(itemCentre - listCentre) < Math.abs(centres[best] - listCentre)) {
return index
}
return best
}, 0)
return { listCentre, centres, nearest }
}
// a real function declaration, so the inline onclick attributes emitted by
// the component always find it on the global scope
function handleCarouselClick(direction, id) {
const list = document.getElementById(id)
if (!list) {
return
}
const items = carouselItems(list)
if (!items.length) {
return
}
// the items are centred snap points, so page one item at a time by
// measuring where each one actually sits instead of guessing a step;
// this keeps the gap between items (which changes at the responsive
// breakpoints) and any width difference honest
const { listCentre, centres, nearest } = carouselCentres(list, items)
const step = direction === 'previous' ? -1 : 1
const target = Math.min(Math.max(nearest + step, 0), items.length - 1)
list.scrollBy({ left: centres[target] - listCentre, behavior: 'smooth' })
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
document.addEventListener('DOMContentLoaded', () => {
const themePreference = localStorage.getItem('data-theme')
if (themePreference === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark')
} else if (themePreference === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
}
})
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
function cycleBgClass(currentClass) {
const parts = currentClass.split('-')
const currentNumber = parseInt(parts.toReversed()[0], 10)
const nextNumber = currentNumber >= 900 ? 100 : currentNumber + 200
const startParts = parts.slice(0, parts.length - 1).join("-")
return `${startParts}-${nextNumber}`
}
function cycleColorShow(el) {
const currentClass = [...el.classList].find(cls => cls.startsWith('bg-'))
const newClass = cycleBgClass(currentClass)
el.classList.toggle(currentClass)
el.classList.toggle(newClass)
el.children[0].innerText = newClass.replace("bg-", "")
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
const closeMenu = (button, menu) => {
button.setAttribute('aria-expanded', 'false')
menu.hidden = true
}
const openMenu = (button, menu) => {
button.setAttribute('aria-expanded', 'true')
menu.hidden = false
}
const closeAllOpenMenus = () => {
const menuContainers = document.querySelectorAll('.flyout-menu-container')
menuContainers.forEach(menuContainer => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
if (menuButton) {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (isExpanded) {
menuButton.click()
}
}
})
}
const HOVER_DELAY = 2000
document.addEventListener('DOMContentLoaded', () => {
const menuContainers = document.querySelectorAll('.flyout-menu-container')
menuContainers.forEach(menuContainer => {
let hoverTimer
menuContainer.addEventListener('mouseenter', () => {
hoverTimer = setTimeout(() => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
if (menuButton) {
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (!isExpanded) {
closeAllOpenMenus()
menuButton.click()
}
}
}, HOVER_DELAY)
})
menuContainer.addEventListener('mouseleave', () => {
clearTimeout(hoverTimer)
})
})
menuContainers.forEach(menuContainer => {
const menuButton = menuContainer.querySelector('.flyout-submenu-button')
const dropdownMenu = menuContainer.querySelector('.flyout-submenu')
if (!menuButton || !dropdownMenu) {
return
}
const menuItems = dropdownMenu.querySelectorAll('a[href], button:not([disabled])')
const firstMenuItem = menuItems[0]
const lastMenuItem = menuItems[menuItems.length - 1]
menuButton.addEventListener('click', (event) => {
event.preventDefault()
const isExpanded = menuButton.getAttribute('aria-expanded') === 'true'
if (isExpanded) {
closeMenu(menuButton, dropdownMenu)
} else {
closeAllOpenMenus()
openMenu(menuButton, dropdownMenu)
}
})
menuButton.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown') {
event.preventDefault()
openMenu(menuButton, dropdownMenu)
if (firstMenuItem) {
firstMenuItem.focus()
}
} else if (event.key === 'Escape') {
closeMenu(menuButton, dropdownMenu)
menuButton.focus()
}
})
dropdownMenu.addEventListener('keydown', (event) => {
const { key } = event
const activeElement = document.activeElement
if (key === 'Escape') {
closeMenu(menuButton, dropdownMenu)
menuButton.focus()
return
}
if (key === 'ArrowUp') {
event.preventDefault()
if (activeElement === firstMenuItem) {
lastMenuItem.focus()
} else {
const currentIndex = Array.from(menuItems).indexOf(activeElement)
if (currentIndex > 0) {
menuItems[currentIndex - 1].focus()
}
}
} else if (key === 'ArrowDown') {
event.preventDefault()
if (activeElement === lastMenuItem) {
firstMenuItem.focus()
} else {
const currentIndex = Array.from(menuItems).indexOf(activeElement)
if (currentIndex < menuItems.length - 1) {
menuItems[currentIndex + 1].focus()
}
}
}
})
})
})
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
The fold on a `code-sample': the source of a specimen is hidden until a
reader asks for it.
The component says everything this needs. The control carries an
`aria-controls' naming the panel and an `aria-expanded' saying whether the
panel is showing, and the panel carries the `hidden' attribute the component
wrote; the whole state the script keeps is those two attributes, so a page
that renders its own markup without the component gains nothing by loading
this file.
Without scripting the panel is revealed by the `noscript' stylesheet and the
control is hidden as a `.js-only' element, so a reader who cannot fold the
source still reads it.
*/
function hygguileToggleCodeSample(button) {
const id = button.getAttribute('aria-controls')
const panel = id && document.getElementById(id)
if (!panel) return
panel.hidden = !panel.hidden
button.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true')
}
</script><script type="text/javascript" defer="">/*
hygguile - cozy and professional user-interfaces for everyone
Copyright © Josep Bigorra <jjbigorra@gmail.com>
hygguile is free software: you can redistribute it and/or modify
it under the terms of the Lesser GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
hygguile is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Lesser GNU General Public License for more details.
You should have received a copy of the Lesser GNU General Public License
along with hygguile. If not, see <https://www.gnu.org/licenses/>.
*/
/*
The tray controls: the two things a page asks of a whole tray of cards at
once, which order it is in and whether all of it is open. Both are declared
in the markup by `sort-control' and `disclosure-controls', so the only
JavaScript a page carries is the call each button already makes.
Ordering is data, not code. A button reaches this file as
hygguileSortTray(this, 'title')
and the tray it orders is whichever container the button's group names in
its `data-hg-sort-target'. The items are ordered by their `data-title'
attribute -- numerically when both values are numbers, so data-order="10"
follows data-order="9" instead of sorting in front of it, and
alphabetically otherwise. The sort is stable, so items that compare equal
keep the order they are already in. A mode that no item carries is the
tray's own order: the items go back to the order the page wrote them in,
which is what a "Page order" button is for and why it needs no attribute
anywhere.
*/
(function () {
/* The order each tray was written in, read once the document is parsed:
by the time a button has been clicked it is too late to ask. */
const written = new Map()
const writtenOrder = (container) => written.get(container)
|| Array.prototype.slice.call(container.children)
const asNumber = (text) => {
const trimmed = text.trim()
return trimmed !== '' && !isNaN(Number(trimmed)) ? Number(trimmed) : null
}
const compare = (mode) => (a, b) => {
const left = a.getAttribute('data-' + mode) || ''
const right = b.getAttribute('data-' + mode) || ''
const leftNumber = asNumber(left)
const rightNumber = asNumber(right)
if (leftNumber !== null && rightNumber !== null) {
return leftNumber - rightNumber
}
return left.localeCompare(right)
}
const groupOf = (button) => button.closest('[data-hg-sort-target]')
/* One button of the group is the order in force; the stylesheet draws that
as the pressed one, and a screen reader reads it out. */
const mark = (button, group) => {
Array.prototype.forEach.call(group.querySelectorAll('button'),
(other) => {
other.setAttribute('aria-pressed',
other === button
? 'true' : 'false')
})
}
window.hygguileSortTray = (button, mode) => {
const group = groupOf(button)
const container = group
&& document.getElementById(group.getAttribute('data-hg-sort-target'))
if (!container) {
return
}
const items = Array.prototype.slice.call(container.children)
const carries = items.some((item) => item.hasAttribute('data-' + mode))
const ordered = carries
? items.sort(compare(mode))
: writtenOrder(container).slice()
ordered.forEach((item) => container.appendChild(item))
mark(button, group)
}
window.hygguileToggleTray = (open, selector) => {
Array.prototype.forEach.call(document.querySelectorAll(selector),
(details) => {
details.open = open
})
}
/* The tray is in the order the page wrote it in, and the first button of a
group is the order it opens in, so a page states its own order by writing
that mode first. */
document.addEventListener('DOMContentLoaded', () => {
Array.prototype.forEach.call(
document.querySelectorAll('[data-hg-sort-target]'),
(group) => {
const container = document.getElementById(
group.getAttribute('data-hg-sort-target'))
if (container) {
written.set(container, Array.prototype.slice.call(container.children))
}
const first = group.querySelector('button')
if (first) {
mark(first, group)
}
})
})
})()
</script><link rel="stylesheet" href="https://assets.example.org/css/byggsteg.olive.min.css"></link><script type="text/javascript" defer="" src="https://assets.example.org/js/symbanta.js" data-endpoint="https://api.symbanta.jointhefreeworld.org/v1/events/register" data-interval="4000"></script><script type="speculationrules">
{
"prerender": [
{
"where": {
"selector_matches": "a"
}
}
]
}
</script><script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareApplication","name":"byggsteg","alternateName":"byggsteg CI/CD","applicationCategory":"DeveloperApplication","operatingSystem":"GNU/Linux","programmingLanguage":"Scheme","description":"byggsteg is a hackable and extensible CI/CD orchestrator written in Guile Scheme: a pipeline is Lisp code, a running job's log is read while it is written, and the runner, the logs, the artifacts and the users are all yours to host.","isAccessibleForFree":true,"license":"https://www.gnu.org/licenses/agpl-3.0.html","codeRepository":"https://codeberg.org/jjba23/byggsteg","downloadUrl":"https://codeberg.org/jjba23/byggsteg","author":{"@type":"Person","name":"Josep Bigorra"},"offers":{"@type":"Offer","price":"0","priceCurrency":"EUR"},"url":"http://127.0.0.1:3000/"}</script></head><body class="bg-stone-100 dark:bg-onyx-900 dark:text-stone-700 font-mono rounded-none text-onyx-100"><div id="notification-tray" role="status" aria-live="polite" aria-atomic="true" class="fixed right-8 top-8 z-32"></div><div class="dark:text-stone-700 flex flex-col justify-between min-h-screen text-onyx-100"><div><header style="--hg-frost-blur: 3rem" class="bg-stone-100/75 border-asparagus-700 border-b-2 dark:bg-onyx-900/75 dark:border-asparagus-900 hg-frost md:px-5 px-2 py-1 shadow-md sm:px-3 sticky top-0 w-full z-10"><div class="pb-1 pt-1 px-2 sm:pb-2 sm:px-3 w-full"><a href="/" class="flex flex-row gap-2 items-center no-underline"><span class="dark:text-asparagus-100 font-mono text-asparagus-900">$</span><h3 class="dark:hover:text-olive-300 dark:text-asparagus-100 font-bold font-sans hg-leading-display hover:text-asparagus-700 lg:text-xl sm:text-lg text-asparagus-900 text-md">byggsteg</h3><span class="animate-pulse dark:text-asparagus-100 font-mono text-asparagus-900">▮</span></a></div><div class="align-center flex flex-row flex-wrap gap-1 items-center sm:gap-2 sm:justify-between"><div class="contents gap-1 sm:flex sm:flex-row sm:flex-wrap sm:gap-2 sm:justify-start"><div><a href="/logs"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-scroll select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>logs</span></div></button></a></div><div><a href="/activity"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-chart-bar select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>activity</span></div></button></a></div><div><a href="/search"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-magnifying-glass select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>search</span></div></button></a></div><div><a href="/profiles"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-folder select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>profiles</span></div></button></a></div><div><a href="/about"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-info select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>about</span></div></button></a></div><div><a class="dark:text-asparagus-100 flex flex-row font-medium font-sans gap-2 items-center lowercase no-underline text-asparagus-900" href="/login"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-sign-in select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>login</span></div></button></a></div></div><div class="contents gap-1 sm:flex sm:flex-row sm:flex-wrap sm:gap-2 sm:justify-start"><a href="/jobs/request"><button onclick="" id="" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-100 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 hover:text-asparagus-700 max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors" visible=""><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph-circuitry select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span>run job</span></div></button></a><button onclick="hygguileToggleDarkTheme()" type="button" class="active:scale-95 bg-stone-500/90 border-2 border-stone-700 cursor-pointer dark:bg-onyx-500/90 dark:border-onyx-300 dark:hover:bg-onyx-900 dark:hover:text-olive-300 dark:text-asparagus-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-light font-mono hover:bg-stone-900/90 hover:text-asparagus-700 js-only max-w-prose md:px-3 md:py-2 md:text-md outline-asparagus-500 px-1 py-1 rounded-none select-none shadow-md shrink-0 sm:px-2 text-asparagus-900 text-sm transition-colors"><div class="flex flex-row gap-1 items-center"><i class="dark:text-asparagus-100 hidden md:text-md ph ph- select-none shrink-0 sm:inline text-asparagus-900 text-sm"></i><span><div><i class="dark:text-asparagus-100 md:text-md ph ph-sun select-none shrink-0 text-asparagus-900 text-sm"></i><i class="dark:text-asparagus-100 md:text-md ph ph-moon select-none shrink-0 text-asparagus-900 text-sm"></i></div></span></div></button></div></div></header><div class="container mx-auto my-4"><div class="bg-stone-300 border-2 border-asparagus-700 dark:bg-onyx-700 dark:border-asparagus-900 rounded-none shadow-md p-4"><img loading="lazy" class="h-auto w-48" src="https://assets.example.org/img/byggsteg-logo.png" /></div></div></div><footer class="bg-stone-300 border-asparagus-700 border-t-2 dark:bg-onyx-900 dark:border-asparagus-900 dark:text-stone-700 flex flex-col font-mono gap-1 items-start justify-start md:px-10 px-3 py-2 sm:px-6 sm:py-3 text-onyx-100"><div class="align-center flex flex-row flex-wrap gap-x-4 gap-y-1 items-center"><a href="https://codeberg.org/jjba23/byggsteg" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">source</a><a href="https://codeberg.org/jjba23/byggsteg/src/branch/trunk/README.org" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">manual</a><a href="https://jointhefreeworld.org/manuals/byggsteg/" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">documentation</a><a href="/about" class="dark:hover:text-moonstone-100 dark:text-asparagus-100 duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 font-bold font-mono hover:text-jeans-blue-900 hover:underline no-underline outline-jeans-blue-500 text-asparagus-900 text-sm transition-colors">about</a><a href="javascript:void(0)" class="cursor-pointer dark:text-asparagus-100 font-medium font-mono no-underline text-asparagus-900 text-sm" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })">back to the top</a></div><div class="align-center flex flex-row flex-wrap gap-x-3 gap-y-1 items-center"><em class="dark:text-stone-900 font-light font-mono italic leading-5 text-onyx-300 text-sm">© Josep Bigorra & NalaGinrut -- AGPL-3.0-or-later</em><em class="dark:text-stone-900 font-light font-mono italic leading-5 text-onyx-300 text-sm">version: 1.19.5 (7b86109)</em></div></footer></div></body></html>
ℹ️ info - ran 1 tasks in: 0 seconds, 148350 microseconds
running unit tests from file: about-page-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] about page
[🧪 test] the account is on the page, and it is this deployment's
[⚙️ assert] the story names the people, the framework and the three doors
✅ got: ()
[⚙️ assert] and it holds nothing the page was never meant to say
✅ got: ()
[⚙️ assert] the page opens under the console's own prompt
✅ got: $ byggsteg --origin
[🧪 test] the page a crawler is handed says what it is
[⚙️ assert] it declares itself a document of this deployment
✅ got: <!DOCTYPE html>
[⚙️ assert] it is dressed in the same sheet as every other page
✅ got: /css/byggsteg.olive.min.css
[⚙️ assert] its canonical address is the page's own
✅ got: /about
[⚙️ assert] its title names the deployment and the page
✅ got: <title>byggsteg - about</title>
[🧪 test] the page can be read on a phone
[⚙️ assert] no panel of it forces a width wider than the screen
✅ got: ()
[⚙️ assert] the panels are told they may shrink
✅ got: min-w-0
[🧪 test] the page is built by the components rather than describing them
[⚙️ assert] no component call was left un-made in the markup
✅ got: ()
[⚙️ assert] every element of the page is an element a browser knows
✅ got: ()
[⚙️ assert] no element of the page is closed on itself
✅ got: ()
ℹ️ info - ran 1 tasks in: 0 seconds, 231361 microseconds
running unit tests from file: tasks-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] Tasks Suite
[🧪 test] The three families are all there, and none is empty
[⚙️ assert] a target a project is likely to have
✅ got: #t
[⚙️ assert] a task done by the machine
✅ got: #t
[⚙️ assert] the one that runs anything
✅ got: #t
[⚙️ assert] The list is the groups, in the order they are read in
✅ got: (build test lint check fmt docs package migrate clean coverage benchmark smoke e2e audit maak-build maak-test maak-lint maak-check maak-fmt maak-docs maak-package maak-migrate maak-clean maak-coverage maak-benchmark maak-smoke maak-e2e maak-audit just-build just-test just-lint just-check just-fmt just-docs just-package just-migrate just-clean just-coverage just-benchmark just-smoke just-e2e just-audit make-build make-test make-lint make-check make-fmt make-docs make-package make-migrate make-clean make-coverage make-benchmark make-smoke make-e2e make-audit deploy maak-deploy make-deploy restart pull-and-deploy pull-and-restart byggsteg-version trivial)
[🧪 test] A prefixed target runs with the runner it names
[⚙️ assert] maak-test asks Maak, even when a Makefile is there
✅ got: #t
[⚙️ assert] make-test asks Make on the same checkout
✅ got: #t
[🧪 test] Every target can also be asked for by runner
[⚙️ assert] Each bare target has a spelling for every advertised runner
✅ got: #t
[⚙️ assert] The bare name and those spellings are the whole target family
✅ got: 56
[🧪 test] The runner is the checkout's
[⚙️ assert] A clone with a maak.scm is a Maak checkout
✅ got: maak
[⚙️ assert] A clone with a justfile is a Just checkout
✅ got: just
[⚙️ assert] a justfile wins over a Makefile beside it
✅ got: just
[⚙️ assert] A clone without one is a Make checkout
✅ got: make
[⚙️ assert] A clone that is not there is read as Make
✅ got: make
[🧪 test] Every task is named once and described
[⚙️ assert] No task is named twice
✅ got: 64
[⚙️ assert] Every task has a reading, since a client is handed it
✅ got: #t
[⚙️ assert] A name is one word, so a job can write it
✅ got: #t
[🧪 test] A bare target runs with the checkout's runner
[⚙️ assert] test on a Make checkout is make test
✅ got: #t
[⚙️ assert] and it ran
✅ got: #t
[⚙️ assert] the target the Makefile has ran
✅ got: make-tested
[⚙️ assert] test on a Maak checkout is maak test
✅ got: #t
[⚙️ assert] with the target named, and not the project name
✅ got: #t
[⚙️ assert] test on a Just checkout is just test
✅ got: #t
[⚙️ assert] and the justfile's target ran
✅ got: #t
[⚙️ assert] a justfile beside a Makefile is still the Just checkout
✅ got: #t
[🧪 test] A task is advertised, or it is a target the project invented
[⚙️ assert] a bare target is advertised
✅ got: #t
[⚙️ assert] so is a spelling of one
✅ got: #t
[⚙️ assert] and Just's spelling of one
✅ got: #t
[⚙️ assert] a target only the project has is not in the vocabulary
✅ got: #f
[⚙️ assert] and neither is a machine task
✅ got: #f
[🧪 test] A target the vocabulary does not name is still a target
[⚙️ assert] publish is run as the target it names
✅ got: #t
[⚙️ assert] and the project's own target did the work
✅ got: make-published
[🧪 test] A name is split into the runner it names and the target it wants
[⚙️ assert] A bare name names no runner
✅ got: (#f . test)
[⚙️ assert] maak- names Maak
✅ got: (maak . test)
[⚙️ assert] just- names Just
✅ got: (just . test)
[⚙️ assert] make- names Make
✅ got: (make . test)
[⚙️ assert] A target nobody has heard of can still name a runner
✅ got: (make . publish)
[⚙️ assert] A machine task is not split
✅ got: (#f . pull-and-restart)
ℹ️ info - ran 1 tasks in: 0 seconds, 39111 microseconds
running unit tests from file: footer-version-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] the release a deployment is
[🧪 test] the footer names it, and the commit beside it
[⚙️ assert] the release the tree is at, read from (byggsteg version)
✅ got: version: 1.19.5 (
[⚙️ assert] and the commit it was built from, bracketed after it
✅ got: #t
ℹ️ info - ran 1 tasks in: 0 seconds, 35854 microseconds
running unit tests from file: heatmap-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] activity heatmap
[🧪 test] the calendar between a day number and a date is the reader's own
[⚙️ assert] every date is read back as the day number it is known by
✅ got: ()
[⚙️ assert] the epoch is day zero, and the day before it is minus one
✅ got: (0 -1)
[⚙️ assert] a leap day is a day of its own
✅ got: 11016
[⚙️ assert] the week starts on Sunday, and the epoch was a Thursday
✅ got: (4 2 3)
[🧪 test] jobs are counted by the day they were queued on
[⚙️ assert] a day is counted once per job, and what is not a date is left out
✅ got: ((20739 . 3) (20730 . 2) (20737 . 1))
[🧪 test] the grid ends with the week today is in
[⚙️ assert] two columns of seven days, from the Sunday before to today
✅ got: ((columns 2) (cells-per-column 7) (first-day 20730) (last-day 20743) (today-is-in-the-last-column 3) (a-day-not-happened-yet #f))
[🧪 test] a square is as dark as its day is busy
[⚙️ assert] the busiest day is darkest, and a day with no jobs is not drawn dark
✅ got: ((20739 . 4) (20730 . 3) (20737 . 2) (20738 . 0))
[⚙️ assert] the four steps between are read off the busiest day
✅ got: ((against-a-busiest-day-of-ten (0 . 0) (2 . 1) (3 . 2) (5 . 2) (6 . 3) (9 . 3) (10 . 4)) (no-busiest-day 1) (a-day-that-has-not-happened #f))
[🧪 test] the months are named along the top of the grid
[⚙️ assert] a year of weeks is labelled, from its first column on
✅ got: ((labels 13) (first-label-name Oct) (first-label-column 0) (two-days-of-one-month-are-named-alike #t) (two-months-are-named-differently #t))
[🧪 test] a deployment with no jobs draws a grid rather than nothing
[⚙️ assert] an empty heatmap has a total, a busiest day and a full year of squares
✅ got: ((total 0) (max 0) (columns 50) (cells 350))
ℹ️ info - ran 1 tasks in: 0 seconds, 28862 microseconds
running unit tests from file: user-bootstrap-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] user bootstrap
[🧪 test] the first account is a super user a sign-in recognizes
[⚙️ assert] the row holds the username, the email, a hash of the password and every permission
✅ got: ((the-account-is-there #t) (with-the-username-given alice) (and-the-email-given alice@example.org) (the-password-is-not-stored #f) (but-the-hash-of-it-is #t) (and-another-password-is-not #f) (it-is-stored-with-a-salt-of-its-own #t) (it-holds-every-permission (byggsteg:job-create byggsteg:job-delete byggsteg:profile-create byggsteg:profile-delete byggsteg:user-create byggsteg:user-delete byggsteg:modify-permissions)))
[🧪 test] an account named its own permissions, or none
[⚙️ assert] a limited account holds what was asked for, an empty one holds nothing, and an email names one account
✅ got: ((the-limited-account-was-made #t) (with-exactly-the-permissions-named (byggsteg:job-create ;;; compiling ./test/veritas/unit/terminal-glass-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/terminal-glass-spec.scm.go
;;; compiling ./test/veritas/unit/log-streaming-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/log-streaming-spec.scm.go
;;; compiling ./test/veritas/unit/deployment-identity-spec.scm
;;; compiling ./app/views/about.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/app/views/about.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/deployment-identity-spec.scm.go
;;; compiling ./test/veritas/unit/runners-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/runners-spec.scm.go
;;; compiling ./test/veritas/unit/logs-live-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/logs-live-spec.scm.go
;;; compiling ./test/veritas/unit/password-strength-spec.scm
;;; compiling ./lib/byggsteg/domain/password.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/password.scm.go
;;; compiling ./app/views/me.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/app/views/me.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/password-strength-spec.scm.go
;;; compiling ./test/veritas/unit/api-v2-spec.scm
;;; compiling ./lib/byggsteg/api.scm
;;; compiling ./lib/byggsteg/auth.scm
;;; compiling ./test/byggsteg/env.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/byggsteg/env.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/auth.scm.go
;;; compiling ./lib/byggsteg/heatmap.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/heatmap.scm.go
;;; compiling ./lib/byggsteg/domain/user.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/domain/user.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/api.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/api-v2-spec.scm.go
;;; compiling ./test/veritas/unit/console-resources-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/console-resources-spec.scm.go
;;; compiling ./test/veritas/unit/about-page-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/about-page-spec.scm.go
;;; compiling ./test/veritas/unit/tasks-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/tasks-spec.scm.go
;;; compiling ./test/veritas/unit/footer-version-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/footer-version-spec.scm.go
;;; compiling ./test/veritas/unit/heatmap-spec.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/heatmap-spec.scm.go
;;; compiling ./test/veritas/unit/user-bootstrap-spec.scm
;;; compiling ./lib/byggsteg/user-bootstrap.scm
;;; compiling ./lib/byggsteg/sqlite/user.scm
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/sqlite/user.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/lib/byggsteg/user-bootstrap.scm.go
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/user-bootstrap-spec.scm.go
;;; compiling ./test/veritas/unit/retry-spec.scm
byggsteg:profile-create)) (its-password-works-too #t) (an-account-can-be-made-with-none ()) (and-it-signs-in-all-the-same #t) (two-accounts-cannot-share-an-email #f))
ℹ️ info - ran 1 tasks in: 0 seconds, 26330 microseconds
running unit tests from file: retry-spec.scm, entrypoint: spec
ℹ️ info - shuffled with seed 359279794 (replay with VERITAS_SEED=359279794)
+----------------------------------------------+
[📑 suite] where a retry is offered
[🧪 test] a list offers a retry only for a job that failed
[⚙️ assert] a green or a running row has nothing to retry; the red one does
✅ got: ((failed . #t) (succeeded . #f) (running . #f) (queued . #f))
[⚙️ assert] and the row the rule answers for is the row that carries the button
✅ got: ((failed . #t) (succeeded . #f) (running . #f) (queued . #f))
[🧪 test] a job's own page offers a retry whatever its status
[⚙️ assert] every job but one still running can be run again from its page
✅ got: ((failed . #t) (succeeded . #t) (running . #f) (queued . #t))
[🧪 test] a reader who may not submit a retry is offered none
[⚙️ assert] with no permission at all, neither a row nor a job's own page offers a retry
✅ got: ((failed . #f) (succeeded . #f) (running . #f) (queued . #f))
[⚙️ assert] and the page is no more generous than the list: a form a reader cannot send is not drawn
✅ got: ((failed . #f) (succeeded . #f) (running . #f) (queued . #f))
[⚙️ assert] holding byggsteg:job-create, the rules read exactly as they do for a super user
✅ got: ((failed . #t) (succeeded . #f) (running . #f) (queued . #f))
[⚙️ assert] and the row that rule answers for is the row that carries the form, for a reader who may send it
✅ got: ((failed . #t) (succeeded . #f) (running . #f) (queued . #f))
[⚙️ assert] while a reader who may not send one is drawn no form, however red the row
✅ got: ((failed . #f) (succeeded . #f) (running . #f) (queued . #f))
ℹ️ info - ran 1 tasks in: 0 seconds, 8592 microseconds
finished running tests
;;; compiled /root/.cache/guile/ccache/3.0-LE-8-4.7/etc/byggsteg/job-clone/byggsteg/trunk/test/veritas/unit/retry-spec.scm.go
--------------------------------------------------------------
Maak 0.14.0: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/byggsteg/trunk/maak.scm
Executing Maak tasks: test
--------------------------------------------------------------
=> Running Maak task: test
Run the project's test suite.
The specs live under test/veritas/unit. That is where the runner looks for
suites -- given the test root ./test, the module prefix `veritas' and the suite
name `unit' -- and it is also what their own `(veritas unit ...)' module names
resolve to on the load path below.
The run names the locale it reads in rather than inheriting one, and it names it
before the runner is asked for: Guile reads a page by the locale in force, and a
pattern over a page that holds a character outside ASCII reports matches where
there are none under a locale that names no encoding. `C.UTF-8' is that name
for a machine that has never generated a locale -- a service, a container, and
this project's own continuous integration, where the job that tests byggsteg is
run by a byggsteg daemon whose Shepherd unit names no locale at all. The suite
reads the pages of this console, whose prose, whose marks and whose console
output are not ASCII; run without a locale it would find elements no browser
knows and fail for a reason that has nothing to do with the change it was run to
judge. Naming it here is part of what the pinned manifest is for: a run of the
suite reads the same pages wherever the pipeline runs.
The specs are compiled rather than read from a cache. A compiled spec outlives
the library it was compiled against -- Guile decides a compiled file is still
good by the spec's own text, and not by the macros and procedures that text was
compiled with -- so a spec compiled before `(veritas veritas)' changed shape is
loaded afterwards as if it still had the old one. What that reads like is a
failure inside this project: `Wrong type to apply: #<syntax-transformer test>'
at the first test of a suite, on a spec whose source is untouched and whose
suite passes when compiled afresh. Compiling from source every run costs a few
seconds and makes the reading depend on the specs alone, which is what a suite
that is run to judge a change has to be.
Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- guile --fresh-auto-compile -L ./lib -L . -L ./test -c '(begin (setlocale LC_ALL "C.UTF-8") (use-modules (veritas runner)) (run-tests))'
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && sudo herd restart byggsteg-daemon || true
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && sudo maak deploy
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
Warning (emacs): Cannot fontify source block (htmlize.el >= 1.34 required). Falling back to plain text. (see ‘org-html-htmlize-output-type’)
finished publishing the Byggsteg manual
--------------------------------------------------------------
Maak 0.14.0: the infinitely extensible command runner
Loading tasks from Maak file: /etc/byggsteg/job-clone/byggsteg/trunk/maak.scm
Executing Maak tasks: deploy
--------------------------------------------------------------
=> Running Maak task: deploy
Deploy the project's documentation, which is its README.
Execute command:
guix time-machine --channels=channels.scm -- shell -m manifest.scm -- emacs -Q --batch --eval '(progn (find-file "./README.org") (org-html-export-to-html) (message "finished publishing the Byggsteg manual"))'
Executing: guix time-machine --channels=channels.scm -- shell -m manifest.scm -- emacs -Q --batch --eval '(progn (find-file "./README.org") (org-html-export-to-html) (message "finished publishing the Byggsteg manual"))'
removed '/srv/http/jointhefreeworld.org/manuals/byggsteg/index.html'
renamed 'README.html' -> '/srv/http/jointhefreeworld.org/manuals/byggsteg/index.html'