Add Docker/Compose setup and fix subpath routing for /toroid deployment

- Add Dockerfile, .dockerignore, and docker-compose.yml to containerize the app
- Add toroid.service systemd unit (uses docker compose)
- Mount presets/ as a bind volume for persistence outside the container
- Fix all fetch() calls in templates to use relative paths (no leading /)
  so they resolve correctly when served under /toroid/ via nginx proxy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brent Perteet
2026-03-02 23:26:27 +00:00
parent 8e51d36a96
commit 9521e0876d
6 changed files with 52 additions and 12 deletions

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.venv/
__pycache__/
*.pyc
*.pyo
.git/
*.bat

12
Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5010
CMD ["python", "app.py"]

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
toroid:
build: .
ports:
- "5010:5010"
volumes:
- ./presets:/app/presets
restart: unless-stopped

View File

@@ -772,7 +772,7 @@ async function runDesign() {
try {
const payload = buildPayload();
const resp = await fetch('/api/design', {
const resp = await fetch('api/design', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
@@ -901,7 +901,7 @@ async function runOperatingPoint() {
constraints: collectConstraints(),
});
const resp = await fetch('/api/simulate', {
const resp = await fetch('api/simulate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
@@ -1001,7 +1001,7 @@ async function runSweep() {
},
});
const resp = await fetch('/api/sweep', {
const resp = await fetch('api/sweep', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
@@ -1463,7 +1463,7 @@ function presetApply(ptype, data) {
async function presetRefresh(ptype) {
const sel = document.getElementById(`preset-sel-${ptype}`);
if (!sel) return null;
const resp = await fetch(`/api/presets/${ptype}`);
const resp = await fetch(`api/presets/${ptype}`);
const data = await resp.json();
if (!data.success) return null;
sel.innerHTML = data.names.length === 0
@@ -1479,7 +1479,7 @@ async function presetLoad(ptype) {
const sel = document.getElementById(`preset-sel-${ptype}`);
const name = sel ? sel.value : '';
if (!name) return;
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`);
const resp = await fetch(`api/presets/${ptype}/${encodeURIComponent(name)}`);
const data = await resp.json();
if (!data.success) { alert('Load failed: ' + data.error); return; }
presetApply(ptype, data.data);
@@ -1493,7 +1493,7 @@ async function presetSave(ptype) {
const name = nameEl ? nameEl.value.trim() : '';
if (!name) { alert('Enter a preset name first.'); return; }
const payload = presetExtract(ptype);
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, {
const resp = await fetch(`api/presets/${ptype}/${encodeURIComponent(name)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
@@ -1511,7 +1511,7 @@ async function presetDelete(ptype) {
const name = sel ? sel.value : '';
if (!name || name === '(no saved presets)') return;
if (!confirm(`Delete preset "${name}"?`)) return;
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, { method: 'DELETE' });
const resp = await fetch(`api/presets/${ptype}/${encodeURIComponent(name)}`, { method: 'DELETE' });
const data = await resp.json();
if (!data.success) { alert('Delete failed: ' + data.error); return; }
await presetRefresh(ptype);
@@ -1523,7 +1523,7 @@ async function initPresets() {
const info = await presetRefresh(ptype);
if (info && info.last) {
// Auto-load last used
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(info.last)}`);
const resp = await fetch(`api/presets/${ptype}/${encodeURIComponent(info.last)}`);
const d = await resp.json();
if (d.success) presetApply(ptype, d.data);
}
@@ -1536,7 +1536,7 @@ async function initPresets() {
(async () => {
// initPresets returns after applying last-used presets (if any)
const windingsInfo = await (async () => {
const r = await fetch('/api/presets/windings');
const r = await fetch('api/presets/windings');
const d = await r.json();
return d.success ? d : null;
})();

View File

@@ -266,7 +266,7 @@
<div class="container">
<div class="header">
<h1>🔧 Manual Simulation</h1>
<a href="/" class="nav-link">⚡ Go to Optimizer</a>
<a href="./" class="nav-link">⚡ Go to Optimizer</a>
</div>
<p class="subtitle">Directly specify tap settings and input voltage to simulate transformer performance</p>
@@ -455,7 +455,7 @@
loading.classList.remove('hidden');
try {
const response = await fetch('/api/simulate', {
const response = await fetch('api/simulate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -523,7 +523,7 @@
// Load transformer info on page load
async function loadTransformerInfo() {
try {
const response = await fetch('/api/transformer_info');
const response = await fetch('api/transformer_info');
const data = await response.json();
// Update tap options based on actual transformer

14
toroid.service Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=Toroid Transformer Designer
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/brent/um-apps/toroid
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target