Automatisches Ausschalten der Drucker nach Ablauf der Auftragszeit
- Verbesserte Funktion check-jobs für automatische Abschaltung - Implentierung von Warteschlange für besetzte Drucker - Neues Datenbank-Feld 'waiting_approval' für Druckaufträge - Neuer API-Endpunkt '/api/jobs/<job_id>/approve' zur Freischaltung - Verbessertes Logging für Debugging-Zwecke 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7bf9d7e5ae
commit
3f2be5b17d
123
backend/app.py
123
backend/app.py
@ -99,6 +99,7 @@ def init_db():
|
|||||||
comments TEXT,
|
comments TEXT,
|
||||||
aborted INTEGER DEFAULT 0,
|
aborted INTEGER DEFAULT 0,
|
||||||
abort_reason TEXT,
|
abort_reason TEXT,
|
||||||
|
waiting_approval INTEGER DEFAULT 0,
|
||||||
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@ -341,13 +342,15 @@ def socket_to_dict(socket):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
latest_job = get_latest_job_for_socket(socket['id'])
|
latest_job = get_latest_job_for_socket(socket['id'])
|
||||||
|
waiting_jobs = get_waiting_jobs_for_socket(socket['id'])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': socket['id'],
|
'id': socket['id'],
|
||||||
'name': socket['name'],
|
'name': socket['name'],
|
||||||
'description': socket['description'],
|
'description': socket['description'],
|
||||||
'status': socket['status'],
|
'status': socket['status'],
|
||||||
'latestJob': job_to_dict(latest_job) if latest_job else None
|
'latestJob': job_to_dict(latest_job) if latest_job else None,
|
||||||
|
'waitingJobs': [job_to_dict(job) for job in waiting_jobs] if waiting_jobs else []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Job-Verwaltung
|
# Job-Verwaltung
|
||||||
@ -374,27 +377,40 @@ def get_expired_jobs():
|
|||||||
rows = db.execute('''
|
rows = db.execute('''
|
||||||
SELECT * FROM job
|
SELECT * FROM job
|
||||||
WHERE aborted = 0
|
WHERE aborted = 0
|
||||||
|
AND waiting_approval = 0
|
||||||
AND datetime(start_at, '+' || duration_in_minutes || ' minutes') <= datetime(?)
|
AND datetime(start_at, '+' || duration_in_minutes || ' minutes') <= datetime(?)
|
||||||
''', (now,)).fetchall()
|
''', (now,)).fetchall()
|
||||||
return [dict(row) for row in rows]
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
def create_job(socket_id, user_id, duration_in_minutes, comments=None):
|
def get_waiting_jobs_for_socket(socket_id):
|
||||||
|
"""Findet alle Jobs, die auf Freischaltung für eine bestimmte Steckdose warten."""
|
||||||
|
db = get_db()
|
||||||
|
rows = db.execute('''
|
||||||
|
SELECT * FROM job
|
||||||
|
WHERE socket_id = ?
|
||||||
|
AND aborted = 0
|
||||||
|
AND waiting_approval = 1
|
||||||
|
ORDER BY start_at ASC
|
||||||
|
''', (socket_id,)).fetchall()
|
||||||
|
return [dict(row) for row in rows]
|
||||||
|
|
||||||
|
def create_job(socket_id, user_id, duration_in_minutes, comments=None, waiting_approval=0):
|
||||||
job_id = str(uuid.uuid4())
|
job_id = str(uuid.uuid4())
|
||||||
start_at = datetime.datetime.utcnow()
|
start_at = datetime.datetime.utcnow()
|
||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
db.execute(
|
db.execute(
|
||||||
'''INSERT INTO job
|
'''INSERT INTO job
|
||||||
(id, socket_id, user_id, start_at, duration_in_minutes, comments, aborted, abort_reason)
|
(id, socket_id, user_id, start_at, duration_in_minutes, comments, aborted, abort_reason, waiting_approval)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''',
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)''',
|
||||||
(job_id, socket_id, user_id, start_at.isoformat(), duration_in_minutes, comments, 0, None)
|
(job_id, socket_id, user_id, start_at.isoformat(), duration_in_minutes, comments, 0, None, waiting_approval)
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return get_job_by_id(job_id)
|
return get_job_by_id(job_id)
|
||||||
|
|
||||||
def update_job(job_id, socket_id=None, user_id=None, duration_in_minutes=None,
|
def update_job(job_id, socket_id=None, user_id=None, duration_in_minutes=None,
|
||||||
comments=None, aborted=None, abort_reason=None):
|
comments=None, aborted=None, abort_reason=None, waiting_approval=None):
|
||||||
job = get_job_by_id(job_id)
|
job = get_job_by_id(job_id)
|
||||||
if not job:
|
if not job:
|
||||||
return None
|
return None
|
||||||
@ -426,6 +442,10 @@ def update_job(job_id, socket_id=None, user_id=None, duration_in_minutes=None,
|
|||||||
values.append('abort_reason = ?')
|
values.append('abort_reason = ?')
|
||||||
params.append(abort_reason)
|
params.append(abort_reason)
|
||||||
|
|
||||||
|
if waiting_approval is not None:
|
||||||
|
values.append('waiting_approval = ?')
|
||||||
|
params.append(1 if waiting_approval else 0)
|
||||||
|
|
||||||
if not values:
|
if not values:
|
||||||
return job
|
return job
|
||||||
|
|
||||||
@ -462,6 +482,9 @@ def job_to_dict(job):
|
|||||||
if not job:
|
if not job:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Bei älteren Jobs könnte waiting_approval fehlen, deshalb mit get abrufen und Default setzen
|
||||||
|
waiting_approval = job.get('waiting_approval', 0) if isinstance(job, dict) else getattr(job, 'waiting_approval', 0)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': job['id'],
|
'id': job['id'],
|
||||||
'socketId': job['socket_id'],
|
'socketId': job['socket_id'],
|
||||||
@ -471,6 +494,7 @@ def job_to_dict(job):
|
|||||||
'comments': job['comments'],
|
'comments': job['comments'],
|
||||||
'aborted': bool(job['aborted']),
|
'aborted': bool(job['aborted']),
|
||||||
'abortReason': job['abort_reason'],
|
'abortReason': job['abort_reason'],
|
||||||
|
'waitingApproval': bool(waiting_approval),
|
||||||
'remainingMinutes': calculate_remaining_time(job)
|
'remainingMinutes': calculate_remaining_time(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,16 +732,32 @@ def create_job_endpoint():
|
|||||||
if not socket:
|
if not socket:
|
||||||
return jsonify({'message': 'Steckdose nicht gefunden!'}), 404
|
return jsonify({'message': 'Steckdose nicht gefunden!'}), 404
|
||||||
|
|
||||||
if socket['status'] != 0: # 0 = available
|
|
||||||
return jsonify({'message': 'Steckdose ist nicht verfügbar!'}), 400
|
|
||||||
|
|
||||||
duration = int(data['durationInMinutes'])
|
duration = int(data['durationInMinutes'])
|
||||||
|
allow_queued_jobs = data.get('allowQueuedJobs', False)
|
||||||
|
|
||||||
|
# Prüfen, ob der Drucker bereits belegt ist
|
||||||
|
if socket['status'] != 0: # 0 = available
|
||||||
|
if allow_queued_jobs:
|
||||||
|
# Erstelle einen Job, der auf Freischaltung wartet
|
||||||
|
job = create_job(
|
||||||
|
socket_id=socket['id'],
|
||||||
|
user_id=g.current_user['id'],
|
||||||
|
duration_in_minutes=duration,
|
||||||
|
comments=data.get('comments', ''),
|
||||||
|
waiting_approval=1 # Job wartet auf Freischaltung
|
||||||
|
)
|
||||||
|
app.logger.info(f"Wartender Job {job['id']} für belegten Drucker {socket['id']} erstellt.")
|
||||||
|
return jsonify(job_to_dict(job)), 201
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'Steckdose ist nicht verfügbar!'}), 400
|
||||||
|
|
||||||
|
# Normaler Job für verfügbaren Drucker
|
||||||
job = create_job(
|
job = create_job(
|
||||||
socket_id=socket['id'],
|
socket_id=socket['id'],
|
||||||
user_id=g.current_user['id'],
|
user_id=g.current_user['id'],
|
||||||
duration_in_minutes=duration,
|
duration_in_minutes=duration,
|
||||||
comments=data.get('comments', '')
|
comments=data.get('comments', ''),
|
||||||
|
waiting_approval=0 # Job ist sofort aktiv
|
||||||
)
|
)
|
||||||
|
|
||||||
# Steckdose als belegt markieren
|
# Steckdose als belegt markieren
|
||||||
@ -726,9 +766,13 @@ def create_job_endpoint():
|
|||||||
# Steckdose einschalten, falls IP-Adresse hinterlegt ist
|
# Steckdose einschalten, falls IP-Adresse hinterlegt ist
|
||||||
if socket['ip_address']:
|
if socket['ip_address']:
|
||||||
try:
|
try:
|
||||||
turn_on_socket(socket['ip_address'])
|
success = turn_on_socket(socket['ip_address'])
|
||||||
|
if success:
|
||||||
|
app.logger.info(f"Steckdose {socket['ip_address']} für Job {job['id']} eingeschaltet.")
|
||||||
|
else:
|
||||||
|
app.logger.warning(f"Konnte Steckdose {socket['ip_address']} für Job {job['id']} nicht einschalten.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Fehler beim Einschalten der Steckdose: {e}")
|
app.logger.error(f"Fehler beim Einschalten der Steckdose {socket['ip_address']}: {e}")
|
||||||
|
|
||||||
return jsonify(job_to_dict(job)), 201
|
return jsonify(job_to_dict(job)), 201
|
||||||
|
|
||||||
@ -854,6 +898,51 @@ def extend_job(job_id):
|
|||||||
|
|
||||||
return jsonify(job_to_dict(updated_job))
|
return jsonify(job_to_dict(updated_job))
|
||||||
|
|
||||||
|
@app.route('/api/jobs/<job_id>/approve', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def approve_job(job_id):
|
||||||
|
"""Aktiviert einen wartenden Job und schaltet die Steckdose ein."""
|
||||||
|
# Nur Admins oder der Job-Ersteller können Jobs freischalten
|
||||||
|
job = get_job_by_id(job_id)
|
||||||
|
if not job:
|
||||||
|
return jsonify({'message': 'Job nicht gefunden!'}), 404
|
||||||
|
|
||||||
|
if g.current_user['role'] != 'admin' and job['user_id'] != g.current_user['id']:
|
||||||
|
return jsonify({'message': 'Keine Berechtigung für diesen Job!'}), 403
|
||||||
|
|
||||||
|
# Prüfen, ob Job auf Freischaltung wartet
|
||||||
|
waiting_approval = job.get('waiting_approval', 0)
|
||||||
|
if not waiting_approval:
|
||||||
|
return jsonify({'message': 'Dieser Job wartet nicht auf Freischaltung!'}), 400
|
||||||
|
|
||||||
|
# Drucker abrufen
|
||||||
|
socket = get_socket_by_id(job['socket_id'])
|
||||||
|
if not socket:
|
||||||
|
return jsonify({'message': 'Drucker nicht gefunden!'}), 404
|
||||||
|
|
||||||
|
# Prüfen, ob der Drucker verfügbar ist
|
||||||
|
if socket['status'] != 0: # 0 = available
|
||||||
|
return jsonify({'message': 'Drucker ist noch belegt! Bitte warten, bis der laufende Job beendet ist.'}), 400
|
||||||
|
|
||||||
|
# Job aktualisieren
|
||||||
|
updated_job = update_job(job_id, waiting_approval=0)
|
||||||
|
|
||||||
|
# Steckdose als belegt markieren
|
||||||
|
update_socket(socket['id'], status=1) # 1 = busy
|
||||||
|
|
||||||
|
# Steckdose einschalten, falls IP-Adresse hinterlegt ist
|
||||||
|
if socket['ip_address']:
|
||||||
|
try:
|
||||||
|
success = turn_on_socket(socket['ip_address'])
|
||||||
|
if success:
|
||||||
|
app.logger.info(f"Steckdose {socket['ip_address']} für freigeschalteten Job {job['id']} eingeschaltet.")
|
||||||
|
else:
|
||||||
|
app.logger.warning(f"Konnte Steckdose {socket['ip_address']} für freigeschalteten Job {job['id']} nicht einschalten.")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Fehler beim Einschalten der Steckdose {socket['ip_address']}: {e}")
|
||||||
|
|
||||||
|
return jsonify(job_to_dict(updated_job))
|
||||||
|
|
||||||
@app.route('/api/jobs/<job_id>/comments', methods=['PUT'])
|
@app.route('/api/jobs/<job_id>/comments', methods=['PUT'])
|
||||||
@login_required
|
@login_required
|
||||||
def update_job_comments(job_id):
|
def update_job_comments(job_id):
|
||||||
@ -1011,12 +1100,13 @@ def stats():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Tägliche Überprüfung der Jobs und automatische Abschaltung der Steckdosen
|
# Regelmäßige Überprüfung der Jobs und automatische Abschaltung der Steckdosen
|
||||||
@app.cli.command("check-jobs")
|
@app.cli.command("check-jobs")
|
||||||
def check_jobs():
|
def check_jobs():
|
||||||
"""Überprüft abgelaufene Jobs und schaltet Steckdosen aus."""
|
"""Überprüft abgelaufene Jobs und schaltet Steckdosen automatisch aus."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
expired_jobs = get_expired_jobs()
|
expired_jobs = get_expired_jobs()
|
||||||
|
handled_jobs = 0
|
||||||
|
|
||||||
for job in expired_jobs:
|
for job in expired_jobs:
|
||||||
socket = get_socket_by_id(job['socket_id'])
|
socket = get_socket_by_id(job['socket_id'])
|
||||||
@ -1024,6 +1114,7 @@ def check_jobs():
|
|||||||
if socket and socket['status'] == 1: # busy
|
if socket and socket['status'] == 1: # busy
|
||||||
update_socket(socket['id'], status=0) # available
|
update_socket(socket['id'], status=0) # available
|
||||||
app.logger.info(f"Job {job['id']} abgelaufen. Steckdose {socket['id']} auf verfügbar gesetzt.")
|
app.logger.info(f"Job {job['id']} abgelaufen. Steckdose {socket['id']} auf verfügbar gesetzt.")
|
||||||
|
handled_jobs += 1
|
||||||
|
|
||||||
# Steckdose ausschalten, falls IP-Adresse hinterlegt ist
|
# Steckdose ausschalten, falls IP-Adresse hinterlegt ist
|
||||||
if socket['ip_address']:
|
if socket['ip_address']:
|
||||||
@ -1033,7 +1124,7 @@ def check_jobs():
|
|||||||
try:
|
try:
|
||||||
success = turn_off_socket(socket['ip_address'])
|
success = turn_off_socket(socket['ip_address'])
|
||||||
if success:
|
if success:
|
||||||
app.logger.info(f"Steckdose {socket['ip_address']} für abgelaufenen Job {job['id']} ausgeschaltet (Versuch {attempt}).")
|
app.logger.info(f"Steckdose {socket['ip_address']} für abgelaufenen Job {job['id']} automatisch ausgeschaltet (Versuch {attempt}).")
|
||||||
break
|
break
|
||||||
app.logger.warning(f"Konnte Steckdose {socket['ip_address']} nicht ausschalten (Versuch {attempt}/{max_attempts}).")
|
app.logger.warning(f"Konnte Steckdose {socket['ip_address']} nicht ausschalten (Versuch {attempt}/{max_attempts}).")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1044,7 +1135,7 @@ def check_jobs():
|
|||||||
import time
|
import time
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft und Steckdosen aktualisiert.")
|
app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft, {handled_jobs} Steckdosen aktualisiert.")
|
||||||
|
|
||||||
@app.route('/api/job/<job_id>/status', methods=['GET'])
|
@app.route('/api/job/<job_id>/status', methods=['GET'])
|
||||||
def job_status(job_id):
|
def job_status(job_id):
|
||||||
|
42
backend/migrations/versions/add_waiting_approval.py
Normal file
42
backend/migrations/versions/add_waiting_approval.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Add waiting_approval column to job table
|
||||||
|
|
||||||
|
Revision ID: add_waiting_approval
|
||||||
|
Revises: af3faaa3844c
|
||||||
|
Create Date: 2025-03-12 14:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_waiting_approval'
|
||||||
|
down_revision = 'af3faaa3844c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Füge die neue Spalte waiting_approval zur job-Tabelle hinzu
|
||||||
|
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('waiting_approval', sa.Integer(), server_default='0', nullable=False))
|
||||||
|
|
||||||
|
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||||
|
try:
|
||||||
|
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('waiting_approval', sa.Boolean(), server_default='0', nullable=False))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Migration für print_job-Tabelle übersprungen: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Entferne die waiting_approval-Spalte aus der job-Tabelle
|
||||||
|
with op.batch_alter_table('job', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('waiting_approval')
|
||||||
|
|
||||||
|
# SQLite-kompatible Migration für die print_job-Tabelle, falls diese existiert
|
||||||
|
try:
|
||||||
|
with op.batch_alter_table('print_job', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('waiting_approval')
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Downgrade für print_job-Tabelle übersprungen: {e}")
|
Loading…
x
Reference in New Issue
Block a user