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:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.git/
|
||||||
|
*.bat
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
8
docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
services:
|
||||||
|
toroid:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5010:5010"
|
||||||
|
volumes:
|
||||||
|
- ./presets:/app/presets
|
||||||
|
restart: unless-stopped
|
||||||
@@ -772,7 +772,7 @@ async function runDesign() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
const resp = await fetch('/api/design', {
|
const resp = await fetch('api/design', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -901,7 +901,7 @@ async function runOperatingPoint() {
|
|||||||
constraints: collectConstraints(),
|
constraints: collectConstraints(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const resp = await fetch('/api/simulate', {
|
const resp = await fetch('api/simulate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload),
|
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',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -1463,7 +1463,7 @@ function presetApply(ptype, data) {
|
|||||||
async function presetRefresh(ptype) {
|
async function presetRefresh(ptype) {
|
||||||
const sel = document.getElementById(`preset-sel-${ptype}`);
|
const sel = document.getElementById(`preset-sel-${ptype}`);
|
||||||
if (!sel) return null;
|
if (!sel) return null;
|
||||||
const resp = await fetch(`/api/presets/${ptype}`);
|
const resp = await fetch(`api/presets/${ptype}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (!data.success) return null;
|
if (!data.success) return null;
|
||||||
sel.innerHTML = data.names.length === 0
|
sel.innerHTML = data.names.length === 0
|
||||||
@@ -1479,7 +1479,7 @@ async function presetLoad(ptype) {
|
|||||||
const sel = document.getElementById(`preset-sel-${ptype}`);
|
const sel = document.getElementById(`preset-sel-${ptype}`);
|
||||||
const name = sel ? sel.value : '';
|
const name = sel ? sel.value : '';
|
||||||
if (!name) return;
|
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();
|
const data = await resp.json();
|
||||||
if (!data.success) { alert('Load failed: ' + data.error); return; }
|
if (!data.success) { alert('Load failed: ' + data.error); return; }
|
||||||
presetApply(ptype, data.data);
|
presetApply(ptype, data.data);
|
||||||
@@ -1493,7 +1493,7 @@ async function presetSave(ptype) {
|
|||||||
const name = nameEl ? nameEl.value.trim() : '';
|
const name = nameEl ? nameEl.value.trim() : '';
|
||||||
if (!name) { alert('Enter a preset name first.'); return; }
|
if (!name) { alert('Enter a preset name first.'); return; }
|
||||||
const payload = presetExtract(ptype);
|
const payload = presetExtract(ptype);
|
||||||
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, {
|
const resp = await fetch(`api/presets/${ptype}/${encodeURIComponent(name)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -1511,7 +1511,7 @@ async function presetDelete(ptype) {
|
|||||||
const name = sel ? sel.value : '';
|
const name = sel ? sel.value : '';
|
||||||
if (!name || name === '(no saved presets)') return;
|
if (!name || name === '(no saved presets)') return;
|
||||||
if (!confirm(`Delete preset "${name}"?`)) 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();
|
const data = await resp.json();
|
||||||
if (!data.success) { alert('Delete failed: ' + data.error); return; }
|
if (!data.success) { alert('Delete failed: ' + data.error); return; }
|
||||||
await presetRefresh(ptype);
|
await presetRefresh(ptype);
|
||||||
@@ -1523,7 +1523,7 @@ async function initPresets() {
|
|||||||
const info = await presetRefresh(ptype);
|
const info = await presetRefresh(ptype);
|
||||||
if (info && info.last) {
|
if (info && info.last) {
|
||||||
// Auto-load last used
|
// 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();
|
const d = await resp.json();
|
||||||
if (d.success) presetApply(ptype, d.data);
|
if (d.success) presetApply(ptype, d.data);
|
||||||
}
|
}
|
||||||
@@ -1536,7 +1536,7 @@ async function initPresets() {
|
|||||||
(async () => {
|
(async () => {
|
||||||
// initPresets returns after applying last-used presets (if any)
|
// initPresets returns after applying last-used presets (if any)
|
||||||
const windingsInfo = await (async () => {
|
const windingsInfo = await (async () => {
|
||||||
const r = await fetch('/api/presets/windings');
|
const r = await fetch('api/presets/windings');
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
return d.success ? d : null;
|
return d.success ? d : null;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -266,7 +266,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>🔧 Manual Simulation</h1>
|
<h1>🔧 Manual Simulation</h1>
|
||||||
<a href="/" class="nav-link">⚡ Go to Optimizer</a>
|
<a href="./" class="nav-link">⚡ Go to Optimizer</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="subtitle">Directly specify tap settings and input voltage to simulate transformer performance</p>
|
<p class="subtitle">Directly specify tap settings and input voltage to simulate transformer performance</p>
|
||||||
|
|
||||||
@@ -455,7 +455,7 @@
|
|||||||
loading.classList.remove('hidden');
|
loading.classList.remove('hidden');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/simulate', {
|
const response = await fetch('api/simulate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -523,7 +523,7 @@
|
|||||||
// Load transformer info on page load
|
// Load transformer info on page load
|
||||||
async function loadTransformerInfo() {
|
async function loadTransformerInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/transformer_info');
|
const response = await fetch('api/transformer_info');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Update tap options based on actual transformer
|
// Update tap options based on actual transformer
|
||||||
|
|||||||
14
toroid.service
Normal file
14
toroid.service
Normal 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
|
||||||
Reference in New Issue
Block a user