counts and dockerfiles

This commit is contained in:
2025-09-28 16:57:03 -04:00
parent e6eb2807ae
commit 9bb763a053
19 changed files with 536 additions and 7 deletions

2
api/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/
*/__pycache__/

8
api/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM python:3
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["fastapi", "run", "main.py"]

View File

@@ -9,6 +9,7 @@ from routers.baskets import basket_router
from routers.combined import combined_router
from routers.reports import report_router
from routers.backuprestore import backup_router
from routers.counts import counts_router
if argv[1] == "run":
app = FastAPI(title="TAM3 API Server", docs_url=None, redoc_url=None)
@@ -21,3 +22,4 @@ app.include_router(basket_router)
app.include_router(combined_router)
app.include_router(report_router)
app.include_router(backup_router)
app.include_router(counts_router)

28
api/routers/counts.py Normal file
View File

@@ -0,0 +1,28 @@
from fastapi import APIRouter
from dataclasses import dataclass
from exceptions import bad_key
from repos.template import Repo
from repos.api_keys import ApiKeyRepo
@dataclass
class Count:
prefix: str
total_sold: int
unique_sold: int
class CountRepo(Repo):
def get_counts(self):
self.cur.execute("SELECT * FROM counts")
results = self.cur.fetchall()
return [Count(*r) for r in results]
counts_router = APIRouter(prefix="/api/counts")
@counts_router.get("/")
def get_ticket_counts(api_key: str):
if not ApiKeyRepo().check_api(api_key):
raise bad_key
return CountRepo().get_counts()

3
db/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM mariadb:lts
COPY schema.sql /docker-entrypoint-initdb.d/tam3-schema.sql

View File

@@ -1,6 +1,6 @@
services:
db:
image: mariadb:lts
image: dbob16/tam3-db:0.0.1
restart: always
hostname: mariadb
container_name: mariadb
@@ -11,7 +11,6 @@ services:
MARIADB_PASSWORD: tam3
volumes:
- "tam3-db:/var/lib/mysql"
- "./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro"
ports:
- 127.0.0.1:3306:3306
adminer:

2
webapp/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
/node_modules/
*.db

23
webapp/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM node:lts-alpine AS build
WORKDIR /app
COPY . .
RUN npm install && npm run build
FROM node:lts-alpine AS prod
WORKDIR /data
WORKDIR /app
ENV DATABASE_URL=file:/data/local.db
COPY --from=build /app/build/. build/.
COPY --from=build /app/package.json /app/drizzle.config.js .
COPY --from=build /app/drizzle drizzle
COPY deploy/start-server.sh start-server.sh
RUN npm install --production && npm install drizzle-kit
EXPOSE 3000
CMD ["sh", "start-server.sh"]

2
webapp/deploy/start-server.sh Executable file
View File

@@ -0,0 +1,2 @@
npx drizzle-kit migrate
node build

View File

@@ -0,0 +1,39 @@
CREATE TABLE `baskets` (
`prefix` text,
`b_id` integer,
`description` text DEFAULT '',
`donors` text DEFAULT '',
`winning_ticket` integer DEFAULT 0,
PRIMARY KEY(`prefix`, `b_id`)
);
--> statement-breakpoint
CREATE TABLE `prefixes` (
`name` text PRIMARY KEY NOT NULL,
`color` text,
`weight` integer DEFAULT 0
);
--> statement-breakpoint
CREATE TABLE `tickets` (
`prefix` text,
`t_id` integer,
`first_name` text DEFAULT '',
`last_name` text DEFAULT '',
`phone_number` text DEFAULT '',
`preference` text DEFAULT 'CALL',
PRIMARY KEY(`prefix`, `t_id`)
);
--> statement-breakpoint
CREATE VIEW `combined` AS SELECT b.prefix, b.b_id, b.winning_ticket, CONCAT(t.last_name, ', ', t.first_name) AS winner
FROM baskets b LEFT JOIN tickets t
ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
ORDER BY b.prefix, b.b_id;--> statement-breakpoint
CREATE VIEW `counts` AS SELECT prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold
FROM tickets
GROUP BY prefix
UNION ALL
SELECT 'Total' AS prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold
FROM tickets;;--> statement-breakpoint
CREATE VIEW `report` AS SELECT b.prefix, CONCAT(t.last_name, ', ', t.first_name) AS winner_name, t.phone_number, t.preference, b.b_id, b.winning_ticket, b.description
FROM baskets b LEFT JOIN tickets t
ON b.prefix = t.prefix AND b.winning_ticket = t.t_id
ORDER BY b.prefix, winner_name, t.phone_number, t.preference, b.b_id;

View File

