Compare commits

...

10 commits
v0.1 ... master

10 changed files with 553 additions and 72 deletions

90
0PLAN.org Normal file
View file

@ -0,0 +1,90 @@
#+title: Frontend
* Plan
** 0.3
- [ ] versioned schema support
- [ ] barschema table
- [ ] function to check
- [ ] function to set
- [ ] initial creation of tables at version 1 assuming OK if tables exist
- [ ] users import from hackerbase
** 0.4
- [ ] add inventory check table
- [ ] add inventory item check table
- [ ] create new check
- [ ] check item
- [ ] close check
- [ ] inventory check component
** Later
- [ ] add static token support
- [ ] hardwired token for the React.JS app?
- [ ] authentication
- [ ] login/logout
- [ ] allow API calls only with login token
* Finished
** 0.1
- [X] statically compiled binary HTTP server
- [X] include directory tree contents
- [X] handle frontend with mime type
- [X] move app to / again
- [X] load certificates
- [X] deployment preparations
- [X] crosscompilation - no, compile in qemu or on separate host
- [X] connect to postgres
- [X] use qr-scanner - no, qr-barcode-scanner is needed
- [X] rsync + build in qemu or host without react (filters or something)
- [X] API infrastructure
- [X] handle API calls
- [X] API registry syntax
- [X] lookup barcode in DB
- [X] separate module brmbar-data for queries
- [X] separate module api-servlets
** 0.2
- [X] use localStorage to store user
- [X] add barcode scanner to get the user
- [X] allow editing
- [X] add UserSelect component
- [X] integrate script for starting build qemu system
- [X] add org file to the repository after cleanup
* Qemu
#+BEGIN_SRC
wget img zip # 2019-04-08-raspbian-stretch.zip
unzip 2019-04-08-raspbian-stretch.zip
sudo mkdir /mnt/image
sudo mount -o loop,offset=4194304 2019-04-08-raspbian-stretch.img /mnt/image/
# In qemu-system-armhf terminal!
cp kernel, DT
umount /mnt/image
resize img
#+END_SRC
sources.list - archive debian (remove rpi), archive raspberry
https://archive.raspberrypi.org/debian/
https://archive.debian.org/debian/
apt-get --no
* React - Vite
#+BEGIN_SRC sh
npm create vite@latest
# "frontend"
# React
# Javascript
cd frontend
npm install
npm run dev
npm run build
#+END_SRC

View file

