changeset 33:1d0ddf9e6efd

misc: general documentation
author David Demelier <markand@malikania.fr>
date Thu, 04 Aug 2022 16:47:10 +0200
parents 081e1c258e64
children e52c762d8ba8
files .hgignore Makefile config.mk doc/Doxyfile doc/doxygen-awesome.css doc/mainpage.c lib/apic.c lib/apic.h lib/log.c lib/log.h lib/strlcpy.c lib/strtonum.c lib/util.c lib/util.h libsci/apic.c libsci/apic.h libsci/log.c libsci/log.h libsci/strlcpy.c libsci/strtonum.c libsci/util.c libsci/util.h scid/crud.c scid/crud.h scid/db.h scid/http.h scid/main.c scid/page-api-jobresults.c scid/page-api-jobresults.h scid/page-api-jobs.c scid/page-api-jobs.h scid/page-api-projects.c scid/page-api-projects.h scid/page-api-todo.c scid/page-api-todo.h scid/page-api-workers.c scid/page-api-workers.h scid/page-api.h scid/page-index.c scid/page-index.h scid/page-static.c scid/page-static.h scid/pageutil.h scid/scid.c scid/scid.h scid/theme.c scid/theme.h
diffstat 47 files changed, 3488 insertions(+), 1005 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Thu Aug 04 14:59:33 2022 +0200
+++ b/.hgignore	Thu Aug 04 16:47:10 2022 +0200
@@ -25,3 +25,6 @@
 
 # tests.
 ^tests/test-db$
+
+# documentation.
+^doc/html
--- a/Makefile	Thu Aug 04 14:59:33 2022 +0200
+++ b/Makefile	Thu Aug 04 16:47:10 2022 +0200
@@ -20,13 +20,14 @@
 
 include config.mk
 
-LIBSCI=                 lib/libsci.a
-LIBSCI_SRCS=            extern/libsqlite/sqlite3.c \
-                        lib/apic.c \
-                        lib/log.c \
-                        lib/strlcpy.c \
-                        lib/strtonum.c \
-                        lib/util.c
+VERSION=                0.1.0
+
+LIBSCI=                 libsci/libsci.a
+LIBSCI_SRCS=            libsci/apic.c \
+                        libsci/log.c \
+                        libsci/strlcpy.c \
+                        libsci/strtonum.c \
+                        libsci/util.c
 LIBSCI_OBJS=            ${LIBSCI_SRCS:.c=.o}
 LIBSCI_DEPS=            ${LIBSCI_SRCS:.c=.d}
 
@@ -54,6 +55,7 @@
 SCID=                   scid/scid
 SCID_SRCS=              extern/libduktape/duktape.c \
                         extern/libmustache4c/mustache.c \
+                        extern/libsqlite/sqlite3.c \
                         scid/crud.c \
                         scid/db.c \
                         scid/http.c \
@@ -100,14 +102,15 @@
                         -Iextern/libgreatest \
                         -Iextern/libmustache4c \
                         -Iextern/libduktape \
-                        -Ilib \
+                        -Ilibsci \
                         -I.
 DEFS=                   -DVARDIR=\"${VARDIR}\" \
-                        -DTMPDIR=\"${TMPDIR}\" \
+                        -DVERSION=\"${VERSION}\" \
                         -DSQLITE_THREADSAFE=0 \
                         -DSQLITE_OMIT_LOAD_EXTENSION=0 \
                         -DSQLITE_OMIT_DEPRECATED=0 \
                         -DSQLITE_DEFAULT_FOREIGN_KEY=1
+SUBST=                  -e "s,@VERSION@,${VERSION},g"
 
 .SUFFIXES:
 .SUFFIXES: .c .o .sql .h
@@ -147,8 +150,7 @@
 ${SCICTL_OBJS}: ${LIBSCI}
 
 ${SCICTL}: ${SCICTL_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCICTL_OBJS} ${LIBSCI} \
-		${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCICTL_OBJS} ${LIBSCI} ${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 # }}}
 
@@ -157,8 +159,7 @@
 ${SCID_OBJS}: ${LIBSCI}
 
 ${SCID}: ${SCID_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} lib/libsci.a \
-		${JANSSON_LIBS} ${KCGI_LIBS} -lm ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCID_OBJS} ${LIBSCI} ${JANSSON_LIBS} ${KCGI_LIBS} -lm ${LDFLAGS}
 
 # }}}
 
@@ -167,8 +168,7 @@
 ${SCIWORKERD_OBJS}: ${LIBSCI}
 
 ${SCIWORKERD}: ${SCIWORKERD_OBJS}
