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;
+}