@ -47,6 +47,7 @@
(define -db-user- (make-parameter #f))
(define -db-name- (make-parameter #f))
(define -db-pass- (make-parameter #f))
(define -db-enabled- (make-parameter #t))
(command-line
print-help
@ -82,6 +83,8 @@
(-db-user- dbuser))
(-dp (dbpass) "Database password"
(-db-pass- dbpass))
(-dd () "Disable database"
(-db-enabled- #f))
)
(define ssl? (and (-certificate-) (-key-) #t))
@ -120,7 +123,8 @@
(print "current user id: " (current-user-id))
(print "current effective user id: " (current-effective-user-id))
(bar-db-init! (-db-name-) (-db-host-) (-db-user-) (-db-pass-))
(when (-db-enabled-)
(bar-db-init! (-db-name-) (-db-host-) (-db-user-) (-db-pass-)))
(define (handle-api-calls)
(define plst (cdr (uri-path (request-uri (current-request)))))

View file

@ -38,7 +38,7 @@
(chicken format))
;; Short banner
(define banner-line "BrmInv 0.1 (c) 2023-2025 Brmlab, z.s.")
(define banner-line "BrmInv 0.2 (c) 2023-2025 Brmlab, z.s.")
;; The license of this file and of the whole suite.
(define license "ISC License

View file

@ -1,6 +1,6 @@
.App {
background-color: var(--background-color);
color: var(--primary-text-color) !important;
min-height: 100vh;
width: 100%;
height: 100vh;
width: 10vh;
}

View file

@ -1,13 +1,88 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import BarcodeScannerComponent from 'react-qr-barcode-scanner';
import { Container, Row, Col } from 'react-bootstrap';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Table from 'react-bootstrap/Table';
import Alert from 'react-bootstrap/Alert';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import Card from 'react-bootstrap/Card';
import './App.css';
import 'bootstrap/dist/css/bootstrap.min.css';
function App() {
// Ensure we have persistent (informal) user information
const [user, setUser] = useState("");
useEffect(() => {
const user = localStorage.getItem('user');
if (user) {
setUser(user);
}
}, []);
useEffect(() => {
localStorage.setItem('user', user);
}, [user]);
// If no user, must be scanned/set otherwise
return (
<>
{user === "" ?
<NoUserView setUser={setUser} />
:
<UserView user={user} setUser={setUser} />}
</>
);
}
function NoUserView({setUser}) {
const [preUser, setPreUser] = useState("");
return (
<Card bg="info" style={{width: '100vw', height: '100vh'}}>
<Card.Body>
<Card.Title>BrmInv Anonymous</Card.Title>
<Form.Group>
<Form.Label>User name:</Form.Label>
<Form.Control type="text" placeholder="Username"
value={preUser} onChange={(v) => setPreUser(v.target.value)} />
<Form.Text>Enter a user name or scan your barcode.</Form.Text>
</Form.Group>
<Card.Text>
<BarcodeScannerComponent
style={{width: 'auto', height: 'auto'}}
onUpdate={(err, result) => {
if (result) {
setPreUser(result.text);
}}}
delay={1500}
/>
</Card.Text>
</Card.Body>
<Card.Footer>
<Button variant="primary"
style={{width: '100%'}}
onClick={() => setUser(preUser)}>Set!</Button>
</Card.Footer>
</Card>
);
}
function UserView({user, setUser}) {
return (
<Row>
<Col>
{user}
</Col>
<Col>
<Button variant="info" onClick={() => setUser('')}>Change</Button>
</Col>
<BarItemScanner />
</Row>
);
}
function BarItemScanner() {
const [reqAccount, setReqAccount] = useState('');
const [actAccount, setActAccount] = useState('');
const [balance, setBalance] = useState(-1);
@ -37,7 +112,7 @@ function App() {
};
return (
<Container>
<>
<Row>
<Col>
<BarcodeScannerComponent
@ -71,7 +146,7 @@ function App() {
<Alert variant={msgType}>{statusMsg}</Alert>
</Col>
</Row>
</Container>
</>
);
}

View file

@ -1,64 +0,0 @@
#!/bin/sh
#
# install-eggs.sh
#
# Local installer of CHICKEN eggs required for building.
#
# ISC License
#
# Copyright 2023 Brmlab, z.s.
# Dominik Pantůček <dominik.pantucek@trustica.cz>
#
# 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.
#
# Source root directory
owd=$(pwd)
cd $(dirname "$0")
SRCDIR=$(pwd)
cd "$owd"
# Make temporary prefix directory (eggs shared throwaway files)
TMPDIR=$(mktemp -d)
# Installs given egg locally
chicken_install() {
echo "Installing $1 ..."
# CHICKEN_INSTALL_PREFIX="$TMPDIR" \
# CHICKEN_REPOSITORY_PATH="$SRCDIR/eggs-arm":`./cross-chicken-arm/bin/arm-chicken-install -repository` \
# CHICKEN_INSTALL_REPOSITORY="$SRCDIR/eggs-arm" \
# ./cross-chicken-arm/bin/arm-chicken-install "$1" 2>&1 | \
# sed -u 's/^/ /'
# CHICKEN_INSTALL_PREFIX="$TMPDIR" \
./cross-chicken-arm/bin/arm-chicken-install "$1" 2>&1 | \
sed -u 's/^/ /'
}
# Removes throwaway files
chicken_cleanup() {
echo "Cleaning up ..."
rm -fr ${TMPDIR}
}
# Always cleanup
trap chicken_cleanup INT QUIT
# Install required eggs
chicken_install spiffy
chicken_install openssl
chicken_install postgresql
# Normal termination cleanup
chicken_cleanup

View file

@ -57,6 +57,7 @@ chicken_install openssl
chicken_install spiffy
chicken_install postgresql
chicken_install json
chicken_install posix-groups
# Normal termination cleanup
chicken_cleanup

313
schema/0000-init.sql Normal file
View file

@ -0,0 +1,313 @@
--
-- 0000-init.sql
--
-- Initial SQL schema construction as of 2025-04-20 (or so)
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- 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.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
-- Privileged schema with protected data
CREATE SCHEMA IF NOT EXISTS brmbar_privileged;
-- Initial versioning
CREATE TABLE IF NOT EXISTS brmbar_privileged.brmbar_schema(
ver INTEGER NOT NULL
);
-- ----------------------------------------------------------------
-- Legacy Schema Initialization
-- ----------------------------------------------------------------
DO $$
DECLARE v INTEGER;
BEGIN
SELECT ver FROM brmbar_privileged.brmbar_schema INTO v;
IF v IS NULL THEN
-- --------------------------------
-- Legacy Types
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='exchange_rate_direction';
IF v=0 THEN
RAISE NOTICE 'Creating type exchange_rate_direction';
CREATE TYPE public.exchange_rate_direction
AS ENUM ('source_to_target', 'target_to_source');
ELSE
RAISE NOTICE 'Type exchange_rate_direction already exists';
END IF;
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='account_type';
IF v=0 THEN
RAISE NOTICE 'Creating type account_type';
CREATE TYPE public.account_type
AS ENUM ('cash', 'debt', 'inventory', 'income', 'expense',
'starting_balance', 'ending_balance');
ELSE
RAISE NOTICE 'Type account_type already exists';
END IF;
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='transaction_split_side';
IF v=0 THEN
RAISE NOTICE 'Creating type transaction_split_side';
CREATE TYPE public.transaction_split_side
AS ENUM ('credit', 'debit');
ELSE
RAISE NOTICE 'Type transaction_split_side already exists';
END IF;
-- --------------------------------
-- Currencies sequence, table and potential initial data
CREATE SEQUENCE IF NOT EXISTS public.currencies_id_seq
START WITH 2 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.currencies (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.currencies_id_seq'::regclass),
name VARCHAR(128) NOT NULL,
UNIQUE(name)
);
INSERT INTO public.currencies (id, name) VALUES (1, '')
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Exchange rates table - no initial data required
CREATE TABLE IF NOT EXISTS public.exchange_rates (
valid_since TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
target INTEGER NOT NULL,
FOREIGN KEY (target) REFERENCES public.currencies (id),
source INTEGER NOT NULL,
FOREIGN KEY (source) REFERENCES public.currencies (id),
rate DECIMAL(12,2) NOT NULL,
rate_dir public.exchange_rate_direction NOT NULL
);
-- --------------------------------
-- Accounts sequence and table and 4 initial accounts
CREATE SEQUENCE IF NOT EXISTS public.accounts_id_seq
START WITH 2 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.accounts (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.accounts_id_seq'::regclass),
name VARCHAR(128) NOT NULL,
UNIQUE (name),
currency INTEGER NOT NULL,
FOREIGN KEY (currency) REFERENCES public.currencies (id),
acctype public.account_type NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);
INSERT INTO public.accounts (id, name, currency, acctype)
VALUES (1, 'BrmBar Cash', (SELECT id FROM public.currencies WHERE name=''), 'cash')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Profits', (SELECT id FROM public.currencies WHERE name=''), 'income')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Excess', (SELECT id FROM public.currencies WHERE name=''), 'income')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Deficit', (SELECT id FROM public.currencies WHERE name=''), 'expense')
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Barcodes
CREATE TABLE IF NOT EXISTS public.barcodes (
barcode VARCHAR(128) PRIMARY KEY NOT NULL,
account INTEGER NOT NULL,
FOREIGN KEY (account) REFERENCES public.accounts (id)
);
INSERT INTO public.barcodes (barcode, account)
VALUES ('_cash_', (SELECT id FROM public.accounts WHERE acctype = 'cash'))
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Transactions
CREATE SEQUENCE IF NOT EXISTS public.transactions_id_seq
START WITH 1 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.transactions (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transactions_id_seq'::regclass),
time TIMESTAMP DEFAULT NOW() NOT NULL,
responsible INTEGER,
FOREIGN KEY (responsible) REFERENCES public.accounts (id),
description TEXT
);
-- --------------------------------
-- Transaction splits
CREATE SEQUENCE IF NOT EXISTS public.transaction_splits_id_seq
START WITH 1 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.transaction_splits (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transaction_splits_id_seq'::regclass),
transaction INTEGER NOT NULL,
FOREIGN KEY (transaction) REFERENCES public.transactions (id),
side public.transaction_split_side NOT NULL,
account INTEGER NOT NULL,
FOREIGN KEY (account) REFERENCES public.accounts (id),
amount DECIMAL(12,2) NOT NULL,
memo TEXT
);
-- --------------------------------
-- Account balances view
CREATE OR REPLACE VIEW public.account_balances AS
SELECT ts.account AS id,
accounts.name,
accounts.acctype,
- sum(
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END) AS crbalance
FROM public.transaction_splits ts
LEFT JOIN public.accounts ON accounts.id = ts.account
GROUP BY ts.account, accounts.name, accounts.acctype
ORDER BY (- sum(
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END));
-- --------------------------------
-- Transaction nice splits view
CREATE OR REPLACE VIEW public.transaction_nicesplits AS
SELECT ts.id,
ts.transaction,
ts.account,
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END AS amount,
a.currency,
ts.memo
FROM public.transaction_splits ts
LEFT JOIN public.accounts a ON a.id = ts.account
ORDER BY ts.id;
-- --------------------------------
-- Transaction cash sums view
CREATE OR REPLACE VIEW public.transaction_cashsums AS
SELECT t.id,
t."time",
sum(credit.credit_cash) AS cash_credit,
sum(debit.debit_cash) AS cash_debit,
a.name AS responsible,
t.description
FROM public.transactions t
LEFT JOIN ( SELECT cts.amount AS credit_cash,
cts.transaction AS cts_t
FROM public.transaction_nicesplits cts
LEFT JOIN public.accounts a_1 ON a_1.id = cts.account OR a_1.id = cts.account
WHERE a_1.currency = (( SELECT accounts.currency
FROM public.accounts
WHERE accounts.name::text = 'BrmBar Cash'::text))
AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type]))
AND cts.amount < 0::numeric) credit ON credit.cts_t = t.id
LEFT JOIN ( SELECT dts.amount AS debit_cash,
dts.transaction AS dts_t
FROM public.transaction_nicesplits dts
LEFT JOIN public.accounts a_1 ON a_1.id = dts.account OR a_1.id = dts.account
WHERE a_1.currency = (( SELECT accounts.currency
FROM public.accounts
WHERE accounts.name::text = 'BrmBar Cash'::text))
AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type]))
AND dts.amount > 0::numeric) debit ON debit.dts_t = t.id
LEFT JOIN public.accounts a ON a.id = t.responsible
GROUP BY t.id, a.name
ORDER BY t.id DESC;
-- --------------------------------
-- Function to check schema version (used in migrations)
CREATE OR REPLACE FUNCTION brmbar_privileged.has_exact_schema_version(
IN i_ver INTEGER
) RETURNS INTEGER
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$
DECLARE
v_ver INTEGER;
BEGIN
SELECT ver INTO v_ver FROM brmbar_privileged.brmbar_schema;
IF v_ver is NULL THEN
RETURN false;
ELSE
RETURN v_ver = i_ver;
END IF;
END;
$x$;
-- --------------------------------
--
CREATE OR REPLACE FUNCTION brmbar_privileged.upgrade_schema_version_to(
IN i_ver INTEGER
) RETURNS INTEGER
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$
DECLARE
v_ver INTEGER;
BEGIN
SELECT ver FROM brmbar_privileged.brmbar_schema INTO v_ver;
IF v_ver=(i_ver-1) THEN
UPDATE brmbar_privileged.brmbar_schema SET ver = i_ver;
ELSE
RAISE EXCEPTION 'Invalid brmbar schema version transition (% -> %)', v_ver, i_ver;
END IF;
END;
$x$;
-- Initialize version 1
INSERT INTO brmbar_privileged.brmbar_schema(ver) VALUES(1);
END IF;
END;
$$;

View file

@ -1,4 +1,28 @@
#!/bin/sh
#
# build-in-qemu.sh
#
# Expects running armhf qemu system, builds the binary inside.
#
# ISC License
#
# Copyright 2025 Brmlab, z.s.
# Dominik Pantůček <dominik.pantucek@trustica.cz>
#
# 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.
#
if [ -z "$1" ] ; then
echo "Usage: $0 password"

View file

@ -0,0 +1,38 @@
#!/bin/sh
#
# run-build-qemu-system.sh
#
# Runs the emulated armhf system for building the application.
#
# ISC License
#
# Copyright 2025 Brmlab, z.s.
# Dominik Pantůček <dominik.pantucek@trustica.cz>
#
# 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.
#
qemu-system-armhf \
-machine raspi2b \
-nographic \
-dtb bcm2710-rpi-3-b-plus.dtb \
-m 1G \
-smp 4 \
-kernel kernel7.img \
-sd 2019-04-08-raspbian-stretch.img \
-append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootdelay=1 dwc_otg.fiq_fsm_enable=0" \
-usb \
-device usb-net,netdev=net0 \
-netdev user,id=net0,hostfwd=tcp::2222-:22