@@ -0,0 +1,290 @@
{
"version": "6",
"dialect": "sqlite",
"id": "b981beaa-405c-48f1-8006-35f66d264062",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"baskets": {
"name": "baskets",
"columns": {
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"b_id": {
"name": "b_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"donors": {
"name": "donors",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"winning_ticket": {
"name": "winning_ticket",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"baskets_prefix_b_id_pk": {
"columns": [
"prefix",
"b_id"
],
"name": "baskets_prefix_b_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
},
"prefixes": {
"name": "prefixes",
"columns": {
"name": {
"name": "name",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"color": {
"name": "color",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tickets": {
"name": "tickets",
"columns": {
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"t_id": {
"name": "t_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"first_name": {
"name": "first_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"last_name": {
"name": "last_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"phone_number": {
"name": "phone_number",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "''"
},
"preference": {
"name": "preference",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'CALL'"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"tickets_prefix_t_id_pk": {
"columns": [
"prefix",
"t_id"
],
"name": "tickets_prefix_t_id_pk"
}
},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {
"combined": {
"columns": {
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"b_id": {
"name": "b_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"winning_ticket": {
"name": "winning_ticket",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"winner": {
"name": "winner",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"name": "combined",
"isExisting": false,
"definition": "SELECT b.prefix, b.b_id, b.winning_ticket, CONCAT(t.last_name, ', ', t.first_name) AS winner\n\tFROM baskets b LEFT JOIN tickets t\n\tON b.prefix = t.prefix AND b.winning_ticket = t.t_id\n\tORDER BY b.prefix, b.b_id"
},
"counts": {
"columns": {
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"total_sold": {
"name": "total_sold",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"unique_sold": {
"name": "unique_sold",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"name": "counts",
"isExisting": false,
"definition": "SELECT prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold\n\tFROM tickets\n\tGROUP BY prefix\n\tUNION ALL\n\tSELECT 'Total' AS prefix, COUNT(*) AS total_sold, COUNT(DISTINCT CONCAT(first_name,last_name,phone_number)) AS unique_sold\n\tFROM tickets;"
},
"report": {
"columns": {
"prefix": {
"name": "prefix",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"winner_name": {
"name": "winner_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"phone_number": {
"name": "phone_number",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"preference": {
"name": "preference",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"b_id": {
"name": "b_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"winning_ticket": {
"name": "winning_ticket",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"name": "report",
"isExisting": false,
"definition": "SELECT b.prefix, CONCAT(t.last_name, ', ', t.first_name) AS winner_name, t.phone_number, \tt.preference, b.b_id, b.winning_ticket, b.description\n\tFROM baskets b LEFT JOIN tickets t\n\tON b.prefix = t.prefix AND b.winning_ticket = t.t_id\n\tORDER BY b.prefix, winner_name, t.phone_number, t.preference, b.b_id"
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1759085800774,
"tag": "0000_init",
"breakpoints": true
}
]
}

View File

@@ -26,6 +26,7 @@
},
"dependencies": {
"hotkeys-js": "^3.13.15",
"drizzle-kit": "^0.30.2",
"@libsql/client": "^0.14.0"
}
}

View File

@@ -1 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="300"
height="300"
viewBox="0 0 79.375 79.375"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1"
transform="translate(-20.218306,-20.438326)">
<path
id="path10"
style="fill:#800000;stroke:#808080;stroke-width:1.06322"
d="m 90.181662,41.388444 -60.604352,0.0043 c 0,2.212943 -1.481519,3.631902 -3.512639,4.063504 l -0.02158,10.850643 c 2.046741,0.04733 3.562282,1.625408 3.562282,3.854622 0,2.229214 -1.578039,3.848839 -3.562282,3.937839 l 0.03118,10.520173 c 1.999037,0.142718 3.522231,2.020114 3.522231,4.233054 l 60.695724,0.01054 c 0.09863,-2.128742 1.494926,-3.716455 3.467073,-3.663485 l 0.0103,-11.216367 c -1.875025,-0.235686 -3.579073,-1.512439 -3.579073,-3.741656 0,-2.229216 1.61552,-3.741912 3.564199,-3.845028 L 93.754261,45.362239 C 91.96847,45.081704 90.380789,43.582451 90.181666,41.388427 Z M 31.793484,52.704816 H 46.04333 v 2.935872 h -2.219771 l -3.398965,-0.910595 v 14.730924 h -3.40376 V 54.638242 l -5.217278,1.397909 z m 18.60689,0.166674 h 2.916687 l 4.355128,16.254259 H 55.2337 l -1.671303,-6.236751 h -3.995396 l -1.738693,6.520937 -3.980047,-0.117991 z m 13.207107,1.532448 h 3.894676 l 2.397476,8.946953 2.294594,-8.564201 h 2.601806 l 3.725355,13.903547 H 76.04429 l -2.863444,-10.686845 -1.851408,8.517196 -2.349513,0.0295 -3.416952,-8.67236 -2.85937,10.671736 h -2.598687 z m 21.862852,0.673509 c 2.104651,0 3.573787,1.876237 3.573787,3.809405 0,1.138072 -0.131525,1.947015 -1.160468,2.367944 1.325151,0.467704 1.458361,1.158997 1.458361,2.593276 0,2.135833 -1.701223,3.537066 -3.97737,3.537066 -1.512235,0 -2.587832,-0.545713 -3.16466,-1.637009 -0.265026,-0.498886 -0.421054,-1.091073 -0.483477,-1.9953 l 1.902331,-0.11785 c 0.09354,1.652535 0.23701,1.8861 1.749236,1.8861 1.418686,0 1.775266,-0.238729 1.775266,-1.657418 0,-1.356327 -0.0784,-1.440989 -1.525694,-1.822194 l -1.195478,-0.05892 -0.31878,0.559785 0.206237,-1.97872 c 0.950986,-0.01559 1.803445,0.03993 2.177618,-0.115974 0.608,-0.265026 0.593906,-0.718917 0.593906,-1.514017 0,-1.216015 -0.34974,-1.801463 -1.565763,-1.801463 -0.857456,0 -1.307748,0.256431 -1.619549,0.895628 -0.171551,0.342972 -0.292694,0.67389 -0.308334,1.359849 h -1.813701 c 0.04676,-2.385268 1.467168,-4.310184 3.696532,-4.310184 z m -34.932782,2.460694 -0.637201,2.803253 h 3.208788 l -0.751356,-2.803253 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -30,6 +30,7 @@ a.styled:hover, button.styled:hover {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}

View File

@@ -2,6 +2,7 @@
import { browser } from "$app/environment";
import { env } from "$env/dynamic/public";
import hotkeys from "hotkeys-js";
import favicon from "$lib/assets/favicon.svg"
let { data } = $props();
let prefix_name = $state("");
@@ -31,7 +32,16 @@
</script>
<div class="main-menu">
<h1>{venue} - Main Menu</h1>
<div class="flex-row">
<img src="{favicon}" alt="TAM3 Icon - Red ticket with TAM3 on it" class="icon">
<h1>{venue} - Main Menu</h1>
</div>
<div class="universal-reports flex-row tb-margin">
<a href="/counts" target="_blank" class="styled">Counts</a>
</div>
<div>
<h2>Select Prefix:</h2>
</div>
<div class="prefix-selector">
<select style="width: 100%; box-sizing: border-box;" bind:value={prefix_name}>
{#each all_prefixes as prefix}
@@ -63,7 +73,7 @@
</div>
<style>
.prefix-selector select {
width: 100%;
img.icon {
max-width: 150px;
}
</style>

View File

@@ -0,0 +1,17 @@
import { env } from "$env/dynamic/private";
import { db } from "$lib/server/db";
import { counts } from "$lib/server/db/schema";
export async function GET() {
if (env.TAM3_REMOTE) {
const res = await fetch(`${env.TAM3_REMOTE}/api/counts/?api_key=${env.TAM3_REMOTE_KEY}`);
if (!res.ok) {
return new Response(JSON.stringify({detail: "Unable to fetch counts."}), {status: res.status, statusText: res.statusText})
};
const data = await res.json();
return new Response(JSON.stringify(data), {status: 200, statusText: "Fetched counts successfully."})
} else {
const data = await db.select().from(counts);
return new Response(JSON.stringify(data), {status: 200, statusText: "Loaded counts successfully"})
}
}

View File

@@ -0,0 +1,7 @@
export async function load({ fetch }) {
const res = await fetch('/api/counts');
const c_data = await res.json();
const p_res = await fetch('/api/prefixes');
const p_data = await p_res.json();
return {counts: c_data, prefixes: p_data}
}

View File

@@ -0,0 +1,61 @@
<script>
import { browser } from '$app/environment';
const { data } = $props();
const counts = data.counts;
const prefixes = data.prefixes;
let colormap = {};
for (let prefix of prefixes) {colormap[prefix.name] = prefix.color}
if (browser) {
document.title = "Counts of tickets entered";
setTimeout(() => window.location.reload(true), 60000);
}
</script>
<h1>Counts of tickets entered</h1>
<table>
<thead>
<tr>
<th>Prefix</th>
<th>Total Tickets</th>
<th>Unique Buyers</th>
</tr>
</thead>
<tbody>
{#each counts as count}
<tr class={colormap[count.prefix]}>
<td>{count.prefix}</td>
<td>{parseInt(count.total_sold).toLocaleString()}</td>
<td>{parseInt(count.unique_sold).toLocaleString()}</td>
</tr>
{/each}
</tbody>
</table>
<style>
table {
width: 100%;
thead {
text-align: left;
}
tbody tr {
color: var(--button-fg);
background-color: var(--button-bg);
td:first-child {
font-weight: bold;
font-style: italic;
}
}
tbody tr td {
border: solid 1px black;
padding: 0.3rem;
}
tbody tr:last-child {
color: #000000;
background-color: #dfdfdf;
font-weight: bold;
}
}
</style>