starting new job...
→ running command: cd "/etc/byggsteg/job-clone//byggsteg/trunk" && git reset --hard && git pull
HEAD is now at 7e3b12b feat: ANSI support
From https://codeberg.org/jjba23/byggsteg
7e3b12b..7c2c9ae trunk -> origin/trunk
Updating 7e3b12b..7c2c9ae
Fast-forward
test/veritas/unit/job-pipelines-spec.scm | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && uname -a
Linux wolk-jjba 7.1.5-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 7c2c9aea3690dab9110e1914928417b458dccb10
Author: Josep Bigorra <jjbigorra@gmail.com>
Date: Wed Oct 14 17:53:58 2026 +0200
feat: ANSI support
M test/veritas/unit/job-pipelines-spec.scm
→ running command: cd /etc/byggsteg/job-clone/byggsteg/trunk && maak test
scanning directories for unit tests...
scanning present working directory for unit tests...
shuffled the files with seed 166757910 (replay with VERITAS_SEED=166757910)
found unit tests: (deployment-identity-spec.scm about-page-spec.scm artifact-spec.scm job-pipelines-spec.scm form-width-spec.scm retention-spec.scm password-strength-spec.scm password-reset-spec.scm job-pipeline-spec.scm ansi-spec.scm console-resources-spec.scm job-search-spec.scm retry-spec.scm user-bootstrap-spec.scm log-streaming-spec.scm terminal-glass-spec.scm error-pages-spec.scm heatmap-spec.scm)
running unit tests from file: deployment-identity-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� suite] what a deployment calls itself
[���� 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
[���� 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"
������ info - ran 1 tasks in: 0 seconds, 31482 microseconds
running unit tests from file: about-page-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� suite] about page
[���� 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: ()
[���� 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 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 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>
������ info - ran 1 tasks in: 0 seconds, 3478 microseconds
running unit tests from file: artifact-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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-19867/spec-artifact-job.tar.gz) (artifact-size . 2048) (created-at . 1791993245) (updated-at . #f)) ((id . spec-artifact-2) (job-id . spec-artifact-job) (title . byggsteg-job-2.tar.gz) (artifact-path . /tmp/byggsteg-artifact-spec-19867/spec-artifact-job-2.tar.gz) (artifact-size . 4096) (created-at . 1791993245) (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-19867/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] the worker's sweep runs on its interval, not on every visit
[������ assert] a due visit removes, the one straight after it does not look
��� got: ((a-due-visit-removed 1) (a-visit-straight-after-it-removed 0) (the-orphan-it-did-not-look-for-is-still-there #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, 18856 microseconds
running unit tests from file: job-pipelines-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 349609 microseconds
running unit tests from file: form-width-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� suite] the width of a field
[���� 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] 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] 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] 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, 20530 microseconds
running unit tests from file: retention-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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))
������ info - ran 1 tasks in: 0 seconds, 1470 microseconds
running unit tests from file: password-strength-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 1025 microseconds
running unit tests from file: password-reset-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 528 microseconds
running unit tests from file: job-pipeline-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 66150 microseconds
running unit tests from file: ansi-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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] 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, 2786 microseconds
running unit tests from file: console-resources-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� suite] the console's own appearance, served by the deployment itself
[���� 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] 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)
[���� test] a deployment that keeps its assets elsewhere names that host
[������ 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 choice = null
try { choice = localStorage.getItem('data-theme') } catch (error) { choice = null }
document.documentElement.setAttribute('data-theme', choice === 'light' ? 'light' : 'dark')
})()
</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 flex flex-col font-sans gap-4 hg-frost max-w-full max-w-prose px-5 py-5 relative rounded-xl text-onyx-300\"><div class=\"flex flex-row flex-wrap gap-10 items-center justify-between\"><h4 id=\"notification-title\" class=\"dark:text-stone-300 text-onyx-500 text-xl\"></h4><i onclick=\"document.getElementById('notification-tray').innerHTML = ''\" class=\"cursor-pointer dark:text-stone-300 ph ph-x text-onyx-500 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="">/*
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="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-olive-100 font-mono rounded-none text-asparagus-900"><div id="notification-tray" role="status" aria-live="polite" aria-atomic="true" class="fixed right-8 top-8 z-32"></div><div class="flex flex-col justify-between min-h-screen"><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-100 dark:text-asparagus-100 font-bold font-sans hg-leading-display hover:text-onyx-900 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 text-sm"></i><span>about</span></div></button></a></div><div><a class="dark:text-white flex flex-row font-medium font-sans gap-2 items-center lowercase no-underline text-onyx-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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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:text-asparagus-300 dark:text-stone-300 duration-200 ease-in-out focus-visible:outline-2 focus-visible:outline-offset-2 font-medium font-mono hover:bg-stone-900/90 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-onyx-300 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-300 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: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 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-300 text-sm"></i><span><div><i class="dark:text-stone-300 md:text-md ph ph-sun select-none shrink-0 text-asparagus-900 text-sm"></i><i class="dark:text-stone-300 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-asparagus-300 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-asparagus-900"><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-olive-300 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-olive-300 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-olive-300 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-olive-300 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-300 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-asparagus-300 font-light font-mono italic leading-5 text-asparagus-900 text-sm">�� Josep Bigorra & NalaGinrut -- AGPL-3.0-or-later</em><em class="dark:text-asparagus-300 font-light font-mono italic leading-5 text-asparagus-900 text-sm">version: 7c2c9ae</em></div></footer></div></body></html>
[���� test] and the Lucida sheet names faces the deployment has
[���� test] no page of the console asks anybody else for it
������ info - ran 1 tasks in: 0 seconds, 8587 microseconds
running unit tests from file: job-search-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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: 0 seconds, 4418 microseconds
running unit tests from file: retry-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 3467 microseconds
running unit tests from file: user-bootstrap-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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 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, 2741 microseconds
running unit tests from file: log-streaming-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 31628 microseconds
running unit tests from file: terminal-glass-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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, 1247 microseconds
running unit tests from file: error-pages-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� suite] error pages
[���� 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] no page closes an element on itself
[������ assert] no element of a status page swallows what follows it
��� 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] 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] 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, 3627 microseconds
running unit tests from file: heatmap-spec.scm, entrypoint: spec
������ info - shuffled with seed 166757910 (replay with VERITAS_SEED=166757910)
+----------------------------------------------+
[���� 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 53) (cells 371))
������ info - ran 1 tasks in: 0 seconds, 5319 microseconds
finished running tests
WARNING: (hygguile hygguile): imported module (hygguile components select) overrides core binding `select'
WARNING: (byggsteg html): `get-short-commit-sha' imported from both (hygguile hygguile) and (kracht prelude)
;;; note: source file ./test/veritas/unit/job-pipelines-spec.scm
;;; newer than 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
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;; or pass the --no-auto-compile argument to disable.
;;; 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
WARNING: (byggsteg html): imported module (hygguile hygguile) overrides core binding `select'
;;; 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
;;; note: source file ./test/veritas/unit/ansi-spec.scm
;;; newer than 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/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/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/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
--------------------------------------------------------------
Maak: 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.
→ 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���)
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'
--------------------------------------------------------------
Maak: 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"))'