changeset 27:dae2de19ca5d

misc: switch to JSON everywhere
author David Demelier <markand@malikania.fr>
date Wed, 03 Aug 2022 15:18:09 +0200
parents 7e10cace67a3
children 4c16bb25e4f1
files Makefile config.mk examples/nsnake.sh lib/apic.c lib/apic.h lib/db.c lib/db.h lib/types.c lib/types.h scictl/scictl.c scid/crud.c scid/crud.h scid/db.c scid/db.h scid/http.c scid/page-api-jobresults.c scid/page-api-jobs.c scid/page-api-projects.c scid/page-api-todo.c scid/page-api-workers.c scid/page-api.h scid/page-index.c sciworkerd/sciworkerd.c sciworkerd/sciworkerd.h sciworkerd/task.c sql/init.sql sql/job-add.sql sql/job-list.sql sql/job-todo.sql sql/jobresult-add.sql sql/jobresult-list-by-job-group.sql sql/jobresult-list-by-job.sql sql/jobresult-list-by-worker.sql sql/project-list.sql sql/worker-list.sql themes/bulma/pages/index.html
diffstat 36 files changed, 952 insertions(+), 1640 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Tue Aug 02 13:24:13 2022 +0200
+++ b/Makefile	Wed Aug 03 15:18:09 2022 +0200
@@ -23,11 +23,9 @@
 LIBSCI=                 lib/libsci.a
 LIBSCI_SRCS=            extern/libsqlite/sqlite3.c \
                         lib/apic.c \
-                        lib/db.c \
                         lib/log.c \
                         lib/strlcpy.c \
                         lib/strtonum.c \
-                        lib/types.c \
                         lib/util.c
 LIBSCI_OBJS=            ${LIBSCI_SRCS:.c=.o}
 LIBSCI_DEPS=            ${LIBSCI_SRCS:.c=.d}
@@ -55,9 +53,10 @@
 
 SCID=                   scid/scid
 SCID_SRCS=              extern/libmustache4c/mustache.c \
+                        scid/crud.c \
+                        scid/db.c \
                         scid/http.c \
                         scid/main.c \
-                        scid/scid.c \
                         scid/page-api-jobresults.c \
                         scid/page-api-jobs.c \
                         scid/page-api-projects.c \
@@ -65,7 +64,8 @@
                         scid/page-api-workers.c \
                         scid/page-index.c \
                         scid/page-static.c \
-                        scid/page.c
+                        scid/page.c \
+                        scid/scid.c
 SCID_OBJS=              ${SCID_SRCS:.c=.o}
 SCID_DEPS=              ${SCID_SRCS:.c=.d}
 
--- a/config.mk	Tue Aug 02 13:24:13 2022 +0200
+++ b/config.mk	Wed Aug 03 15:18:09 2022 +0200
@@ -1,5 +1,5 @@
 CC=             cc
-CFLAGS=         -g -O0 -Wall -Wextra -fsanitize=address
+CFLAGS=         -g -O0 -Wall -Wextra -fsanitize=address -Wno-format-truncation
 #CFLAGS=         -Wall -Wextra -fsanitize=address,undefined -g -O0
 #LDFLAGS=        -fsanitize=address,undefined
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/examples/nsnake.sh	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+set -e
+
+readonly wrkdir="$(mktemp -d /tmp/nsnake-XXXXXX)"
+readonly repo="http://hg.malikania.fr/nsnake"
+
+trap "cleanup" INT TERM EXIT
+
+cleanup()
+{
+	rm -rf $wrkdir
+}
+
+if [ "$#" -ne 1 ]; then
+	echo "abort: $(basename $0) revision" 1>&2
+	exit 1
+fi
+
+echo "=> Cloning repository $repo (revision $1) into $wrkdir"
+hg clone -r "$1" "$repo" "$wrkdir"
+cd "$wrkdir"
+
+echo "=> Building"
+make
--- a/lib/apic.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/lib/apic.c	Wed Aug 03 15:18:09 2022 +0200
@@ -7,7 +7,6 @@
 #include <curl/curl.h>
 
 #include "apic.h"
-#include "types.h"
 #include "util.h"
 
 struct curlpack {
@@ -16,13 +15,6 @@
 	struct curl_slist *headers;
 };
 
-struct converter {
-	void *data;
-	size_t datasz;
-	ssize_t (*unpack)(void *, size_t, json_t *);
-	json_t *(*pack)(const void *, size_t);
-};
-
 struct apiconf apiconf = {
 	.baseurl = "http://127.0.0.1"
 };
@@ -39,7 +31,7 @@
 static inline char *
 create_url(const char *fmt, va_list args)
 {
-	static _Thread_local char ret[256];
+	static _Thread_local char ret[1024];
 	char page[128];
 	va_list ap;
 
@@ -93,14 +85,14 @@
 	return pack;
 }
 
-static int
+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;
-	int ret = -1;
 	struct curlpack curl;
 
 	memset(req, 0, sizeof (*req));
@@ -119,10 +111,8 @@
 
 		if (req->status != 200)
 			snprintf(req->error, sizeof (req->error), "HTTP returned %ld", req->status);
-		if (response[0] && !(req->doc = json_loads(response, 0, &error)))
+		if (response[0] && !(doc = json_loads(response, 0, &error)))
 			snprintf(req->error, sizeof (req->error), "JSON parse error: %s", error.text);
-		else
-			ret = 0;
 	}
 
 	curl_easy_cleanup(curl.curl);
@@ -130,112 +120,59 @@
 
 	free(response);
 
-	return ret;
+	return doc;
 }
 
-static ssize_t
-get(struct apic *req, const struct converter *cv, const char *fmt, ...)
+static json_t *
+get(struct apic *req, const char *fmt, ...)
 {
 	va_list ap;
-	ssize_t ret;
+	json_t *ret;
 
 	va_start(ap, fmt);
 	ret = perform(req, NULL, fmt, ap);
 	va_end(ap);
 
-	if (ret < 0)
-		return -1;
-	if (!req->doc || (!json_is_object(req->doc) && !json_is_array(req->doc)))
-		return snprintf(req->error, sizeof (req->error), "invalid JSON document received"), -1;
-	if ((ret = cv->unpack(cv->data, cv->datasz, req->doc)) < 0)
-		return snprintf(req->error, sizeof (req->error), "%s", strerror(errno));
+	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, const struct converter *cv, const char *fmt, ...)
+create(struct apic *req, json_t *doc, const char *fmt, ...)
 {
 	va_list ap;
-	int ret;
-	json_t *doc;
+	json_t *ret;
 	char *body;
 
 	memset(req, 0, sizeof (*req));
 
-	if (!(doc = cv->pack(cv->data, cv->datasz)))
-		return snprintf(req->error, sizeof (req->error), "%s", strerror(errno));
 	if (!(body = json_dumps(doc, JSON_COMPACT))) {
 		json_decref(doc);
-		return snprintf(req->error, sizeof (req->error), "%s", strerror(errno));
+		return snprintf(req->error, sizeof (req->error), "%s", strerror(errno)), -1;
 	}
 
 	va_start(ap, fmt);
 	ret = perform(req, body, fmt, ap);
 	va_end(ap);
 
-	json_decref(doc);
+	/* TODO: update id. */
+	(void)ret;
+
 	free(body);
 
-	return ret;
-}
-
-static json_t *
-wrap_job_to(const void *data, size_t datasz)
-{
-	return job_to(data, datasz);
-}
-
-static ssize_t
-wrap_job_from(void *data, size_t datasz, json_t *doc)
-{
-	return job_from(data, datasz, doc);
-}
-
-static json_t *
-wrap_jobresult_to(const void *data, size_t datasz)
-{
-	return jobresult_to(data, datasz);
+	return 0;
 }
 
-static ssize_t
-wrap_project_from(void *data, size_t datasz, json_t *doc)
-{
-	return project_from(data, datasz, doc);
-}
-
-static json_t *
-wrap_project_to(const void *data, size_t datasz)
-{
-	return project_to(data, datasz);
-}
-
-static ssize_t
-wrap_worker_from(void *data, size_t datasz, json_t *doc)
-{
-	return worker_from(data, datasz, doc);
-}
-
-static json_t *
-wrap_worker_to(const void *data, size_t datasz)
-{
-	return worker_to(data, datasz);
-}
-
-static ssize_t
-wrap_jobresult_from(void *data, size_t datasz, json_t *doc)
-{
-	return jobresult_from(data, datasz, doc);
-}
-
-int
+json_t *
 apic_get(struct apic *req, const char *fmt, ...)
 {
 	assert(req);
 	assert(fmt);
 
 	va_list ap;
-	int ret;
+	json_t *ret;
 
 	va_start(ap, fmt);
 	ret = perform(req, NULL, fmt, ap);
@@ -244,14 +181,14 @@
 	return ret;
 }
 
-int
+json_t *
 apic_post(struct apic *req, const json_t *doc, const char *fmt, ...)
 {
 	assert(req);
 	assert(fmt);
 
 	va_list ap;
-	int ret;
+	json_t *ret;
 	char *body;
 
 	if (!(body = json_dumps(doc, JSON_COMPACT)))
@@ -267,167 +204,80 @@
 }
 
 int
-apic_job_add(struct apic *req, struct job *job)
+apic_job_add(struct apic *req, json_t *job)
 {
 	assert(req);
 	assert(job);
 
-	const struct converter cv = {
-		.data = job,
-		.datasz = 1,
-		.pack = wrap_job_to,
-		.unpack = wrap_job_from
-	};
-
-	return create(req, &cv, "api/v1/jobs");
+	return create(req, job, "api/v1/jobs");
 }
 
-ssize_t
-apic_job_todo(struct apic *req, struct job *jobs, size_t jobsz, const char *worker_name)
+json_t *
+apic_job_todo(struct apic *req, const char *worker_name)
 {
 	assert(req);
-	assert(jobs);
+	assert(worker_name);
 
-	struct converter cv = {
-		.data = jobs,
-		.datasz = jobsz,
-		.unpack = wrap_job_from
-	};
-
-	return get(req, &cv, "api/v1/todo/%s", worker_name);
+	return get(req, "api/v1/todo/%s", worker_name);
 }
 
 int
-apic_jobresult_add(struct apic *req, struct jobresult *result)
+apic_jobresult_add(struct apic *req, json_t *res)
 {
 	assert(req);
-	assert(result);
+	assert(res);
 
-	struct converter cv = {
-		.data = result,
-		.datasz = 1,
-		.pack = wrap_jobresult_to,
-		.unpack = wrap_jobresult_from
-	};
-
-	return create(req, &cv, "api/v1/jobresults");
+	return create(req, res, "api/v1/jobresults");
 }
 
 int
-apic_project_save(struct apic *req, struct project *project)
+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);
-	assert(project);
+
+	return get(req, "api/v1/projects");
+}
 
-	struct converter cv = {
-		.data = project,
-		.datasz = 1,
-		.pack = wrap_project_to,
-		.unpack = wrap_project_from
-	};
+json_t *
+apic_project_find(struct apic *req, const char *name)
+{
+	assert(req);
+	assert(name);
 
-	return create(req, &cv, "api/v1/projects");
+	return get(req, "api/v1/projects/%s", name);
 }
 
 int
-apic_project_update(struct apic *req, struct project *project)
-{
-	assert(req);
-	assert(project);
-
-	struct converter cv = {
-		.data = project,
-		.datasz = 1,
-		.pack = wrap_project_to,
-		.unpack = wrap_project_from
-	};
-
-	return create(req, &cv, "api/v1/projects");
-}
-
-ssize_t
-apic_project_list(struct apic *req, struct project *projects, size_t projectsz)
-{
-	assert(req);
-	assert(projects);
-
-	struct converter cv = {
-		.data = projects,
-		.datasz = projectsz,
-		.unpack = wrap_project_from
-	};
-
-	return get(req, &cv, "api/v1/projects");
-}
-
-int
-apic_project_find(struct apic *req, struct project *project, const char *name)
-{
-	assert(req);
-	assert(project);
-
-	struct converter cv = {
-		.data = project,
-		.datasz = 1,
-		.unpack = wrap_project_from
-	};
-
-	return get(req, &cv, "api/v1/projects/%s", name);
-}
-
-int
-apic_worker_save(struct apic *req, struct worker *wk)
+apic_worker_save(struct apic *req, json_t *wk)
 {
 	assert(req);
 	assert(wk);
 
-	struct converter cv = {
-		.data = wk,
-		.datasz = 1,
-		.pack = wrap_worker_to
-	};
-
-	return create(req, &cv, "api/v1/workers");
+	return create(req, wk, "api/v1/workers");
 }
 
-ssize_t
-apic_worker_list(struct apic *req, struct worker *wk, size_t wksz)
-{
-	assert(req);
-	assert(wk);
-	assert(wksz);
-
-	struct converter cv = {
-		.data = wk,
-		.datasz = wksz,
-		.unpack = wrap_worker_from
-	};
-
-	return get(req, &cv, "api/v1/workers");
-}
-
-int
-apic_worker_find(struct apic *req, struct worker *wk, const char *name)
-{
-	assert(req);
-	assert(wk);
-
-	struct converter cv = {
-		.data = wk,
-		.datasz = 1,
-		.unpack = wrap_worker_from
-	};
-
-	return get(req, &cv, "api/v1/workers/%s", name);
-}
-
-void
-apic_finish(struct apic *req)
+json_t *
+apic_worker_list(struct apic *req)
 {
 	assert(req);
 
-	if (req->doc)
-		json_decref(req->doc);
+	return get(req, "api/v1/workers");
+}
 
-	memset(req, 0, sizeof (*req));
+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	Tue Aug 02 13:24:13 2022 +0200
+++ b/lib/apic.h	Wed Aug 03 15:18:09 2022 +0200
@@ -1,75 +1,56 @@
 #ifndef SCI_APIC_H
 #define SCI_APIC_H
 
-#include <sys/types.h>
-
 #include <jansson.h>
 
-#include "config.h"
-
 #define APIC_ERR_MAX 128
 
-struct job;
-struct jobresult;
-struct project;
-struct worker;
-
 struct apic {
-	json_t *doc;
 	char error[APIC_ERR_MAX];
 	long status;
 };
 
 extern struct apiconf {
-	char baseurl[SCI_URL_MAX];
+	char baseurl[512];
 } apiconf;
 
 /* Generic HTTP commands using JSON. */
 
 /* Perform GET request. */
-int
+json_t *
 apic_get(struct apic *, const char *, ...);
 
 /* Perform POST request with JSON body. */
-int
+json_t *
 apic_post(struct apic *, const json_t *, const char *, ...);
 
-/*
- * Commands to fetch, create, delete or update data.
- *
- * Any of the following commands need to keep apic structure alive as long as
- * data objects are being used because they reference JSON values directly from
- * the HTTP response.
- */
+/* --- */
 
 int
-apic_job_add(struct apic *, struct job *);
+apic_job_add(struct apic *, json_t *);
 
-ssize_t
-apic_job_todo(struct apic *, struct job *, size_t, const char *);
+json_t *
+apic_job_todo(struct apic *, const char *);
 
 int
-apic_jobresult_add(struct apic *, struct jobresult *);
+apic_jobresult_add(struct apic *, json_t *);
 
 int
-apic_project_save(struct apic *, struct project *);
+apic_project_save(struct apic *, json_t *);
 
-ssize_t
-apic_project_list(struct apic *, struct project *, size_t);
+json_t *
+apic_project_list(struct apic *);
 
-int
-apic_project_find(struct apic *, struct project *, const char *);
+json_t *
+apic_project_find(struct apic *, const char *);
 
 int
-apic_worker_save(struct apic *, struct worker *);
-
-ssize_t
-apic_worker_list(struct apic *, struct worker *, size_t);
+apic_worker_save(struct apic *, json_t *);
 
-int
-apic_worker_find(struct apic *, struct worker *, const char *);
+json_t *
+apic_worker_list(struct apic *);
 
-void
-apic_finish(struct apic *);
+json_t *
+apic_worker_find(struct apic *, const char *);
 
 #endif /* !SCI_APIC_H */
