timekeeper

[Abandoned unfinished] CGI web application in C for time tracking. (My first, just a learning project)
Log | Files | Refs | README

commit 2204be2ba369d22740ed26bf4f5e982bd0746ba8
parent 36090048851cdf7e07ac53209d78429cb3a5f157
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Sat,  2 Mar 2024 16:09:51 -0800

Add backend to main page

The time when the timer was started is now stored in the database
and it is displayed in a time element as the duration in HH:MM:SS
format. main.js was updated to work for the new main page aswell.

Diffstat:
Mscripts/main.js | 24+++++++++++++-----------
Mtimekeeper.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
2 files changed, 137 insertions(+), 41 deletions(-)

diff --git a/scripts/main.js b/scripts/main.js @@ -1,16 +1,18 @@ -counter = document.querySelector("#counter") +start = document.querySelector("#start") +let starttime = Math.trunc(Date.parse(start.dateTime) / 1000) -function updatetime() { - counter.textContent = String(Number(counter.textContent) + 1) - if (!document.hidden) - setTimeout(updatetime, 1000) +function sn(n) { + return String(n).padStart(2, "0") } -<!-- -document.addEventListener("visibilitychange", () => { - if (!document.hidden) { - updatetime() -}); ---> +function updatetime() { + let elapsed = Math.trunc(Date.now() / 1000) - starttime + let mn = Math.trunc(elapsed / 60) + let sc = elapsed % 60 + let hr = Math.trunc(mn / 60) + mn = mn % 60 + start.textContent = sn(hr) + ':' + sn(mn) + ':' + sn(sc) + setTimeout(updatetime, 1000) +} updatetime() diff --git a/timekeeper.c b/timekeeper.c @@ -9,8 +9,10 @@ #include <pwd.h> #include <stdarg.h> #include <stdint.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> #include <unistd.h> #include <kcgi.h> @@ -32,6 +34,8 @@ enum Field { KeyHash, /* Authentication hash */ KeyCreate, /* Whether to create a new user in login page */ KeyDelete, /* Delete account in account page */ + KeyStart, + KeyStop, KeyMax }; @@ -46,11 +50,15 @@ enum Page { }; enum StmtID { - StmtInit, + StmtInitUsers, + StmtInitUserdata, StmtAddUser, + StmtAddUserdata, StmtGetUserByHash, StmtGetUserByName, StmtDeleteUser, + StmtStartTime, + StmtGetStartTime, StmtMax }; @@ -108,24 +116,15 @@ enum LoginStatus { int initdb(struct pagedata *pd) { - size_t stmtid; - struct sqlbox_parmset *res; - - if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtInit, 0, NULL, 0))) + if (sqlbox_exec(pd->db, pd->dbid, StmtInitUsers, 0, NULL, 0) || + sqlbox_exec(pd->db, pd->dbid, StmtInitUserdata, 0, NULL, 0)) return 1; - if (!(res = sqlbox_step(pd->db, stmtid))) - err(1, "step"); - if (!sqlbox_finalise(pd->db, stmtid)) - err(1, "finalize"); - return 0; } int adduser(struct pagedata *pd, char *name, char *key) { - size_t stmtid; - struct sqlbox_parmset *res; char *salt, *hash; salt = bcrypt_gensalt(8); @@ -138,11 +137,8 @@ adduser(struct pagedata *pd, char *name, char *key) { .sparm = name, .type = SQLBOX_PARM_STRING } }; - if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtAddUser, Len(p), p, 0))) - return 1; - if (!(res = sqlbox_step(pd->db, stmtid))) - return 1; - if (!sqlbox_finalise(pd->db, stmtid)) + if (sqlbox_exec(pd->db, pd->dbid, StmtAddUser, Len(p), p, 0) != SQLBOX_CODE_OK || + sqlbox_exec(pd->db, pd->dbid, StmtAddUserdata, 1, p, 0) != SQLBOX_CODE_OK) return 1; return 0; } @@ -181,7 +177,7 @@ getuser(struct pagedata *pd, char *field, char *value) err(1, "step"); if (!res->psz && res->code == SQLBOX_CODE_OK) { - kutil_info(NULL, NULL, "%s: %s not found", value, field); + kutil_info(&pd->req, NULL, "getuser: %s: %s not found", value, field); sqlbox_finalise(pd->db, stmtid); return NULL; } @@ -528,27 +524,119 @@ pageaccount(struct pagedata *pd) return khtml_close(&pd->html); } +time_t +setstart(struct pagedata *pd, char *hash, time_t time) +{ + struct sqlbox_parm ps[] = { + { .iparm = time, .type = SQLBOX_PARM_INT }, + { .sparm = hash, .type = SQLBOX_PARM_STRING }, + }; + + return sqlbox_exec(pd->db, pd->dbid, StmtStartTime, + Len(ps), ps, 0) != SQLBOX_CODE_OK; +} + +time_t +getstart(struct pagedata *pd, char *hash) +{ + size_t stmtid; + struct sqlbox_parm p = { + .sparm = hash, .type = SQLBOX_PARM_STRING + }; + struct sqlbox_parmset *r; + time_t time; + + stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtGetStartTime, 1, &p, 0); + if (stmtid == 0) + err(1, "prepare bind"); + + r = sqlbox_step(pd->db, stmtid); + if (!r || r->code != SQLBOX_CODE_OK) + err(1, "step"); + + if (r->psz > 1) + err(1, "too many rows"); + + if (r->psz == 1 && (r->ps[0].type != SQLBOX_PARM_INT && + r->ps[0].type != SQLBOX_PARM_NULL)) + err(1, "invalid type"); + + time = r->psz == 1 ? r->ps[0].iparm : 0; + kutil_info(NULL,NULL,"gettime %lld %zu", time, r->psz); + + if (!sqlbox_finalise(pd->db, stmtid)) + err(1, "finalise"); + return time; +} + enum kcgi_err pagemain(struct pagedata *pd) { enum kcgi_err status; struct user *user; + time_t start; + time_t elapsed; + char datetime[25]; + int hr, mn, sc; + struct tm *tm; user = sitegetlogin(pd); if (!user) return errorpage(pd, KHTTP_401); - if ((status = khttp_head(&pd->req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200])) != KCGI_OK || - (status = khttp_head(&pd->req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML])) != KCGI_OK || - (status = khttp_body(&pd->req))) + if (pd->req.fieldmap[KeyStart] && setstart(pd, user->hash, time(NULL))) + return KCGI_SYSTEM; + + status = starthtmldoc(pd, KHTTP_200); + if (status != KCGI_OK) + return status; + + start = getstart(pd, user->hash); + + if ((status = htmlwithin(pd, KELEM_H1, "Timekeeper")) != KCGI_OK || + (status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_INPUT, + KATTR_TYPE, "hidden", + KATTR_NAME, pd->keys[start ? KeyStop : KeyStart].name, + KATTR_VALUE, "yes", + KATTR__MAX)) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_INPUT, + KATTR_TYPE, "submit", + KATTR_VALUE, start ? "Stop Time" : "Start Time", KATTR__MAX)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) return status; - khttp_puts(&pd->req, "<!doctype html><html><head><title>Main</title></head><body><h1>Main</h1>" - "<noscript><a href=\".\">Reload</a></noscript>" - "<p id=\"counter\">0</p>" - "<script src=\"scripts/main.js\"></script>" - "</body></html>"); + + if (start) { + elapsed = time(NULL) - start; + mn = elapsed / 60; + sc = elapsed % 60; + hr = mn / 60; + mn = mn % 60; + tm = gmtime(&start); + if (!tm) + err(1, "gmtime"); + if (strftime(datetime, sizeof(datetime), "%FT%T+0000", tm) >= (int)sizeof(datetime)) + err(1, "strftime"); + if ((status = khtml_attr(&pd->html, KELEM_TIME, + KATTR_ID, "start", KATTR_DATETIME, datetime, + KATTR__MAX)) != KCGI_OK || + (status = khtml_printf(&pd->html, "%.2d:%.2d:%.2d", hr, mn, sc)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) + return status; + + if ((status = khtml_elem(&pd->html, KELEM_NOSCRIPT)) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_A, + KATTR_HREF, pages[PageMain], KATTR__MAX)) != KCGI_OK || + (status = htmlwithin(pd, KELEM_BUTTON, "Reload")) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_SCRIPT, + KATTR_SRC, "scripts/main.js", KATTR__MAX)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) + return status; + } + freeuser(user); - return KCGI_OK; + return khtml_close(&pd->html); } int @@ -561,7 +649,9 @@ main(void) [KeyPassword] = { kvalid_pass, "password" }, [KeyHash] = { kvalid_stringne, "hash" }, [KeyCreate] = { kvalid_stringne, "create" }, - [KeyDelete] = { kvalid_stringne, "delete" } + [KeyDelete] = { kvalid_stringne, "delete" }, + [KeyStart] = { kvalid_stringne, "start" }, + [KeyStop] = { kvalid_stringne, "stop" } }; struct pagedata pd = { .keys = keys @@ -571,11 +661,15 @@ main(void) { .fname = "/tmp/timekeeper.db", .mode = SQLBOX_SRC_RWC } }; struct sqlbox_pstmt pstmts[] = { - [StmtInit] = { .stmt = "CREATE TABLE IF NOT EXISTS users (hash TEXT PRIMARY KEY, name TEXT UNIQUE)" }, + [StmtInitUsers] = { .stmt = "CREATE TABLE IF NOT EXISTS users (hash TEXT PRIMARY KEY, name TEXT UNIQUE)" }, [StmtAddUser] = { .stmt = "INSERT INTO users (hash, name) VALUES (?, ?)" }, [StmtGetUserByName] = { .stmt = "SELECT * FROM users WHERE name IS ?" }, [StmtGetUserByHash] = { .stmt = "SELECT * FROM users WHERE hash IS ?" }, - [StmtDeleteUser] = { .stmt = "DELETE FROM users WHERE hash IS ?" } + [StmtDeleteUser] = { .stmt = "DELETE FROM users WHERE hash IS ?" }, + [StmtInitUserdata] = { .stmt = "CREATE TABLE IF NOT EXISTS userdata (hash TEXT PRIMARY KEY, start INTEGER)" }, + [StmtAddUserdata] = { .stmt = "INSERT INTO userdata (hash) VALUES (?)" }, + [StmtStartTime] = { .stmt = "UPDATE userdata SET start = ? WHERE hash IS ?" }, + [StmtGetStartTime] = { .stmt = "SELECT start FROM userdata WHERE hash IS ?" } }; enum kcgi_err (*pagefunctions[])(struct pagedata *) = { [PageIndex] = pageindex,