generated from MrSphay/codex-agent-repository-kit
Initial commit
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 3m1s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 3m1s
This commit is contained in:
26
.gitea/workflows/deploy.yml
Normal file
26
.gitea/workflows/deploy.yml
Normal 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
14
Dockerfile
Normal 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
86
app/main.py
Normal 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
29
app/templates/base.html
Normal 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
32
app/templates/form.html
Normal 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
62
app/templates/index.html
Normal 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
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
sqlalchemy
|
||||||
|
jinja2
|
||||||
|
python-multipart
|
||||||
Reference in New Issue
Block a user