--- a/lib/db.c	Tue Aug 02 13:24:13 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,378 +0,0 @@
-/*
- * db.c -- scid database access
- *
- * Copyright (c) 2021 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 <stdlib.h>
-#include <string.h>
-
-#include <sqlite3.h>
-
-#include <utlist.h>
-
-#include "db.h"
-#include "log.h"
-#include "types.h"
-#include "util.h"
-
-#include "sql/init.h"
-#include "sql/job-add.h"
-#include "sql/job-list.h"
-#include "sql/job-todo.h"
-#include "sql/jobresult-add.h"
-#include "sql/jobresult-list-by-job.h"
-#include "sql/jobresult-list-by-job-group.h"
-#include "sql/jobresult-list-by-worker.h"
-#include "sql/project-find.h"
-#include "sql/project-list.h"
-#include "sql/project-save.h"
-#include "sql/worker-find.h"
-#include "sql/worker-list.h"
-#include "sql/worker-save.h"
-
-#define CHAR(v) (const char *)(v)
-
-static sqlite3 *db;
-
-struct list {
-	void (*unpack)(sqlite3_stmt *, void *);
-	void *data;
-	size_t datasz;
-	size_t elemwidth;
-};
-
-static void
-project_unpacker(sqlite3_stmt *stmt, void *data)
-{
-	struct project *project = data;
-
-	project->name = util_strdup(CHAR(sqlite3_column_text(stmt, 0)));
-	project->desc = util_strdup(CHAR(sqlite3_column_text(stmt, 1)));
-	project->url = util_strdup(CHAR(sqlite3_column_text(stmt, 2)));
-	project->script = util_strdup(CHAR(sqlite3_column_text(stmt, 3)));
-}
-
-static void
-worker_unpacker(sqlite3_stmt *stmt, void *data)
-{
-	struct worker *w = data;
-
-	w->name = util_strdup(CHAR(sqlite3_column_text(stmt, 0)));
-	w->desc = util_strdup(CHAR(sqlite3_column_text(stmt, 1)));
-}
-
-static void
-job_unpacker(sqlite3_stmt *stmt, void *data)
-{
-	struct job *job = data;
-
-	job->id = sqlite3_column_int64(stmt, 0);
-	job->tag = util_strdup(CHAR(sqlite3_column_text(stmt, 1)));
-	job->project_name = util_strdup(CHAR(sqlite3_column_text(stmt, 2)));
-}
-
-static inline char *
-dup(sqlite3_stmt *stmt, int col)
-{
-	const unsigned char *s;
-
-	if ((s = sqlite3_column_text(stmt, col)))
-		return util_strdup(CHAR(s));
-
-	return util_strdup("");
-}
-
-static void
-jobresult_unpacker(sqlite3_stmt *stmt, void *data)
-{
-	struct jobresult *r = data;
-
-	r->id = sqlite3_column_int64(stmt, 0);
-	r->job_id = sqlite3_column_int64(stmt, 1);
-	r->worker_name = util_strdup(CHAR(sqlite3_column_text(stmt, 2)));
-	r->exitcode = sqlite3_column_int(stmt, 3);
-	r->log = dup(stmt, 4);
-}
-
-static void
-vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap)
-{
-	for (int index = 1; *fmt; ++fmt) {
-		switch (*fmt) {
-		case 'i':
-			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
-			break;
-		case 'j':
-			sqlite3_bind_int64(stmt, index++, va_arg(ap, intmax_t));
-			break;
-		case 's':
-			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
-			break;
-		case 'z':
-			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
-			break;
-		default:
-			break;
-		}
-	}
-}
-
-static int
-insert(const char *sql, const char *fmt, ...)
-{
-	assert(sql);
-	assert(fmt);
-
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int ret = -1;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, fmt);
-	vbind(stmt, fmt, ap);
-	va_end(ap);
-
-	if (sqlite3_step(stmt) != SQLITE_DONE)
-		log_warn("db: %s", sqlite3_errmsg(db));
-	else
-		ret = sqlite3_last_insert_rowid(db);
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-static ssize_t
-list(struct list *sel, const char *sql, const char *args, ...)
-{
-	sqlite3_stmt *stmt = NULL;
-	va_list ap;
-	int step;
-	ssize_t ret = -1;
-	size_t tot = 0;
-
-	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
-		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
-
-	va_start(ap, args);
-	vbind(stmt, args, ap);
-	va_end(ap);
-
-	while (tot < sel->datasz && (step = sqlite3_step(stmt)) == SQLITE_ROW)
-		sel->unpack(stmt, (unsigned char *)sel->data + (tot++ * sel->elemwidth));
-
-	if (step == SQLITE_OK || step == SQLITE_DONE || step == SQLITE_ROW)
-		ret = tot;
-	else
-		memset(sel->data, 0, sel->datasz * sel->elemwidth);
-
-	sqlite3_finalize(stmt);
-
-	return ret;
-}
-
-int
-db_open(const char *path)
-{
-	assert(path);
-
-	if (sqlite3_open(path, &db) != SQLITE_OK)
-		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
-
-	/* Wait for 30 seconds to lock the database. */
-	sqlite3_busy_timeout(db, 30000);
-
-	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
-		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
-
-	return 0;
-}
-
-int
-db_project_save(struct project *p)
-{
-	return insert(CHAR(sql_project_save), "ssss", p->name, p->desc,
-	    p->url, p->script) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_project_list(struct project *projects, size_t projectsz)
-{
-	struct list sel = {
-		.unpack = project_unpacker,
-		.data = projects,
-		.datasz = projectsz,
-		.elemwidth = sizeof (*projects),
-	};
-
-	return list(&sel, CHAR(sql_project_list), "z", projectsz);
-}
-
-int
-db_project_find(struct project *project, const char *name)
-{
-	struct list sel = {
-		.unpack = project_unpacker,
-		.data = project,
-		.datasz = 1,
-		.elemwidth = sizeof (*project),
-	};
-
-	return list(&sel, CHAR(sql_project_find), "s", name) == 1 ? 0 : -1;
-}
-
-int
-db_worker_save(struct worker *wk)
-{
-	assert(wk);
-
-	return insert(CHAR(sql_worker_save), "ss", wk->name, wk->desc) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_worker_list(struct worker *wk, size_t wksz)
-{
-	assert(wk);
-
-	struct list sel = {
-		.unpack = worker_unpacker,
-		.data = wk,
-		.datasz = wksz,
-		.elemwidth = sizeof (*wk),
-	};
-
-	return list(&sel, CHAR(sql_worker_list), "z", wksz);
-}
-
-int
-db_worker_find(struct worker *wk, const char *name)
-{
-	struct list sel = {
-		.unpack = worker_unpacker,
-		.data = wk,
-		.datasz = 1,
-		.elemwidth = sizeof (*wk),
-	};
-
-	return list(&sel, CHAR(sql_worker_find), "s", name) == 1 ? 0 : -1;
-}
-
-int
-db_job_add(struct job *job)
-{
-	assert(job);
-
-	return (job->id = insert(CHAR(sql_job_add),
-	    "ss", job->tag, job->project_name)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_job_todo(struct job *jobs, size_t jobsz, const char *worker)
-{
-	assert(jobs);
-
-	struct list sel = {
-		.unpack = job_unpacker,
-		.data = jobs,
-		.datasz = jobsz,
-		.elemwidth = sizeof (*jobs)
-	};
-
-	return list(&sel, CHAR(sql_job_todo), "ssz", worker, worker, jobsz);
-}
-
-ssize_t
-db_job_list(struct job *jobs, size_t jobsz, const char *project)
-{
-	assert(jobs);
-	assert(project);
-
-	struct list sel = {
-		.unpack = job_unpacker,
-		.data = jobs,
-		.datasz = jobsz,
-		.elemwidth = sizeof (*jobs)
-	};
-
-	return list(&sel, CHAR(sql_job_list), "sz", project, jobsz);
-}
-
-int
-db_jobresult_add(struct jobresult *r)
-{
-	assert(r);
-
-	return (r->id = insert(CHAR(sql_jobresult_add), "jsis", r->job_id,
-	    r->worker_name, r->exitcode, r->log)) < 0 ? -1 : 0;
-}
-
-ssize_t
-db_jobresult_list_by_job(struct jobresult *r, size_t rsz, intmax_t job_id)
-{
-	assert(r);
-
-	struct list sel = {
-		.unpack = jobresult_unpacker,
-		.data = r,
-		.datasz = rsz,
-		.elemwidth = sizeof (*r)
-	};
-
-	return list(&sel, CHAR(sql_jobresult_list_by_job), "jz", job_id, rsz);
-}
-
-ssize_t
-db_jobresult_list_by_job_group(struct jobresult *r, size_t rsz, intmax_t job_id)
-{
-	assert(r);
-
-	struct list sel = {
-		.unpack = jobresult_unpacker,
-		.data = r,
-		.datasz = rsz,
-		.elemwidth = sizeof (*r)
-	};
-
-	return list(&sel, CHAR(sql_jobresult_list_by_job_group), "jz", job_id, rsz);
-}
-
-ssize_t
-db_jobresult_list_by_worker(struct jobresult *r, size_t rsz, const char *worker)
-{
-	assert(r);
-	assert(worker);
-
-	struct list sel = {
-		.unpack = jobresult_unpacker,
-		.data = r,
-		.datasz = rsz,
-		.elemwidth = sizeof (*r)
-	};
-
-	return list(&sel, CHAR(sql_jobresult_list_by_worker), "sz", worker, rsz);
-}
-
-void
-db_finish(void)
-{
-	if (db) {
-		sqlite3_close(db);
-		db = NULL;
-	}
-}
--- a/lib/db.h	Tue Aug 02 13:24:13 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/*
- * db.h -- scid database access
- *
- * Copyright (c) 2021 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_DB_H
-#define SCI_DB_H
-
-#include <sys/types.h>
-#include <stddef.h>
-#include <stdint.h>
-
-struct project;
-struct worker;
-struct job;
-struct jobresult;
-
-int
-db_open(const char *);
-
-int
-db_job_add(struct job *);
-
-ssize_t
-db_job_todo(struct job *, size_t, const char *);
-
-ssize_t
-db_job_list(struct job *, size_t, const char *);
-
-int
-db_jobresult_add(struct jobresult *);
-
-ssize_t
-db_jobresult_list_by_job(struct jobresult *, size_t, intmax_t);
-
-ssize_t
-db_jobresult_list_by_job_group(struct jobresult *, size_t, intmax_t);
-
-ssize_t
-db_jobresult_list_by_worker(struct jobresult *, size_t, const char *);
-
-int
-db_project_save(struct project *);
-
-ssize_t
-db_project_list(struct project *, size_t);
-
-int
-db_project_find(struct project *, const char *);
-
-int
-db_worker_save(struct worker *);
-
-ssize_t
-db_worker_list(struct worker *, size_t);
-
-int
-db_worker_find(struct worker *, const char *);
-
-void
-db_finish(void);
-
-#endif /* !SCI_DB_H */
--- a/lib/types.c	Tue Aug 02 13:24:13 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,285 +0,0 @@
-/*
- * types.c -- type definitions and conversions
- *
- * Copyright (c) 2021 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 "types.h"
-#include "util.h"
-
-typedef json_t * (*packer)(const void *);
-typedef int      (*unpacker)(void *, json_t *);
-
-static inline json_t *
-job_packer(const struct job *job)
-{
-	return json_pack("{si ss ss}",
-	    "id",               job->id,
-	    "project_name",     job->project_name,
-	    "tag",              job->tag
-	);
-}
-
-static inline int
-job_unpacker(struct job *job, json_t *doc)
-{
-	const int ret = json_unpack(doc, "{si ss ss}",
-	    "id",               &job->id,
-	    "project_name",     &job->project_name,
-	    "tag",              &job->tag
-	);
-
-	if (ret == 0) {
-		job->project_name = util_strdup(job->project_name);
-		job->tag = util_strdup(job->tag);
-	}
-
-	return ret;
-}
-
-static inline json_t *
-jobresult_packer(const struct jobresult *res)
-{
-	return json_pack("{si si ss si ss}",
-	    "id",               res->id,
-	    "job_id",           res->job_id,
-	    "worker_name",      res->worker_name,
-	    "exitcode",         res->exitcode,
-	    "log",              res->log
-	);
-}
-
-static inline int
-jobresult_unpacker(struct jobresult *res, json_t *doc)
-{
-	const int ret = json_unpack(doc, "{si si ss si ss}",
-	    "id",               &res->id,
-	    "job_id",           &res->job_id,
-	    "worker_name",      &res->worker_name,
-	    "exitcode",         &res->exitcode,
-	    "log",              &res->log
-	);
-
-	if (ret == 0) {
-		res->worker_name = util_strdup(res->worker_name);
-		res->log = util_strdup(res->log);
-	}
-
-	return ret;
-}
-
-static inline json_t *
-worker_packer(const struct worker *w)
-{
-	return json_pack("{ss ss}",
-	    "name",             w->name,
-	    "desc",             w->desc
-	);
-}
-
-static inline int
-worker_unpacker(struct worker *w, json_t *doc)
-{
-	const int ret = json_unpack(doc, "{ss ss}",
-	    "name",             &w->name,
-	    "desc",             &w->desc
-	);
-
-	if (ret == 0) {
-		w->name = util_strdup(w->name);
-		w->desc = util_strdup(w->desc);
-	}
-
-	return ret;
-}
-
-static inline json_t *
-project_packer(struct project *p)
-{
-	return json_pack("{ss ss ss ss}",
-	    "name",             p->name,
-	    "desc",             p->desc,
-	    "url",              p->url,
-	    "script",           p->script
-	);
-}
-
-static inline int
-project_unpacker(struct project *p, json_t *doc)
-{
-	const int ret = json_unpack(doc, "{ss ss ss ss}",
-	    "name",             &p->name,
-	    "desc",             &p->desc,
-	    "url",              &p->url,
-	    "script",           &p->script
-	);
-
-	if (ret == 0) {
-		p->name = util_strdup(p->name);
-		p->desc = util_strdup(p->desc);
-		p->url = util_strdup(p->url);
-		p->script = util_strdup(p->script);
-	}
-
-	return ret;
-}
-
-static json_t *
-to(const void *array, size_t arraysz, size_t width, packer fn)
-{
-	json_t *doc;
-
-	if (arraysz == 1)
-		doc = fn(array);
-	else {
-		doc = json_array();
-
-		for (size_t i = 0; i < arraysz; ++i)
-			json_array_append(doc, fn((char *)array + (i * width)));
-	}
-
-	return doc;
-}
-
-static ssize_t
-from(void *array, size_t arraysz, size_t width, json_t *doc, unpacker fn)
-{
-	json_t *val;
-	size_t i, tot = 0;
-
-	if (json_is_array(doc)) {
-		json_array_foreach(doc, i, val) {
-			if (tot >= arraysz)
-				return errno = ERANGE, -1;
-			if (fn((char *)array + (tot++ * width), val) < 0)
-				return errno = EILSEQ, -1;
-		}
-	} else if (json_is_object(doc)) {
-		tot = 1;
-
-		if (fn(array, doc) < 0)
-			return errno = EILSEQ, -1;
-	} else
-		return errno = EINVAL, -1;
-
-	return tot;
-}
-
-json_t *
-job_to(const struct job *jobs, size_t jobsz)
-{
-	assert(jobs);
-
-	return to(jobs, jobsz, sizeof (*jobs), (packer)job_packer);
-}
-
-ssize_t
-job_from(struct job *jobs, size_t jobsz, json_t *doc)
-{
-	assert(jobs);
-	assert(doc);
-
-	return from(jobs, jobsz, sizeof (*jobs), doc, (unpacker)job_unpacker);
-}
-
-void
-job_finish(struct job *job)
-{
-	assert(job);
-
-	free(job->tag);
-}
-
-json_t *
-jobresult_to(const struct jobresult *res, size_t resz)
-{
-	assert(res);
-
-	return to(res, resz, sizeof (*res), (packer)jobresult_packer);
-}
-
-ssize_t
-jobresult_from(struct jobresult *res, size_t resz, json_t *doc)
-{
-	assert(res);
-	assert(doc);
-
-	return from(res, resz, sizeof (*res), doc, (unpacker)jobresult_unpacker);
-}
-
-void
-jobresult_finish(struct jobresult *res)
-{
-	assert(res);
-
-	free(res->log);
-}
-
-json_t *
-worker_to(const struct worker *w, size_t wsz)
-{
-	assert(w);
-
-	return to(w, wsz, sizeof (*w), (packer)worker_packer);
-}
-
-ssize_t
-worker_from(struct worker *w, size_t wsz, json_t *doc)
-{
-	assert(w);
-	assert(doc);
-
-	return from(w, wsz, sizeof (*w), doc, (unpacker)worker_unpacker);
-}
-
-void
-worker_finish(struct worker *w)
-{
-	assert(w);
-
-	free(w->name);
-	free(w->desc);
-}
-
-json_t *
-project_to(const struct project *proj, size_t projsz)
-{
-	assert(proj);
-
-	return to(proj, projsz, sizeof (*proj), (packer)project_packer);
-}
-
-ssize_t
-project_from(struct project *proj, size_t projsz, json_t *doc)
-{
-	assert(proj);
-	assert(doc);
-
-	return from(proj, projsz, sizeof (*proj), doc, (unpacker)project_unpacker);
-}
-
-void
-project_finish(struct project *proj)
-{
-	assert(proj);
-
-	free(proj->name);
-	free(proj->desc);
-	free(proj->url);
-	free(proj->script);
-}
--- a/lib/types.h	Tue Aug 02 13:24:13 2022 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,98 +0,0 @@
-/*
- * types.h -- type definitions and conversions
- *
- * Copyright (c) 2021 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_TYPES_H
-#define SCI_TYPES_H
-
-#include <sys/types.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#include <jansson.h>
-
-struct job {
-	intmax_t id;
-	char *project_name;
-	char *tag;
-};
-
-struct jobresult {
-	intmax_t id;
-	intmax_t job_id;
-	char *worker_name;
-	int exitcode;
-	char *log;
-};
-
-struct worker {
-	char *name;
-	char *desc;
-};
-
-struct project {
-	char *name;
-	char *desc;
-	char *url;
-	char *script;
-};
-
-/* job */
-
-json_t *
-job_to(const struct job *, size_t);
-
-ssize_t
-job_from(struct job *, size_t, json_t *);
-
-void
-job_finish(struct job *);
-
-/* jobresult */
-
-json_t *
-jobresult_to(const struct jobresult *, size_t);
-
-ssize_t
-jobresult_from(struct jobresult *, size_t, json_t *);
-
-void
-jobresult_finish(struct jobresult *);
-
-/* worker */
-
-json_t *
-worker_to(const struct worker *, size_t);
-
-ssize_t
-worker_from(struct worker *, size_t, json_t *);
-
-void
-worker_finish(struct worker *);
-
-/* project */
-
-json_t *
-project_to(const struct project *, size_t);
-
-ssize_t
-project_from(struct project *, size_t, json_t *);
-
-void
-project_finish(struct project *);
-
-#endif /* !SCI_TYPES_H */
--- a/scictl/scictl.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scictl/scictl.c	Wed Aug 03 15:18:09 2022 +0200
@@ -16,22 +16,24 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <errno.h>
 #include <limits.h>
-#include <errno.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 
+#include <utlist.h>
+
 #include "apic.h"
 #include "config.h"
-#include "types.h"
 #include "util.h"
 
 static void
 usage(void)
 {
-	fprintf(stderr, "usage: %s [-u baseurl] command [args...]\n", getprogname());
+	fprintf(stderr, "usage: scictl [-u baseurl] command [args...]\n");
 	exit(1);
 }
 
@@ -40,7 +42,6 @@
 {
 	fprintf(stderr, "usage: scictl job-add project tag\n");
 	fprintf(stderr, "       scictl job-todo worker\n");
-	fprintf(stderr, "       scictl jobresult-add id worker exitcode console\n");
 	fprintf(stderr, "       scictl project-add name desc url script\n");
 	fprintf(stderr, "       scictl project-info name\n");
 	fprintf(stderr, "       scictl project-list\n");
@@ -50,13 +51,6 @@
 	exit(0);
 }
 
-static inline void
-replace(char **str, const char *new)
-{
-	free(*str);
-	*str = util_strdup(new);
-}
-
 long long
 toint(const char *s)
 {
@@ -98,97 +92,103 @@
 	return console;
 }
 
+static inline intmax_t
+get_int(json_t *obj, const char *key)
+{
+	json_t *val;
+
+	if ((val = json_object_get(obj, key)) && json_is_integer(val))
+		return json_integer_value(val);
+
+	return 0;
+}
+
+static inline const char *
+get_string(json_t *obj, const char *key)
+{
+	json_t *val;
+
+	if ((val = json_object_get(obj, key)) && json_is_string(val))
+		return json_string_value(val);
+
+	return "";
+}
+
 static void
 cmd_job_add(int argc, char **argv)
 {
-	struct job job = {0};
 	struct apic req;
+	json_t *obj;
 
 	if (argc < 3)
 		usage();
 
-	job.project_name = util_strdup(argv[1]);
-	job.tag = util_strdup(argv[2]);
+	obj = json_pack("{ss ss}",
+		"project_name", argv[1],
+		"tag",          argv[2]
+	);
 
-	if (apic_job_add(&req, &job) < 0)
+	if (apic_job_add(&req, obj) < 0)
 		util_die("abort: %s\n", req.error);
 
-	apic_finish(&req);
-	job_finish(&job);
+	json_decref(obj);
 }
 
 static void
 cmd_job_todo(int argc, char **argv)
 {
-	struct job jobs[SCI_JOB_LIST_MAX] = {0};
-	size_t jobsz;
 	struct apic req;
+	json_t *array, *obj;
+	size_t i;
 
 	if (argc < 2)
 		usage();
 
-	if ((jobsz = apic_job_todo(&req, jobs, UTIL_SIZE(jobs), argv[1])))
+	if ((array = apic_job_todo(&req, argv[1])))
 		util_die("abort: %s\n", req.error);
 
-	for (size_t i = 0; i < jobsz; ++i) {
-		printf("%-16s%jd\n", "id:", jobs[i].id);
-		printf("%-16s%s\n", "tag:", jobs[i].tag);
-		printf("%-16s%s\n", "project:", jobs[i].project_name);
+	json_array_foreach(array, i, obj) {
+		printf("%-16s%jd\n", "id:", get_int(obj, "id"));
+		printf("%-16s%s\n", "tag:", get_string(obj, "tag"));
+		printf("%-16s%s\n", "project:", get_string(obj, "project_name"));
 
-		if (i + 1 < jobsz)
+		if (i + 1 < json_array_size(array))
 			printf("\n");
-
-		job_finish(&jobs[i]);
 	}
 
-	apic_finish(&req);
-}
-
-static void
-cmd_jobresult_add(int argc, char **argv)
-{
-	struct jobresult res = {0};
-	struct apic req;
-
-	if (argc < 5)
-		usage();
-
-	res.job_id = toint(argv[1]);
-	res.worker_name = util_strdup(argv[2]);
-	res.exitcode = toint(argv[3]);
-	res.log = readfile(argv[4]);
-
-	if (apic_jobresult_add(&req, &res) < 0)
-		util_die("abort: unable to add job result: %s\n", req.error);
-
-	apic_finish(&req);
-	jobresult_finish(&res);
+	json_decref(array);
 }
 
 static void
 cmd_project_add(int argc, char **argv)
 {
-	struct project pc = {0};
 	struct apic req;
+	char *script;
+	json_t *obj;
 
 	if (argc < 5)
 		usage();
 
-	pc.name = util_strdup(argv[1]);
-	pc.desc = util_strdup(argv[2]);
-	pc.url = util_strdup(argv[3]);
-	pc.script = readfile(argv[4]);
+	script = readfile(argv[4]);
+	obj = json_pack("{ss ss ss ss}",
+		"name",         argv[1],
+		"desc",         argv[2],
+		"url",          argv[3],
+		"script",	script
+	);
 
-	if (apic_project_save(&req, &pc) < 0)
+	if (apic_project_save(&req, obj) < 0)
 		util_die("abort: unable to create project: %s\n", req.error);
 
-	apic_finish(&req);
-	project_finish(&pc);
+	json_decref(obj);
 }
 
 static void
 cmd_project_update(int argc, char **argv)
 {
+	(void)argc;
+	(void)argv;
+#if 0
 	struct project pc;
 	struct apic req;
 
@@ -212,27 +212,27 @@
 
 	apic_finish(&req);
 	project_finish(&pc);
+#endif
 }
 
 static void
 cmd_project_info(int argc, char **argv)
 {
-	struct project project = {0};
 	struct apic req;
+	json_t *obj;
 
 	if (argc < 2)
 		usage();
-	if (apic_project_find(&req, &project, argv[1]) < 0)
+	if (!(obj = apic_project_find(&req, argv[1])))
 		util_die("abort: unable to find project: %s\n", req.error);
 
-	printf("%-16s%s\n", "name:", project.name);
-	printf("%-16s%s\n", "desc:", project.desc);
-	printf("%-16s%s\n", "url:", project.url);
+	printf("%-16s%s\n", "name:", get_string(obj, "name"));
+	printf("%-16s%s\n", "desc:", get_string(obj, "desc"));
+	printf("%-16s%s\n", "url:", get_string(obj, "url"));
 	printf("\n");
-	printf("%s", project.script);
+	printf("%s", get_string(obj, "script"));
 
-	apic_finish(&req);
-	project_finish(&project);
+	json_decref(obj);
 }
 
 static void
@@ -241,44 +241,43 @@
 	(void)argc;
 	(void)argv;
 
-	struct project projects[SCI_PROJECT_MAX] = {0};
 	struct apic req;
-	ssize_t projectsz;
+	json_t *array, *obj;
+	size_t i;
 
-	if ((projectsz = apic_project_list(&req, projects, UTIL_SIZE(projects))) < 0)
+	if (!(array = apic_project_list(&req)))
 		util_die("abort: unable to list projects: %s\n", req.error);
 
-	for (ssize_t i = 0; i < projectsz; ++i) {
-		printf("%-16s%s\n", "name:", projects[i].name);
-		printf("%-16s%s\n", "desc:", projects[i].desc);
-		printf("%-16s%s\n", "url:", projects[i].url);
+	json_array_foreach(array, i, obj) {
+		printf("%-16s%s\n", "name:", get_string(obj, "name"));
+		printf("%-16s%s\n", "desc:", get_string(obj, "desc"));
+		printf("%-16s%s\n", "url:", get_string(obj, "url"));
 
-		if (i + 1 < projectsz)
+		if (i + 1 < json_array_size(array))
 			printf("\n");
-
-		project_finish(&projects[i]);
 	}
 
-	apic_finish(&req);
+	json_decref(array);
 }
 
 static void
 cmd_worker_add(int argc, char **argv)
 {
-	struct worker wk = {0};
 	struct apic req;
+	json_t *obj;
 
 	if (argc < 3)
 		usage();
 
-	wk.name = util_strdup(argv[1]);
-	wk.desc = util_strdup(argv[2]);
+	obj = json_pack("{ss ss}",
+		"name", argv[1],
+		"desc", argv[2]
+	);
 
-	if (apic_worker_save(&req, &wk) < 0)
+	if (apic_worker_save(&req, obj) < 0)
 		util_die("abort: unable to save worker: %s\n", req.error);
 
-	worker_finish(&wk);
-	apic_finish(&req);
+	json_decref(obj);
 }
 
 static void
@@ -287,24 +286,22 @@
 	(void)argc;
 	(void)argv;
 
-	struct worker wk[SCI_WORKER_MAX];
 	struct apic req;
-	ssize_t wksz;
+	json_t *array, *obj;
+	size_t i;
 
-	if ((wksz = apic_worker_list(&req, wk, UTIL_SIZE(wk))) < 0)
+	if (!(array = apic_worker_list(&req)))
 		util_die("abort: unable to list worker: %s\n", req.error);
 
-	for (ssize_t i = 0; i < wksz; ++i) {
-		printf("%-16s%s\n", "name:", wk[i].name);
-		printf("%-16s%s\n", "desc:", wk[i].desc);
+	json_array_foreach(array, i, obj) {
+		printf("%-16s%s\n", "name:", get_string(obj, "name"));
+		printf("%-16s%s\n", "desc:", get_string(obj, "desc"));
 
-		if (i + 1 < wksz)
+		if (i + 1 < json_array_size(array))
 			printf("\n");
-
-		worker_finish(&wk[i]);
 	}
 
-	apic_finish(&req);
+	json_decref(array);
 }
 
 static struct {
@@ -313,7 +310,6 @@
 } commands[] = {
 	{ "job-add",            cmd_job_add             },
 	{ "job-todo",           cmd_job_todo            },
-	{ "jobresult-add",      cmd_jobresult_add       },
 	{ "project-add",        cmd_project_add         },
 	{ "project-info",       cmd_project_info        },
 	{ "project-list",       cmd_project_list        },
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/crud.c	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,79 @@
+/*
+ * crud.c -- convenient helpers for page-api-*
+ *
+ * Copyright (c) 2021 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 "log.h"
+#include "page.h"
+
+static int
+save(struct kreq *r, int (*saver)(json_t *), const char *topic)
+{
+	json_t *doc;
+	json_error_t err;
+	int ret = -1;
+
+	if (!(doc = json_loads(r->fields[0].val, 0, &err)))
+		log_warn("%s: invalid JSON input: %s", topic, err.text);
+	else {
+		if (saver(doc) < 0)
+			log_warn("%s: database insertion failed", topic);
+		else
+			ret = 0;
+
+		json_decref(doc);
+	}
+
+	return ret;
+}
+
+void
+crud_insert(struct kreq *r, int (*saver)(json_t *), const char *topic)
+{
+	if (r->fieldsz < 1)
+		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
+	else if (save(r, saver, topic) < 0)
+		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
+	else {
+		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(r);
+		khttp_free(r);
+	}
+}
+
+void
+crud_list(struct kreq *r, json_t *doc)
+{
+	char *str;
+
+	if (!doc)
+		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
+	else {
+		if (!(str = json_dumps(doc, JSON_COMPACT)))
+			page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
+		else {
+			khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
+			khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+			khttp_body(r);
+			khttp_printf(r, "%s", str);
+			khttp_free(r);
+			json_decref(doc);
+		}
+
+		free(str);
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/crud.h	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,32 @@
+/*
+ * crud.h -- convenient helpers for page-api-*
+ *
+ * Copyright (c) 2021 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_CRUD_H
+#define SCID_CRUD_H
+
+#include <jansson.h>
+
+struct kreq;
+
+void
+crud_insert(struct kreq *, int (*)(json_t *), const char *);
+
+void
+crud_list(struct kreq *, json_t *);
+
+#endif /* !SCID_CRUD_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/db.c	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,413 @@
+/*
+ * db.c -- scid database access
+ *
+ * Copyright (c) 2021 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 <stdlib.h>
+#include <string.h>
+
+#include <sqlite3.h>
+
+#include <utlist.h>
+
+#include "db.h"
+#include "log.h"
+#include "util.h"
+
+#include "sql/init.h"
+#include "sql/job-add.h"
+#include "sql/job-list.h"
+#include "sql/job-todo.h"
+#include "sql/jobresult-add.h"
+#include "sql/jobresult-list-by-job.h"
+#include "sql/jobresult-list-by-job-group.h"
+#include "sql/jobresult-list-by-worker.h"
+#include "sql/project-find.h"
+#include "sql/project-list.h"
+#include "sql/project-save.h"
+#include "sql/worker-find.h"
+#include "sql/worker-list.h"
+#include "sql/worker-save.h"
+
+#define CHAR(v) (const char *)(v)
+
+static sqlite3 *db;
+
+static json_t *
+project_packer(sqlite3_stmt *stmt)
+{
+	return json_pack("{ss ss ss ss sI}",
+		"name",         sqlite3_column_text(stmt, 0),
+		"desc",         sqlite3_column_text(stmt, 1),
+		"url",          sqlite3_column_text(stmt, 2),
+		"script",       sqlite3_column_text(stmt, 3),
+		"date",         (json_int_t)sqlite3_column_int64(stmt, 4)
+	);
+}
+
+static int
+project_binder(json_t *doc, sqlite3_stmt *stmt)
+{
+	const char *name, *desc, *url, *script;
+	int ret;
+
+	ret = json_unpack(doc, "{ss ss ss ss}",
+		"name",         &name,
+		"desc",         &desc,
+		"url",          &url,
+		"script",       &script
+	);
+
+	if (ret < 0)
+		return -1;
+
+	sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 2, desc, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 3, url, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 4, script, -1, SQLITE_STATIC);
+
+	return 0;
+}
+
+static json_t *
+worker_packer(sqlite3_stmt *stmt)
+{
+	return json_pack("{ss ss}",
+		"name",         sqlite3_column_text(stmt, 0),
+		"desc",         sqlite3_column_text(stmt, 1)
+	);
+}
+
+static int
+worker_binder(json_t *doc, sqlite3_stmt *stmt)
+{
+	const char *name, *desc;
+	int ret;
+
+	ret = json_unpack(doc, "{ss ss}",
+		"name",         &name,
+		"desc",         &desc
+	);
+
+	if (ret < 0)
+		return -1;
+
+	sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 2, desc, -1, SQLITE_STATIC);
+
+	return 0;
+}
+
+static json_t *
+job_packer(sqlite3_stmt *stmt)
+{
+	return json_pack("{sI ss ss sI}",
+		"id",           (json_int_t)sqlite3_column_int64(stmt, 0),
+		"tag",          sqlite3_column_text(stmt, 1),
+		"project_name", sqlite3_column_text(stmt, 2),
+		"date",         (json_int_t)sqlite3_column_int64(stmt, 3)
+	);
+}
+
+static int
+job_binder(json_t *doc, sqlite3_stmt *stmt)
+{
+	const char *tag, *project_name;
+	int ret;
+
+	ret = json_unpack(doc, "{ss ss}",
+		"tag",          &tag,
+		"project_name", &project_name
+	);
+
+	if (ret)
+		return -1;
+
+	sqlite3_bind_text(stmt, 1, tag, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 2, project_name, -1, SQLITE_STATIC);
+
+	return 0;
+}
+
+static json_t *
+jobresult_packer(sqlite3_stmt *stmt)
+{
+	return json_pack("{sI sI ss ss si si sI}",
+		"id",           (json_int_t)sqlite3_column_int64(stmt, 0),
+		"job_id",       (json_int_t)sqlite3_column_int64(stmt, 1),
+		"worker_name",  sqlite3_column_text(stmt, 2),
+		"console",      sqlite3_column_text(stmt, 3),
+		"exitcode",     sqlite3_column_int(stmt, 4),
+		"sigcode",      sqlite3_column_int(stmt, 5),
+		"date",         (json_int_t)sqlite3_column_int64(stmt, 6)
+	);
+}
+
+static int
+jobresult_binder(json_t *doc, sqlite3_stmt *stmt)
+{
+	json_int_t job_id;
+	int exitcode, sigcode, ret;
+	const char *worker_name, *console;
+
+	ret = json_unpack(doc, "{sI ss ss si si}",
+		"job_id",       &job_id,
+		"worker_name",  &worker_name,
+		"console",      &console,
+		"exitcode",     &exitcode,
+		"sigcode",      &sigcode
+	);
+
+	if (ret < 0)
+		return -1;
+
+	sqlite3_bind_int64(stmt, 1, job_id);
+	sqlite3_bind_text(stmt, 2, worker_name, -1, SQLITE_STATIC);
+	sqlite3_bind_text(stmt, 3, console, -1, SQLITE_STATIC);
+	sqlite3_bind_int(stmt, 4, exitcode);
+	sqlite3_bind_int(stmt, 5, sigcode);
+
+	return 0;
+}
+
+static void
+bindva(sqlite3_stmt *stmt, const char *fmt, va_list ap)
+{
+	for (int index = 1; *fmt; ++fmt) {
+		switch (*fmt) {
+		case 'i':
+			sqlite3_bind_int(stmt, index++, va_arg(ap, int));
+			break;
+		case 'j':
+			sqlite3_bind_int64(stmt, index++, va_arg(ap, intmax_t));
+			break;
+		case 's':
+			sqlite3_bind_text(stmt, index++, va_arg(ap, const char *), -1, SQLITE_STATIC);
+			break;
+		case 'z':
+			sqlite3_bind_int64(stmt, index++, va_arg(ap, size_t));
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+static int
+insert(int (*binder)(json_t *, sqlite3_stmt *), json_t *obj, const char *sql)
+{
+	assert(binder);
+	assert(obj);
+	assert(sql);
+
+	sqlite3_stmt *stmt = NULL;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), -1;
+
+	if (binder(obj, stmt))
+		log_warn("db: unable to bind parameter");
+	else {
+		if (sqlite3_step(stmt) != SQLITE_DONE)
+			log_warn("db: %s", sqlite3_errmsg(db));
+		else
+			json_object_set(obj, "id", json_integer(sqlite3_last_insert_rowid(db)));
+	}
+
+	sqlite3_finalize(stmt);
+
+	return 0;
+}
+
+static json_t *
+listva(json_t * (*unpacker)(sqlite3_stmt *), const char *sql, const char *args, va_list ap)
+{
+	sqlite3_stmt *stmt = NULL;
+	json_t *array, *obj;
+
+	if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK)
+		return log_warn("db: %s", sqlite3_errmsg(db)), NULL;
+
+	bindva(stmt, args, ap);
+	array = json_array();
+
+	while (sqlite3_step(stmt) == SQLITE_ROW)
+		if ((obj = unpacker(stmt)))
+			json_array_append(array, obj);
+
+	sqlite3_finalize(stmt);
+
+	return array;
+}
+
+static json_t *
+list(json_t * (*unpacker)(sqlite3_stmt *), const char *sql, const char *args, ...)
+{
+	va_list ap;
+	json_t *ret;
+
+	va_start(ap, args);
+	ret = listva(unpacker, sql, args, ap);
+	va_end(ap);
+
+	return ret;
+}
+
+/*
+ * Same as list but the array should have only one element that we extract for
+ * convenience.
+ */
+static json_t *
+get(json_t * (*unpacker)(sqlite3_stmt *), const char *sql, const char *args, ...)
+{
+	va_list ap;
+	json_t *ret, *obj = NULL;
+
+	va_start(ap, args);
+	ret = listva(unpacker, sql, args, ap);
+	va_end(ap);
+
+	if (json_array_size(ret) == 1) {
+		obj = json_array_get(ret, 0);
+		json_incref(obj);
+	}
+
+	if (ret)
+		json_decref(ret);
+
+	return obj;
+}
+
+int
+db_open(const char *path)
+{
+	assert(path);
+
+	if (sqlite3_open(path, &db) != SQLITE_OK)
+		return log_warn("db: open error: %s", sqlite3_errmsg(db)), -1;
+
+	/* Wait for 30 seconds to lock the database. */
+	sqlite3_busy_timeout(db, 30000);
+
+	if (sqlite3_exec(db, CHAR(sql_init), NULL, NULL, NULL) != SQLITE_OK)
+		return log_warn("db: initialization error: %s", sqlite3_errmsg(db)), -1;
+
+	return 0;
+}
+
+int
+db_job_add(json_t *job)
+{
+	assert(job);
+
+	return insert(job_binder, job, CHAR(sql_job_add));
+}
+
+json_t *
+db_job_todo(const char *worker)
+{
+	assert(worker);
+
+	return list(job_packer, CHAR(sql_job_todo), "ss", worker, worker);
+}
+
+json_t *
+db_job_list(const char *project)
+{
+	assert(project);
+
+	return list(job_packer, CHAR(sql_job_list), "s", project);
+}
+
+int
+db_jobresult_add(json_t *res)
+{
+	assert(res);
+
+	return insert(jobresult_binder, res, CHAR(sql_jobresult_add));
+}
+
+json_t *
+db_jobresult_list_by_job(intmax_t job_id)
+{
+	return list(jobresult_packer, CHAR(sql_jobresult_list_by_job), "j", job_id);
+}
+
+json_t *
+db_jobresult_list_by_job_group(intmax_t job_id)
+{
+	return list(jobresult_packer, CHAR(sql_jobresult_list_by_job_group), "j", job_id);
+}
+
+json_t *
+db_jobresult_list_by_worker(const char *worker)
+{
+	assert(worker);
+
+	return list(jobresult_packer, CHAR(sql_jobresult_list_by_worker), "s", worker);
+}
+
+int
+db_project_save(json_t *p)
+{
+	assert(p);
+
+	return insert(project_binder, p, CHAR(sql_project_save));
+}
+
+json_t *
+db_project_list(void)
+{
+	return list(project_packer, CHAR(sql_project_list), "");
+}
+
+json_t *
+db_project_find(const char *name)
+{
+	return get(project_packer, CHAR(sql_project_find), "s", name);
+}
+
+int
+db_worker_save(json_t *wk)
+{
+	assert(wk);
+
+	return insert(worker_binder, wk, CHAR(sql_worker_save));
+}
+
+json_t *
+db_worker_list(void)
+{
+	return list(worker_packer, CHAR(sql_worker_list), "");
+}
+
+json_t *
+db_worker_find(const char *name)
+{
+	assert(name);
+
+	return get(worker_packer, CHAR(sql_worker_find), "s", name);
+}
+
+void
+db_finish(void)
+{
+	if (db) {
+		sqlite3_close(db);
+		db = NULL;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/db.h	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,71 @@
+/*
+ * db.h -- scid database access
+ *
+ * Copyright (c) 2021 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_DB_H
+#define SCI_DB_H
+
+#include <stdint.h>
+
+#include <jansson.h>
+
+int
+db_open(const char *);
+
+int
+db_job_add(json_t *);
+
+json_t *
+db_job_todo(const char *);
+
+json_t *
+db_job_list(const char *);
+
+int
+db_jobresult_add(json_t *);
+
+json_t *
+db_jobresult_list_by_job(intmax_t);
+
+json_t *
+db_jobresult_list_by_job_group(intmax_t);
+
+json_t *
+db_jobresult_list_by_worker(const char *);
+
+int
+db_project_save(json_t *);
+
+json_t *
+db_project_list(void);
+
+json_t *
+db_project_find(const char *);
+
+int
+db_worker_save(json_t *);
+
+json_t *
+db_worker_list(void);
+
+json_t *
+db_worker_find(const char *);
+
+void
+db_finish(void);
+
+#endif /* !SCI_DB_H */
--- a/scid/http.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/http.c	Wed Aug 03 15:18:09 2022 +0200
@@ -19,8 +19,9 @@
 #include <sys/types.h>
 #include <assert.h>
 #include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
 #include <stdlib.h>
-#include <stdio.h>
 #include <string.h>
 
 #include <kcgi.h>
--- a/scid/page-api-jobresults.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-api-jobresults.c	Wed Aug 03 15:18:09 2022 +0200
@@ -16,54 +16,16 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/types.h>
 #include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <kcgi.h>
-
-#include "db.h"
-#include "log.h"
-#include "page.h"
-#include "types.h"
-
-static int
-save(const char *json)
-{
-	struct jobresult res = {0};
-	int ret = -1;
 
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (jobresult_from(&res, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if (db_jobresult_add(&res) < 0)
-		log_warn("api/post: database save error");
-	else
-		ret = 0;
-
-	json_decref(doc);
-	jobresult_finish(&res);
-
-	return ret;
-}
+#include "crud.h"
+#include "db.h"
+#include "page.h"
 
 static void
 post(struct kreq *r)
 {
-	if (r->fieldsz < 1)
-		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
-	else if (save(r->fields[0].val) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		khttp_free(r);
-	}
+	crud_insert(r, db_jobresult_add, "page-api-jobresults");
 }
 
 void
--- a/scid/page-api-jobs.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-api-jobs.c	Wed Aug 03 15:18:09 2022 +0200
@@ -16,54 +16,16 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/types.h>
 #include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <kcgi.h>
-
-#include "db.h"
-#include "log.h"
-#include "page.h"
-#include "types.h"
-
-static int
-save(const char *json)
-{
-	struct job job = {0};
-	int ret = -1;
 
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (job_from(&job, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if (db_job_add(&job) < 0)
-		log_warn("api/post: database save error");
-	else
-		ret = 0;
-
-	json_decref(doc);
-	job_finish(&job);
-
-	return ret;
-}
+#include "crud.h"
+#include "db.h"
+#include "page.h"
 
 static void
 post(struct kreq *r)
 {
-	if (r->fieldsz < 1)
-		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
-	else if (save(r->fields[0].val) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		khttp_free(r);
-	}
+	crud_insert(r, db_job_add, "page-api-job");
 }
 
 void
--- a/scid/page-api-projects.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-api-projects.c	Wed Aug 03 15:18:09 2022 +0200
@@ -17,103 +17,11 @@
  */
 
 #include <assert.h>
-
-#include "config.h"
-#include "db.h"
-#include "log.h"
-#include "page-api-projects.h"
-#include "page.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct project *projects, size_t projectsz)
-{
-	struct json_t *doc;
-	char *dump;
-
-	doc = project_to(projects, projectsz);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-static int
-save(const char *json)
-{
-	struct project res = {0};
-	int ret = -1;
-
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (project_from(&res, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if (db_project_save(&res) < 0)
-		log_warn("api/post: database save error");
-	else
-		ret = 0;
-
-	json_decref(doc);
-
-	return ret;
-}
+#include <stdio.h>
 
-static void
-push(struct kreq *r, const struct project *p)
-{
-	struct json_t *json;
-	char *dump;
-
-	json = project_to(p, 1);
-	dump = json_dumps(json, JSON_COMPACT);
-
-	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-	khttp_body(r);
-	khttp_puts(r, dump);
-	khttp_free(r);
-
-	free(dump);
-	json_decref(json);
-}
-
-static void
-get_one(struct kreq *r, const char *name)
-{
-	struct project project = {0};
-
-	if (db_project_find(&project, name) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		push(r, &project);
-		project_finish(&project);
-	}
-}
-
-static void
-get_all(struct kreq *r)
-{
-	struct project projects[SCI_PROJECT_MAX] = {0};
-	ssize_t projectsz;
-
-	if ((projectsz = db_project_list(projects, UTIL_SIZE(projects))) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, projects, projectsz);
-		khttp_free(r);
-	}
-
-	for (ssize_t i = 0; i < projectsz; ++i)
-		project_finish(&projects[i]);
-}
+#include "crud.h"
+#include "db.h"
+#include "page.h"
 
 /*
  * GET /api/v1/projects[/<name>]
@@ -128,9 +36,9 @@
 	char name[128] = {0};
 
 	if (sscanf(r->path, "v1/projects/%127s", name) == 1)
-		get_one(r, name);
+		crud_list(r, db_project_find(name));
 	else
-		get_all(r);
+		crud_list(r, db_project_list());
 }
 
 /*
@@ -142,16 +50,7 @@
 static void
 post(struct kreq *r)
 {
-	if (r->fieldsz < 1)
-		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
-	else if (save(r->fields[0].val) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		khttp_free(r);
-	}
+	crud_insert(r, db_project_save, "page-api-projects");
 }
 
 void
@@ -170,5 +69,4 @@
 		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
 		break;
 	}
-
 }
--- a/scid/page-api-todo.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-api-todo.c	Wed Aug 03 15:18:09 2022 +0200
@@ -16,63 +16,13 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/types.h>
 #include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <string.h>
-
-#include <kcgi.h>
-#include <jansson.h>
-
-#include "config.h"
-#include "db.h"
-#include "log.h"
-#include "page-api-todo.h"
-#include "page.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct job *jobs, size_t jobsz)
-{
-	json_t *doc;
-	char *dump;
-
-	doc = job_to(jobs, jobsz);
-	dump = json_dumps(doc, JSON_COMPACT);
+#include <stdio.h>
 
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-#if 0
-
-static int
-save(const char *json)
-{
-	struct jobresult res = {0};
-	int ret = -1;
-
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (jobresult_from(&res, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if (db_jobresult_add(&res) < 0)
-		log_warn("api/post: database save error");
-	else
-		ret = 0;
-
-	json_decref(doc);
-
-	return ret;
-}
-
-#endif
+#include "crud.h"
+#include "db.h"
+#include "page.h"
+#include "util.h"
 
 /*
  * GET /api/v1/todo/<worker-name>
@@ -83,21 +33,7 @@
 static void
 get(struct kreq *r)
 {
-	struct job jobs[SCI_JOB_LIST_MAX];
-	ssize_t jobsz;
-
-	if ((jobsz = db_job_todo(jobs, UTIL_SIZE(jobs), util_basename(r->path))) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, jobs, jobsz);
-		khttp_free(r);
-	}
-
-	for (ssize_t i = 0; i < jobsz; ++i)
-		job_finish(&jobs[i]);
+	crud_list(r, db_job_todo(util_basename(r->path)));
 }
 
 void
--- a/scid/page-api-workers.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-api-workers.c	Wed Aug 03 15:18:09 2022 +0200
@@ -1,101 +1,27 @@
-#include <assert.h>
-
-#include "config.h"
-#include "db.h"
-#include "log.h"
-#include "page-api-workers.h"
-#include "page.h"
-#include "types.h"
-#include "util.h"
-
-static void
-list(struct kreq *r, const struct worker *workers, size_t workersz)
-{
-	json_t *doc;
-	char *dump;
-
-	doc = worker_to(workers, workersz);
-	dump = json_dumps(doc, JSON_COMPACT);
-
-	khttp_puts(r, dump);
-	free(dump);
-	json_decref(doc);
-}
-
-static int
-save(const char *json)
-{
-	struct worker res = {0};
-	int ret = -1;
-
-	json_t *doc;
-	json_error_t err;
-
-	if (!(doc = json_loads(json, 0, &err)))
-		log_warn("api/post: invalid JSON input: %s", err.text);
-	else if (worker_from(&res, 1, doc) < 0)
-		log_warn("api/post: failed to decode parameters");
-	else if (db_worker_save(&res) < 0)
-		log_warn("api/post: database save error");
-	else
-		ret = 0;
-
-	json_decref(doc);
-
-	return ret;
-}
+/*
+ * page-api-workers.c -- /api/v?/workers route
+ *
+ * Copyright (c) 2021 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.
+ */
 
-static void
-push(struct kreq *r, const struct worker *p)
-{
-	struct json_t *json;
-	char *dump;
-
-	json = worker_to(p, 1);
-	dump = json_dumps(json, JSON_COMPACT);
-
-	khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-	khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-	khttp_body(r);
-	khttp_puts(r, dump);
-	khttp_free(r);
-
-	free(dump);
-	json_decref(json);
-}
-
-static void
-get_one(struct kreq *r, const char *name)
-{
-	struct worker worker = {0};
+#include <assert.h>
+#include <stdio.h>
 
-	if (db_worker_find(&worker, name) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		push(r, &worker);
-		worker_finish(&worker);
-	}
-}
-
-static void
-get_all(struct kreq *r)
-{
-	struct worker workers[SCI_PROJECT_MAX] = {0};
-	ssize_t workersz;
-
-	if ((workersz = db_worker_list(workers, UTIL_SIZE(workers))) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		list(r, workers, workersz);
-		khttp_free(r);
-	}
-
-	for (ssize_t i = 0; i < workersz; ++i)
-		worker_finish(&workers[i]);
-}
+#include "crud.h"
+#include "db.h"
+#include "page.h"
 
 /*
  * GET /api/v1/workers[/<name>]
@@ -110,9 +36,9 @@
 	char name[128] = {0};
 
 	if (sscanf(r->path, "v1/workers/%127s", name) == 1)
-		get_one(r, name);
+		crud_list(r, db_worker_find(name));
 	else
-		get_all(r);
+		crud_list(r, db_worker_list());
 }
 
 /*
@@ -124,16 +50,7 @@
 static void
 post(struct kreq *r)
 {
-	if (r->fieldsz < 1)
-		page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL);
-	else if (save(r->fields[0].val) < 0)
-		page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL);
-	else {
-		khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]);
-		khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-		khttp_body(r);
-		khttp_free(r);
-	}
+	crud_insert(r, db_worker_save, "page-api-workers");
 }
 
 void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/scid/page-api.h	Wed Aug 03 15:18:09 2022 +0200
@@ -0,0 +1,6 @@
+#ifndef SCID_PAGE_API_H
+#define SCID_PAGE_API_H
+
+
+
+#endif /* !SCID_PAGE_API_H */
--- a/scid/page-index.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/scid/page-index.c	Wed Aug 03 15:18:09 2022 +0200
@@ -23,9 +23,10 @@
 #include "config.h"
 #include "db.h"
 #include "page.h"
-#include "types.h"
 #include "util.h"
 
+#if 0
+
 /*
  * Document we create for templatize.
  *
@@ -102,9 +103,14 @@
 	);
 }
 
+#endif
+
+static void
+
 static void
 get(struct kreq *r)
 {
+#if 0
 	(void)r;
 	struct project projects[SCI_PROJECT_MAX] = {0};
 	ssize_t projectsz = 0;
@@ -122,11 +128,21 @@
 	page(r, KHTTP_200, KMIME_TEXT_HTML, "pages/index.html", json_pack("{so}",
 		"projects", array
 	));
+#endif
+	json_t *array;
+
+	if (!(db_project_list())) {
+		log_warn("page-index: %s", db.error);
+		page();
+	} else
+		render(array);
 }
 
 void
 page_index(struct kreq *r)
 {
+	(void)r;
+
 	switch (r->method) {
 	case KMETHOD_GET:
 		get(r);
--- a/sciworkerd/sciworkerd.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/sciworkerd/sciworkerd.c	Wed Aug 03 15:18:09 2022 +0200
@@ -1,6 +1,7 @@
 #include <errno.h>
 #include <poll.h>
 #include <signal.h>
+#include <stdint.h>
 #include <string.h>
 #include <time.h>
 
@@ -10,21 +11,21 @@
 #include "log.h"
 #include "sciworkerd.h"
 #include "task.h"
-#include "types.h"
 #include "util.h"
 
 #define TAG "sciworkerd: "
 
 struct taskentry {
+	intmax_t job_id;
+	char *tag;
+	char *project_name;
 	struct task *task;
-	struct job job;
 	struct taskentry *next;
 };
 
 static struct taskentry *taskpending;
 static struct taskentry *tasks;
 static struct taskentry *taskfinished;
-static struct worker worker;
 static int run = 1;
 
 struct sciworkerd sciworkerd = {
@@ -53,41 +54,57 @@
 }
 
 static inline int
-pending(int id)
+pending(intmax_t job_id)
 {
 	const struct taskentry *iter;
 
 	LL_FOREACH(taskpending, iter)
-		if (iter->job.id == id)
+		if (iter->job_id == job_id)
 			return 1;
 
 	return 0;
 }
 
 static inline void
-queue(const struct job *job)
+queue(intmax_t id, const char *tag, const char *project_name)
 {
 	struct taskentry *tk;
 
-	log_info(TAG "queued job build (%d) for tag %s", job->id, job->tag);
+	log_info(TAG "queued job build (%d) for tag %s", id, tag);
 
 	tk = util_calloc(1, sizeof (*tk));
-	tk->task = task_new(job->tag);
-	memcpy(&tk->job, job, sizeof (*job));
+	tk->job_id = id;
+	tk->tag = util_strdup(tag);
+	tk->project_name = util_strdup(project_name);
+	tk->task = task_new(tag);
 	LL_APPEND(taskpending, tk);
 }
 
 static void
-merge(struct job *jobs, size_t jobsz)
+merge(json_t *jobs)
 {
-	size_t total = 0;
+	json_int_t id;
+	json_t *val;
+	const char *tag, *project_name;
+	size_t total = 0, i;
+	int parse;
 
-	for (size_t i = 0; i < jobsz; ++i) {
-		if (!pending(jobs[i].id)) {
-			queue(&jobs[i]);
+	json_array_foreach(jobs, i, val) {
+		parse = json_unpack(val, "{sI ss ss}",
+			"id",           &id,
+			"tag",          &tag,
+			"project_name", &project_name
+		);
+
+		if (parse < 0) {
+			log_warn(TAG "unable to parse job");
+			continue;
+		}
+
+		if (!pending(id)) {
+			queue(id, tag, project_name);
 			total++;
-		} else
-			job_finish(&jobs[i]);
+		}
 	}
 
 	log_info(TAG "added %zu new pending tasks", total);
@@ -102,38 +119,21 @@
 	static time_t startup;
 	time_t now;
 	struct apic req;
-	struct job todo[SCI_JOB_LIST_MAX];
-	ssize_t todosz;
+	json_t *jobs;
 
 	if (difftime((now = time(NULL)), startup) >= sciworkerd.fetchinterval) {
 		startup = now;
-
 		log_info(TAG "fetching jobs");
 
-		if ((todosz = apic_job_todo(&req, todo, UTIL_SIZE(todo), worker.name)) < 0)
+		if (!(jobs = apic_job_todo(&req, sciworkerd.name)))
 			log_warn(TAG "unable to fetch jobs: %s", req.error);
-		else
-			merge(todo, todosz);
-
-		apic_finish(&req);
+		else {
+			merge(jobs);
+			json_decref(jobs);
+		}
 	}
 }
 
-/*
- * Fetch information about myself.
- */
-static void
-fetch_worker(void)
-{
-	struct apic req;
-
-	if (apic_worker_find(&req, &worker, sciworkerd.name) < 0)
-		log_die(TAG "unable to fetch worker info: %s", req.error);
-
-	log_info("sciworkerd: worker %s (%s)", worker.name, worker.desc);
-	apic_finish(&req);
-}
-
 static inline size_t
 count(const struct taskentry *head)
 {
@@ -154,14 +154,19 @@
 start(struct taskentry *entry)
 {
 	struct apic req;
-	struct project project;
+	json_t *doc;
+	const char *script;
 	pid_t pid;
 	int ret = -1;
 
-	if (apic_project_find(&req, &project, entry->job.project_name) < 0)
+	if (!(doc = apic_project_find(&req, entry->project_name)))
 		return log_warn(TAG "unable to fetch project, dropping task"), -1;
+	if (json_unpack(doc, "{ss}", "script", &script) < 0) {
+		json_decref(doc);
+		return log_warn(TAG "invalid project JSON object"), -1;
+	}
 
-	if (task_setup(entry->task, project.script) < 0)
+	if (task_setup(entry->task, script) < 0)
 		log_warn(TAG "unable to setup script code: %s, dropping task", strerror(errno));
 	else if ((pid = task_start(entry->task)) < 0)
 		log_warn(TAG "unable to spawn task process: %s", strerror(errno));
@@ -170,7 +175,7 @@
 		ret = 0;
 	}
 
-	project_finish(&project);
+	json_decref(doc);
 
 	return ret;
 }
@@ -297,24 +302,24 @@
 static int
 publish(struct taskentry *iter)
 {
-	// TODO: add sigcode.
-	struct taskcode code = task_code(iter->task);
-	struct jobresult res = {
-		.job_id = iter->job.id,
-		.exitcode = code.exitcode,
-		.log = util_strdup(task_console(iter->task)),
-		.worker_name = worker.name
-	};
 	struct apic req;
+	json_t *obj;
 	int ret = 0; 
 
-	if (apic_jobresult_add(&req, &res) < 0) {
+	obj = json_pack("{sI ss ss si si}",
+		"job_id",       iter->job_id,
+		"worker_name",  sciworkerd.name,
+		"console",      task_console(iter->task),
+		"exitcode",     task_code(iter->task).exitcode,
+		"sigcode",      task_code(iter->task).sigcode
+	);
+
+	if (apic_jobresult_add(&req, obj) < 0) {
 		log_warn(TAG "unable to publish task: %s", req.error);
 		ret = -1;
 	}
 
-	apic_finish(&req);
-	jobresult_finish(&res);
+	json_decref(obj);
 
 	return ret;
 }
@@ -339,6 +344,9 @@
 
 	log_open("sigworkerd");
 
+	if (strlen(sciworkerd.name) == 0)
+		log_die(TAG "no worker name defined");
+
 	sigemptyset(&sa.sa_mask);
 	sa.sa_handler = stop;
 
@@ -346,8 +354,6 @@
 
 	if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0)
 		log_die(TAG "sigaction: %s", strerror(errno));
-
-	fetch_worker();
 }
 
 void
--- a/sciworkerd/sciworkerd.h	Tue Aug 02 13:24:13 2022 +0200
+++ b/sciworkerd/sciworkerd.h	Wed Aug 03 15:18:09 2022 +0200
@@ -1,11 +1,12 @@
 #ifndef SCIWORKERD_H
 #define SCIWORKERD_H
 
-#include "config.h"
+#define SCIWORKERD_URL_MAX      512
+#define SCIWORKERD_NAME_MAX     64
 
 extern struct sciworkerd {
-	char url[SCI_URL_MAX];
-	char name[SCI_WORKER_MAX];
+	char url[SCIWORKERD_URL_MAX];
+	char name[SCIWORKERD_NAME_MAX];
 	unsigned int fetchinterval;
 	unsigned int maxjobs;
 	unsigned int timeout;
--- a/sciworkerd/task.c	Tue Aug 02 13:24:13 2022 +0200
+++ b/sciworkerd/task.c	Wed Aug 03 15:18:09 2022 +0200
@@ -13,7 +13,6 @@
 
 #include "log.h"
 #include "task.h"
-#include "types.h"
 #include "util.h"
 
 #define MODE S_IRUSR | S_IWUSR | S_IXUSR
@@ -28,7 +27,6 @@
 	FILE *fp;
 	char *console;
 	size_t consolesz;
-	int scriptfd;
 	char scriptpath[PATH_MAX];
 	time_t startup;
 };
@@ -58,23 +56,26 @@
 	assert(script);
 
 	const size_t len = strlen(script);
+	int fd = -1, ret = -1;
 
 	snprintf(self->scriptpath, sizeof (self->scriptpath), "/tmp/sciworkerd-XXXXXX");
 
-	if ((self->scriptfd = mkstemp(self->scriptpath)) < 0)
+	if ((fd = mkstemp(self->scriptpath)) < 0)
 		goto failed;
-	if (fchmod(self->scriptfd, MODE) < 0)
+	if (fchmod(fd, MODE) < 0)
 		goto failed;
-	if (write(self->scriptfd, script, len) != (ssize_t)len)
+	if (write(fd, script, len) != (ssize_t)len)
 		goto failed;
 
-	return 0;
+	ret = 0;
 
 failed:
-	log_warn("%s", strerror(errno));
-	unlink(self->scriptpath);
+	if (ret == -1)
+		log_warn("%s", strerror(errno));
 
-	return -1;
+	close(fd);
+
+	return ret;
 }
 
 pid_t
--- a/sql/init.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/init.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -31,7 +31,7 @@
 );
 
 CREATE TABLE IF NOT EXISTS job(
-	`tag` TEXT NOT NULL UNIQUE,
+	`tag` TEXT NOT NULL,
 	`project_name` INTEGER NOT NULL REFERENCES project (name),
 	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
@@ -39,8 +39,9 @@
 CREATE TABLE IF NOT EXISTS jobresult(
 	`job_id` INTEGER NOT NULL REFERENCES job (rowid),
 	`worker_name` INTEGER NOT NULL REFERENCES worker (name),
+	`console` TEXT DEFAULT NULL,
 	`exitcode` INTEGER DEFAULT 0,
-	`console` TEXT DEFAULT NULL,
+	`sigcode` INTEGER DEFAULT 0,
 	`date` INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
 );
 
--- a/sql/job-add.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/job-add.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -17,6 +17,6 @@
 --
 
 INSERT INTO job(
-  `tag`,
-  `project_name`
+	`tag`,
+	`project_name`
 ) VALUES (?, ?)
--- a/sql/job-list.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/job-list.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -3,4 +3,3 @@
     FROM `job`
    WHERE `project_name` = ?
 ORDER BY `date` ASC
-   LIMIT ?
--- a/sql/job-todo.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/job-todo.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -36,4 +36,3 @@
                    FROM `worker`
                   WHERE `worker`.`name` = ?
                  )
- LIMIT ?
--- a/sql/jobresult-add.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/jobresult-add.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -19,6 +19,7 @@
 INSERT INTO jobresult(
 	`job_id`,
 	`worker_name`,
+	`console`,
 	`exitcode`,
-	`console`
-) VALUES (?, ?, ?, ?)
+	`sigcode`
+) VALUES (?, ?, ?, ?, ?)
--- a/sql/jobresult-list-by-job-group.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/jobresult-list-by-job-group.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -4,4 +4,3 @@
     FROM `jobresult`
    WHERE `job_id` = ?
 GROUP BY `worker_name`
-   LIMIT ?
--- a/sql/jobresult-list-by-job.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/jobresult-list-by-job.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -3,4 +3,3 @@
      FROM `jobresult`
     WHERE `job_id` = ?
  ORDER BY `job_id` ASC
-    LIMIT ?
--- a/sql/jobresult-list-by-worker.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/jobresult-list-by-worker.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -2,4 +2,3 @@
      FROM `jobresult`
     WHERE `worker_name` = ?
  ORDER BY `job_id` ASC
-    LIMIT ?
--- a/sql/project-list.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/project-list.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -18,4 +18,3 @@
 
 SELECT *
   FROM `project`
- LIMIT ?
--- a/sql/worker-list.sql	Tue Aug 02 13:24:13 2022 +0200
+++ b/sql/worker-list.sql	Wed Aug 03 15:18:09 2022 +0200
@@ -18,4 +18,3 @@
 
 SELECT *
   FROM `worker`
- LIMIT ?
--- a/themes/bulma/pages/index.html	Tue Aug 02 13:24:13 2022 +0200
+++ b/themes/bulma/pages/index.html	Wed Aug 03 15:18:09 2022 +0200
@@ -1,5 +1,5 @@
 <div class="columns" style="flex-wrap: wrap">
-{{#projects}}
+	@@projects@@
 <div class="card">
 	<div class="card-content">
 		<div class="media">
@@ -30,4 +30,5 @@
 			<p>No jobs yet.</p>
 			{{/jobs}}
 		</div>
-{{/projects}}
+	</div>
+</div>