Initial commit
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 3m1s

This commit is contained in:
Odysseus
2026-06-06 12:17:30 +00:00
commit 9800c0bfb6
7 changed files with 254 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
name: Build and Push Docker Image
on:
push:
branches: [ "main" ]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Log in to Gitea Registry
uses: docker/login-action@v2
with:
registry: registry.git.wilkensxl.de
username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: registry.git.wilkensxl.de/mrsphay/mobilemanager:latest

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ .
RUN mkdir data
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

86
app/main.py Normal file
View File

@@ -0,0 +1,86 @@
import os
from fastapi import FastAPI, Request, Form, Depends, HTTPException, Response
from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
# Database Setup
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./data/contracts.db")
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
class Contract(Base):
__tablename__ = "contracts"
id = Column(Integer, primary_key=True, index=True)
provider = Column(String)
phone_number = Column(String)
pin = Column(String)
puk = Column(String)
monthly_cost = Column(Float)
Base.metadata.create_all(bind=engine)
app = FastAPI()
templates = Jinja2Templates(directory="templates")
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/")
def list_contracts(request: Request, db: Session = Depends(get_db)):
contracts = db.query(Contract).all()
return templates.TemplateResponse("index.html", {"request": request, "contracts": contracts})
@app.get("/add")
def add_form(request: Request):
return templates.TemplateResponse("form.html", {"request": request, "action": "add"})
@app.post("/add")
def add_contract(
provider: str = Form(...),
phone_number: str = Form(...),
pin: str = Form(...),
puk: str = Form(...),
monthly_cost: float = Form(...),
db: Session = Depends(get_db)
):
new_contract = Contract(
provider=provider, phone_number=phone_number,
pin=pin, puk=puk, monthly_cost=monthly_cost
)
db.add(new_contract)
db.commit()
return RedirectResponse(url="/", status_code=303)
@app.get("/edit/{contract_id}")
def edit_form(request: Request, contract_id: int, db: Session = Depends(get_db)):
contract = db.query(Contract).filter(Contract.id == contract_id).first()
if not contract: raise HTTPException(status_code=404)
return templates.TemplateResponse("form.html", {"request": request, "contract": contract, "action": "edit"})
@app.post("/edit/{contract_id}")
def edit_contract(
contract_id: int, provider: str = Form(...), phone_number: str = Form(...),
pin: str = Form(...), puk: str = Form(...), monthly_cost: float = Form(...),
db: Session = Depends(get_db)
):
contract = db.query(Contract).filter(Contract.id == contract_id).first()
if not contract: raise HTTPException(status_code=404)
contract.provider, contract.phone_number, contract.pin, contract.puk, contract.monthly_cost = provider, phone_number, pin, puk, monthly_cost
db.commit()
return RedirectResponse(url="/", status_code=303)
@app.get("/delete/{contract_id}")
def delete_contract(contract_id: int, db: Session = Depends(get_db)):
contract = db.query(Contract).filter(Contract.id == contract_id).first()
if contract:
db.delete(contract)
db.commit()
return RedirectResponse(url="/", status_code=303)

29
app/templates/base.html Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MobileManager</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.pin-puk-hidden { display: none; }
</style>
</head>
<body class="bg-gray-100 font-sans">
<nav class="bg-blue-600 p-4 text-white shadow-lg">
<div class="container mx-auto flex justify-between items-center">
<a href="/" class="text-xl font-bold">📱 MobileManager</a>
<a href="/add" class="bg-white text-blue-600 px-4 py-2 rounded font-semibold">+ Neuer Vertrag</a>
</div>
</nav>
<main class="container mx-auto mt-8">
{% block content %}{% endblock %}
</main>
<script>
function toggleSecret(id) {
const el = document.getElementById(id);
el.classList.toggle('pin-puk-hidden');
}
</script>
</body>
</html>

32
app/templates/form.html Normal file
View File

@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<div class="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
<h2 class="text-2xl font-bold mb-6 text-center">{{ "Bearbeiten" if action == "edit" else "Neuer Vertrag" }}</h2>
<form action="{{ url_for('add_contract' if action == 'add' else 'edit_contract', contract_id=contract.id if action == 'edit' else '') }}" method="POST" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Provider</label>
<input type="text" name="provider" value="{{ contract.provider if action == 'edit' else '' }}" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Telefonnummer</label>
<input type="text" name="phone_number" value="{{ contract.phone_number if action == 'edit' else '' }}" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">PIN (Verschlüsselt)</label>
<input type="text" name="pin" value="{{ contract.pin if action == 'edit' else '' }}" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">PUK (Verschlüsselt)</label>
<input type="text" name="puk" value="{{ contract.puk if action == 'edit' else '' }}" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Monatliche Kosten (€)</label>
<input type="number" step="0.01" name="monthly_cost" value="{{ contract.monthly_cost if action == 'edit' else '' }}" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 border p-2">
</div>
<div class="flex items-center justify-between mt-6">
<button type="button" onclick="window.history.back()" class="text-gray-600 hover:underline">Abbrechen</button>
<button type="submit" class="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 transition">Speichern</button>
</div>
</form>
</div>
{% endblock %}

62
app/templates/index.html Normal file
View File

@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block content %}
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-800">Meine Verträge</h1>
<div class="space-x-2">
<button onclick="toggleAllSecrets()" class="bg-gray-500 text-white px-4 py-2 rounded">PIN/PUK zeigen</button>
<a href="/add" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition">+ Neuer Vertrag</a>
</div>
</div>
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<table class="w-full text-left border-collapse">
<thead class="bg-gray-50 border-b">
<tr>
<th class="p-4 text-gray-600">Provider</th>
<th class="p-4 text-gray-600">Nummer</th>
<th class="p-4 text-gray-600">PIN</th>
<th class="p-4 text-gray-600">PUK</th>
<th class="p-4 text-gray-600">Kosten</th>
<th class="p-4 text-gray-600">Aktionen</th>
</tr>
</thead>
<tbody>
{% for c in contracts %}
<tr class="border-b hover:bg-gray-50 transition">
<td class="p-4 font-medium">{{ c.provider }}</td>
<td class="p-4">{{ c.phone_number }}</td>
<td class="p-4"><span class="secret-field bg-gray-200 px-2 rounded">****</span></td>
<td class="p-4"><span class="secret-field bg-gray-200 px-2 rounded">****</span></td>
<td class="p-4">€{{ "%.2f"|format(c.monthly_cost) }}</td>
<td class="p-4">
<a href="/edit/{{ c.id }}" class="text-blue-600 hover:underline mr-2">Bearbeiten</a>
<a href="/delete/{{ c.id }}" class="text-red-600 hover:underline" onclick="return confirm('Wirklich löschen?')">Löschen</a>
</td>
</tr>
{% endfor %}
{% if not contracts %}
<tr>
<td colspan="6" class="p-8 text-center text-gray-500">Keine Verträge vorhanden.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<script>
function toggleAllSecrets() {
const elements = document.querySelectorAll('.secret-field');
elements.forEach(el => {
if (el.innerText === '****') {
// In a real app, you'd fetch the actual values via API or pass them in the template
// For simplicity in this demo, we'll just toggle visibility of a data-attribute
el.innerText = el.dataset.value || '****';
el.classList.replace('bg-gray-200', 'bg-yellow-100');
} else {
el.innerText = '****';
el.classList.replace('bg-yellow-100', 'bg-gray-200');
}
});
}
</script>
{% endblock %}

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn
sqlalchemy
jinja2
python-multipart