diff --git a/Dockerfile b/Dockerfile index b8fb2f181d8693ca2c06b6f795de760acd71b5fe..fb4281aaf2f03be05eb1afde26a3156ccc93396f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM node:22-alpine -WORKDIR /doorsense +FROM node:22-slim EXPOSE 5200 -COPY package.json package.json -COPY static static -COPY main.js main.js -COPY .env .env -COPY pages pages +COPY src src +WORKDIR src +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends inotify-tools mosquitto-clients -y RUN npm install -CMD ["node", "main.js"] +CMD ["chmod", "+x", "./dispense_door_opener_lookup.sh"] +CMD ["chmod", "+x", "./start.sh"] +CMD ["bash", "./start.sh"] + diff --git a/README.md b/README.md index f07f53959b8d6846084b98f73b0836f47724a0cf..375539bf90bf069270ef7fd1f6d8e135dd028d8b 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,9 @@ A project to surface the status of the UCC door via HTTP ## Alt: Docker @pre: docker compose v2 -1. edit compose.yaml to suit your need, mainly port -2. docker compose up -d --build +1. Edit compose.yaml to suit your need, mainly port and cokelog path & corresponding envvar +2. `docker compose up -d --build` + +@notes: +1. Cokelog has to mount as rw so `inotifywait` can pick it up and broadcast opener msg to mqtt broker. This *might be* unsafe. Considering original copy is on `merlo` it should be acceptable. + diff --git a/compose.yaml b/compose.yaml index f1a254d75f4d86f284a565fb8ba0475f901c2d8c..d39c8f5a90d0c7269a011d7d60736d4b76ba7913 100644 --- a/compose.yaml +++ b/compose.yaml @@ -3,4 +3,15 @@ services: build: . ports: - "5200:5200" + volumes: + - type: bind + source: /home/other/coke/cokelog + target: /var/cokelog + environment: + COKELOGFILE: /var/cokelog + develop: + watch: + - action: sync + path: /home/other/coke/cokelog + target: /var/cokelog restart: unless-stopped diff --git a/src/dispense_door_opener_lookup.sh b/src/dispense_door_opener_lookup.sh new file mode 100755 index 0000000000000000000000000000000000000000..1efd061db1ff6aff44c547cb3e6f75c6b0e1d234 --- /dev/null +++ b/src/dispense_door_opener_lookup.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# [ROY] 20250525 +set -e +set -x +export $(cat .env | xargs) + +# this path should be injected as envvar; only use it as debug +#COKELOGFILE=/home/other/coke/cokelog + +[[ -n "$COKELOGFILE" ]] +[[ -n "$MQTT_HOST" ]] +[[ -n "$MQTT_PORT" ]] +[[ -n "$MQTT_USER" ]] +[[ -n "$MQTT_PASS" ]] +[[ -r "$COKELOGFILE" ]] + +PATTERN_DOOR="dispense 'door' \(door:[[:digit:]]\) for (\S*) by (\S*)" +PATTERN_TAKEDOOR="dispense 'takedoor' \(pseudo:[[:digit:]][[:digit:]]\) for (\S*) by (\S*)" + +inotifywait -m -e close_write "$COKELOGFILE" | while read -r filename event event_filename; do + opener="" + declare -i offset + offset=0 + while [[ -z ${opener} ]]; do + lines=$(tail -n "+${offset}" "${COKELOGFILE}" | tac) + # Either not double quote $lines or set IFS to empty or else $lines will be one line + while IFS='' read -r line; do + if [[ "$line" =~ $PATTERN_DOOR || "$line" =~ $PATTERN_TAKEDOOR ]]; then + echo "${BASH_REMATCH[1]}" + opener="${BASH_REMATCH[1]}" + break + fi + done <<<"$lines" + offset+=10 + done + mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" --username "$MQTT_USER" --pw "$MQTT_PASS" -t "door/ucc-door/opener" -m "$opener" +done + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000000000000000000000000000000000000..ff2ab3007ec3c6afee0e3e2ff98e24da746974da --- /dev/null +++ b/src/main.js @@ -0,0 +1,338 @@ +// edited 2025-05-24 [ROY] for fixing error-state npm program +require('dotenv').config() +const mqtt = require('mqtt'); +const http = require('http'); +const fs = require('node:fs'); +const crypto = require("crypto"); + +// Prepend timestamp and severity to all console messages +require('console-stamp')(console); + +class DoorInfo { + state = null; + opener = null; + lastChange = null; + history = []; + historyEnabled = false; +} + +const web_host = process.env.HTTP_BIND; +const web_port = process.env.HTTP_PORT; + +const mqtt_host = process.env.MQTT_HOST; +const mqtt_port = process.env.MQTT_PORT; + +// Use a randomized client ID to avoid collisions, as we don't need any kind +// of session resumption +const clientId = "doorsense-"+crypto.randomUUID(); + +// Create an MQTT client instance +const options = { + clean: true, + connectTimeout: 5000, + clientId: clientId, + username: process.env.MQTT_USER, + password: process.env.MQTT_PASS, +}; + +const client = mqtt.connect(`mqtt://${mqtt_host}:${mqtt_port}`, options); + +client.on('connect', () => { + console.log(`Connected to MQTT broker ${mqtt_host}!`); + client.subscribe('door/#', () => {}); +}); + +let doors = { + 'ucc-door': new DoorInfo(), + 'unisfa-door': new DoorInfo(), + 'uwaes-door': new DoorInfo(), +}; + +doors['ucc-door'].historyEnabled = true; +doors['uwaes-door'].historyEnabled = false; +doors['unisfa-door'].historyEnabled = false; + +// Load history from file if it exists +const loadHistory = () => { + try { + const history = JSON.parse(fs.readFileSync(`${__dirname}/door_history.json`, 'utf8')); + for (const [door, info] of Object.entries(history)) { + if (!doors[door]) continue; + doors[door].history = info.history || []; + } + console.log('Loaded door history from file'); + } catch (err) { + console.log('No history file found, starting fresh'); + } +}; + +// Save history to file +const saveHistory = () => { + const history = {}; + for (const [door, info] of Object.entries(doors)) { + history[door] = { + history: info.history + }; + } + fs.writeFileSync(`${__dirname}/door_history.json`, JSON.stringify(history, null, 2)); + console.log('Saved door history to file'); +}; + +const update_door_state = (door, state) => { + if (doors[door] === undefined) { + console.log(`Tried to update state for unknown door ${door}`); + return; + } + + const oldState = doors[door].state; + + switch (state) { + case "ON": + console.log(`Door ${door} opened!`); + doors[door].state = true; + break; + + case "OFF": + console.log(`Door ${door} closed!`); + doors[door].state = false; + break; + + default: + console.log(`Unknown message received for door ${door}`); + return; + } + + // Only update lastChange if the state actually changed + if (oldState !== doors[door].state) { + const timestamp = new Date().toISOString(); + doors[door].lastChange = timestamp; + + // Add to history + doors[door].history.unshift({ + timestamp, + state: doors[door].state, + opener: doors[door].opener + }); + + // Keep only last 1000 changes + if (doors[door].history.length > 1000) { + doors[door].history = doors[door].history.slice(0, 1000); + } + + // Save history to file + saveHistory(); + } +}; + +const update_door_opener = (door, opener) => { + if (doors[door] === undefined) { + console.log(`Tried to update opener for unknown door ${door}`); + return; + } + + if (opener) { + console.log(`Marked door ${door} as opened by ${opener}`); + doors[door].opener = opener; + + if (doors[door].state && doors[door].history.length > 0) { + doors[door].history[0].timestamp = new Date().toISOString(); + doors[door].history[0].opener = opener; + saveHistory(); + } + } +}; + +// Receive messages +client.on('message', (topic, message) => { + console.log(`Received message on topic ${topic}`); + + const topicparts = topic.split('/'); + if (topicparts.length != 3) { + console.log(`Unknown topic ${topic} received`); + return; + } + + if (topicparts[0] === 'door' && topicparts[2] === 'state') { + update_door_state(topicparts[1], message.toString()); + } + else if (topicparts[0] === 'door' && topicparts[2] === 'opener') { + update_door_opener(topicparts[1], message.toString()); + } + else { + console.log(`Unknown topic ${topic} received`); + + } +}); + +const send_error = (res, code) => { + res.writeHead(code); + res.end(); +}; + +const send_redirect = (res, code, loc) => { + res.writeHead(code, {'Location': loc}); + res.end(); +}; + +const send_json = (res, code, data) => { + res.writeHead(code, {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'}); + res.end(JSON.stringify(data)); +}; + +const send_file = (res, code, fname, ctype) => { + const data = fs.readFileSync(fname); + + res.writeHead(code, {'Content-Type': ctype}); + res.end(data); +}; + +const handleHistoryRequest = (req, res, door) => { + const n = parseInt(req.url.split('/').pop()); + if (isNaN(n) || n <= 0) { + return send_json(res, 400, { error: 'Invalid number of changes requested' }); + } + + const history = doors[door].history.slice(0, n); + return send_json(res, 200, history); +}; + +const handleLastChangeRequest = (res, door) => { + if (doors[door].lastChange === null) { + return send_json(res, 500, null); + } + + return send_json(res, 200, { + lastChange: doors[door].lastChange, + state: doors[door].state + }); +}; + +const handleStateRequest = (req, res) => { + const parts = req.url.split('/').filter(Boolean); + if (parts.length < 2) return send_error(res, 404); + + const door = parts[1]; + const doorId = `${door}-door`; + + if (!doors[doorId]) { + return send_error(res, 404); + } + + if (parts.length === 2) { + // handle /state/{door} + if (doors[doorId].state === null) { + return send_json(res, 500, null); + } + return send_json(res, 200, doors[doorId].state); + } + + if (parts[2] === 'lastchange') { + return handleLastChangeRequest(res, doorId); + } + + if (parts[2] === 'history') { + if (!doors[doorId].historyEnabled) { + return send_error(res, 404); + } + if (parts.length !== 4) return send_error(res, 404); + return handleHistoryRequest(req, res, doorId); + } + + return send_error(res, 404); +}; + +// Serve state on http +const server = http.createServer((req, res) => { + console.log(`Serving request for ${req.url}`); + + if (req.method !== 'GET') { + return send_error(res, 400); + } + + if (req.url === '/') { + return send_redirect(res, 307, '/ucc'); + } + + if (req.url === '/ucc') { + if (doors['ucc-door'].state === null) { + return send_file(res, 500, './pages/ucc_door_unavail.html', 'text/html'); + } + + const fname = doors['ucc-door'].state ? './pages/ucc_door_open.html' : './pages/ucc_door_closed.html'; + return send_file(res, 200, fname, 'text/html'); + } + + if (req.url === '/unisfa') { + if (doors['unisfa-door'].state === null) { + return send_file(res, 500, './pages/unisfa_door_unavail.html', 'text/html'); + } + + const fname = doors['unisfa-door'].state ? './pages/unisfa_door_open.html' : './pages/unisfa_door_closed.html'; + return send_file(res, 200, fname, 'text/html'); + } + + if (req.url === '/uwaes') { + if (doors['uwaes-door'].state === null) { + return send_file(res, 500, './pages/uwaes_door_unavail.html', 'text/html'); + } + + const fname = doors['uwaes-door'].state ? './pages/uwaes_door_open.html' : './pages/uwaes_door_closed.html'; + return send_file(res, 200, fname, 'text/html'); + } + + if (req.url.startsWith('/state/')) { + return handleStateRequest(req, res); + } + + if (req.url === '/opener/ucc') { + if (doors['ucc-door'].opener === null) { + return send_json(res, 500, null); + } + + return send_json(res, 200, doors['ucc-door'].state ? doors['ucc-door'].opener : null); + } + + if (req.url === '/static/door_ucc_open.jpg') { + return send_file(res, 200, './static/door_ucc_open.jpg', 'image/jpeg'); + } + + if (req.url === '/static/door_ucc_closed.jpg') { + return send_file(res, 200, './static/door_ucc_closed.jpg', 'image/jpeg'); + } + + if (req.url === '/static/door_unisfa_open.jpg') { + return send_file(res, 200, './static/door_unisfa_open.jpg', 'image/jpeg'); + } + + if (req.url === '/static/door_unisfa_closed.jpg') { + return send_file(res, 200, './static/door_unisfa_closed.jpg', 'image/jpeg'); + } + + if (req.url === '/static/door_uwaes_open.jpg') { + return send_file(res, 200, './static/door_uwaes_open.jpg', 'image/jpeg'); + } + + if (req.url === '/static/door_uwaes_closed.jpg') { + return send_file(res, 200, './static/door_uwaes_closed.jpg', 'image/jpeg'); + } + + if (req.url === '/static/style.css') { + return send_file(res, 200, './static/style.css', 'text/css'); + } + + if (req.url === '/static/darkmode.js') { + return send_file(res, 200, './static/darkmode.js', 'text/javascript'); + } + + return send_error(res, 404); +}); + +// Load history when server starts +loadHistory(); + +// Run the server +server.listen(web_port, web_host, () => { + console.log(`HTTP server started on http://${web_host}:${web_port}`); +}); + +// vim: set ts=2 sts=2 sw=2 et: diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6b491a0c97ad01995df46070d2e5645ae22956df --- /dev/null +++ b/src/package.json @@ -0,0 +1,13 @@ +{ + "name": "@ucc/doorsense", + "private": true, + "dependencies": { + "console-stamp": "^3.1.2", + "dotenv": "^16.5.0", + "mqtt": "^5.10.1", + "package.json": "^2.0.1" + }, + "scripts": { + "start": "node main.js" + } +} diff --git a/src/pages/ucc_door_closed.html b/src/pages/ucc_door_closed.html new file mode 100644 index 0000000000000000000000000000000000000000..f97bb8c0c70207b3e1f3fd03324a08b54bb4c4e8 --- /dev/null +++ b/src/pages/ucc_door_closed.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UCC Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UCC Door Status</h1> + <p>The door is currently closed.</p> + <img + src="/static/door_ucc_closed.jpg" + width="574" + height="1020" + alt="A plain image of the UCC door. Surely nothing sinister lies within..." + title="A plain image of the UCC door. Surely nothing sinister lies within..." + > + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/ucc_door_open.html b/src/pages/ucc_door_open.html new file mode 100644 index 0000000000000000000000000000000000000000..b8dc468d709d6960313277abb78eeb70f8003ef4 --- /dev/null +++ b/src/pages/ucc_door_open.html @@ -0,0 +1,33 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UCC Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UCC Door Status</h1> + <p>The door is currently open!</p> + <p>Opened by: <span id="opener"></span></p> + <img + src="/static/door_ucc_open.jpg" + width="574" + height="1020" + alt="A wild Gary lurking in the open doorway to the UCC clubroom." + title="A wild Gary lurking in the open doorway to the UCC clubroom." + > + <script> + fetch('/opener/ucc').then(response => { + response.json().then(opener => { + if (opener) { + document.getElementById('opener').innerText = opener; + } + }); + }); + </script> + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/ucc_door_unavail.html b/src/pages/ucc_door_unavail.html new file mode 100644 index 0000000000000000000000000000000000000000..8ea943d9a0969e9cf088db9d363c209609a733fd --- /dev/null +++ b/src/pages/ucc_door_unavail.html @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UCC Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UCC Door Status</h1> + <p>The door sensor is currently unavailable. Sorry :-(</p> + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/unisfa_door_closed.html b/src/pages/unisfa_door_closed.html new file mode 100644 index 0000000000000000000000000000000000000000..28b29de1c196b4c66f393f5fe1c654aab2e9aa76 --- /dev/null +++ b/src/pages/unisfa_door_closed.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Unisfa Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>Unisfa Door Status</h1> + <p>The door is currently closed.</p> + <img + src="/static/door_unisfa_closed.jpg" + width="756" + height="1008" + alt="A plain image of the Unisfa door. There are some posters stuck upon it." + title="A plain image of the Unisfa door. There are some posters stuck upon it." + > + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/unisfa_door_open.html b/src/pages/unisfa_door_open.html new file mode 100644 index 0000000000000000000000000000000000000000..0bb2161c7a529d7a45b59b3ef3247f2eb12b258d --- /dev/null +++ b/src/pages/unisfa_door_open.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Unisfa Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>Unisfa Door Status</h1> + <p>The door is currently open!</p> + <img + src="/static/door_unisfa_open.jpg" + width="756" + height="1008" + alt="The door is open, however the Guardian of the Books blocks your path. Tread carefully..." + title="The door is open, however the Guardian of the Books blocks your path. Tread carefully..." + > + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/unisfa_door_unavail.html b/src/pages/unisfa_door_unavail.html new file mode 100644 index 0000000000000000000000000000000000000000..a445da55c8726a3d7c2796b7a7f5144509132fd5 --- /dev/null +++ b/src/pages/unisfa_door_unavail.html @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>Unisfa Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>Unisfa Door Status</h1> + <p>The door sensor is currently unavailable. Sorry :-(</p> + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/uwaes_door_closed.html b/src/pages/uwaes_door_closed.html new file mode 100644 index 0000000000000000000000000000000000000000..a4424a64e6ab24a71f3c48a7d7d0c92fef2d4635 --- /dev/null +++ b/src/pages/uwaes_door_closed.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UWA Esports Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UWA Esports Door Status</h1> + <p>The door is currently closed.</p> + <img + src="/static/door_uwaes_closed.jpg" + width="756" + height="1008" + alt="Who is it that lies in wait behind the door?" + title="Who is it that lies in wait behind the door?" + > + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/uwaes_door_open.html b/src/pages/uwaes_door_open.html new file mode 100644 index 0000000000000000000000000000000000000000..0fd400eea8e1990d8406546ad30a07dbd2080ba6 --- /dev/null +++ b/src/pages/uwaes_door_open.html @@ -0,0 +1,23 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UWA Esports Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UWA Esports Door Status</h1> + <p>The door is currently open!</p> + <img + src="/static/door_uwaes_open.jpg" + width="756" + height="1008" + alt="It is he, wearer of many hats. Beware!" + title="It is he, wearer of many hats. Beware!" + > + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/pages/uwaes_door_unavail.html b/src/pages/uwaes_door_unavail.html new file mode 100644 index 0000000000000000000000000000000000000000..92c30ab9923aa308b68f165bd838b4fbaad40686 --- /dev/null +++ b/src/pages/uwaes_door_unavail.html @@ -0,0 +1,16 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>UWA Esports Door Status</title> + <link rel="stylesheet" href="/static/style.css"> + </head> + <body> + <p><a href="#light">lightmode</a> <a href="#dark">darkmode</a></p> + <h1>UWA Esports Door Status</h1> + <p>The door sensor is currently unavailable. Sorry :-(</p> + <script src="/static/darkmode.js"></script> + </body> +</html> + +<!-- vim: set ts=2 sts=2 sw=2 et: --> diff --git a/src/start.sh b/src/start.sh new file mode 100644 index 0000000000000000000000000000000000000000..c0901dc24d33ad758ca5a622b499eff7e7e51e52 --- /dev/null +++ b/src/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +bash ./dispense_door_opener_lookup.sh & +node main.js & + +wait -n +exit $? diff --git a/src/static/darkmode.js b/src/static/darkmode.js new file mode 100644 index 0000000000000000000000000000000000000000..619726da046679c7aca196ee4fa90375095b0936 --- /dev/null +++ b/src/static/darkmode.js @@ -0,0 +1,17 @@ +const darkmode = () => { + const DARK_FRAG = "#dark"; + const DARK_CLASS = "dark"; + const rootClasses = document.documentElement.classList; + const hash = window.location.hash; + + if (hash === DARK_FRAG) { + rootClasses.add(DARK_CLASS); + } + else { + rootClasses.remove(DARK_CLASS); + } +}; + +window.onhashchange = darkmode; + +darkmode(); diff --git a/src/static/door_ucc_closed.jpg b/src/static/door_ucc_closed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b1a61e3659fa73af6455bb75e7c9c10cf95ea751 Binary files /dev/null and b/src/static/door_ucc_closed.jpg differ diff --git a/src/static/door_ucc_open.jpg b/src/static/door_ucc_open.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2d66b46a1855a51e75f90cd643352ec57f396c27 Binary files /dev/null and b/src/static/door_ucc_open.jpg differ diff --git a/src/static/door_unisfa_closed.jpg b/src/static/door_unisfa_closed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ad965cfb7d1f4a58a8b331eaad45123d77594db5 Binary files /dev/null and b/src/static/door_unisfa_closed.jpg differ diff --git a/src/static/door_unisfa_open.jpg b/src/static/door_unisfa_open.jpg new file mode 100644 index 0000000000000000000000000000000000000000..efce08c24982abfd97b50994e79bd1c86fa6c451 Binary files /dev/null and b/src/static/door_unisfa_open.jpg differ diff --git a/src/static/door_uwaes_closed.jpg b/src/static/door_uwaes_closed.jpg new file mode 100644 index 0000000000000000000000000000000000000000..53cf3a7d06980354b2797ae30e2fd71554753ad7 Binary files /dev/null and b/src/static/door_uwaes_closed.jpg differ diff --git a/src/static/door_uwaes_open.jpg b/src/static/door_uwaes_open.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3e6c8b8cbcdcfd302a4cee655d9f309ad18bb088 Binary files /dev/null and b/src/static/door_uwaes_open.jpg differ diff --git a/src/static/style.css b/src/static/style.css new file mode 100644 index 0000000000000000000000000000000000000000..2afcdecf7732f1ab7f4e714dbc030ac18dc6d9ff --- /dev/null +++ b/src/static/style.css @@ -0,0 +1,13 @@ +html { + background-color: white; +} + +/* FSCK CHROME FOR MAKING THIS HARDER THAN IT SHOULD BE */ +html.dark { + background-color: black; +} + +html.dark h1, +html.dark p { + color: white; +}