diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d485eb0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/__pycache__/
+/static/
+/venv/
+/pythagoras.tar.xz
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c97fea2
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+.PHONY: default
+default: run
+
+include client/make.mk
+
+.PHONY: run
+run: $(CLIENT_TARGETS) venv
+ source venv/bin/activate && python main.py
+
+.PHONY: build
+build: $(CLIENT_TARGETS)
+
+.PHONY: pack
+pack: pythagoras.tar.xz
+
+.PHONY: clean
+clean: client_clean
+ rm -rf __pycache__
+ rm -rf venv
+ rm -f pythagoras.tar.xz
+
+venv:
+ python -m venv venv
+ source venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt
+
+pythagoras.tar.xz: main.py $(CLIENT_TARGETS)
+ tar --transform='s|^|pythagoras/|' -Jcvf $@ $^
diff --git a/README.md b/README.md
index c10968d..164f168 100644
--- a/README.md
+++ b/README.md
@@ -10,19 +10,29 @@ Clone the repository:
Install the dependencies:
-`cd pythagoras`
-
-`python -m venv venv`
-
-`source venv/bin/activate`
-
-`pip install -r requirements.txt`
+`sudo pacman -S python-virtualenv`
## Running the app
-Simply run the main Python file to start the server:
+Simply execute the **run** recipe in the Makefile to start the server:
- `python main.py`
+`make run`
+
+Note that run is also the default recipe. So `make` works too.
+
+The run recipe should take care of everything for you... enjoy!
+
+## Cleaning up the mess
+
+If you wish to go back to a clean slate just run:
+
+`make clean`
+
+## Packing for production
+
+To make an archive `pythagoras.tar.xz` in project root simply run:
+
+`make pack`
## Usage
diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..2ccbe46
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1 @@
+/node_modules/
diff --git a/client/bun.lockb b/client/bun.lockb
new file mode 100755
index 0000000..f9c0643
Binary files /dev/null and b/client/bun.lockb differ
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..554c142
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/make.mk b/client/make.mk
new file mode 100644
index 0000000..d40bcaa
--- /dev/null
+++ b/client/make.mk
@@ -0,0 +1,33 @@
+CLIENT_PAGES := index.html
+CLIENT_STYLES := style.css
+CLIENT_SCRIPTS := script.js
+CLIENT_TARGETS := $(CLIENT_PAGES:%=static/%) \
+ $(CLIENT_STYLES:%=static/%) \
+ $(CLIENT_SCRIPTS:%=static/%)
+
+.PHONY: client_clean
+client_clean:
+ rm -rf static
+ rm -rf client/node_modules
+
+client/node_modules:
+ cd client && bun install
+
+static/%.html: client/%.html client/node_modules
+ @mkdir -p $(@D)
+ cat $< | \
+ bun run --cwd client html-minifier \
+ --collapse-inline-tag-whitespace \
+ --collapse-boolean-attributes \
+ --collapse-whitespace \
+ --remove-attribute-quotes \
+ --remove-comments \
+ --remove-redundant-attributes > $@
+
+static/%.css: client/%.scss client/node_modules
+ @mkdir -p $(@D)
+ bun run --cwd client sass $(notdir $<) --style compressed > $@
+
+static/%.js: client/%.ts client/node_modules
+ @mkdir -p $(@D)
+ bun build $< --minify --outfile $@
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..1ef9211
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "client",
+ "type": "module",
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "html-minifier": "^4.0.0",
+ "sass": "^1.87.0"
+ }
+}
diff --git a/client/script.ts b/client/script.ts
new file mode 100644
index 0000000..e69de29
diff --git a/client/style.scss b/client/style.scss
new file mode 100644
index 0000000..e69de29
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/main.py b/main.py
index eb86375..191e344 100644
--- a/main.py
+++ b/main.py
@@ -1,8 +1,9 @@
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect, BackgroundTasks
-from fastapi.responses import JSONResponse
+from fastapi.responses import HTMLResponse, JSONResponse
import logging
import uvicorn
from typing import Dict, List, Any
+from dataclasses import dataclass
import json
import httpx
import asyncio
@@ -43,6 +44,19 @@ class ConnectionManager:
manager = ConnectionManager()
+# Static files
+def read_file(filepath: str) -> str:
+ with open(filepath, "r", encoding="utf-8") as f:
+ return f.read()
+
+@dataclass
+class StaticFiles:
+ index_html: str = read_file("static/index.html")
+
+@app.get("/presentation/")
+async def presentation_index(_: Request):
+ return HTMLResponse(status_code=200, content=StaticFiles.index_html)
+
# Endpoints
@app.post("/control")
async def control_endpoint(request: Request):