-	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} lib/libsci.a \
-		${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
+	${CC} ${CFLAGS} -o $@ ${SCIWORKERD_OBJS} ${LIBSCI} ${LIBCURL_LIBS} ${JANSSON_LIBS} ${LDFLAGS}
 
 # }}}
 
@@ -188,9 +188,12 @@
 	rm -f ${SCIWORKERD}${SCIWORKERD_OBJS} ${SCIWORKERD_DEPS}
 	rm -f ${TESTS_OBJS} ${TESTS_DEPS}
 
+doxygen:
+	sed ${SUBST} < doc/Doxyfile | doxygen -
+
 ${TESTS_OBJS}: lib/libsci.a
 
 tests: lib/libsci.a ${TESTS_OBJS}
 	for t in ${TESTS_OBJS}; do $$t -v; done
 
-.PHONY: all clean install tests
+.PHONY: all clean doxygen install tests
--- a/config.mk	Thu Aug 04 14:59:33 2022 +0200
+++ b/config.mk	Thu Aug 04 16:47:10 2022 +0200
@@ -1,6 +1,5 @@
-CC=             cc
-CFLAGS= -g -O0
-#CFLAGS=         -g -O0 -Wall -Wextra -fsanitize=address -Wno-format-truncation
+CC=             clang
+CFLAGS=         -g -O0 -Wall -fsanitize=address
 #CFLAGS=         -Wall -Wextra -fsanitize=address,undefined -g -O0
 #LDFLAGS=        -fsanitize=address,undefined
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/Doxyfile	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,36 @@
+DOXYFILE_ENCODING      = UTF-8
+
+PROJECT_NAME           = "sci"
+PROJECT_NUMBER         = @VERSION@
+PROJECT_BRIEF          = "simple continuous integration"
+
+OUTPUT_DIRECTORY       = doc
+
+TAB_SIZE               = 8
+OPTIMIZE_OUTPUT_FOR_C  = YES
+MARKDOWN_SUPPORT       = YES
+AUTOLINK_SUPPORT       = NO
+MAX_INITIALIZER_LINES  = 0
+
+HIDE_UNDOC_MEMBERS     = YES
+HIDE_UNDOC_CLASSES     = YES
+
+SHOW_INCLUDE_FILES     = NO
+QUIET                  = YES
+
+GENERATE_TREEVIEW      = YES
+HTML_EXTRA_STYLESHEET  = doc/doxygen-awesome.css
+
+WARNINGS               = YES
+WARN_IF_UNDOCUMENTED   = NO
+
+INPUT                  = lib scictl scid sciworkerd doc/mainpage.c
+FILE_PATTERNS          = *.c *.h
+RECURSIVE              = YES
+
+GENERATE_HTML          = YES
+GENERATE_LATEX         = NO
+HTML_OUTPUT            = html
+HTML_FILE_EXTENSION    = .html
+
+HAVE_DOT               = YES
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/doxygen-awesome.css	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,2008 @@
+/**
+
+Doxygen Awesome
+https://github.com/jothepro/doxygen-awesome-css
+
+MIT License
+
+Copyright (c) 2021 - 2022 jothepro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+*/
+
+html {
+    /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */
+    --primary-color: #1779c4;
+    --primary-dark-color: #335c80;
+    --primary-light-color: #70b1e9;
+
+    /* page base colors */
+    --page-background-color: white;
+    --page-foreground-color: #2f4153;
+    --page-secondary-foreground-color: #637485;
+
+    /* color for all separators on the website: hr, borders, ... */
+    --separator-color: #dedede;
+
+    /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */
+    --border-radius-large: 8px;
+    --border-radius-small: 4px;
+    --border-radius-medium: 6px;
+
+    /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */
+    --spacing-small: 5px;
+    --spacing-medium: 10px;
+    --spacing-large: 16px;
+
+    /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */
+    --box-shadow: 0 2px 8px 0 rgba(0,0,0,.075);
+
+    --odd-color: rgba(0,0,0,.028);
+
+    /* font-families. will affect all text on the website
+     * font-family: the normal font for text, headlines, menus
+     * font-family-monospace: used for preformatted text in memtitle, code, fragments
+     */
+    --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;
+    --font-family-monospace: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
+
+    /* font sizes */
+    --page-font-size: 15.6px;
+    --navigation-font-size: 14.4px;
+    --code-font-size: 14px; /* affects code, fragment */
+    --title-font-size: 22px;
+
+    /* content text properties. These only affect the page content, not the navigation or any other ui elements */
+    --content-line-height: 27px;
+    /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/
+    --content-maxwidth: 1000px;
+
+    /* colors for various content boxes: @warning, @note, @deprecated @bug */
+    --warning-color: #f8d1cc;
+    --warning-color-dark: #b61825;
+    --warning-color-darker: #75070f;
+    --note-color: #faf3d8;
+    --note-color-dark: #f3a600;
+    --note-color-darker: #5f4204;
+    --todo-color: #e4f3ff;
+    --todo-color-dark: #1879C4;
+    --todo-color-darker: #274a5c;
+    --deprecated-color: #ecf0f3;
+    --deprecated-color-dark: #5b6269;
+    --deprecated-color-darker: #43454a;
+    --bug-color: #e4dafd;
+    --bug-color-dark: #5b2bdd;
+    --bug-color-darker: #2a0d72;
+    --invariant-color: #d8f1e3;
+    --invariant-color-dark: #44b86f;
+    --invariant-color-darker: #265532;
+
+    /* blockquote colors */
+    --blockquote-background: #f8f9fa;
+    --blockquote-foreground: #636568;
+
+    /* table colors */
+    --tablehead-background: #f1f1f1;
+    --tablehead-foreground: var(--page-foreground-color);
+
+    /* menu-display: block | none
+     * Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible.
+     * `GENERATE_TREEVIEW` MUST be enabled!
+     */
+    --menu-display: block;
+
+    --menu-focus-foreground: var(--page-background-color);
+    --menu-focus-background: var(--primary-color);
+    --menu-selected-background: rgba(0,0,0,.05);
+
+
+    --header-background: var(--page-background-color);
+    --header-foreground: var(--page-foreground-color);
+
+    /* searchbar colors */
+    --searchbar-background: var(--side-nav-background);
+    --searchbar-foreground: var(--page-foreground-color);
+
+    /* searchbar size
+     * (`searchbar-width` is only applied on screens >= 768px.
+     * on smaller screens the searchbar will always fill the entire screen width) */
+    --searchbar-height: 33px;
+    --searchbar-width: 210px;
+    --searchbar-border-radius: var(--searchbar-height);
+
+    /* code block colors */
+    --code-background: #f5f5f5;
+    --code-foreground: var(--page-foreground-color);
+
+    /* fragment colors */
+    --fragment-background: #F8F9FA;
+    --fragment-foreground: #37474F;
+    --fragment-keyword: #bb6bb2;
+    --fragment-keywordtype: #8258b3;
+    --fragment-keywordflow: #d67c3b;
+    --fragment-token: #438a59;
+    --fragment-comment: #969696;
+    --fragment-link: #5383d6;
+    --fragment-preprocessor: #46aaa5;
+    --fragment-linenumber-color: #797979;
+    --fragment-linenumber-background: #f4f4f5;
+    --fragment-linenumber-border: #e3e5e7;
+    --fragment-lineheight: 20px;
+
+    /* sidebar navigation (treeview) colors */
+    --side-nav-background: #fbfbfb;
+    --side-nav-foreground: var(--page-foreground-color);
+    --side-nav-arrow-opacity: 0;
+    --side-nav-arrow-hover-opacity: 0.9;
+
+    --toc-background: var(--side-nav-background);
+    --toc-foreground: var(--side-nav-foreground);
+
+    /* height of an item in any tree / collapsable table */
+    --tree-item-height: 30px;
+
+    --memname-font-size: var(--code-font-size);
+    --memtitle-font-size: 18px;
+
+    --webkit-scrollbar-size: 7px;
+    --webkit-scrollbar-padding: 4px;
+    --webkit-scrollbar-color: var(--separator-color);
+}
+
+@media screen and (max-width: 767px) {
+    html {
+        --page-font-size: 16px;
+        --navigation-font-size: 16px;
+        --code-font-size: 15px; /* affects code, fragment */
+        --title-font-size: 22px;
+    }
+}
+
+@media (prefers-color-scheme: dark) {
+    html:not(.light-mode) {
+        color-scheme: dark;
+
+        --primary-color: #1982d2;
+        --primary-dark-color: #86a9c4;
+        --primary-light-color: #4779ac;
+
+        --box-shadow: 0 2px 8px 0 rgba(0,0,0,.35);
+
+        --odd-color: rgba(100,100,100,.06);
+
+        --menu-selected-background: rgba(0,0,0,.4);
+
+        --page-background-color: #1C1D1F;
+        --page-foreground-color: #d2dbde;
+        --page-secondary-foreground-color: #859399;
+        --separator-color: #38393b;
+        --side-nav-background: #252628;
+
+        --code-background: #2a2c2f;
+
+        --tablehead-background: #2a2c2f;
+    
+        --blockquote-background: #222325;
+        --blockquote-foreground: #7e8c92;
+
+        --warning-color: #2e1917;
+        --warning-color-dark: #ad2617;
+        --warning-color-darker: #f5b1aa;
+        --note-color: #3b2e04;
+        --note-color-dark: #f1b602;
+        --note-color-darker: #ceb670;
+        --todo-color: #163750;
+        --todo-color-dark: #1982D2;
+        --todo-color-darker: #dcf0fa;
+        --deprecated-color: #2e323b;
+        --deprecated-color-dark: #738396;
+        --deprecated-color-darker: #abb0bd;
+        --bug-color: #2a2536;
+        --bug-color-dark: #7661b3;
+        --bug-color-darker: #ae9ed6;
+        --invariant-color: #303a35;
+        --invariant-color-dark: #76ce96;
+        --invariant-color-darker: #cceed5;
+
+        --fragment-background: #282c34;
+        --fragment-foreground: #dbe4eb;
+        --fragment-keyword: #cc99cd;
+        --fragment-keywordtype: #ab99cd;
+        --fragment-keywordflow: #e08000;
+        --fragment-token: #7ec699;
+        --fragment-comment: #999999;
+        --fragment-link: #98c0e3;
+        --fragment-preprocessor: #65cabe;
+        --fragment-linenumber-color: #cccccc;
+        --fragment-linenumber-background: #35393c;
+        --fragment-linenumber-border: #1f1f1f;
+    }
+}
+
+/* dark mode variables are defined twice, to support both the dark-mode without and with doxygen-awesome-darkmode-toggle.js */
+html.dark-mode {
+    color-scheme: dark;
+
+    --primary-color: #1982d2;
+    --primary-dark-color: #86a9c4;
+    --primary-light-color: #4779ac;
+
+    --box-shadow: 0 2px 8px 0 rgba(0,0,0,.30);
+
+    --odd-color: rgba(100,100,100,.06);
+
+    --menu-selected-background: rgba(0,0,0,.4);
+
+    --page-background-color: #1C1D1F;
+    --page-foreground-color: #d2dbde;
+    --page-secondary-foreground-color: #859399;
+    --separator-color: #38393b;
+    --side-nav-background: #252628;
+
+    --code-background: #2a2c2f;
+
+    --tablehead-background: #2a2c2f;
+
+    --blockquote-background: #222325;
+    --blockquote-foreground: #7e8c92;
+
+    --warning-color: #2e1917;
+    --warning-color-dark: #ad2617;
+    --warning-color-darker: #f5b1aa;
+    --note-color: #3b2e04;
+    --note-color-dark: #f1b602;
+    --note-color-darker: #ceb670;
+    --todo-color: #163750;
+    --todo-color-dark: #1982D2;
+    --todo-color-darker: #dcf0fa;
+    --deprecated-color: #2e323b;
+    --deprecated-color-dark: #738396;
+    --deprecated-color-darker: #abb0bd;
+    --bug-color: #2a2536;
+    --bug-color-dark: #7661b3;
+    --bug-color-darker: #ae9ed6;
+    --invariant-color: #303a35;
+    --invariant-color-dark: #76ce96;
+    --invariant-color-darker: #cceed5;
+
+    --fragment-background: #282c34;
+    --fragment-foreground: #dbe4eb;
+    --fragment-keyword: #cc99cd;
+    --fragment-keywordtype: #ab99cd;
+    --fragment-keywordflow: #e08000;
+    --fragment-token: #7ec699;
+    --fragment-comment: #999999;
+    --fragment-link: #98c0e3;
+    --fragment-preprocessor: #65cabe;
+    --fragment-linenumber-color: #cccccc;
+    --fragment-linenumber-background: #35393c;
+    --fragment-linenumber-border: #1f1f1f;
+}
+
+body {
+    color: var(--page-foreground-color);
+    background-color: var(--page-background-color);
+    font-size: var(--page-font-size);
+}
+
+body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover {
+    font-family: var(--font-family);
+}
+
+h1, h2, h3, h4, h5 {
+    margin-top: .9em;
+    font-weight: 600;
+    line-height: initial;
+}
+
+p, div, table, dl {
+    font-size: var(--page-font-size);
+}
+
+a:link, a:visited, a:hover, a:focus, a:active {
+    color: var(--primary-color) !important;
+    font-weight: 500;
+}
+
+a.anchor {
+    scroll-margin-top: var(--spacing-large);
+}
+
+/*
+ Title and top navigation
+ */
+
+#top {
+    background: var(--header-background);
+    border-bottom: 1px solid var(--separator-color);
+}
+
+@media screen and (min-width: 768px) {
+    #top {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        align-items: center;
+    }
+}
+
+#main-nav {
+    flex-grow: 5;
+    padding: var(--spacing-small) var(--spacing-medium);
+}
+
+#titlearea {
+    width: auto;
+    padding: var(--spacing-medium) var(--spacing-large);
+    background: none;
+    color: var(--header-foreground);
+    border-bottom: none;
+}
+
+@media screen and (max-width: 767px) {
+    #titlearea {
+        padding-bottom: var(--spacing-small);
+    }
+}
+
+#titlearea table tbody tr {
+    height: auto !important;
+}
+
+#projectname {
+    font-size: var(--title-font-size);
+    font-weight: 600;
+}
+
+#projectnumber {
+    font-family: inherit;
+    font-size: 60%;
+}
+
+#projectbrief {
+    font-family: inherit;
+    font-size: 80%;
+}
+
+#projectlogo {
+    vertical-align: middle;
+}
+
+#projectlogo img {
+    max-height: calc(var(--title-font-size) * 2);
+    margin-right: var(--spacing-small);
+}
+
+.sm-dox, .tabs, .tabs2, .tabs3 {
+    background: none;
+    padding: 0;
+}
+
+.tabs, .tabs2, .tabs3 {
+    border-bottom: 1px solid var(--separator-color);
+    margin-bottom: -1px;
+}
+
+@media screen and (max-width: 767px) {
+    .sm-dox a span.sub-arrow {
+        background: var(--code-background);
+    }
+
+    #main-menu a.has-submenu span.sub-arrow {
+        color: var(--page-secondary-foreground-color);
+        border-radius: var(--border-radius-medium);
+    }
+
+    #main-menu a.has-submenu:hover span.sub-arrow {
+        color: var(--page-foreground-color);
+    }
+}
+
+@media screen and (min-width: 768px) {
+    .sm-dox li, .tablist li {
+        display: var(--menu-display);
+    }
+
+    .sm-dox a span.sub-arrow {
+        border-color: var(--header-foreground) transparent transparent transparent;
+    }
+
+    .sm-dox a:hover span.sub-arrow {
+        border-color: var(--menu-focus-foreground) transparent transparent transparent;
+    }
+
+    .sm-dox ul a span.sub-arrow {
+        border-color: transparent transparent transparent var(--page-foreground-color);
+    }
+
+    .sm-dox ul a:hover span.sub-arrow {
+        border-color: transparent transparent transparent var(--menu-focus-foreground);
+    }
+}
+
+.sm-dox ul {
+    background: var(--page-background-color);
+    box-shadow: var(--box-shadow);
+    border: 1px solid var(--separator-color);
+    border-radius: var(--border-radius-medium) !important;
+    padding: var(--spacing-small);
+    animation: ease-out 150ms slideInMenu;
+}
+
+@keyframes slideInMenu {
+    from {
+        opacity: 0;
+        transform: translate(0px, -2px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translate(0px, 0px);
+    }
+}
+
+.sm-dox ul a {
+    color: var(--page-foreground-color) !important;
+    background: var(--page-background-color);
+    font-size: var(--navigation-font-size);
+}
+
+.sm-dox>li>ul:after {
+    border-bottom-color: var(--page-background-color) !important;
+}
+
+.sm-dox>li>ul:before {
+    border-bottom-color: var(--separator-color) !important;
+}
+
+.sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus {
+    font-size: var(--navigation-font-size) !important;
+    color: var(--menu-focus-foreground) !important;
+    text-shadow: none;
+    background-color: var(--menu-focus-background);
+    border-radius: var(--border-radius-small) !important;
+}
+
+.sm-dox a, .sm-dox a:focus, .tablist li, .tablist li a, .tablist li.current a {
+    text-shadow: none;
+    background: transparent;
+    background-image: none !important;
+    color: var(--header-foreground) !important;
+    font-weight: normal;
+    font-size: var(--navigation-font-size);
+    border-radius: var(--border-radius-small) !important;
+}
+
+.sm-dox a:focus {
+    outline: auto;
+}
+
+.sm-dox a:hover, .sm-dox a:active, .tablist li a:hover {
+    text-shadow: none;
+    font-weight: normal;
+    background: var(--menu-focus-background);
+    color: var(--menu-focus-foreground) !important;
+    border-radius: var(--border-radius-small) !important;
+    font-size: var(--navigation-font-size);
+}
+
+.tablist li.current {
+    border-radius: var(--border-radius-small);
+    background: var(--menu-selected-background);
+}
+
+.tablist li {
+    margin: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small);
+}
+
+.tablist a {
+    padding: 0 var(--spacing-large);
+}
+
+
+/*
+ Search box
+ */
+
+#MSearchBox {
+    height: var(--searchbar-height);
+    background: var(--searchbar-background);
+    border-radius: var(--searchbar-border-radius);
+    border: 1px solid var(--separator-color);
+    overflow: hidden;
+    width: var(--searchbar-width);
+    position: relative;
+    box-shadow: none;
+    display: block;
+    margin-top: 0;
+}
+
+.left #MSearchSelect {
+    left: 0;
+    user-select: none;
+}
+
+.SelectionMark {
+    user-select: none;
+}
+
+.tabs .left #MSearchSelect {
+    padding-left: 0;
+}
+
+.tabs #MSearchBox {
+    position: absolute;
+    right: var(--spacing-medium);
+}
+
+@media screen and (max-width: 767px) {
+    .tabs #MSearchBox {
+        position: relative;
+        right: 0;
+        margin-left: var(--spacing-medium);
+        margin-top: 0;
+    }
+}
+
+#MSearchSelectWindow, #MSearchResultsWindow {
+    z-index: 9999;
+}
+
+#MSearchBox.MSearchBoxActive {
+    border-color: var(--primary-color);
+    box-shadow: inset 0 0 0 1px var(--primary-color);
+}
+
+#main-menu > li:last-child {
+    margin-right: 0;
+}
+
+@media screen and (max-width: 767px) {
+    #main-menu > li:last-child {
+        height: 50px;
+    }
+}
+
+#MSearchField {
+    font-size: var(--navigation-font-size);
+    height: calc(var(--searchbar-height) - 2px);
+    background: transparent;
+    width: calc(var(--searchbar-width) - 64px);
+}
+
+.MSearchBoxActive #MSearchField {
+    color: var(--searchbar-foreground);
+}
+
+#MSearchSelect {
+    top: calc(calc(var(--searchbar-height) / 2) - 11px);
+}
+
+.left #MSearchSelect {
+    padding-left: 8px;
+}
+
+#MSearchBox span.left, #MSearchBox span.right {
+    background: none;
+}
+
+#MSearchBox span.right {
+    padding-top: calc(calc(var(--searchbar-height) / 2) - 12px);
+    position: absolute;
+    right: var(--spacing-small);
+}
+
+.tabs #MSearchBox span.right {
+    top: calc(calc(var(--searchbar-height) / 2) - 12px);
+}
+
+@keyframes slideInSearchResults {
+    from {
+        opacity: 0;
+        transform: translate(0, 15px);
+    }
+
+    to {
+        opacity: 1;
+        transform: translate(0, 20px);
+    }
+}
+
+#MSearchResultsWindow {
+    left: auto !important;
+    right: var(--spacing-medium);
+    border-radius: var(--border-radius-large);
+    border: 1px solid var(--separator-color);
+    transform: translate(0, 20px);
+    box-shadow: var(--box-shadow);
+    animation: ease-out 280ms slideInSearchResults;
+    background: var(--page-background-color);
+}
+
+iframe#MSearchResults {
+    margin: 4px;
+}
+
+iframe {
+    color-scheme: normal;
+}
+
+@media (prefers-color-scheme: dark) {
+    html:not(.light-mode) iframe#MSearchResults {
+        filter: invert() hue-rotate(180deg);
+    }
+}
+
+html.dark-mode iframe#MSearchResults {
+    filter: invert() hue-rotate(180deg);
+}
+
+#MSearchSelectWindow {
+    border: 1px solid var(--separator-color);
+    border-radius: var(--border-radius-medium);
+    box-shadow: var(--box-shadow);
+    background: var(--page-background-color);
+    padding-top: var(--spacing-small);
+    padding-bottom: var(--spacing-small);
+}
+
+#MSearchSelectWindow a.SelectItem {
+    font-size: var(--navigation-font-size);
+    line-height: var(--content-line-height);
+    margin: 0 var(--spacing-small);
+    border-radius: var(--border-radius-small);
+    color: var(--page-foreground-color) !important;
+    font-weight: normal;
+}
+
+#MSearchSelectWindow a.SelectItem:hover {
+    background: var(--menu-focus-background);
+    color: var(--menu-focus-foreground) !important;
+}
+
+@media screen and (max-width: 767px) {
+    #MSearchBox {
+        margin-top: var(--spacing-medium);
+        margin-bottom: var(--spacing-medium);
+        width: calc(100vw - 30px);
+    }
+
+    #main-menu > li:last-child {
+        float: none !important;
+    }
+
+    #MSearchField {
+        width: calc(100vw - 110px);
+    }
+
+    @keyframes slideInSearchResultsMobile {
+        from {
+            opacity: 0;
+            transform: translate(0, 15px);
+        }
+
+        to {
+            opacity: 1;
+            transform: translate(0, 20px);
+        }
+    }
+
+    #MSearchResultsWindow {
+        left: var(--spacing-medium) !important;
+        right: var(--spacing-medium);
+        overflow: auto;
+        transform: translate(0, 20px);
+        animation: ease-out 280ms slideInSearchResultsMobile;
+    }
+
+    /*
+     * Overwrites for fixing the searchbox on mobile in doxygen 1.9.2
+     */
+    label.main-menu-btn ~ #searchBoxPos1 {
+        top: 3px !important;
+        right: 6px !important;
+        left: 45px;
+        display: flex;
+    }
+
+    label.main-menu-btn ~ #searchBoxPos1 > #MSearchBox {
+        margin-top: 0;
+        margin-bottom: 0;
+        flex-grow: 2;
+        float: left;
+    }
+}
+
+/*
+ Tree view
+ */
+
+#side-nav {
+    padding: 0 !important;
+    background: var(--side-nav-background);
+}
+
+@media screen and (max-width: 767px) {
+    #side-nav {
+        display: none;
+    }
+
+    #doc-content {
+        margin-left: 0 !important;
+    }
+}
+
+#nav-tree {
+    background: transparent;
+}
+
+#nav-tree .label {
+    font-size: var(--navigation-font-size);
+}
+
+#nav-tree .item {
+    height: var(--tree-item-height);
+    line-height: var(--tree-item-height);
+}
+
+#nav-sync {
+    bottom: 12px;
+    right: 12px;
+    top: auto !important;
+    user-select: none;
+}
+
+#nav-tree .selected {
+    text-shadow: none;
+    background-image: none;
+    background-color: transparent;
+    box-shadow: inset 4px 0 0 0 var(--primary-color);
+}
+
+#nav-tree a {
+    color: var(--side-nav-foreground) !important;
+    font-weight: normal;
+}
+
+#nav-tree a:focus {
+    outline-style: auto;
+}
+
+#nav-tree .arrow {
+    opacity: var(--side-nav-arrow-opacity);
+}
+
+.arrow {
+    color: inherit;
+    cursor: pointer;
+    font-size: 45%;
+    vertical-align: middle;
+    margin-right: 2px;
+    font-family: serif;
+    height: auto;
+    text-align: right;
+}
+
+#nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow {
+    opacity: var(--side-nav-arrow-hover-opacity);
+}
+
+#nav-tree .selected a {
+    color: var(--primary-color) !important;
+    font-weight: bolder;
+    font-weight: 600;
+}
+
+.ui-resizable-e {
+    background: var(--separator-color);
+    width: 1px;
+}
+
+/*
+ Contents
+ */
+
+div.header {
+    border-bottom: 1px solid var(--separator-color);
+    background-color: var(--page-background-color);
+    background-image: none;
+}
+
+div.contents, div.header .title, div.header .summary {
+    max-width: var(--content-maxwidth);
+}
+
+div.contents, div.header .title  {
+    line-height: initial;
+    margin: calc(var(--spacing-medium) + .2em) auto var(--spacing-medium) auto;
+}
+
+div.header .summary {
+    margin: var(--spacing-medium) auto 0 auto;
+}
+
+div.headertitle {
+    padding: 0;
+}
+
+div.header .title {
+    font-weight: 600;
+    font-size: 210%;
+    padding: var(--spacing-medium) var(--spacing-large);
+    word-break: break-word;
+}
+
+div.header .summary {
+    width: auto;
+    display: block;
+    float: none;
+    padding: 0 var(--spacing-large);
+}
+
+td.memSeparator {
+    border-color: var(--separator-color);
+}
+
+.mdescLeft, .mdescRight, .memItemLeft, .memItemRight, .memTemplItemLeft, .memTemplItemRight, .memTemplParams {
+    background: var(--code-background);
+}
+
+span.mlabel {
+    background: var(--primary-color);
+    border: none;
+    padding: 4px 9px;
+    border-radius: 12px;
+    margin-right: var(--spacing-medium);
+}
+
+span.mlabel:last-of-type {
+    margin-right: 2px;
+}
+
+div.contents {
+    padding: 0 var(--spacing-large);
+}
+
+div.contents p, div.contents li {
+    line-height: var(--content-line-height);
+}
+
+div.contents div.dyncontent {
+    margin: var(--spacing-medium) 0;
+}
+
+@media (prefers-color-scheme: dark) {
+    html:not(.light-mode) div.contents div.dyncontent img,
+    html:not(.light-mode) div.contents center img,
+    html:not(.light-mode) div.contents table img,
+    html:not(.light-mode) div.contents div.dyncontent iframe,
+    html:not(.light-mode) div.contents center iframe,
+    html:not(.light-mode) div.contents table iframe {
+        filter: hue-rotate(180deg) invert();
+    }
+}
+
+html.dark-mode div.contents div.dyncontent img,
+html.dark-mode div.contents center img,
+html.dark-mode div.contents table img,
+html.dark-mode div.contents div.dyncontent iframe,
+html.dark-mode div.contents center iframe,
+html.dark-mode div.contents table iframe {
+    filter: hue-rotate(180deg) invert();
+}
+
+h2.groupheader {
+    border-bottom: 0px;
+    color: var(--page-foreground-color);
+    box-shadow: 
+        100px 0 var(--page-background-color), 
+        -100px 0 var(--page-background-color),
+        100px 1px var(--separator-color),
+        -100px 1px var(--separator-color),
+        500px 0 var(--page-background-color), 
+        -500px 0 var(--page-background-color),
+        500px 1px var(--separator-color),
+        -500px 1px var(--separator-color),
+        1500px 0 var(--page-background-color), 
+        1500px 1px var(--separator-color),
+        var(--content-maxwidth) 0 var(--page-background-color),
+        calc(0px - var(--content-maxwidth)) 0 var(--page-background-color),
+        var(--content-maxwidth) 1px var(--separator-color),
+        calc(0px - var(--content-maxwidth)) 1px var(--separator-color),
+        calc(2 * var(--content-maxwidth)) 0 var(--page-background-color),
+        calc(0px - 2 * var(--content-maxwidth)) 0 var(--page-background-color),
+        calc(2 * var(--content-maxwidth)) 1px var(--separator-color),
+        calc(0px - 2 * var(--content-maxwidth)) 1px var(--separator-color);
+}
+
+blockquote {
+    margin: 0 var(--spacing-medium) 0 var(--spacing-medium);
+    padding: var(--spacing-small) var(--spacing-large);
+    background: var(--blockquote-background);
+    color: var(--blockquote-foreground);
+    border-left: 0;
+    overflow: visible;
+    border-radius: var(--border-radius-medium);
+    overflow: visible;
+    position: relative;
+}
+
+blockquote::before, blockquote::after {
+    font-weight: bold;
+    font-family: serif;
+    font-size: 360%;
+    opacity: .15;
+    position: absolute;
+}
+
+blockquote::before {
+    content: "“";
+    left: -10px;
+    top: 4px;
+}
+
+blockquote::after {
+    content: "”";
+    right: -8px;
+    bottom: -25px;
+}
+
+blockquote p {
+    margin: var(--spacing-small) 0 var(--spacing-medium) 0;
+}
+.paramname {
+    font-weight: 600;
+    color: var(--primary-dark-color);
+}
+
+.paramname > code {
+    border: 0;
+}
+
+table.params .paramname {
+    font-weight: 600;
+    font-family: var(--font-family-monospace);
+    font-size: var(--code-font-size);
+    padding-right: var(--spacing-small);
+}
+
+.glow {
+    text-shadow: 0 0 15px var(--primary-light-color) !important;
+}
+
+.alphachar a {
+    color: var(--page-foreground-color);
+}
+
+/*
+ Table of Contents
+ */
+
+div.toc {
+    z-index: 10;
+    position: relative;
+    background-color: var(--toc-background);
+    border: 1px solid var(--separator-color);
+    border-radius: var(--border-radius-medium);
+    box-shadow: var(--box-shadow);
+    padding: 0 var(--spacing-large);
+    margin: 0 0 var(--spacing-medium) var(--spacing-medium);
+}
+
+div.toc h3 {
+    color: var(--toc-foreground);
+    font-size: var(--navigation-font-size);
+    margin: var(--spacing-large) 0;
+}
+
+div.toc li {
+    font-size: var(--navigation-font-size);
+    padding: 0;
+    background: none;
+}
+
+div.toc li:before {
+    content: '↓';
+    font-weight: 800;
+    font-family: var(--font-family);
+    margin-right: var(--spacing-small);
+    color: var(--toc-foreground);
+    opacity: .4;
+}
+
+div.toc ul li.level1 {
+    margin: 0;
+}
+
+div.toc ul li.level2, div.toc ul li.level3 {
+    margin-top: 0;
+}
+
+
+@media screen and (max-width: 767px) {
+    div.toc {
+        float: none;
+        width: auto;
+        margin: 0 0 var(--spacing-medium) 0;
+    }
+}
+
+/*
+ Code & Fragments
+ */
+
+code, div.fragment, pre.fragment {
+    border-radius: var(--border-radius-small);
+    border: 1px solid var(--separator-color);
+    overflow: hidden;
+}
+
+code {
+    display: inline;
+    background: var(--code-background);
+    color: var(--code-foreground);
+    padding: 2px 6px;
+    word-break: break-word;
+}
+
+div.fragment, pre.fragment {
+    margin: var(--spacing-medium) 0;
+    padding: calc(var(--spacing-large) - (var(--spacing-large) / 6)) var(--spacing-large);
+    background: var(--fragment-background);
+    color: var(--fragment-foreground);
+    overflow-x: auto;
+}
+
+@media screen and (max-width: 767px) {
+    div.fragment, pre.fragment {
+        border-top-right-radius: 0;
+        border-bottom-right-radius: 0;
+        border-right: 0;
+    }
+
+    .contents > div.fragment,
+    .textblock > div.fragment,
+    .textblock > pre.fragment,
+    .contents > .doxygen-awesome-fragment-wrapper > div.fragment,
+    .textblock > .doxygen-awesome-fragment-wrapper > div.fragment,
+    .textblock > .doxygen-awesome-fragment-wrapper > pre.fragment {
+        margin: var(--spacing-medium) calc(0px - var(--spacing-large));
+        border-radius: 0;
+        border-left: 0;
+    }
+
+    .textblock li > .fragment,
+    .textblock li > .doxygen-awesome-fragment-wrapper > .fragment {
+        margin: var(--spacing-medium) calc(0px - var(--spacing-large));
+    }
+
+    .memdoc li > .fragment,
+    .memdoc li > .doxygen-awesome-fragment-wrapper > .fragment {
+        margin: var(--spacing-medium) calc(0px - var(--spacing-medium));
+    }
+
+    .textblock ul, .memdoc ul {
+        overflow: initial;
+    }
+
+    .memdoc > div.fragment,
+    .memdoc > pre.fragment,
+    dl dd > div.fragment,
+    dl dd pre.fragment,
+    .memdoc > .doxygen-awesome-fragment-wrapper > div.fragment,
+    .memdoc > .doxygen-awesome-fragment-wrapper > pre.fragment,
+    dl dd > .doxygen-awesome-fragment-wrapper > div.fragment,
+    dl dd .doxygen-awesome-fragment-wrapper > pre.fragment {
+        margin: var(--spacing-medium) calc(0px - var(--spacing-medium));
+        border-radius: 0;
+        border-left: 0;
+    }
+}
+
+code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span {
+    font-family: var(--font-family-monospace);
+    font-size: var(--code-font-size) !important;
+}
+
+div.line:after {
+    margin-right: var(--spacing-medium);
+}
+
+div.fragment .line, pre.fragment {
+    white-space: pre;
+    word-wrap: initial;
+    line-height: var(--fragment-lineheight);
+}
+
+div.fragment span.keyword {
+    color: var(--fragment-keyword);
+}
+
+div.fragment span.keywordtype {
+    color: var(--fragment-keywordtype);
+}
+
+div.fragment span.keywordflow {
+    color: var(--fragment-keywordflow);
+}
+
+div.fragment span.stringliteral {
+    color: var(--fragment-token)
+}
+
+div.fragment span.comment {
+    color: var(--fragment-comment);
+}
+
+div.fragment a.code {
+    color: var(--fragment-link) !important;
+}
+
+div.fragment span.preprocessor {
+    color: var(--fragment-preprocessor);
+}
+
+div.fragment span.lineno {
+    display: inline-block;
+    width: 27px;
+    border-right: none;
+    background: var(--fragment-linenumber-background);
+    color: var(--fragment-linenumber-color);
+}
+
+div.fragment span.lineno a {
+    background: none;
+    color: var(--fragment-link) !important;
+}
+
+div.fragment .line:first-child .lineno {
+    box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border);
+}
+
+/*
+ dl warning, attention, note, deprecated, bug, ...
+ */
+
+dl.bug dt a, dl.deprecated dt a, dl.todo dt a {
+    font-weight: bold !important;
+}
+
+dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre, dl.todo, dl.remark {
+    padding: var(--spacing-medium);
+    margin: var(--spacing-medium) 0;
+    color: var(--page-background-color);
+    overflow: hidden;
+    margin-left: 0;
+    border-radius: var(--border-radius-small);
+}
+
+dl.section dd {
+    margin-bottom: 2px;
+}
+
+dl.warning, dl.attention {
+    background: var(--warning-color);
+    border-left: 8px solid var(--warning-color-dark);
+    color: var(--warning-color-darker);
+}
+
+dl.warning dt, dl.attention dt {
+    color: var(--warning-color-dark);
+}
+
+dl.note, dl.remark {
+    background: var(--note-color);
+    border-left: 8px solid var(--note-color-dark);
+    color: var(--note-color-darker);
+}
+
+dl.note dt, dl.remark dt {
+    color: var(--note-color-dark);
+}
+
+dl.todo {
+    background: var(--todo-color);
+    border-left: 8px solid var(--todo-color-dark);
+    color: var(--todo-color-darker);
+}
+
+dl.todo dt {
+    color: var(--todo-color-dark);
+}
+
+dl.bug dt a {
+    color: var(--todo-color-dark) !important;
+}
+
+dl.bug {
+    background: var(--bug-color);
+    border-left: 8px solid var(--bug-color-dark);
+    color: var(--bug-color-darker);
+}
+
+dl.bug dt a {
+    color: var(--bug-color-dark) !important;
+}
+
+dl.deprecated {
+    background: var(--deprecated-color);
+    border-left: 8px solid var(--deprecated-color-dark);
+    color: var(--deprecated-color-darker);
+}
+
+dl.deprecated dt a {
+    color: var(--deprecated-color-dark) !important;
+}
+
+dl.section dd, dl.bug dd, dl.deprecated dd, dl.todo dd {
+    margin-inline-start: 0px;
+}
+
+dl.invariant, dl.pre {
+    background: var(--invariant-color);
+    border-left: 8px solid var(--invariant-color-dark);
+    color: var(--invariant-color-darker);
+}
+
+dl.invariant dt, dl.pre dt {
+    color: var(--invariant-color-dark);
+}
+
+/*
+ memitem
+ */
+
+div.memdoc, div.memproto, h2.memtitle {
+    box-shadow: none;
+    background-image: none;
+    border: none;
+}
+
+div.memdoc {
+    padding: 0 var(--spacing-medium);
+    background: var(--page-background-color);
+}
+
+h2.memtitle, div.memitem {
+    border: 1px solid var(--separator-color);
+    box-shadow: var(--box-shadow);
+}
+
+h2.memtitle {
+    box-shadow: 0px var(--spacing-medium) 0 -1px var(--fragment-background), var(--box-shadow);
+}
+
+div.memitem {
+    transition: none;
+}
+
+div.memproto, h2.memtitle {
+    background: var(--fragment-background);
+    text-shadow: none;
+}
+
+h2.memtitle {
+    font-weight: 500;
+    font-size: var(--memtitle-font-size);
+    font-family: var(--font-family-monospace);
+    border-bottom: none;
+    border-top-left-radius: var(--border-radius-medium);
+    border-top-right-radius: var(--border-radius-medium);
+    word-break: break-all;
+    position: relative;
+}
+
+h2.memtitle:after {
+    content: "";
+    display: block;
+    background: var(--fragment-background);
+    height: var(--spacing-medium);
+    bottom: calc(0px - var(--spacing-medium));
+    left: 0;
+    right: -14px;
+    position: absolute;
+    border-top-right-radius: var(--border-radius-medium);
+}
+
+h2.memtitle > span.permalink {
+    font-size: inherit;
+}
+
+h2.memtitle > span.permalink > a {
+    text-decoration: none;
+    padding-left: 3px;
+    margin-right: -4px;
+    user-select: none;
+    display: inline-block;
+    margin-top: -6px;
+}
+
+h2.memtitle > span.permalink > a:hover {
+    color: var(--primary-dark-color) !important;
+}
+
+a:target + h2.memtitle, a:target + h2.memtitle + div.memitem {
+    border-color: var(--primary-light-color);
+}
+
+div.memitem {
+    border-top-right-radius: var(--border-radius-medium);
+    border-bottom-right-radius: var(--border-radius-medium);
+    border-bottom-left-radius: var(--border-radius-medium);
+    overflow: hidden;
+    display: block !important;
+}
+
+div.memdoc {
+    border-radius: 0;
+}
+
+div.memproto {
+    border-radius: 0 var(--border-radius-small) 0 0;
+    overflow: auto;
+    border-bottom: 1px solid var(--separator-color);
+    padding: var(--spacing-medium);
+    margin-bottom: -1px;
+}
+
+div.memtitle {
+    border-top-right-radius: var(--border-radius-medium);
+    border-top-left-radius: var(--border-radius-medium);
+}
+
+div.memproto table.memname {
+    font-family: var(--font-family-monospace);
+    color: var(--page-foreground-color);
+    font-size: var(--memname-font-size);
+}
+
+table.mlabels, table.mlabels > tbody {
+    display: block;
+}
+
+td.mlabels-left {
+    width: auto;
+}
+
+td.mlabels-right {
+    margin-top: 3px;
+    position: sticky;
+    left: 0;
+}
+
+table.mlabels > tbody > tr:first-child {
+    display: flex;
+    justify-content: space-between;
+    flex-wrap: wrap;
+}
+
+.memname, .memitem span.mlabels {
+    margin: 0
+}
+
+/*
+ reflist
+ */
+
+dl.reflist {
+    box-shadow: var(--box-shadow);
+    border-radius: var(--border-radius-medium);
+    border: 1px solid var(--separator-color);
+    overflow: hidden;
+    padding: 0;
+}
+
+
+dl.reflist dt, dl.reflist dd {
+    box-shadow: none;
+    text-shadow: none;
+    background-image: none;
+    border: none;
+    padding: 12px;
+}
+
+
+dl.reflist dt {
+    font-weight: 500;
+    border-radius: 0;
+    background: var(--code-background);
+    border-bottom: 1px solid var(--separator-color);
+    color: var(--page-foreground-color)
+}
+
+
+dl.reflist dd {
+    background: none;
+}
+
+/*
+ Table
+ */
+
+table.markdownTable, table.fieldtable {
+    width: 100%;
+    border: none;
+    margin: var(--spacing-medium) 0;
+    box-shadow: 0 0 0 1px var(--separator-color);
+    border-radius: var(--border-radius-small);
+}
+
+th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone {
+    background: var(--tablehead-background);
+    color: var(--tablehead-foreground);
+    font-weight: 600;
+    font-size: var(--page-font-size);
+}
+
+th.markdownTableHeadLeft:first-child, th.markdownTableHeadRight:first-child, th.markdownTableHeadCenter:first-child, th.markdownTableHeadNone:first-child {
+    border-top-left-radius: var(--border-radius-small);
+}
+
+th.markdownTableHeadLeft:last-child, th.markdownTableHeadRight:last-child, th.markdownTableHeadCenter:last-child, th.markdownTableHeadNone:last-child {
+    border-top-right-radius: var(--border-radius-small);
+}
+
+table.markdownTable td, table.markdownTable th, table.fieldtable dt {
+    border: none;
+    border-right: 1px solid var(--separator-color);
+    padding: var(--spacing-small) var(--spacing-medium);
+}
+
+table.markdownTable td:last-child, table.markdownTable th:last-child, table.fieldtable dt:last-child {
+    border: none;
+}
+
+table.markdownTable tr, table.markdownTable tr {
+    border-bottom: 1px solid var(--separator-color);
+}
+
+table.markdownTable tr:last-child, table.markdownTable tr:last-child {
+    border-bottom: none;
+}
+
+table.fieldtable th {
+    font-size: var(--page-font-size);
+    font-weight: 600;
+    background-image: none;
+    background-color: var(--tablehead-background);
+    color: var(--tablehead-foreground);
+    border-bottom: 1px solid var(--separator-color);
+}
+
+.fieldtable td.fieldtype, .fieldtable td.fieldname {
+    border-bottom: 1px solid var(--separator-color);
+    border-right: 1px solid var(--separator-color);
+}
+
+.fieldtable td.fielddoc {
+    border-bottom: 1px solid var(--separator-color);
+}
+
+.memberdecls td.glow, .fieldtable tr.glow {
+    background-color: var(--primary-light-color);
+    box-shadow: 0 0 15px var(--primary-light-color);
+}
+
+table.memberdecls {
+    display: block;
+}
+
+table.memberdecls tr[class^='memitem'] {
+    font-family: var(--font-family-monospace);
+    font-size: var(--code-font-size);
+}
+
+table.memberdecls .memItemLeft, table.memberdecls .memItemRight {
+    transition: none;
+    padding-top: var(--spacing-small);
+    padding-bottom: var(--spacing-small);
+    border-top: 1px solid var(--separator-color);
+    border-bottom: 1px solid var(--separator-color);
+    background-color: var(--fragment-background);
+}
+
+table.memberdecls .memItemLeft {
+    border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
+    border-left: 1px solid var(--separator-color);
+    padding-left: var(--spacing-medium);
+    padding-right: 0;
+}
+
+table.memberdecls .memItemRight {
+    border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
+    border-right: 1px solid var(--separator-color);
+    padding-right: var(--spacing-medium);
+    padding-left: 0;
+
+}
+
+table.memberdecls .mdescLeft, table.memberdecls .mdescRight {
+    background: none;
+    color: var(--page-foreground-color);
+    padding: var(--spacing-small) 0;
+}
+
+table.memberdecls .memSeparator {
+    background: var(--page-background-color);
+    height: var(--spacing-large);
+    border: 0;
+    transition: none;
+}
+
+table.memberdecls .groupheader {
+    margin-bottom: var(--spacing-large);
+}
+
+table.memberdecls .inherit_header td {
+    padding: 0 0 var(--spacing-medium) 0;
+    text-indent: -12px;
+    line-height: 1.5em;
+    color: var(--page-secondary-foreground-color);
+}
+
+@media screen and (max-width: 767px) {
+
+    table.memberdecls .memItemLeft, table.memberdecls .memItemRight, table.memberdecls .mdescLeft, table.memberdecls .mdescRight {
+        display: block;
+        text-align: left;
+        padding-left: var(--spacing-large);
+        margin: 0 calc(0px - var(--spacing-large)) 0 calc(0px - var(--spacing-large));
+        border-right: none;
+        border-left: none;
+        border-radius: 0;
+    }
+
+    table.memberdecls .memItemLeft, table.memberdecls .mdescLeft {
+        border-bottom: 0;
+        padding-bottom: 0;
+    }
+
+    table.memberdecls .mdescLeft {
+        margin-top: calc(0px - var(--page-font-size));
+    }
+
+    table.memberdecls .memItemRight, table.memberdecls .mdescRight {
+        border-top: 0;
+        padding-top: 0;
+        padding-right: var(--spacing-large);
+    }
+
+    table.memberdecls tr[class^='memitem']:not(.inherit) {
+        display: block;
+        width: calc(100vw - 2 * var(--spacing-large));
+    }
+
+    table.memberdecls .mdescRight {
+        color: var(--page-foreground-color);
+    }
+
+    table.memberdecls tr.inherit {
+        visibility: hidden;
+    }
+
+    table.memberdecls tr[style="display: table-row;"] {
+        display: block !important;
+        visibility: visible;
+        width: calc(100vw - 2 * var(--spacing-large));
+        animation: fade .5s;
+    }
+
+    @keyframes fade {
+        0% {
+            opacity: 0;
+            max-height: 0;
+        }
+
+        100% {
+            opacity: 1;
+            max-height: 200px;
+        }
+    }
+}
+
+
+/*
+ Horizontal Rule
+ */
+
+hr {
+    margin-top: var(--spacing-large);
+    margin-bottom: var(--spacing-large);
+    border-top:1px solid var(--separator-color);
+}
+
+.contents hr {
+    box-shadow: 100px 0 0 0 var(--separator-color),
+                -100px 0 0 0 var(--separator-color),
+                var(--content-maxwidth) 0 0 0 var(--separator-color),
+                calc(0px - var(--content-maxwidth)) 0 0 0 var(--separator-color),
+                calc(2 * var(--content-maxwidth)) 0 0 0 var(--separator-color),
+                calc(0px - 2* var(--content-maxwidth)) 0 0 0 var(--separator-color);
+}
+
+.contents img, .contents .center, .contents center, .contents div.image object {
+    max-width: 100%;
+    overflow: auto;
+}
+
+/*
+ Directories
+ */
+div.directory {
+    border-top: 1px solid var(--separator-color);
+    border-bottom: 1px solid var(--separator-color);
+    width: auto;
+}
+
+table.directory {
+    font-family: var(--font-family);
+    font-size: var(--page-font-size);
+    font-weight: normal;
+}
+
+.directory td.entry {
+    padding: var(--spacing-small);
+}
+
+.directory tr.even {
+    background-color: var(--odd-color);
+}
+
+.icona {
+    width: auto;
+    height: auto;
+    margin: 0 var(--spacing-small);
+}
+
+.icon {
+    background: var(--primary-color);
+    width: 18px;
+    height: 18px;
+    line-height: 18px;
+}
+
+.iconfopen, .icondoc, .iconfclosed {
+    background-position: center;
+    margin-bottom: 0;
+}
+
+.icondoc {
+    filter: saturate(0.2);
+}
+
+@media screen and (max-width: 767px) {
+    div.directory {
+        margin-left: calc(0px - var(--spacing-medium));
+        margin-right: calc(0px - var(--spacing-medium));
+    }
+}
+
+@media (prefers-color-scheme: dark) {
+    html:not(.light-mode) .iconfopen, html:not(.light-mode) .iconfclosed {
+        filter: hue-rotate(180deg) invert();
+    }
+}
+
+html.dark-mode .iconfopen, html.dark-mode .iconfclosed {
+    filter: hue-rotate(180deg) invert();
+}
+
+/*
+ Class list
+ */
+
+.classindex dl.odd {
+    background: var(--odd-color);
+    border-radius: var(--border-radius-small);
+}
+
+@media screen and (max-width: 767px) {
+    .classindex {
+        margin: 0 calc(0px - var(--spacing-small));
+    }
+}
+
+/* 
+ Class Index Doxygen 1.8 
+*/
+
+table.classindex table div.ah {
+    background-image: none;
+    background-color: initial;
+    border-color: var(--separator-color);
+    color: var(--page-foreground-color);
+    box-shadow: var(--box-shadow);
+    border-radius: var(--border-radius-large);
+    padding: var(--spacing-small);
+}
+
+div.qindex {
+    background-color: var(--odd-color);
+    border-radius: var(--border-radius-small);
+    border: 1px solid var(--separator-color);
+    padding: var(--spacing-small) 0;
+}
+
+/*
+  Footer and nav-path
+ */
+
+#nav-path {
+    margin-bottom: -1px;
+    width: 100%;
+}
+
+#nav-path ul {
+    background-image: none;
+    background: var(--page-background-color);
+    border: none;
+    border-top: 1px solid var(--separator-color);
+    border-bottom: 1px solid var(--separator-color);
+    font-size: var(--navigation-font-size);
+}
+
+img.footer {
+    width: 60px;
+}
+
+.navpath li.footer {
+    color: var(--page-secondary-foreground-color);
+}
+
+address.footer {
+    margin-bottom: var(--spacing-large);
+}
+
+#nav-path li.navelem {
+    background-image: none;
+    display: flex;
+    align-items: center;
+}
+
+.navpath li.navelem a {
+    text-shadow: none;
+    display: inline-block;
+    color: var(--primary-color) !important;
+}
+
+.navpath li.navelem b {
+    color: var(--primary-dark-color);
+    font-weight: 500;
+}
+
+li.navelem {
+    padding: 0;
+    margin-left: -8px;
+}
+
+li.navelem:first-child {
+    margin-left: var(--spacing-large);
+}
+
+li.navelem:first-child:before {
+    display: none;
+}
+
+#nav-path li.navelem:after {
+    content: '';
+    border: 5px solid var(--page-background-color);
+    border-bottom-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    transform: scaleY(4.2);
+    z-index: 10;
+    margin-left: 6px;
+}
+
+#nav-path li.navelem:before {
+    content: '';
+    border: 5px solid var(--separator-color);
+    border-bottom-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    transform: scaleY(3.2);
+    margin-right: var(--spacing-small);
+}
+
+.navpath li.navelem a:hover {
+    color: var(--primary-color);
+}
+
+/*
+ Scrollbars for Webkit
+*/
+
+#nav-tree::-webkit-scrollbar,
+div.fragment::-webkit-scrollbar,
+pre.fragment::-webkit-scrollbar,
+div.memproto::-webkit-scrollbar,
+.contents center::-webkit-scrollbar {
+    width: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding));
+    height: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding));
+}
+
+#nav-tree::-webkit-scrollbar-thumb,
+div.fragment::-webkit-scrollbar-thumb,
+pre.fragment::-webkit-scrollbar-thumb,
+div.memproto::-webkit-scrollbar-thumb,
+.contents center::-webkit-scrollbar-thumb {
+    background-color: transparent;
+    border: var(--webkit-scrollbar-padding) solid transparent;
+    border-radius: calc(var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding));
+    background-clip: padding-box;  
+}
+
+#nav-tree:hover::-webkit-scrollbar-thumb,
+div.fragment:hover::-webkit-scrollbar-thumb,
+pre.fragment:hover::-webkit-scrollbar-thumb,
+div.memproto:hover::-webkit-scrollbar-thumb,
+.contents center:hover::-webkit-scrollbar-thumb {
+    background-color: var(--webkit-scrollbar-color);
+}
+
+#nav-tree::-webkit-scrollbar-track,
+div.fragment::-webkit-scrollbar-track,
+pre.fragment::-webkit-scrollbar-track,
+div.memproto::-webkit-scrollbar-track,
+.contents center::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+#nav-tree, div.fragment, pre.fragment, div.memproto, .contents center {
+    overflow-x: overlay;
+}
+
+/*
+ Scrollbars for Firefox
+*/
+
+#nav-tree, div.fragment, pre.fragment, div.memproto, .contents center {
+    scrollbar-width: thin;
+}
+
+/*
+  Optional Dark mode toggle button
+*/
+
+doxygen-awesome-dark-mode-toggle {
+    display: inline-block;
+    margin: 0 0 0 var(--spacing-small);
+    padding: 0;
+    width: var(--searchbar-height);
+    height: var(--searchbar-height);
+    background: none;
+    border: none;
+    border-radius: var(--searchbar-height);
+    vertical-align: middle;
+    text-align: center;
+    line-height: var(--searchbar-height);
+    font-size: 22px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    user-select: none;
+    cursor: pointer;
+}
+
+doxygen-awesome-dark-mode-toggle > svg {
+    transition: transform .1s ease-in-out;
+}
+
+doxygen-awesome-dark-mode-toggle:active > svg {
+    transform: scale(.5);
+}
+
+doxygen-awesome-dark-mode-toggle:hover {
+    background-color: rgba(0,0,0,.03);
+}
+
+html.dark-mode doxygen-awesome-dark-mode-toggle:hover {
+    background-color: rgba(0,0,0,.18);
+}
+
+/*
+ Optional fragment copy button
+*/
+.doxygen-awesome-fragment-wrapper {
+    position: relative;
+}
+
+doxygen-awesome-fragment-copy-button {
+    opacity: 0;
+    background: var(--fragment-background);
+    width: 28px;
+    height: 28px;
+    position: absolute;
+    right: calc(var(--spacing-large) - (var(--spacing-large) / 2.5));
+    top: calc(var(--spacing-large) - (var(--spacing-large) / 2.5));
+    border: 1px solid var(--fragment-foreground);
+    cursor: pointer;
+    border-radius: var(--border-radius-small);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.doxygen-awesome-fragment-wrapper:hover doxygen-awesome-fragment-copy-button, doxygen-awesome-fragment-copy-button.success {
+    opacity: .28;
+}
+
+doxygen-awesome-fragment-copy-button:hover, doxygen-awesome-fragment-copy-button.success {
+    opacity: 1 !important;
+}
+
+doxygen-awesome-fragment-copy-button:active:not([class~=success]) svg {
+    transform: scale(.91);
+}
+
+doxygen-awesome-fragment-copy-button svg {
+    fill: var(--fragment-foreground);
+    width: 18px;
+    height: 18px;
+}
+
+doxygen-awesome-fragment-copy-button.success svg {
+    fill: rgb(14, 168, 14);
+}
+
+doxygen-awesome-fragment-copy-button.success {
+    border-color: rgb(14, 168, 14);
+}
+
+@media screen and (max-width: 767px) {
+    .textblock > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button,
+    .textblock li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button,
+    .memdoc li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button,
+    .memdoc > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button,
+    dl dd > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button {
+        right: 0;
+    }
+}
+
+/*
+ Optional paragraph link button
+*/
+
+a.anchorlink {
+    font-size: 90%;
+    margin-left: var(--spacing-small);
+    color: var(--page-foreground-color) !important;
+    text-decoration: none;
+    opacity: .15;
+    display: none;
+    transition: opacity .1s ease-in-out, color .1s ease-in-out;
+}
+
+a.anchorlink svg {
+    fill: var(--page-foreground-color);
+}
+
+h3 a.anchorlink svg, h4 a.anchorlink svg {
+    margin-bottom: -3px;
+    margin-top: -4px;
+}
+
+a.anchorlink:hover {
+    opacity: .45;
+}
+
+h2:hover a.anchorlink, h1:hover a.anchorlink, h3:hover a.anchorlink, h4:hover a.anchorlink  {
+    display: inline-block;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/mainpage.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,58 @@
+/**
+ * \mainpage
+ * \brief Welcome to sci documentation
+ *
+ * This is sci, the simple continuous integration framework.
+ *
+ * The documentation available here targets users who want to hack on sci code,
+ * for user general documentation please read the appropriate manual pages.
+ *
+ * ## General overview
+ *
+ * The framework is split between three applications:
+ *
+ * - `scid`: the main daemon providing database access through HTTP (using
+ *   CGI/FastCGI).
+ * - `scictl`: main user utility to manipulate the sci framework using the
+ *   command line.
+ * - `sciworkerd`: daemon that fetches jobs, run them and send the result.
+ *
+ * ## Data models
+ *
+ * Every data model is converted back-and-forth using JSON all over the
+ * application to avoid converting over and over into native types. Also,
+ * because the framework uses [mustache][] and Javascript based themes it's
+ * easier to share the data model using this format.
+ *
+ * ### project
+ *
+ * - `name` (string): unique project identifir
+ * - `desc` (string): project description
+ * - `url` (string): project homepage or repository URL
+ * - `script` (string): code to execute
+ * - `date` (int): created timestamp
+ *
+ * ### worker
+ *
+ * - `name` (string): unique worker identifier
+ * - `desc` (string): worker description
+ *
+ * ### job
+ *
+ * - `id` (int): unique job id
+ * - `tag` (string): job tag (e.g. repository revision)
+ * - `project_name` (string): project name referenced
+ * - `date` (int): created timestamp
+ *
+ * ### jobresult
+ *
+ * - `id` (int): unique jobresult id
+ * - `job_id` (int): job id referenced
+ * - `worker_name` (string): worker name referenced
+ * - `console` (string): script console output
+ * - `exitcode` (int): script exit code (only if sigcode == 0)
+ * - `sigcode` (int): termination signal if interrupted (exitcode will be 0)
+ * - `date` (int): created timestamp (not the worker job timestamp)
+ *
+ * [mustache]: https://mustache.github.io/
+ */
--- a/lib/apic.c	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,283 +0,0 @@
-#include <assert.h>
-#include <errno.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <string.h>
-
-#include <curl/curl.h>
-
-#include "apic.h"
-#include "util.h"
-
-struct curlpack {
-	CURL *curl;
-	CURLcode code;
-	struct curl_slist *headers;
-};
-
-struct apiconf apiconf = {
-	.baseurl = "http://127.0.0.1"
-};
-
-static size_t
-writer(char *in, size_t n, size_t w, FILE *fp)
-{
-	if (fwrite(in, n, w, fp) != w)
-		return 0;
-
-	return w;
-}
-
-static inline char *
-create_url(const char *fmt, va_list args)
-{
-	static _Thread_local char ret[1024];
-	char page[128];
-	va_list ap;
-
-	ret[0] = 0;
-	va_copy(ap, args);
-	vsnprintf(page, sizeof (page), fmt, ap);
-	va_end(ap);
-
-	snprintf(ret, sizeof (ret), "%s/%s", apiconf.baseurl, page);
-
-	return ret;
-}
-
-static inline FILE *
-create_file(char **buf, size_t *bufsz)
-{
-	FILE *fp;
-
-	*buf = NULL;
-	*bufsz = 0;
-
-	if (!(fp = open_memstream(buf, bufsz)))
-		util_die("open_memstream: %s\n", strerror(errno));
-
-	return fp;
-}
-
-static struct curlpack
-create_curl(FILE *fp, const char *body, const char *url)
-{
-	struct curlpack pack = {0};
-
-	pack.headers = curl_slist_append(pack.headers, "Content-Type: application/json");
-	pack.curl = curl_easy_init();
-	curl_easy_setopt(pack.curl, CURLOPT_HTTPHEADER, pack.headers);
-	curl_easy_setopt(pack.curl, CURLOPT_URL, url);
-	curl_easy_setopt(pack.curl, CURLOPT_FOLLOWLOCATION, 1L);
-	curl_easy_setopt(pack.curl, CURLOPT_TIMEOUT, 3L);
-	curl_easy_setopt(pack.curl, CURLOPT_WRITEFUNCTION, writer);
-	curl_easy_setopt(pack.curl, CURLOPT_WRITEDATA, fp);
-	curl_easy_setopt(pack.curl, CURLOPT_NOSIGNAL, 1L);
-
-	/* Assume POST request if there is a body. */
-	if (body) {
-		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDS, body);
-		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDSIZE, strlen(body));
-	}
-
-	pack.code = curl_easy_perform(pack.curl);
-
-	return pack;
-}
-
-static json_t *
-perform(struct apic *req, const char *body, const char *fmt, va_list ap)
-{
-	FILE *fp;
-	char *response, *url;
-	size_t responsesz;
-	json_t *doc = NULL;
-	json_error_t error;
-	struct curlpack curl;
-
-	memset(req, 0, sizeof (*req));
-
-	url  = create_url(fmt, ap);
-	fp   = create_file(&response, &responsesz);
-	curl = create_curl(fp, body, url);
-
-	/* Perform that request now. */
-	fclose(fp);
-
-	if (curl.code != CURLE_OK)
-		snprintf(req->error, sizeof (req->error), "%s", curl_easy_strerror(curl.code));
-	else {
-		curl_easy_getinfo(curl.curl, CURLINFO_RESPONSE_CODE, &req->status);
-
-		if (req->status != 200)
-			snprintf(req->error, sizeof (req->error), "HTTP returned %ld", req->status);
-		if (response[0] && !(doc = json_loads(response, 0, &error)))
-			snprintf(req->error, sizeof (req->error), "JSON parse error: %s", error.text);
-	}
-
-	curl_easy_cleanup(curl.curl);
-	curl_slist_free_all(curl.headers);
-
-	free(response);
-
-	return doc;
-}
-
-static json_t *
-get(struct apic *req, const char *fmt, ...)
-{
-	va_list ap;
-	json_t *ret;
-
-	va_start(ap, fmt);
-	ret = perform(req, NULL, fmt, ap);
-	va_end(ap);
-
-	if (!ret || (!json_is_object(ret) && !json_is_array(ret)))
-		snprintf(req->error, sizeof (req->error), "invalid JSON document received");
-
-	return ret;
-}
-
-static int
-create(struct apic *req, json_t *doc, const char *fmt, ...)
-{
-	va_list ap;
-	json_t *ret;
-	char *body;
-
-	memset(req, 0, sizeof (*req));
-
-	if (!(body = json_dumps(doc, JSON_COMPACT))) {
-		json_decref(doc);
-		return snprintf(req->error, sizeof (req->error), "%s", strerror(errno)), -1;
-	}
-
-	va_start(ap, fmt);
-	ret = perform(req, body, fmt, ap);
-	va_end(ap);
-
-	/* TODO: update id. */
-	(void)ret;
-
-	free(body);
-
-	return 0;
-}
-
-json_t *
-apic_get(struct apic *req, const char *fmt, ...)
-{
-	assert(req);
-	assert(fmt);
-
-	va_list ap;
-	json_t *ret;
-
-	va_start(ap, fmt);
-	ret = perform(req, NULL, fmt, ap);
-	va_end(ap);
-
-	return ret;
-}
-
-json_t *
-apic_post(struct apic *req, const json_t *doc, const char *fmt, ...)
-{
-	assert(req);
-	assert(fmt);
-
-	va_list ap;
-	json_t *ret;
-	char *body;
-
-	if (!(body = json_dumps(doc, JSON_COMPACT)))
-		util_die("%s", strerror(ENOMEM));
-
-	va_start(ap, fmt);
-	ret = perform(req, body, fmt, ap);
-	va_end(ap);
-
-	free(body);
-
-	return ret;
-}
-
-int
-apic_job_add(struct apic *req, json_t *job)
-{
-	assert(req);
-	assert(job);
-
-	return create(req, job, "api/v1/jobs");
-}
-
-json_t *
-apic_job_todo(struct apic *req, const char *worker_name)
-{
-	assert(req);
-	assert(worker_name);
-
-	return get(req, "api/v1/todo/%s", worker_name);
-}
-
-int
-apic_jobresult_add(struct apic *req, json_t *res)
-{
-	assert(req);
-	assert(res);
-
-	return create(req, res, "api/v1/jobresults");
-}
-
-int
-apic_project_save(struct apic *req, json_t *p)
-{
-	assert(req);
-	assert(p);
-
-	return create(req, p, "api/v1/projects");
-}
-
-json_t *
-apic_project_list(struct apic *req)
-{
-	assert(req);
-
-	return get(req, "api/v1/projects");
-}
-
-json_t *
-apic_project_find(struct apic *req, const char *name)
-{
-	assert(req);
-	assert(name);
-
-	return get(req, "api/v1/projects/%s", name);
-}
-
-int
-apic_worker_save(struct apic *req, json_t *wk)
-{
-	assert(req);
-	assert(wk);
-
-	return create(req, wk, "api/v1/workers");
-}
-
-json_t *
-apic_worker_list(struct apic *req)
-{
-	assert(req);
-
-	return get(req, "api/v1/workers");
-}
-
-json_t *
-apic_worker_find(struct apic *req, const char *name)
-{
-	assert(req);
-	assert(name);
-
-	return get(req, "api/v1/workers/%s", name);
-}
--- a/lib/apic.h	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-#ifndef SCI_APIC_H
-#define SCI_APIC_H
-
-#include <jansson.h>
-
-#define APIC_ERR_MAX 128
-
-struct apic {
-	char error[APIC_ERR_MAX];
-	long status;
-};
-
-extern struct apiconf {
-	char baseurl[512];
-} apiconf;
-
-/* Generic HTTP commands using JSON. */
-
-/* Perform GET request. */
-json_t *
-apic_get(struct apic *, const char *, ...);
-
-/* Perform POST request with JSON body. */
-json_t *
-apic_post(struct apic *, const json_t *, const char *, ...);
-
-/* --- */
-
-int
-apic_job_add(struct apic *, json_t *);
-
-json_t *
-apic_job_todo(struct apic *, const char *);
-
-int
-apic_jobresult_add(struct apic *, json_t *);
-
-int
-apic_project_save(struct apic *, json_t *);
-
-json_t *
-apic_project_list(struct apic *);
-
-json_t *
-apic_project_find(struct apic *, const char *);
-
-int
-apic_worker_save(struct apic *, json_t *);
-
-json_t *
-apic_worker_list(struct apic *);
-
-json_t *
-apic_worker_find(struct apic *, const char *);
-
-#endif /* !SCI_APIC_H */
--- a/lib/log.c	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/*
- * log.c -- logging routines
- *
- * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <assert.h>
-#include <stdio.h>
-#include <syslog.h>
-#include <time.h>
-
-#include "log.h"
-
-/* TODO: replace. */
-static struct {
-	enum log_level verbosity;
-} config = {
-	.verbosity = LOG_LEVEL_DEBUG
-};
-
-static int syslog_levels[] = {
-	[LOG_LEVEL_DEBUG]       = LOG_DEBUG,
-	[LOG_LEVEL_INFO]        = LOG_INFO,
-	[LOG_LEVEL_WARNING]     = LOG_WARNING
-};
-
-static const char * const levelsyms[] = {
-	[LOG_LEVEL_DEBUG]       = "D",
-	[LOG_LEVEL_INFO]        = "I",
-	[LOG_LEVEL_WARNING]     = "W"
-};
-
-void
-log_open(const char *name)
-{
-	openlog(name, 0, LOG_USER);
-}
-
-void
-log_write(enum log_level level, const char *fmt, ...)
-{
-	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
-	assert(fmt);
-
-	if (config.verbosity >= level) {
-		va_list ap;
-
-		va_start(ap, fmt);
-		log_vwrite(level, fmt, ap);
-		va_end(ap);
-	}
-}
-
-void
-log_vwrite(enum log_level level, const char *fmt, va_list ap)
-{
-	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
-	assert(fmt);
-
-	char line[BUFSIZ] = {0}, timebuf[32] = {0};
-	time_t timestamp = time(NULL);
-	struct tm *tm = localtime(&timestamp);
-
-	vsnprintf(line, sizeof (line), fmt, ap);
-	syslog(syslog_levels[level], "%s", line);
-
-	strftime(timebuf, sizeof (timebuf), "%F %T", tm);
-	printf("%s %s %s\n", timebuf, levelsyms[level], line);
-}
-
-void
-log_finish(void)
-{
-	if (config.verbosity > 0)
-		closelog();
-}
--- a/lib/log.h	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-/*
- * log.h -- logging routines
- *
- * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_LOG_H
-#define SCI_LOG_H
-
-#include <stdarg.h>
-#include <stdlib.h>
-
-enum log_level {
-	LOG_LEVEL_WARNING = 1,
-	LOG_LEVEL_INFO,
-	LOG_LEVEL_DEBUG
-};
-
-#define log_debug(...)  log_write(LOG_LEVEL_DEBUG, __VA_ARGS__)
-#define log_warn(...)   log_write(LOG_LEVEL_WARNING, __VA_ARGS__)
-#define log_info(...)   log_write(LOG_LEVEL_INFO, __VA_ARGS__)
-#define log_die(...) do {                                               \
-        log_write(LOG_LEVEL_WARNING, __VA_ARGS__);                      \
-        exit(1);                                                        \
-} while (0)
-
-void
-log_open(const char *);
-
-void
-log_write(enum log_level, const char *, ...);
-
-void
-log_vwrite(enum log_level, const char *, va_list);
-
-void
-log_finish(void);
-
-#endif /* !SCI_LOG_H */
--- a/lib/strlcpy.c	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-/*	$OpenBSD: strlcpy.c,v 1.16 2019/01/25 00:19:25 millert Exp $	*/
-
-/*
- * Copyright (c) 1998, 2015 Todd C. Miller <millert@openbsd.org>
- *
- * Permission to use, copy, modify, and distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/types.h>
-
-/*
- * Copy string src to buffer dst of size dsize.  At most dsize-1
- * chars will be copied.  Always NUL terminates (unless dsize == 0).
- * Returns strlen(src); if retval >= dsize, truncation occurred.
- */
-size_t
-util_strlcpy(char *dst, const char *src, size_t dsize)
-{
-	const char *osrc = src;
-	size_t nleft = dsize;
-
-	/* Copy as many bytes as will fit. */
-	if (nleft != 0) {
-		while (--nleft != 0) {
-			if ((*dst++ = *src++) == '\0')
-				break;
-		}
-	}
-
-	/* Not enough room in dst, add NUL and traverse rest of src. */
-	if (nleft == 0) {
-		if (dsize != 0)
-			*dst = '\0';		/* NUL-terminate dst */
-		while (*src++)
-			;
-	}
-
-	return(src - osrc - 1);	/* count does not include NUL */
-}
--- a/lib/strtonum.c	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/*	$OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $	*/
-
-/*
- * Copyright (c) 2004 Ted Unangst and Todd Miller
- * All rights reserved.
- *
- * Permission to use, copy, modify, and distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <errno.h>
-#include <limits.h>
-#include <stdlib.h>
-
-#define	INVALID		1
-#define	TOOSMALL	2
-#define	TOOLARGE	3
-
-long long
-util_strtonum(const char *numstr, long long minval, long long maxval,
-    const char **errstrp)
-{
-	long long ll = 0;
-	int error = 0;
-	char *ep;
-	struct errval {
-		const char *errstr;
-		int err;
-	} ev[4] = {
-		{ NULL,		0 },
-		{ "invalid",	EINVAL },
-		{ "too small",	ERANGE },
-		{ "too large",	ERANGE },
-	};
-
-	ev[0].err = errno;
-	errno = 0;
-	if (minval > maxval) {
-		error = INVALID;
-	} else {
-		ll = strtoll(numstr, &ep, 10);
-		if (numstr == ep || *ep != '\0')
-			error = INVALID;
-		else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
-			error = TOOSMALL;
-		else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
-			error = TOOLARGE;
-	}
-	if (errstrp != NULL)
-		*errstrp = ev[error].errstr;
-	errno = ev[error].err;
-	if (error)
-		ll = 0;
-
-	return (ll);
-}
--- a/lib/util.c	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,215 +0,0 @@
-/*
- * util.c -- miscellaneous utilities
- *
- * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <sys/stat.h>
-#include <assert.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <libgen.h>
-#include <limits.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#include "util.h"
-
-void *
-util_malloc(size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		util_die("malloc: %s\n", strerror(errno));
-
-	return ret;
-}
-
-void *
-util_calloc(size_t n, size_t size)
-{
-	void *ret;
-
-	if (!(ret = calloc(n, size)))
-		util_die("calloc: %s\n", strerror(errno));
-
-	return ret;
-}
-
-void *
-util_realloc(void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = realloc(ptr, size)) && size)
-		util_die("realloc: %s\n", strerror(errno));
-
-	return ret;
-}
-
-void *
-util_memdup(const void *ptr, size_t size)
-{
-	void *ret;
-
-	if (!(ret = malloc(size)))
-		util_die("malloc: %s\n", strerror(errno));
-
-	return memcpy(ret, ptr, size);
-}
-
-char *
-util_strdup(const char *src)
-{
-	char *ret;
-
-	if (!(ret = strdup(src)))
-		util_die("strdup: %s\n", strerror(errno));
-
-	return ret;
-}
-
-char *
-util_strndup(const char *src, size_t n)
-{
-	assert(src);
-
-	char *ret;
-
-	if (!(ret = strndup(src, n)))
-		util_die("strndup: %s\n", strerror(errno));
-
-	return ret;
-}
-
-char *
-util_basename(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	util_strlcpy(tmp, str, sizeof (tmp));
-	util_strlcpy(ret, basename(tmp), sizeof (ret));
-
-	return ret;
-}
-
-char *
-util_dirname(const char *str)
-{
-	static char ret[PATH_MAX];
-	char tmp[PATH_MAX];
-
-	util_strlcpy(tmp, str, sizeof (tmp));
-	util_strlcpy(ret, dirname(tmp), sizeof (ret));
-
-	return ret;
-}
-
-FILE *
-util_fmemopen(void *buf, size_t size, const char *mode)
-{
-	FILE *fp;
-
-	if (!(fp = fmemopen(buf, size, mode)))
-		util_die("fmemopen: %s\n", strerror(errno));
-
-	return fp;
-}
-
-FILE *
-util_open_memstream(char **out, size_t *outsz)
-{
-	assert(out);
-	assert(outsz);
-
-	FILE *fp;
-
-	if (!(fp = open_memstream(out, outsz)))
-		util_die("open_memstream: %s\n", strerror(errno));
-
-	return fp;
-}
-
-char *
-util_read(const char *path)
-{
-	int fd;
-	struct stat st;
-	char *ret;
-
-	if ((fd = open(path, O_RDONLY)) < 0)
-		return NULL;
-	if (fstat(fd, &st) < 0)
-		return close(fd), NULL;
-
-	ret = util_calloc(1, st.st_size + 1);
-
-	if (read(fd, ret, st.st_size) != st.st_size) {
-		free(ret);
-		ret = NULL;
-	}
-
-	close(fd);
-
-	return ret;
-}
-
-void
-util_die(const char *fmt, ...)
-{
-	assert(fmt);
-
-	va_list ap;
-
-	va_start(ap, fmt);
-	vfprintf(stderr, fmt, ap);
-	va_end(ap);
-	exit(1);
-}
-
-json_t *
-util_json_pack(const char *fmt, ...)
-{
-	va_list ap;
-	json_t *doc;
-	json_error_t err;
-
-	va_start(ap, fmt);
-	doc = json_vpack_ex(&err, 0, fmt, ap);
-	va_end(ap);
-
-	if (!doc)
-		util_die("json_vpack_ex: %s\n", err.text);
-
-	return doc;
-}
-
-char *
-util_json_dump(const json_t *json)
-{
-	assert(json);
-
-	char *ret;
-
-	if (!(ret = json_dumps(json, JSON_COMPACT)))
-		util_die("json_dump: %s\n", strerror(ENOMEM));
-
-	return ret;
-}
--- a/lib/util.h	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,82 +0,0 @@
-/*
- * util.h -- miscellaneous utilities
- *
- * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef SCI_UTIL_H
-#define SCI_UTIL_H
-
-#include <stddef.h>
-#include <stdio.h>
-
-#include <jansson.h>
-
-#define UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
-
-void *
-util_malloc(size_t);
-
-void *
-util_calloc(size_t, size_t);
-
-void *
-util_realloc(void *, size_t);
-
-void *
-util_reallocarray(void *, size_t, size_t);
-
-void *
-util_memdup(const void *, size_t);
-
-char *
-util_strdup(const char *);
-
-char *
-util_strndup(const char *, size_t);
-
-char *
-util_basename(const char *);
-
-char *
-util_dirname(const char *);
-
-FILE *
-util_fmemopen(void *, size_t, const char *);
-
-FILE *
-util_open_memstream(char **, size_t *);
-
-char *
-util_read(const char *);
-
-void
-util_die(const char *, ...);
-
-json_t *
-util_json_pack(const char *, ...);
-
-char *
-util_json_dump(const json_t *);
-
-/* defined in extern/ */
-
-size_t
-util_strlcpy(char *, const char *, size_t);
-
-long long
-util_strtonum(const char *, long long, long long, const char **);
-
-#endif /* !SCI_UTIL_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/apic.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,298 @@
+/*
+ * apic.c -- synchronous HTTP request
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <curl/curl.h>
+
+#include "apic.h"
+#include "util.h"
+
+struct curlpack {
+	CURL *curl;
+	CURLcode code;
+	struct curl_slist *headers;
+};
+
+struct apiconf apiconf = {
+	.baseurl = "http://127.0.0.1"
+};
+
+static size_t
+writer(char *in, size_t n, size_t w, FILE *fp)
+{
+	if (fwrite(in, n, w, fp) != w)
+		return 0;
+
+	return w;
+}
+
+static inline char *
+create_url(const char *fmt, va_list args)
+{
+	static _Thread_local char ret[1024];
+	char page[128];
+	va_list ap;
+
+	ret[0] = 0;
+	va_copy(ap, args);
+	vsnprintf(page, sizeof (page), fmt, ap);
+	va_end(ap);
+
+	snprintf(ret, sizeof (ret), "%s/%s", apiconf.baseurl, page);
+
+	return ret;
+}
+
+static inline FILE *
+create_file(char **buf, size_t *bufsz)
+{
+	FILE *fp;
+
+	*buf = NULL;
+	*bufsz = 0;
+
+	if (!(fp = open_memstream(buf, bufsz)))
+		util_die("open_memstream: %s\n", strerror(errno));
+
+	return fp;
+}
+
+static struct curlpack
+create_curl(FILE *fp, const char *body, const char *url)
+{
+	struct curlpack pack = {0};
+
+	pack.headers = curl_slist_append(pack.headers, "Content-Type: application/json");
+	pack.curl = curl_easy_init();
+	curl_easy_setopt(pack.curl, CURLOPT_HTTPHEADER, pack.headers);
+	curl_easy_setopt(pack.curl, CURLOPT_URL, url);
+	curl_easy_setopt(pack.curl, CURLOPT_FOLLOWLOCATION, 1L);
+	curl_easy_setopt(pack.curl, CURLOPT_TIMEOUT, 3L);
+	curl_easy_setopt(pack.curl, CURLOPT_WRITEFUNCTION, writer);
+	curl_easy_setopt(pack.curl, CURLOPT_WRITEDATA, fp);
+	curl_easy_setopt(pack.curl, CURLOPT_NOSIGNAL, 1L);
+
+	/* Assume POST request if there is a body. */
+	if (body) {
+		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDS, body);
+		curl_easy_setopt(pack.curl, CURLOPT_POSTFIELDSIZE, strlen(body));
+	}
+
+	pack.code = curl_easy_perform(pack.curl);
+
+	return pack;
+}
+
+static json_t *
+perform(struct apic *req, const char *body, const char *fmt, va_list ap)
+{
+	FILE *fp;
+	char *response, *url;
+	size_t responsesz;
+	json_t *doc = NULL;
+	json_error_t error;
+	struct curlpack curl;
+
+	memset(req, 0, sizeof (*req));
+
+	url  = create_url(fmt, ap);
+	fp   = create_file(&response, &responsesz);
+	curl = create_curl(fp, body, url);
+
+	/* Perform that request now. */
+	fclose(fp);
+
+	if (curl.code != CURLE_OK)
+		snprintf(req->error, sizeof (req->error), "%s", curl_easy_strerror(curl.code));
+	else {
+		curl_easy_getinfo(curl.curl, CURLINFO_RESPONSE_CODE, &req->status);
+
+		if (req->status != 200)
+			snprintf(req->error, sizeof (req->error), "HTTP returned %ld", req->status);
+		if (response[0] && !(doc = json_loads(response, 0, &error)))
+			snprintf(req->error, sizeof (req->error), "JSON parse error: %s", error.text);
+	}
+
+	curl_easy_cleanup(curl.curl);
+	curl_slist_free_all(curl.headers);
+
+	free(response);
+
+	return doc;
+}
+
+static json_t *
+get(struct apic *req, const char *fmt, ...)
+{
+	va_list ap;
+	json_t *ret;
+
+	va_start(ap, fmt);
+	ret = perform(req, NULL, fmt, ap);
+	va_end(ap);
+
+	if (!ret || (!json_is_object(ret) && !json_is_array(ret)))
+		snprintf(req->error, sizeof (req->error), "invalid JSON document received");
+
+	return ret;
+}
+
+static int
+create(struct apic *req, json_t *doc, const char *fmt, ...)
+{
+	va_list ap;
+	json_t *ret;
+	char *body;
+
+	memset(req, 0, sizeof (*req));
+	body = util_json_dump(doc);
+
+	va_start(ap, fmt);
+	ret = perform(req, body, fmt, ap);
+	va_end(ap);
+
+	/* TODO: update id. */
+	(void)ret;
+
+	free(body);
+	json_decref(ret);
+
+	return 0;
+}
+
+json_t *
+apic_get(struct apic *req, const char *fmt, ...)
+{
+	assert(req);
+	assert(fmt);
+
+	va_list ap;
+	json_t *ret;
+
+	va_start(ap, fmt);
+	ret = perform(req, NULL, fmt, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+json_t *
+apic_post(struct apic *req, const json_t *doc, const char *fmt, ...)
+{
+	assert(req);
+	assert(fmt);
+
+	va_list ap;
+	json_t *ret;
+	char *body;
+
+	if (!(body = json_dumps(doc, JSON_COMPACT)))
+		util_die("%s", strerror(ENOMEM));
+
+	va_start(ap, fmt);
+	ret = perform(req, body, fmt, ap);
+	va_end(ap);
+
+	free(body);
+
+	return ret;
+}
+
+int
+apic_job_add(struct apic *req, json_t *job)
+{
+	assert(req);
+	assert(job);
+
+	return create(req, job, "api/v1/jobs");
+}
+
+json_t *
+apic_job_todo(struct apic *req, const char *worker_name)
+{
+	assert(req);
+	assert(worker_name);
+
+	return get(req, "api/v1/todo/%s", worker_name);
+}
+
+int
+apic_jobresult_add(struct apic *req, json_t *res)
+{
+	assert(req);
+	assert(res);
+
+	return create(req, res, "api/v1/jobresults");
+}
+
+int
+apic_project_save(struct apic *req, json_t *p)
+{
+	assert(req);
+	assert(p);
+
+	return create(req, p, "api/v1/projects");
+}
+
+json_t *
+apic_project_list(struct apic *req)
+{
+	assert(req);
+
+	return get(req, "api/v1/projects");
+}
+
+json_t *
+apic_project_find(struct apic *req, const char *name)
+{
+	assert(req);
+	assert(name);
+
+	return get(req, "api/v1/projects/%s", name);
+}
+
+int
+apic_worker_save(struct apic *req, json_t *wk)
+{
+	assert(req);
+	assert(wk);
+
+	return create(req, wk, "api/v1/workers");
+}
+
+json_t *
+apic_worker_list(struct apic *req)
+{
+	assert(req);
+
+	return get(req, "api/v1/workers");
+}
+
+json_t *
+apic_worker_find(struct apic *req, const char *name)
+{
+	assert(req);
+	assert(name);
+
+	return get(req, "api/v1/workers/%s", name);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/apic.h	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,68 @@
+/*
+ * apic.h -- synchronous HTTP request
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_APIC_H
+#define SCI_APIC_H
+
+#include <jansson.h>
+
+#define APIC_ERR_MAX 128
+
+struct apic {
+	char error[APIC_ERR_MAX];
+	long status;
+};
+
+extern struct apiconf {
+	char baseurl[512];
+} apiconf /*! Global variable. */;
+
+json_t *
+apic_get(struct apic *, const char *, ...);
+
+json_t *
+apic_post(struct apic *, const json_t *, const char *, ...);
+
+int
+apic_job_add(struct apic *, json_t *);
+
+json_t *
+apic_job_todo(struct apic *, const char *);
+
+int
+apic_jobresult_add(struct apic *, json_t *);
+
+int
+apic_project_save(struct apic *, json_t *);
+
+json_t *
+apic_project_list(struct apic *);
+
+json_t *
+apic_project_find(struct apic *, const char *);
+
+int
+apic_worker_save(struct apic *, json_t *);
+
+json_t *
+apic_worker_list(struct apic *);
+
+json_t *
+apic_worker_find(struct apic *, const char *);
+
+#endif /* !SCI_APIC_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/log.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,88 @@
+/*
+ * log.c -- logging routines
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <syslog.h>
+#include <time.h>
+
+#include "log.h"
+
+/* TODO: replace. */
+static struct {
+	enum log_level verbosity;
+} config = {
+	.verbosity = LOG_LEVEL_DEBUG
+};
+
+static int syslog_levels[] = {
+	[LOG_LEVEL_DEBUG]       = LOG_DEBUG,
+	[LOG_LEVEL_INFO]        = LOG_INFO,
+	[LOG_LEVEL_WARNING]     = LOG_WARNING
+};
+
+static const char * const levelsyms[] = {
+	[LOG_LEVEL_DEBUG]       = "D",
+	[LOG_LEVEL_INFO]        = "I",
+	[LOG_LEVEL_WARNING]     = "W"
+};
+
+void
+log_open(const char *name)
+{
+	openlog(name, 0, LOG_USER);
+}
+
+void
+log_write(enum log_level level, const char *fmt, ...)
+{
+	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
+	assert(fmt);
+
+	if (config.verbosity >= level) {
+		va_list ap;
+
+		va_start(ap, fmt);
+		log_vwrite(level, fmt, ap);
+		va_end(ap);
+	}
+}
+
+void
+log_vwrite(enum log_level level, const char *fmt, va_list ap)
+{
+	assert(level >= LOG_LEVEL_WARNING && level <= LOG_LEVEL_DEBUG);
+	assert(fmt);
+
+	char line[BUFSIZ] = {0}, timebuf[32] = {0};
+	time_t timestamp = time(NULL);
+	struct tm *tm = localtime(&timestamp);
+
+	vsnprintf(line, sizeof (line), fmt, ap);
+	syslog(syslog_levels[level], "%s", line);
+
+	strftime(timebuf, sizeof (timebuf), "%F %T", tm);
+	printf("%s %s %s\n", timebuf, levelsyms[level], line);
+}
+
+void
+log_finish(void)
+{
+	if (config.verbosity > 0)
+		closelog();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/log.h	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,51 @@
+/*
+ * log.h -- logging routines
+ *
+ * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_LOG_H
+#define SCI_LOG_H
+
+#include <stdarg.h>
+#include <stdlib.h>
+
+enum log_level {
+	LOG_LEVEL_WARNING = 1,
+	LOG_LEVEL_INFO,
+	LOG_LEVEL_DEBUG
+};
+
+#define log_debug(...)  log_write(LOG_LEVEL_DEBUG, __VA_ARGS__)
+#define log_warn(...)   log_write(LOG_LEVEL_WARNING, __VA_ARGS__)
+#define log_info(...)   log_write(LOG_LEVEL_INFO, __VA_ARGS__)
+#define log_die(...) do {                                               \
+        log_write(LOG_LEVEL_WARNING, __VA_ARGS__);                      \
+        exit(1);                                                        \
+} while (0)
+
+void
+log_open(const char *);
+
+void
+log_write(enum log_level, const char *, ...);
+
+void
+log_vwrite(enum log_level, const char *, va_list);
+
+void
+log_finish(void);
+
+#endif /* !SCI_LOG_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/strlcpy.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,49 @@
+/*	$OpenBSD: strlcpy.c,v 1.16 2019/01/25 00:19:25 millert Exp $	*/
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <millert@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+/*
+ * Copy string src to buffer dst of size dsize.  At most dsize-1
+ * chars will be copied.  Always NUL terminates (unless dsize == 0).
+ * Returns strlen(src); if retval >= dsize, truncation occurred.
+ */
+size_t
+util_strlcpy(char *dst, const char *src, size_t dsize)
+{
+	const char *osrc = src;
+	size_t nleft = dsize;
+
+	/* Copy as many bytes as will fit. */
+	if (nleft != 0) {
+		while (--nleft != 0) {
+			if ((*dst++ = *src++) == '\0')
+				break;
+		}
+	}
+
+	/* Not enough room in dst, add NUL and traverse rest of src. */
+	if (nleft == 0) {
+		if (dsize != 0)
+			*dst = '\0';		/* NUL-terminate dst */
+		while (*src++)
+			;
+	}
+
+	return(src - osrc - 1);	/* count does not include NUL */
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/strtonum.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,65 @@
+/*	$OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $	*/
+
+/*
+ * Copyright (c) 2004 Ted Unangst and Todd Miller
+ * All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <errno.h>
+#include <limits.h>
+#include <stdlib.h>
+
+#define	INVALID		1
+#define	TOOSMALL	2
+#define	TOOLARGE	3
+
+long long
+util_strtonum(const char *numstr, long long minval, long long maxval,
+    const char **errstrp)
+{
+	long long ll = 0;
+	int error = 0;
+	char *ep;
+	struct errval {
+		const char *errstr;
+		int err;
+	} ev[4] = {
+		{ NULL,		0 },
+		{ "invalid",	EINVAL },
+		{ "too small",	ERANGE },
+		{ "too large",	ERANGE },
+	};
+
+	ev[0].err = errno;
+	errno = 0;
+	if (minval > maxval) {
+		error = INVALID;
+	} else {
+		ll = strtoll(numstr, &ep, 10);
+		if (numstr == ep || *ep != '\0')
+			error = INVALID;
+		else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval)
+			error = TOOSMALL;
+		else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval)
+			error = TOOLARGE;
+	}
+	if (errstrp != NULL)
+		*errstrp = ev[error].errstr;
+	errno = ev[error].err;
+	if (error)
+		ll = 0;
+
+	return (ll);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/util.c	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,215 @@
+/*
+ * util.c -- miscellaneous utilities
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/stat.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <libgen.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+
+void *
+util_malloc(size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		util_die("malloc: %s\n", strerror(errno));
+
+	return ret;
+}
+
+void *
+util_calloc(size_t n, size_t size)
+{
+	void *ret;
+
+	if (!(ret = calloc(n, size)))
+		util_die("calloc: %s\n", strerror(errno));
+
+	return ret;
+}
+
+void *
+util_realloc(void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = realloc(ptr, size)) && size)
+		util_die("realloc: %s\n", strerror(errno));
+
+	return ret;
+}
+
+void *
+util_memdup(const void *ptr, size_t size)
+{
+	void *ret;
+
+	if (!(ret = malloc(size)))
+		util_die("malloc: %s\n", strerror(errno));
+
+	return memcpy(ret, ptr, size);
+}
+
+char *
+util_strdup(const char *src)
+{
+	char *ret;
+
+	if (!(ret = strdup(src)))
+		util_die("strdup: %s\n", strerror(errno));
+
+	return ret;
+}
+
+char *
+util_strndup(const char *src, size_t n)
+{
+	assert(src);
+
+	char *ret;
+
+	if (!(ret = strndup(src, n)))
+		util_die("strndup: %s\n", strerror(errno));
+
+	return ret;
+}
+
+char *
+util_basename(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	util_strlcpy(tmp, str, sizeof (tmp));
+	util_strlcpy(ret, basename(tmp), sizeof (ret));
+
+	return ret;
+}
+
+char *
+util_dirname(const char *str)
+{
+	static char ret[PATH_MAX];
+	char tmp[PATH_MAX];
+
+	util_strlcpy(tmp, str, sizeof (tmp));
+	util_strlcpy(ret, dirname(tmp), sizeof (ret));
+
+	return ret;
+}
+
+FILE *
+util_fmemopen(void *buf, size_t size, const char *mode)
+{
+	FILE *fp;
+
+	if (!(fp = fmemopen(buf, size, mode)))
+		util_die("fmemopen: %s\n", strerror(errno));
+
+	return fp;
+}
+
+FILE *
+util_open_memstream(char **out, size_t *outsz)
+{
+	assert(out);
+	assert(outsz);
+
+	FILE *fp;
+
+	if (!(fp = open_memstream(out, outsz)))
+		util_die("open_memstream: %s\n", strerror(errno));
+
+	return fp;
+}
+
+char *
+util_read(const char *path)
+{
+	int fd;
+	struct stat st;
+	char *ret;
+
+	if ((fd = open(path, O_RDONLY)) < 0)
+		return NULL;
+	if (fstat(fd, &st) < 0)
+		return close(fd), NULL;
+
+	ret = util_calloc(1, st.st_size + 1);
+
+	if (read(fd, ret, st.st_size) != st.st_size) {
+		free(ret);
+		ret = NULL;
+	}
+
+	close(fd);
+
+	return ret;
+}
+
+void
+util_die(const char *fmt, ...)
+{
+	assert(fmt);
+
+	va_list ap;
+
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
+	exit(1);
+}
+
+json_t *
+util_json_pack(const char *fmt, ...)
+{
+	va_list ap;
+	json_t *doc;
+	json_error_t err;
+
+	va_start(ap, fmt);
+	doc = json_vpack_ex(&err, 0, fmt, ap);
+	va_end(ap);
+
+	if (!doc)
+		util_die("json_vpack_ex: %s\n", err.text);
+
+	return doc;
+}
+
+char *
+util_json_dump(const json_t *json)
+{
+	assert(json);
+
+	char *ret;
+
+	if (!(ret = json_dumps(json, JSON_COMPACT)))
+		util_die("json_dump: %s\n", strerror(ENOMEM));
+
+	return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libsci/util.h	Thu Aug 04 16:47:10 2022 +0200
@@ -0,0 +1,82 @@
+/*
+ * util.h -- miscellaneous utilities
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef SCI_UTIL_H
+#define SCI_UTIL_H
+
+#include <stddef.h>
+#include <stdio.h>
+
+#include <jansson.h>
+
+#define UTIL_SIZE(x) (sizeof (x) / sizeof (x[0]))
+
+void *
+util_malloc(size_t);
+
+void *
+util_calloc(size_t, size_t);
+
+void *
+util_realloc(void *, size_t);
+
+void *
+util_reallocarray(void *, size_t, size_t);
+
+void *
+util_memdup(const void *, size_t);
+
+char *
+util_strdup(const char *);
+
+char *
+util_strndup(const char *, size_t);
+
+char *
+util_basename(const char *);
+
+char *
+util_dirname(const char *);
+
+FILE *
+util_fmemopen(void *, size_t, const char *);
+
+FILE *
+util_open_memstream(char **, size_t *);
+
+char *
+util_read(const char *);
+
+void
+util_die(const char *, ...);
+
+json_t *
+util_json_pack(const char *, ...);
+
+char *
+util_json_dump(const json_t *);
+
+/* defined in extern/ */
+
+size_t
+util_strlcpy(char *, const char *, size_t);
+
+long long
+util_strtonum(const char *, long long, long long, const char **);
+
+#endif /* !SCI_UTIL_H */
--- a/scid/crud.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/crud.c	Thu Aug 04 16:47:10 2022 +0200
@@ -30,10 +30,10 @@
 	int ret = -1;
 
 	if (!(doc = json_loads(r->fields[0].val, 0, &err)))
-		log_warn("%s: invalid JSON input: %s", topic, err.text);
+		log_warn("crud: %s: invalid JSON input: %s", topic, err.text);
 	else {
 		if (saver(doc) < 0)
-			log_warn("%s: database insertion failed", topic);
+			log_warn("crud: %s: database insertion failed", topic);
 		else
 			ret = 0;
 
--- a/scid/crud.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/crud.h	Thu Aug 04 16:47:10 2022 +0200
@@ -19,14 +19,47 @@
 #ifndef SCID_CRUD_H
 #define SCID_CRUD_H
 
+/**
+ * \file crud.h
+ * \brief Convenient helpers for API pages.
+ *
+ * This module uses the database to fetch or insert data and finally show the
+ * result as HTTP response.
+ *
+ * This module logs message with tag `crud`.
+ */
+
 #include <jansson.h>
 
 struct kreq;
 
+/**
+ * Decode the JSON input and insert them using the appropriate database
+ * function provided.
+ *
+ * \pre r != NULL
+ * \pre saver != NULL
+ * \pre topic != NULL
+ * \param r the request
+ * \param saver the function to save in database
+ * \param topic the log topic for diagnostic
+ */
 void
-crud_insert(struct kreq *, int (*)(json_t *), const char *);
+crud_insert(struct kreq *r, int (*saver)(json_t *), const char *topic);
 
+/**
+ * Convert the JSON provided value.
+ *
+ * For convenience, if the document is NULL (which happen if the database
+ * access failed) it will send a HTTP 500 error. Otherwise it is listed and
+ * free'd.
+ *
+ * \pre r != NULL
+ * \param r the request
+ * \param doc the document to send (maybe NULL)
+ * \warning The document will be destroyed using json_decref.
+ */
 void
-crud_list(struct kreq *, json_t *);
+crud_list(struct kreq *r, json_t *doc);
 
 #endif /* !SCID_CRUD_H */
--- a/scid/db.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/db.h	Thu Aug 04 16:47:10 2022 +0200
@@ -19,52 +19,175 @@
 #ifndef SCI_DB_H
 #define SCI_DB_H
 
+/**
+ * \file db.h
+ * \brief Database access.
+ *
+ * This module access the SQLite database.
+ *
+ * A global variable is used to access the database, the function \ref db_open
+ * must be called before any other functions.
+ *
+ * Many of the function that insert or update model will also update the JSON
+ * object to fill the generated row id if any.
+ *
+ * This module logs message with tag `db`.
+ */
+
 #include <stdint.h>
 
 #include <jansson.h>
 
-int
-db_open(const char *);
-
+/**
+ * Open database specified by path.
+ *
+ * \pre path != NULL
+ * \param path path to the database
+ */
 int
-db_job_add(json_t *);
+db_open(const char *path);
+
+/**
+ * Add a new job.
+ *
+ * \pre job != NULL
+ * \param job the job to add
+ * \return 0 on success or -1 on error
+ */
+int
+db_job_add(json_t *job);
 
+/**
+ * Get a list of jobs to perform for this worker.
+ *
+ * The returned list will only contain jobs to perform if their date are
+ * greater or equal to the worker creation date to avoid running jobs that are
+ * older and probably too numerous.
+ *
+ * \pre worker != NULL
+ * \param worker the worker name
+ * \return the JSON model or NULL on failure
+ */
 json_t *
-db_job_todo(const char *);
+db_job_todo(const char *worker);
 
+/**
+ * Return the whole list of jobs for the given project.
+ *
+ * \pre project != NULL
+ * \param project the project name
+ * \return the JSON model or NULL on failure
+ */
 json_t *
-db_job_list(const char *);
+db_job_list(const char *project);
 
+/**
+ * Add a new job result.
+ *
+ * \pre result != NULL
+ * \param result the job result to add
+ * \return 0 on success or -1 on error
+ */
 int
-db_jobresult_add(json_t *);
+db_jobresult_add(json_t *result);
 
+/**
+ * Get a list of job results by job id.
+ *
+ * This will contain every job done by all workers for this job id.
+ *
+ * \param job_id the job id
+ * \return the JSON model or NULL on failure
+ */
 json_t *
-db_jobresult_list_by_job(intmax_t);
+db_jobresult_list_by_job(intmax_t job_id);
 
+/**
+ * Get a list of job results for this job id but only the more recent per each
+ * worker.
+ *
+ * This function is handy if you want to retrieve all last jobs realized by all
+ * workers.
+ *
+ * \param job_id the job id
+ * \return the JSON model or NULL on failure
+ */
 json_t *
 db_jobresult_list_by_job_group(intmax_t);
 
+/**
+ * Get a list of job results realized by this worker.
+ *
+ * \pre worker != NULL
+ * \param worker the worker name
+ * \return the JSON model or NULL on failure
+ */
 json_t *
 db_jobresult_list_by_worker(const char *);
 
+/**
+ * Add or update the given project.
+ *
+ * In case of update, all fields must be present anyway.
+ *
+ * \pre project != NULL
+ * \param project the project to update
+ * \return 0 on success or -1 on error
+ */
 int
-db_project_save(json_t *);
+db_project_save(json_t *project);
 
+/**
+ * Get a list of all projects.
+ *
+ * \return the JSON model or NULL on failure
+ */
 json_t *
 db_project_list(void);
 
+/**
+ * Find a project by name.
+ *
+ * \pre name != NULL
+ * \param name the project name
+ * \return the JSON model or NULL on failure
+ */
 json_t *
-db_project_find(const char *);
+db_project_find(const char *name);
 
+/**
+ * Add or update the given worker.
+ *
+ * In case of update, all fields must be present anyway.
+ *
+ * \pre worker != NULL
+ * \param worker the worker to update
+ * \return 0 on success or -1 on error
+ */
 int
-db_worker_save(json_t *);
+db_worker_save(json_t *worker);
 
+/**
+ * Get a list of all workers.
+ *
+ * \return the JSON model or NULL on failure
+ */
 json_t *
 db_worker_list(void);
 
+/**
+ * Find a worker by name.
+ *
+ * \pre name != NULL
+ * \param name the worker name
+ * \return the JSON model or NULL on failure
+ */
 json_t *
 db_worker_find(const char *);
 
+/**
+ * Close database and associated resources.
+ */
 void
 db_finish(void);
 
--- a/scid/http.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/http.h	Thu Aug 04 16:47:10 2022 +0200
@@ -19,9 +19,24 @@
 #ifndef SCI_HTTP_H
 #define SCI_HTTP_H
 
+/**
+ * \file http.h
+ * \brief HTTP parser.
+ *
+ * This module uses kcgi to run as simple CGI or FastCGI.
+ *
+ * This module logs message with tag `http`.
+ */
+
+/**
+ * Loop forever for new requests as a FastCGI program.
+ */
 void
 http_fcgi_run(void);
 
+/**
+ * Run one time as simple CGI program.
+ */
 void
 http_cgi_run(void);
 
--- a/scid/main.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/main.c	Thu Aug 04 16:47:10 2022 +0200
@@ -31,24 +31,11 @@
 	exit(1);
 }
 
-static void
-run_cgi(void)
-{
-	http_cgi_run();
-}
-
-static void
-run_fcgi(void)
-{
-	for (;;)
-		http_fcgi_run();
-}
-
 int
 main(int argc, char **argv)
 {
 	int ch;
-	void (*run)(void) = &(run_cgi);
+	void (*run)(void) = &(http_cgi_run);
 
 	while ((ch = getopt(argc, argv, "d:ft:")) != -1) {
 		switch (ch) {
@@ -56,7 +43,7 @@
 			util_strlcpy(scid.dbpath, optarg, sizeof (scid.dbpath));
 			break;
 		case 'f':
-			run = &(run_fcgi);
+			run = &(http_fcgi_run);
 			break;
 		case 't':
 			util_strlcpy(scid.themedir, optarg, sizeof (scid.themedir));
--- a/scid/page-api-jobresults.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-jobresults.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-jobresults.c -- /api/v?/jobresults route
+ * page-api-jobresults.c -- page /api/v?/jobresults route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
--- a/scid/page-api-jobresults.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-jobresults.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-jobresults.h -- /api/v?/jobresults route
+ * page-api-jobresults.h -- page /api/v?/jobresults route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -19,9 +19,22 @@
 #ifndef SCI_PAGE_API_JOBRESULTS_H
 #define SCI_PAGE_API_JOBRESULTS_H
 
+/**
+ * \file page-api-jobresults.h
+ * \brief Page /api/v?/jobresults route.
+ *
+ * This module logs message with tag `page-api-jobresults`.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_api_v1_jobresults(struct kreq *);
+page_api_v1_jobresults(struct kreq *r);
 
 #endif /* !SCI_PAGE_API_JOBRESULTS_H */
--- a/scid/page-api-jobs.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-jobs.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-jobs.c -- /api/v?/jobs route
+ * page-api-jobs.c -- page /api/v?/jobs route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
--- a/scid/page-api-jobs.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-jobs.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-jobs.h -- /api/v?/jobs route
+ * page-api-jobs.h -- page /api/v?/jobs route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -19,9 +19,22 @@
 #ifndef SCI_PAGE_API_JOBS_H
 #define SCI_PAGE_API_JOBS_H
 
+/**
+ * \file page-api-jobs.h
+ * \brief Page /api/v?/jobs route.
+ *
+ * This module logs message with tag `page-api-jobs`.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_api_v1_jobs(struct kreq *);
+page_api_v1_jobs(struct kreq *r);
 
 #endif /* !SCI_PAGE_API_JOBS_H */
--- a/scid/page-api-projects.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-projects.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-projects.c -- /api/v?/projects route
+ * page-api-projects.c -- page /api/v?/projects route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
--- a/scid/page-api-projects.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-projects.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-projects.h -- /api/v?/projects route
+ * page-api-projects.h -- page /api/v?/projects route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -19,9 +19,22 @@
 #ifndef SCI_PAGE_API_PROJECTS_H
 #define SCI_PAGE_API_PROJECTS_H
 
+/**
+ * \file page-api-projects.h
+ * \brief Page /api/v?/projects route.
+ *
+ * This module logs message with tag `page-api-projects`.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_api_v1_projects(struct kreq *);
+page_api_v1_projects(struct kreq *r);
 
 #endif /* !SCI_PAGE_API_PROJECTS_H */
--- a/scid/page-api-todo.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-todo.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-todo.c -- /api/v?/todo route
+ * page-api-todo.c -- page /api/v?/todo route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
--- a/scid/page-api-todo.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-todo.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-todo.h -- /api/v?/todo route
+ * page-api-todo.h -- page /api/v?/todo route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -19,9 +19,22 @@
 #ifndef SCI_PAGE_API_TODO_H
 #define SCI_PAGE_API_TODO_H
 
+/**
+ * \file page-api-todo.h
+ * \brief Page /api/v?/todo route.
+ *
+ * This module logs message with tag `page-api-todo`.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_api_v1_todo(struct kreq *);
+page_api_v1_todo(struct kreq *r);
 
 #endif /* !SCI_PAGE_API_TODO_H */
--- a/scid/page-api-workers.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-workers.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-api-workers.c -- /api/v?/workers route
+ * page-api-workers.c -- page /api/v?/workers route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
--- a/scid/page-api-workers.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-api-workers.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,9 +1,40 @@
+/*
+ * page-api-workers.h -- page /api/v?/workers route
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
 #ifndef SCI_PAGE_API_WORKERS_H
 #define SCI_PAGE_API_WORKERS_H
 
+/**
+ * \file page-api-workers.h
+ * \brief Page /api/v?/workers route.
+ *
+ * This module logs message with tag `page-api-workers`.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_api_v1_workers(struct kreq *);
+page_api_v1_workers(struct kreq *r);
 
 #endif /* !SCI_PAGE_API_WORKERS_H */
--- a/scid/page-api.h	Thu Aug 04 14:59:33 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-#ifndef SCID_PAGE_API_H
-#define SCID_PAGE_API_H
-
-
-
-#endif /* !SCID_PAGE_API_H */
--- a/scid/page-index.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-index.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-index.c -- page /
+ * page-index.c -- page / route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -20,7 +20,6 @@
 #include <string.h>
 
 #include "db.h"
-#include "log.h"
 #include "pageutil.h"
 #include "scid.h"
 #include "theme.h"
--- a/scid/page-index.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-index.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,5 +1,5 @@
 /*
- * page-index.h -- / route
+ * page-index.h -- page / route
  *
  * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
  *
@@ -19,9 +19,22 @@
 #ifndef SCI_PAGE_INDEX_H
 #define SCI_PAGE_INDEX_H
 
+/**
+ * \file page-index.h
+ * \brief Page / route.
+ *
+ * This module does not log messages.
+ */
+
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_index(struct kreq *);
+page_index(struct kreq *r);
 
 #endif /* !SCI_PAGE_INDEX_H */
--- a/scid/page-static.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-static.c	Thu Aug 04 16:47:10 2022 +0200
@@ -2,11 +2,11 @@
  * page-static.c -- page /static
  *
  * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
- * 
+ *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
--- a/scid/page-static.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/page-static.h	Thu Aug 04 16:47:10 2022 +0200
@@ -2,11 +2,11 @@
  * page-static.h -- page /static
  *
  * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr>
- * 
+ *
  * Permission to use, copy, modify, and/or distribute this software for any
  * purpose with or without fee is hereby granted, provided that the above
  * copyright notice and this permission notice appear in all copies.
- * 
+ *
  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
@@ -16,12 +16,25 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef IMGUP_PAGE_STATIC_H
-#define IMGUP_PAGE_STATIC_H
+#ifndef SCID_PAGE_STATIC_H
+#define SCID_PAGE_STATIC_H
+
+/**
+ * \file page-static.h
+ * \brief Page /static
+ *
+ * This module does not log messages.
+ */
 
 struct kreq;
 
+/**
+ * Run the page.
+ *
+ * \pre r != NULL
+ * \param r the request
+ */
 void
-page_static(struct kreq *);
+page_static(struct kreq *r);
 
-#endif /* !IMGUP_PAGE_STATIC_H */
+#endif /* !SCID_PAGE_STATIC_H */
--- a/scid/pageutil.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/pageutil.h	Thu Aug 04 16:47:10 2022 +0200
@@ -19,6 +19,16 @@
 #ifndef SCI_PAGE_H
 #define SCI_PAGE_H
 
+/**
+ * \file pageutil.h
+ * \brief Page utilities.
+ *
+ * This module provides convenient helpers to generate pages, it will call the
+ * current theme so make sure to initialize before using this module.
+ *
+ * This module does not log messages.
+ */
+
 #include <sys/types.h>
 #include <stdarg.h>
 #include <stdint.h>
@@ -27,13 +37,38 @@
 
 #include <jansson.h>
 
-void
-pageutil_render(struct kreq *, enum khttp, enum kmime, const char *);
-
+/**
+ * Render a page using the given content.
+ *
+ * \pre r != NULL
+ * \param r the request
+ * \param status the HTTP status code
+ * \param mime the MIME type
+ * \param body the body content (maybe NULL)
+ */
 void
-pageutil_status(struct kreq *, enum khttp);
+pageutil_render(struct kreq *r, enum khttp status, enum kmime mime, const char *body);
+
+/**
+ * Render a status code page (e.g. 400).
+ *
+ * \pre r != NULL
+ * \param r the request
+ * \param status the HTTP status code
+ */
+void
+pageutil_status(struct kreq *r, enum khttp status);
 
+/**
+ * Render a page with a JSON as MIME type.
+ *
+ * \pre r != NULL
+ * \param r != NULL
+ * \param status the HTTP status code
+ * \param doc the JSON document (may be NULL)
+ * \warning The document will be destroyed using json_decref.
+ */
 void
-pageutil_json(struct kreq *req, enum khttp status, json_t *doc);
+pageutil_json(struct kreq *r, enum khttp status, json_t *doc);
 
 #endif /* !SCI_PAGE_H */
--- a/scid/scid.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/scid.c	Thu Aug 04 16:47:10 2022 +0200
@@ -1,3 +1,21 @@
+/*
+ * scid.c -- main scid file and configuration
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
 #include <assert.h>
 #include <stdio.h>
 
@@ -13,11 +31,11 @@
 void
 scid_init(void)
 {
-	log_open("scid");
-	log_info("opening database %s", scid.dbpath);
+	log_open("scid: version " VERSION);
+	log_info("scid: opening database %s", scid.dbpath);
 
 	if (db_open(scid.dbpath) < 0)
-		log_die("abort: unable to open database");
+		log_die("scid: abort: unable to open database");
 
 	theme_open(scid.themedir);
 }
--- a/scid/scid.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/scid.h	Thu Aug 04 16:47:10 2022 +0200
@@ -1,16 +1,51 @@
+/*
+ * scid.h -- main scid file and configuration
+ *
+ * Copyright (c) 2021-2022 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
 #ifndef SCID_H
 #define SCID_H
 
+/**
+ * \file scid.h
+ * \brief Main scid file and configuration.
+ *
+ * This module logs message with tag `scid`.
+ */
+
 #include <limits.h>
 
+/**
+ * \struct scid
+ * \brief Main scid structure for configuration.
+ */
 extern struct scid {
-	char themedir[PATH_MAX];
-	char dbpath[PATH_MAX];
-} scid;
+	char themedir[PATH_MAX];        /*!< Path to the theme. */
+	char dbpath[PATH_MAX];          /*!< Path to the database file. */
+} scid /*! Global variable. */;
 
+/**
+ * Initialize scid.
+ */
 void
 scid_init(void);
 
+/**
+ * Cleanup scid.
+ */
 void
 scid_finish(void);
 
--- a/scid/theme.c	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/theme.c	Thu Aug 04 16:47:10 2022 +0200
@@ -26,15 +26,13 @@
 #include <mustache.h>
 
 #include "log.h"
-#include "scid.h"
 #include "theme.h"
 #include "util.h"
 
 #define SIGNATURE DUK_HIDDEN_SYMBOL("File")
 
-static struct {
-	duk_context *ctx;
-} theme;
+static duk_context *context;
+static char base[PATH_MAX];
 
 /* {{{ mustache support */
 
@@ -289,24 +287,24 @@
 	size_t outsz = 0;
 	FILE *fp;
 
-	duk_get_global_string(theme.ctx, function);
+	duk_get_global_string(context, function);
 
-	if (duk_is_callable(theme.ctx, -1)) {
+	if (duk_is_callable(context, -1)) {
 		fp   = util_open_memstream(&out, &outsz);
 		dump = util_json_dump(json);
 
-		duk_push_pointer(theme.ctx, fp);
-		duk_push_string(theme.ctx, dump);
-		duk_json_decode(theme.ctx, -1);
+		duk_push_pointer(context, fp);
+		duk_push_string(context, dump);
+		duk_json_decode(context, -1);
 
-		if (duk_pcall(theme.ctx, 2) != 0)
-			log_warn("theme: %s", duk_safe_to_string(theme.ctx, -1));
+		if (duk_pcall(context, 2) != 0)
+			log_warn("theme: %s", duk_safe_to_string(context, -1));
 
-		duk_pop(theme.ctx);
+		duk_pop(context);
 		fclose(fp);
 		free(dump);
 	} else
-		duk_pop(theme.ctx);
+		duk_pop(context);
 
 	if (!out)
 		out = util_strdup("");
@@ -322,19 +320,20 @@
 	const char *path;
 	char *data;
 
-	theme.ctx = duk_create_heap_default();
+	util_strlcpy(base, directory, sizeof (base));
+	context = duk_create_heap_default();
 	path = theme_path("theme.js");
 
 	if (!(data = util_read(path)))
 		log_warn("theme: %s: %s", path, strerror(errno));
 	else {
-		if (duk_peval_string(theme.ctx, data) != 0)
-			log_warn("theme: %s", duk_safe_to_string(theme.ctx, -1));
+		if (duk_peval_string(context, data) != 0)
+			log_warn("theme: %s", duk_safe_to_string(context, -1));
 
-		duk_pop(theme.ctx);
-		duk_push_object(theme.ctx);
-		duk_put_function_list(theme.ctx, -1, functions);
-		duk_put_global_string(theme.ctx, "Scid");
+		duk_pop(context);
+		duk_push_object(context);
+		duk_put_function_list(context, -1, functions);
+		duk_put_global_string(context, "Scid");
 		free(data);
 	}
 }
@@ -347,7 +346,7 @@
 	/* Build path to the template file. */
 	static _Thread_local char path[PATH_MAX];
 
-	snprintf(path, sizeof (path), "%s/%s", scid.themedir, filename);
+	snprintf(path, sizeof (path), "%s/%s", base, filename);
 
 	return path;
 }
@@ -377,5 +376,5 @@
 void
 theme_free(void)
 {
-	duk_destroy_heap(theme.ctx);
+	duk_destroy_heap(context);
 }
--- a/scid/theme.h	Thu Aug 04 14:59:33 2022 +0200
+++ b/scid/theme.h	Thu Aug 04 16:47:10 2022 +0200
@@ -34,6 +34,10 @@
  * This module uses a global Javascript context to perform rendering, you must
  * call theme_open before doing anything else. Once done, use theme_finish to
  * cleanup resources.
+ *
+ * This module logs message with tag `theme`.
+ *
+ * [mustache]: https://mustache.github.io/
  */
 
 #include <jansson.h>
@@ -91,7 +95,6 @@
  * \return a newly allocated rendered string
  * \note You must free the return value
  */
-[[nodiscard]]
 char *
 theme_page_index(const json_t *doc);
 
@@ -110,7 +113,6 @@
  * \return a newly allocated rendered string
  * \note You must free the return value
  */
-[[nodiscard]]
 char *
 theme_page_status(enum khttp status);