design final
This commit is contained in:
parent
99a690972e
commit
2d69d919a6
9 changed files with 149 additions and 227 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
78
DEV_SETUP.md
78
DEV_SETUP.md
|
|
@ -1,78 +0,0 @@
|
||||||
# Development Setup Guide
|
|
||||||
|
|
||||||
## Quick Start for Local Development
|
|
||||||
|
|
||||||
The `run_app.py` script mimics Docker production conditions while enabling fast iteration.
|
|
||||||
|
|
||||||
### Standard Development Mode (Recommended)
|
|
||||||
```bash
|
|
||||||
python run_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- ✓ Loads environment variables from `.env` file
|
|
||||||
- ✓ Single Uvicorn worker with **fast auto-reload**
|
|
||||||
- ✓ Mimics Docker environment (`PYTHONUNBUFFERED=1`)
|
|
||||||
- ✓ Creates upload directory automatically
|
|
||||||
- ✓ Binds to `0.0.0.0:8000` (same as Docker)
|
|
||||||
|
|
||||||
**Best for:** Quick code iterations, template changes, debugging
|
|
||||||
|
|
||||||
### Production-Like Mode
|
|
||||||
```bash
|
|
||||||
python run_app.py --gunicorn
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- ✓ Gunicorn with 4 Uvicorn workers (exactly like Docker)
|
|
||||||
- ✓ 120s timeout (same as Docker)
|
|
||||||
- ✓ Gunicorn's reload (slower but tests production server)
|
|
||||||
- ✓ Tests multi-worker behavior
|
|
||||||
|
|
||||||
**Best for:** Testing before deploying to Docker, load testing
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
The script automatically loads `.env` file variables:
|
|
||||||
- `RATE_PER_M2_BLACK` - Black & white print rate (default: 4.0)
|
|
||||||
- `RATE_PER_M2_COLOR` - Color print rate (default: 5.0)
|
|
||||||
- `SERVER_HOSTNAME` - Server hostname (default: einszwovier.local)
|
|
||||||
- `BOOKSTACK_PORT`, `OPENWEBUI_PORT`, `PORTAINER_PORT` - Service ports
|
|
||||||
- `MATRIX_USER`, `MATRIX_PASS`, `MATRIX_HOMESERVER` - Matrix integration
|
|
||||||
|
|
||||||
## Comparison: Dev vs Docker
|
|
||||||
|
|
||||||
| Feature | `python run_app.py` | `docker-compose up` |
|
|
||||||
|---------|---------------------|---------------------|
|
|
||||||
| Server | Uvicorn (single worker) | Gunicorn + Uvicorn (4 workers) |
|
|
||||||
| Auto-reload | ✓ Fast (watches all files) | ✓ Slow (Docker rebuild) |
|
|
||||||
| Env loading | `.env` file | `.env` file |
|
|
||||||
| Port | 8000 | 80→8000 |
|
|
||||||
| Memory limit | None | 1GB limit |
|
|
||||||
| Health checks | No | Yes |
|
|
||||||
| Dependencies | All services | Isolated |
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### `.env` file not found
|
|
||||||
Create `.env` file from template:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port 8000 already in use
|
|
||||||
```bash
|
|
||||||
# Find and kill process
|
|
||||||
lsof -ti:8000 | xargs kill -9
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment variables not loading
|
|
||||||
Ensure `python-dotenv` is installed:
|
|
||||||
```bash
|
|
||||||
pip install python-dotenv
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Matrix integration locally
|
|
||||||
1. Start Matrix server: `docker-compose up synapse -d`
|
|
||||||
2. Run app: `python run_app.py`
|
|
||||||
3. Submit test order via `/cost` endpoint
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
services:
|
|
||||||
bookstack:
|
|
||||||
image: lscr.io/linuxserver/bookstack:v25.07.2-ls220
|
|
||||||
container_name: bookstack
|
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- APP_URL=http://einszwovier.local:6875
|
|
||||||
- APP_KEY=base64:3qjlIoUX4Tw6fUQgZcxMbz6lb8+dAzqpvItqHvahW1c=
|
|
||||||
- DB_HOST=bookstack-mariadb
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_DATABASE=bookstack
|
|
||||||
- DB_USERNAME=bookstack
|
|
||||||
- DB_PASSWORD=bookstack8432
|
|
||||||
volumes:
|
|
||||||
- ./bookstack_app_data:/config
|
|
||||||
ports:
|
|
||||||
- 6875:80
|
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
|
||||||
- bookstack-mariadb
|
|
||||||
|
|
||||||
bookstack-mariadb:
|
|
||||||
image: lscr.io/linuxserver/mariadb:11.4.4
|
|
||||||
container_name: bookstack-mariadb
|
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- MYSQL_ROOT_PASSWORD=mysupersecretrootpassword
|
|
||||||
- MYSQL_DATABASE=bookstack
|
|
||||||
- MYSQL_USER=bookstack
|
|
||||||
- MYSQL_PASSWORD=bookstack8432
|
|
||||||
volumes:
|
|
||||||
- ./bookstack_db_data:/config
|
|
||||||
restart: unless-stopped
|
|
||||||
3
main.py
3
main.py
|
|
@ -101,7 +101,8 @@ async def about(request: Request):
|
||||||
|
|
||||||
return templates.TemplateResponse("about.html", {
|
return templates.TemplateResponse("about.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
"courses": courses
|
"courses": courses,
|
||||||
|
"bookstack_url": f"http://{SERVER_HOSTNAME}:{BOOKSTACK_PORT}"
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.get("/cost", response_class=HTMLResponse)
|
@app.get("/cost", response_class=HTMLResponse)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
services:
|
|
||||||
synapse:
|
|
||||||
image: matrixdotorg/synapse:latest
|
|
||||||
container_name: synapse
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "8008:8008" # client-server API (internal)
|
|
||||||
volumes:
|
|
||||||
- ./matrix/data:/data
|
|
||||||
environment:
|
|
||||||
- SYNAPSE_SERVER_NAME=localhost
|
|
||||||
- SYNAPSE_REPORT_STATS=no
|
|
||||||
|
|
@ -616,6 +616,109 @@ th {
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-left: 4px solid #1976d2;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5em;
|
||||||
|
margin: 1.5em 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box a {
|
||||||
|
color: #1976d2;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pricing Card - Prominent pricing display */
|
||||||
|
.pricing-card {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border: 2px solid #28a745;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2em;
|
||||||
|
margin: 2em 0;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-card h2 {
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.bw {
|
||||||
|
border-top: 4px solid #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.color {
|
||||||
|
border-top: 4px solid #fd7e14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #1C1C1E;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: 'HealTheWebB', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pricing-note {
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0;
|
||||||
|
padding-top: 1em;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
.disclaimer-alert {
|
.disclaimer-alert {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
<p>Seit Dezember 2024 trägt unser Maker Space den Namen <strong>studio einszwovier</strong>. Er ist ein
|
<p>Seit Dezember 2024 trägt unser Maker Space den Namen <strong>studio einszwovier</strong>. Er ist ein
|
||||||
innovativer, digitaler Lernraum, der Kreativität, Technik und Bildungsgerechtigkeit verbindet. Hier wird
|
innovativer, digitaler Lernraum, der Kreativität, Technik und Bildungsgerechtigkeit verbindet. Hier wird
|
||||||
„Making“ erlebbar: Lernende gestalten ihren Lernprozess aktiv, entdecken individuelle Stärken und erleben
|
„Making“ erlebbar: Lernende gestalten ihren Lernprozess aktiv, entdecken individuelle Stärken und erleben
|
||||||
durch Selbstwirksamkeit besondere Motivation.</p>
|
durch Selbstwirksamkeit besondere Motivation.
|
||||||
|
Betreut wird der Maker Space von <strong>Aron Petau</strong> und <strong>Friedrich Weber</strong>. Einfach vorbeischauen, Ideen vorstellen und loslegen!
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -27,25 +29,24 @@
|
||||||
<li><strong>Drohnen:</strong> Flugexperimente und Luftbildfotografie.</li>
|
<li><strong>Drohnen:</strong> Flugexperimente und Luftbildfotografie.</li>
|
||||||
<li><strong>LEGO SPIKE Roboter:</strong> Spielerisches Erlernen von Robotik und Programmierung.</li>
|
<li><strong>LEGO SPIKE Roboter:</strong> Spielerisches Erlernen von Robotik und Programmierung.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
Wenn du mehr über unsere Ausstattung erfahren möchtest oder spezielle Geräte suchst, schau in unsere
|
||||||
|
<a href="{{ bookstack_url }}" target="_blank">Wissenssammlung</a> oder frag uns direkt im Studio!
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Betreuungsteam</h2>
|
<h2>Öffnungszeiten + Kontakt</h2>
|
||||||
<p>Betreut wird der Maker Space von <strong>Aron Petau</strong> und <strong>Friedrich Weber</strong>. Sie sind
|
|
||||||
von Dienstag bis Donnerstag von 11:00 bis 16:00 Uhr vor Ort. Einfach vorbeischauen, Ideen vorstellen und loslegen!
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Öffnungszeiten</h2>
|
|
||||||
<p>Dienstag bis Donnerstag: 11:00 – 16:00 Uhr<br>
|
<p>Dienstag bis Donnerstag: 11:00 – 16:00 Uhr<br>
|
||||||
Raum 124, Gabriele-von-Bülow-Gymnasium</p>
|
Raum 124, Gabriele-von-Bülow-Gymnasium</p>
|
||||||
|
<p>E-Mail: <a href="mailto:einszwovier@gvb-gymnasium.de">einszwovier@gvb-gymnasium.de</a></p>
|
||||||
|
<h2>Standort</h2>
|
||||||
|
<p>Gabriele-von-Bülow-Gymnasium<br>
|
||||||
|
Tile-Brügge-Weg 63, 13509 Berlin (Tegel)<br>
|
||||||
|
Telefon: 030 21 00 52 460<br>
|
||||||
|
E-Mail: <a href="mailto:info@gvb-gymnasium.de">info@gvb-gymnasium.de</a></p>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Kontakt</h2>
|
|
||||||
<p>E-Mail: <a href="mailto:einszwovier@gvb-gymnasium.de">einszwovier@gvb-gymnasium.de</a></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Aktuelle Kurse</h2>
|
<h2>Aktuelle Kurse</h2>
|
||||||
|
|
@ -114,13 +115,5 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
|
||||||
<h2>Standort</h2>
|
|
||||||
<p>Gabriele-von-Bülow-Gymnasium<br>
|
|
||||||
Tile-Brügge-Weg 63, 13509 Berlin (Tegel)<br>
|
|
||||||
Telefon: 030 21 00 52 460<br>
|
|
||||||
E-Mail: <a href="mailto:info@gvb-gymnasium.de">info@gvb-gymnasium.de</a></p>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -8,6 +8,36 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Kostenrechner für Drucke</h1>
|
<h1>Kostenrechner für Drucke</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p>
|
||||||
|
<strong>So funktioniert's:</strong> Lade dein PDF hoch, um eine Kostenschätzung zu erhalten.
|
||||||
|
Du kannst beliebige PDFs hochladen, um die Kosten zu berechnen.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Wichtig:</strong> Schicke den Auftrag nur ab, wenn du die Datei wirklich drucken lassen möchtest!
|
||||||
|
Für reine Kostenschätzung reicht es, die Berechnung anzusehen, ohne den Auftrag zu senden.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Alternative:</strong> Du kannst uns deine Datei und Infos auch per E-Mail schicken:
|
||||||
|
<a href="mailto:einszwovier@gvb-gymnasium.de">einszwovier@gvb-gymnasium.de</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h2>📊 Aktuelle Preise</h2>
|
||||||
|
<div class="price-grid">
|
||||||
|
<div class="price-item bw">
|
||||||
|
<span class="price-label">Schwarz/Weiß</span>
|
||||||
|
<span class="price-value">{{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} €/m²</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-item color">
|
||||||
|
<span class="price-label">Farbe</span>
|
||||||
|
<span class="price-value">{{ rate_color if rate_color else 'RATE_PER_M2_COLOR' }} €/m²</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="pricing-note">💡 Die Preise decken nur Materialkosten(Tinte & Papier) – kein Gewinn!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<p class="error">{{ error }}</p>
|
<p class="error">{{ error }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -17,12 +47,6 @@
|
||||||
<button type="submit">Hochladen & Berechnen</button>
|
<button type="submit">Hochladen & Berechnen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="rate-info">
|
|
||||||
Preise werden über Umgebungsvariablen festgelegt:<br>
|
|
||||||
S/W: {{ rate_black if rate_black else 'RATE_PER_M2_BLACK' }} € / m²,
|
|
||||||
Farbe: {{ rate_color if rate_color else 'RATE_PER_M2_COLOR' }} € / m²
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if result %}
|
{% if result %}
|
||||||
<h2>Ergebnisse für {{ result.filename }}</h2>
|
<h2>Ergebnisse für {{ result.filename }}</h2>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import os
|
|
||||||
import asyncio
|
|
||||||
from io import BytesIO
|
|
||||||
from nio import AsyncClient, UploadResponse, RoomSendResponse
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
# Get credentials from environment (adjust if needed)
|
|
||||||
matrix_user = os.environ.get("MATRIX_USER", "@einszwovier_bot:localhost")
|
|
||||||
matrix_pass = os.environ.get("MATRIX_PASS")
|
|
||||||
homeserver = os.environ.get("MATRIX_HOMESERVER", "http://localhost:8008")
|
|
||||||
room_id = os.environ.get("MATRIX_ROOM") # e.g. "!abc123:localhost"
|
|
||||||
|
|
||||||
if not all([matrix_user, matrix_pass, room_id]):
|
|
||||||
raise RuntimeError("Missing MATRIX_USER, MATRIX_PASS or MATRIX_ROOM")
|
|
||||||
|
|
||||||
client = AsyncClient(homeserver, matrix_user)
|
|
||||||
login_resp = await client.login(matrix_pass)
|
|
||||||
if getattr(login_resp, "access_token", None) is None:
|
|
||||||
print("❌ Login failed:", login_resp)
|
|
||||||
return
|
|
||||||
|
|
||||||
print("✅ Logged in as", matrix_user)
|
|
||||||
|
|
||||||
pdf_path = "data/uploads/einszwovier infographics 2.pdf" # <-- put any small PDF here
|
|
||||||
with open(pdf_path, "rb") as f:
|
|
||||||
pdf_bytes = f.read()
|
|
||||||
|
|
||||||
# ✅ Upload PDF (nio returns (resp, err))
|
|
||||||
upload_resp, upload_err = await client.upload(
|
|
||||||
data_provider=BytesIO(pdf_bytes),
|
|
||||||
content_type="application/pdf",
|
|
||||||
filename=os.path.basename(pdf_path),
|
|
||||||
filesize=len(pdf_bytes),
|
|
||||||
)
|
|
||||||
|
|
||||||
if upload_err:
|
|
||||||
print("❌ Upload error:", upload_err)
|
|
||||||
await client.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(upload_resp, UploadResponse) and upload_resp.content_uri:
|
|
||||||
print("✅ Upload succeeded:", upload_resp.content_uri)
|
|
||||||
else:
|
|
||||||
print("❌ Upload failed:", upload_resp)
|
|
||||||
await client.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send file message to room
|
|
||||||
file_resp = await client.room_send(
|
|
||||||
room_id=room_id,
|
|
||||||
message_type="m.room.message",
|
|
||||||
content={
|
|
||||||
"msgtype": "m.file",
|
|
||||||
"body": os.path.basename(pdf_path),
|
|
||||||
"url": upload_resp.content_uri,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(file_resp, RoomSendResponse) and file_resp.event_id:
|
|
||||||
print("✅ PDF sent to room", room_id)
|
|
||||||
else:
|
|
||||||
print("❌ Failed to send PDF:", file_resp)
|
|
||||||
|
|
||||||
await client.logout()
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue