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:
root 2025-03-12 13:24:26 +01:00
parent 7bf9d7e5ae
commit 3f2be5b17d
2 changed files with 149 additions and 16 deletions

View File

@ -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):

View 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}")