dockerize app
This commit is contained in:
parent
89bffd2ee3
commit
d5da62d800
|
|
@ -0,0 +1,8 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
queue.db
|
||||||
|
*.db
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
FLASK_ENV=production \
|
||||||
|
FLASK_APP=app.py
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN mkdir -p /data
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
ENV DB_PATH=/data/queue.db
|
||||||
|
|
||||||
|
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app", "--workers", "3", "--worker-class", "gthread", "--threads", "4"]
|
||||||
79
app.py
79
app.py
|
|
@ -2,9 +2,13 @@ from flask import Flask, render_template, request, redirect, url_for, g, jsonify
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
|
|
||||||
DB = "queue.db"
|
DB = os.environ.get("DB_PATH", "queue.db")
|
||||||
|
ADMIN_SUFFIX = os.environ.get("ADMIN_URL", "admin").strip("/")
|
||||||
|
ADMIN_PATH = f"/{ADMIN_SUFFIX}" if ADMIN_SUFFIX else "/admin"
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = getattr(g, "_db", None)
|
db = getattr(g, "_db", None)
|
||||||
if db is None:
|
if db is None:
|
||||||
|
|
@ -15,40 +19,51 @@ def get_db():
|
||||||
init_db(db)
|
init_db(db)
|
||||||
return db
|
return db
|
||||||
|
|
||||||
|
|
||||||
def init_db(db):
|
def init_db(db):
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute("""CREATE TABLE items (
|
cur.execute(
|
||||||
|
"""CREATE TABLE items (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
number TEXT,
|
number TEXT,
|
||||||
status TEXT -- 'waiting', 'called', 'done'
|
status TEXT -- 'waiting', 'called', 'done'
|
||||||
)""")
|
)"""
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
def close_db(exc):
|
def close_db(exc):
|
||||||
db = getattr(g, "_db", None)
|
db = getattr(g, "_db", None)
|
||||||
if db:
|
if db:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
# Main display for clients
|
# Main display for clients
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
cur = db.execute("SELECT number FROM items WHERE status='called' ORDER BY id DESC LIMIT 1")
|
cur = db.execute(
|
||||||
|
"SELECT number FROM items WHERE status='called' ORDER BY id DESC LIMIT 1"
|
||||||
|
)
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
current = row["number"] if row else ""
|
current = row["number"] if row else ""
|
||||||
return render_template("index.html", current=current)
|
return render_template("index.html", current=current)
|
||||||
|
|
||||||
|
|
||||||
# API endpoint used by clients to poll current number (JSON)
|
# API endpoint used by clients to poll current number (JSON)
|
||||||
@app.route("/current")
|
@app.route("/current")
|
||||||
def current_api():
|
def current_api():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
cur = db.execute("SELECT number FROM items WHERE status='called' ORDER BY id DESC LIMIT 1")
|
cur = db.execute(
|
||||||
row = cur.fetchone()
|
"SELECT number FROM items WHERE status='called' ORDER BY id"
|
||||||
return jsonify(current=(row["number"] if row else ""))
|
).fetchall()
|
||||||
|
nums = [r["number"] for r in cur]
|
||||||
|
return jsonify(current=nums)
|
||||||
|
|
||||||
|
|
||||||
# Admin UI: start/reset system by providing count of tickets (1..N)
|
# Admin UI: start/reset system by providing count of tickets (1..N)
|
||||||
@app.route("/admin", methods=["GET", "POST"])
|
# @app.route("/admin", methods=["GET", "POST"])
|
||||||
def admin():
|
def admin():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
@ -56,30 +71,56 @@ def admin():
|
||||||
if action == "start":
|
if action == "start":
|
||||||
n = int(request.form.get("count", "0"))
|
n = int(request.form.get("count", "0"))
|
||||||
db.execute("DELETE FROM items")
|
db.execute("DELETE FROM items")
|
||||||
for i in range(1, n+1):
|
for i in range(1, n + 1):
|
||||||
db.execute("INSERT INTO items (number, status) VALUES (?, 'waiting')", (str(i),))
|
db.execute(
|
||||||
|
"INSERT INTO items (number, status) VALUES (?, 'waiting')",
|
||||||
|
(str(i),),
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
elif action == "call":
|
elif action == "call":
|
||||||
num = request.form.get("num")
|
num = request.form.get("num")
|
||||||
# mark chosen number as called
|
# mark chosen number as called
|
||||||
db.execute("UPDATE items SET status='called' WHERE number=? AND status='waiting'", (num,))
|
db.execute(
|
||||||
|
"UPDATE items SET status='called' WHERE number=? AND status='waiting'",
|
||||||
|
(num,),
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
elif action == "done":
|
elif action == "done":
|
||||||
num = request.form.get("num")
|
num = request.form.get("num")
|
||||||
db.execute("UPDATE items SET status='done' WHERE number=? AND status='called'", (num,))
|
db.execute(
|
||||||
|
"UPDATE items SET status='done' WHERE number=? AND status='called'",
|
||||||
|
(num,),
|
||||||
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
elif action == "reset":
|
elif action == "reset":
|
||||||
db.execute("DELETE FROM items")
|
db.execute("DELETE FROM items")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
elif action == "return":
|
||||||
|
num = request.form.get("num")
|
||||||
|
if num:
|
||||||
|
# ustaw ponownie na 'waiting' tylko jeśli aktualnie było 'called'
|
||||||
|
db.execute("UPDATE items SET status='waiting' WHERE number=? AND status='called'", (num,))
|
||||||
|
db.commit()
|
||||||
return redirect(url_for("admin"))
|
return redirect(url_for("admin"))
|
||||||
|
|
||||||
cur_wait = db.execute("SELECT number FROM items WHERE status='waiting' ORDER BY id").fetchall()
|
cur_wait = db.execute(
|
||||||
cur_called = db.execute("SELECT number FROM items WHERE status='called' ORDER BY id").fetchall()
|
"SELECT number FROM items WHERE status='waiting' ORDER BY id"
|
||||||
cur_done = db.execute("SELECT number FROM items WHERE status='done' ORDER BY id").fetchall()
|
).fetchall()
|
||||||
return render_template("admin.html",
|
cur_called = db.execute(
|
||||||
waiting=[r["number"] for r in cur_wait],
|
"SELECT number FROM items WHERE status='called' ORDER BY id"
|
||||||
called=[r["number"] for r in cur_called],
|
).fetchall()
|
||||||
done=[r["number"] for r in cur_done])
|
cur_done = db.execute(
|
||||||
|
"SELECT number FROM items WHERE status='done' ORDER BY id"
|
||||||
|
).fetchall()
|
||||||
|
return render_template(
|
||||||
|
"admin.html",
|
||||||
|
waiting=[r["number"] for r in cur_wait],
|
||||||
|
called=[r["number"] for r in cur_called],
|
||||||
|
done=[r["number"] for r in cur_done],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_url_rule(ADMIN_PATH, endpoint="admin", view_func=admin, methods=["GET", "POST"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
queue:
|
||||||
|
image: "queue-app:latest" # zmień na registry/name:tag jeśli pobierasz z rejestru
|
||||||
|
ports: 5000
|
||||||
|
volumes:
|
||||||
|
- queue_data:/data
|
||||||
|
environment:
|
||||||
|
- DB_PATH=/data/queue.db
|
||||||
|
- ADMIN_URL=admin-panel
|
||||||
|
restart: always
|
||||||
|
deploy:
|
||||||
|
replicas: 1
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
queue_data:
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Flask==2.3.2
|
||||||
|
gunicorn==20.1.0
|
||||||
|
|
@ -3,26 +3,40 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Panel admina — kolejka</title>
|
<title>Panel admina — kolejka</title>
|
||||||
<style>body{font-family:Helvetica,Arial;margin:20px} .col{float:left;width:30%;padding:10px} .btn{display:inline-block;margin:3px;padding:6px 10px;background:#2b7;padding:color:#fff;border-radius:4px}</style>
|
<style>
|
||||||
|
body{font-family:Helvetica,Arial;margin:20px}
|
||||||
|
.grid{display:flex;gap:16px;align-items:flex-start}
|
||||||
|
.col{flex:1;padding:10px;border:1px solid #eee;border-radius:6px;background:#fafafa}
|
||||||
|
h2{margin-top:0}
|
||||||
|
.inline-form{display:inline-block;margin:3px}
|
||||||
|
.btn{padding:6px 10px;border-radius:4px;border:1px solid #888;background:#f0f0f0;cursor:pointer}
|
||||||
|
.num-label{font-size:18px;margin-right:8px;display:inline-block;width:40px;text-align:center}
|
||||||
|
|
||||||
|
/* Responsive: na wąskich ekranach kolumny jedna pod drugą */
|
||||||
|
@media (max-width:700px){
|
||||||
|
.grid{flex-direction:column}
|
||||||
|
.num-label{width:60px}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Panel admina</h2>
|
<h2>Panel admina</h2>
|
||||||
|
|
||||||
<form method="post">
|
<form method="post" style="margin-bottom:12px">
|
||||||
<label>Start — liczba numerów:
|
<label>Start — liczba numerów:
|
||||||
<input name="count" type="number" min="1" value="20">
|
<input name="count" type="number" min="1" value="20" style="width:80px;margin-left:6px">
|
||||||
</label>
|
</label>
|
||||||
<button name="action" value="start">Start</button>
|
<button class="btn" name="action" value="start">Start</button>
|
||||||
<button name="action" value="reset">Reset</button>
|
<button class="btn" name="action" value="reset">Reset</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div style="overflow:hidden;margin-top:20px">
|
<div class="grid">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>Oczekujące</h3>
|
<h3>Oczekujące</h3>
|
||||||
{% for n in waiting %}
|
{% for n in waiting %}
|
||||||
<form style="display:inline" method="post">
|
<form class="inline-form" method="post">
|
||||||
<input type="hidden" name="num" value="{{n}}">
|
<input type="hidden" name="num" value="{{n}}">
|
||||||
<button name="action" value="call">{{n}}</button>
|
<button class="btn" name="action" value="call">{{n}}</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>brak</p>
|
<p>brak</p>
|
||||||
|
|
@ -32,11 +46,19 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>Wywołane (obecne)</h3>
|
<h3>Wywołane (obecne)</h3>
|
||||||
{% for n in called %}
|
{% for n in called %}
|
||||||
<form style="display:block;margin-bottom:6px" method="post">
|
<div style="margin-bottom:8px;display:flex;align-items:center;flex-wrap:wrap">
|
||||||
<span style="font-size:20px;margin-right:8px">{{n}}</span>
|
<span class="num-label">{{n}}</span>
|
||||||
<input type="hidden" name="num" value="{{n}}">
|
|
||||||
<button name="action" value="done">Przyszedł / Usuń</button>
|
<form style="display:inline-block;margin-right:6px" method="post">
|
||||||
</form>
|
<input type="hidden" name="num" value="{{n}}">
|
||||||
|
<button class="btn" name="action" value="done">Przyszedł / Usuń</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form style="display:inline-block" method="post">
|
||||||
|
<input type="hidden" name="num" value="{{n}}">
|
||||||
|
<button class="btn" name="action" value="return">Wróć do kolejki</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>brak</p>
|
<p>brak</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,37 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Wywoływany numer</title>
|
<title>Wywoływane numery</title>
|
||||||
<style>body{font-family:Helvetica,Arial;margin:30px;text-align:center} .num{font-size:120px;font-weight:700}</style>
|
<style>
|
||||||
|
body{font-family:Helvetica,Arial;margin:30px;text-align:center}
|
||||||
|
.num{display:inline-block;margin:10px;padding:20px 30px;font-size:72px;font-weight:700;border:2px solid #333;border-radius:8px}
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
async function fetchCurrent(){
|
async function fetchCurrent(){
|
||||||
try{
|
try{
|
||||||
const r = await fetch('/current');
|
const r = await fetch('/current');
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
document.getElementById('num').textContent = j.current || '-';
|
const cont = document.getElementById('nums');
|
||||||
|
cont.innerHTML = '';
|
||||||
|
const list = j.current || [];
|
||||||
|
if(list.length === 0){
|
||||||
|
cont.textContent = '-';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.forEach(n => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'num';
|
||||||
|
el.textContent = n;
|
||||||
|
cont.appendChild(el);
|
||||||
|
});
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
// poll co 2s
|
|
||||||
setInterval(fetchCurrent, 2000);
|
setInterval(fetchCurrent, 2000);
|
||||||
window.addEventListener('load', fetchCurrent);
|
window.addEventListener('load', fetchCurrent);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Proszę podejść do okienka</h1>
|
<h1>Zapraszamy do stanowiska osoby z numerem:</h1>
|
||||||
<div class="num" id="num">-</div>
|
<div id="nums">-</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue