Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e85337fc5 | |||
| 0114fec928 | |||
| b16ad56b98 | |||
| d2c21bc519 |
+6
-3
@@ -542,7 +542,8 @@ def _enforce_module_access():
|
||||
msg = {
|
||||
'library': "Bibliotheks-Modul ist deaktiviert.",
|
||||
'inventory': "Inventar-Modul ist deaktiviert.",
|
||||
'student_cards': "Schülerausweis-Modul ist deaktiviert."
|
||||
'student_cards': "Schülerausweis-Modul ist deaktiviert.",
|
||||
'terminplaner': "Termin-Modul ist deaktiviert."
|
||||
}.get(name, f"{name.capitalize()}-Modul ist deaktiviert.")
|
||||
|
||||
if request.path.startswith('/api/') or request.is_json:
|
||||
@@ -554,6 +555,8 @@ def _enforce_module_access():
|
||||
return redirect(url_for('home_admin'))
|
||||
elif name != 'library' and cfg.MODULES.is_enabled('library'):
|
||||
return redirect('/library')
|
||||
elif name != 'terminplaner' and cfg.MODULES.is_enabled('terminplaner'):
|
||||
return redirect('/terminplaner')
|
||||
|
||||
return redirect(url_for('my_borrowed_items'))
|
||||
|
||||
@@ -706,7 +709,7 @@ def _action_access_allowed(permissions, action_key):
|
||||
def _permission_denied_fallback_endpoint(permissions, current_endpoint=None):
|
||||
username = session.get('username')
|
||||
|
||||
for candidate in ('home_admin', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
|
||||
for candidate in ('home_admin', 'library_view', 'terminplaner', 'my_borrowed_items', 'tutorial_page', 'notifications_view', 'impressum'):
|
||||
if current_endpoint and candidate == current_endpoint:
|
||||
continue
|
||||
if _page_access_allowed(permissions, candidate):
|
||||
@@ -13163,7 +13166,7 @@ def get_optimal_image_quality(img, target_size_kb=80):
|
||||
|
||||
@app.route('/health')
|
||||
def health_check():
|
||||
return 'OK', 200
|
||||
return jsonify({"status": "healthy"}), 200
|
||||
|
||||
|
||||
@app.route('/api/push/subscribe', methods=['POST'])
|
||||
|
||||
@@ -451,6 +451,27 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.physical-scanner-modal {
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.physical-scanner-modal .modal-content {
|
||||
max-width: 520px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.physical-scanner-code {
|
||||
min-height: 28px;
|
||||
margin: 18px 0 8px;
|
||||
padding: 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
color: var(--ui-text);
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
|
||||
.detail-gallery-container {
|
||||
display: flex;
|
||||
@@ -518,10 +539,9 @@
|
||||
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
|
||||
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
|
||||
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
|
||||
<input type="checkbox" id="keyboardScannerToggle">
|
||||
<span style="font-size:0.9em;">Physischer Scanner</span>
|
||||
</label>
|
||||
<button id="physicalScannerBtn" class="button" type="button" style="margin-left:6px;">
|
||||
Physischer Scanner starten
|
||||
</button>
|
||||
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
|
||||
</div>
|
||||
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
|
||||
@@ -612,6 +632,16 @@
|
||||
<div id="detailContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="physicalScannerModal" class="modal physical-scanner-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="physicalScannerTitle">
|
||||
<div class="modal-content">
|
||||
<span class="close" id="closePhysicalScannerBtn" role="button" tabindex="0" aria-label="Physischen Scanner schließen">×</span>
|
||||
<h2 id="physicalScannerTitle">Physischer Scanner</h2>
|
||||
<p id="physicalScannerInstruction">Scanner ist bereit. Bitte den angezeigten Code scannen.</p>
|
||||
<div id="physicalScannerCode" class="physical-scanner-code" aria-live="polite"></div>
|
||||
<p id="physicalScannerStatus" class="library-scan-status" aria-live="polite">Warte auf Scan...</p>
|
||||
<button id="stopPhysicalScannerBtn" class="button" type="button">Scanner schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -842,7 +872,10 @@
|
||||
let keyboardScannerEnabled = false;
|
||||
let keyboardScanBuffer = '';
|
||||
let keyboardLastKeyAt = 0;
|
||||
let physicalScannerModalOpen = false;
|
||||
const KEYBOARD_SCAN_INTERCHAR_MS = 100; // max time between keystrokes to consider them one scan
|
||||
const physicalScanQueue = [];
|
||||
let keyboardScanProcessing = false;
|
||||
let editLibraryState = {
|
||||
itemId: '',
|
||||
seriesGroupId: '',
|
||||
@@ -1184,42 +1217,114 @@
|
||||
// =========================================================================
|
||||
// Keyboard scanner handling (physical scanners that send chars then Enter)
|
||||
// =========================================================================
|
||||
function getPhysicalScannerInstruction() {
|
||||
const mode = document.getElementById('scanModeSelect')?.value || 'card_only';
|
||||
if (mode === 'return_only') return 'Bitte den Mediencode scannen.';
|
||||
if (mode === 'card_only') return 'Bitte den Bibliotheksausweis scannen.';
|
||||
if (mode === 'quick_toggle' && !activeStudentCardId) return 'Bitte zuerst den Bibliotheksausweis scannen.';
|
||||
if (mode === 'quick_toggle') return 'Bitte den Mediencode scannen.';
|
||||
if (mode === 'continuous' && !activeStudentCardId) return 'Bitte zuerst den Bibliotheksausweis scannen.';
|
||||
return 'Bitte den nächsten Mediencode scannen.';
|
||||
}
|
||||
|
||||
function updatePhysicalScannerModal(message, kind = '') {
|
||||
const instruction = document.getElementById('physicalScannerInstruction');
|
||||
const status = document.getElementById('physicalScannerStatus');
|
||||
if (instruction) instruction.textContent = getPhysicalScannerInstruction();
|
||||
if (status) {
|
||||
status.textContent = message;
|
||||
status.classList.remove('ok', 'warn', 'error');
|
||||
if (kind) status.classList.add(kind);
|
||||
}
|
||||
}
|
||||
|
||||
function openPhysicalScannerModal() {
|
||||
const modal = document.getElementById('physicalScannerModal');
|
||||
if (!modal) return;
|
||||
|
||||
physicalScannerModalOpen = true;
|
||||
keyboardScannerEnabled = true;
|
||||
keyboardScanBuffer = '';
|
||||
keyboardLastKeyAt = 0;
|
||||
modal.style.display = 'flex';
|
||||
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
|
||||
document.addEventListener('keydown', keyboardScanKeydownHandler);
|
||||
document.getElementById('closePhysicalScannerBtn')?.focus();
|
||||
}
|
||||
|
||||
function closePhysicalScannerModal() {
|
||||
physicalScannerModalOpen = false;
|
||||
keyboardScannerEnabled = false;
|
||||
keyboardScanBuffer = '';
|
||||
keyboardLastKeyAt = 0;
|
||||
document.removeEventListener('keydown', keyboardScanKeydownHandler);
|
||||
const modal = document.getElementById('physicalScannerModal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
const code = document.getElementById('physicalScannerCode');
|
||||
if (code) code.textContent = '';
|
||||
}
|
||||
|
||||
function keyboardScanKeydownHandler(e) {
|
||||
// Only active when explicitly enabled
|
||||
if (!keyboardScannerEnabled) return;
|
||||
if (!keyboardScannerEnabled || !physicalScannerModalOpen) return;
|
||||
|
||||
// Ignore if focus is in an input/textarea/contenteditable to avoid interfering with typing
|
||||
const active = document.activeElement;
|
||||
if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA' || active.isContentEditable)) return;
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closePhysicalScannerModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// If Enter/Return pressed -> finalize buffer
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const code = keyboardScanBuffer.trim();
|
||||
keyboardScanBuffer = '';
|
||||
keyboardLastKeyAt = 0;
|
||||
if (!code) return;
|
||||
|
||||
// Process exactly like a camera scan - centralized logic handles the rest!
|
||||
handleScanSuccess(code);
|
||||
// Scanner keystrokes must not be submitted into the focused form field.
|
||||
const displayCode = document.getElementById('physicalScannerCode');
|
||||
if (displayCode) displayCode.textContent = code;
|
||||
updatePhysicalScannerModal('Code empfangen. Verarbeitung läuft...', 'warn');
|
||||
processPhysicalScan(code);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only accept common printable characters; ignore modifier keys
|
||||
if (e.key.length === 1) {
|
||||
// Physical scanners in this workflow emit numeric codes only.
|
||||
if (e.key.length === 1 && /\d/.test(e.key)) {
|
||||
// If time gap too big, start new buffer
|
||||
if (keyboardLastKeyAt && (now - keyboardLastKeyAt) > KEYBOARD_SCAN_INTERCHAR_MS) {
|
||||
keyboardScanBuffer = '';
|
||||
}
|
||||
keyboardScanBuffer += e.key;
|
||||
keyboardLastKeyAt = now;
|
||||
// Prevent default so scanner input doesn't accidentally move focus or trigger shortcuts
|
||||
const displayCode = document.getElementById('physicalScannerCode');
|
||||
if (displayCode) displayCode.textContent = keyboardScanBuffer;
|
||||
// Prevent default so scanner input is captured even while an input has focus.
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScanSuccess(decodedText) {
|
||||
async function processPhysicalScan(code) {
|
||||
physicalScanQueue.push(code);
|
||||
if (keyboardScanProcessing) return;
|
||||
|
||||
keyboardScanProcessing = true;
|
||||
try {
|
||||
while (physicalScanQueue.length > 0) {
|
||||
await handleScanSuccess(physicalScanQueue.shift());
|
||||
if (physicalScannerModalOpen) {
|
||||
updatePhysicalScannerModal('Aktion abgeschlossen. Bereit für den nächsten Scan.', 'ok');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
keyboardScanProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScanSuccess(decodedText) {
|
||||
const scannedCode = typeof normalizeScannedCode === "function" ? normalizeScannedCode(decodedText) : decodedText;
|
||||
if (!scannedCode) return;
|
||||
|
||||
@@ -1235,26 +1340,27 @@
|
||||
|
||||
// 1. Modus: Nur Rückgabe
|
||||
if (mode === 'return_only') {
|
||||
returnByCode(scannedCode);
|
||||
await returnByCode(scannedCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Modus: Nur Ausweis
|
||||
if (mode === 'card_only') {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Ausweis scannen.`, 'ok');
|
||||
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Sie können den nächsten scannen.`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Modus: Schnellmodus (1x Ausweis, 1x Buch)
|
||||
if (mode === 'quick_toggle') {
|
||||
processQuickToggleScan(scannedCode);
|
||||
await processQuickToggleScan(scannedCode);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Modus: Dauermodus (1x Ausweis, Nx Bücher)
|
||||
if (mode === 'continuous') {
|
||||
processContinuousScan(scannedCode);
|
||||
await processContinuousScan(scannedCode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1341,7 +1447,8 @@
|
||||
// 2. Wenn kein Ausweis gesetzt ist, wird der Code als Ausweis interpretiert
|
||||
if (!activeStudentCardId) {
|
||||
setActiveStudentCard(scannedCode);
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}. Bitte den nächsten Mediencode scannen.`, 'ok');
|
||||
showSmallConfirm(`Ausweis erkannt: ${activeStudentCardId}. Bitte jetzt den nächsten Mediencode scannen.`, 'ok');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1649,7 +1756,7 @@
|
||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||
const resetBtn = document.getElementById('resetCardBtn');
|
||||
const modeSelect = document.getElementById('scanModeSelect');
|
||||
const keyboardToggle = document.getElementById('keyboardScannerToggle');
|
||||
const physicalScannerBtn = document.getElementById('physicalScannerBtn');
|
||||
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', async () => {
|
||||
@@ -1670,6 +1777,9 @@
|
||||
|
||||
if (modeSelect) {
|
||||
modeSelect.addEventListener('change', () => {
|
||||
if (physicalScannerModalOpen) {
|
||||
updatePhysicalScannerModal('Warte auf Scan...', 'warn');
|
||||
}
|
||||
if (modeSelect.value === 'quick_toggle' && !activeStudentCardId) {
|
||||
setScanStatus('Schnellmodus: zuerst Bibliotheksausweis scannen.', 'warn');
|
||||
} else if (modeSelect.value === 'card_only') {
|
||||
@@ -1678,18 +1788,24 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (keyboardToggle) {
|
||||
keyboardToggle.addEventListener('change', () => {
|
||||
keyboardScannerEnabled = !!keyboardToggle.checked;
|
||||
if (keyboardScannerEnabled) {
|
||||
document.addEventListener('keydown', keyboardScanKeydownHandler);
|
||||
setScanStatus('Physischer Scanner aktiv (Schnellmodus empfohlen).', 'ok');
|
||||
} else {
|
||||
document.removeEventListener('keydown', keyboardScanKeydownHandler);
|
||||
setScanStatus('Physischer Scanner deaktiviert.', 'warn');
|
||||
if (physicalScannerBtn) {
|
||||
physicalScannerBtn.addEventListener('click', openPhysicalScannerModal);
|
||||
}
|
||||
|
||||
const closePhysicalScannerBtn = document.getElementById('closePhysicalScannerBtn');
|
||||
const stopPhysicalScannerBtn = document.getElementById('stopPhysicalScannerBtn');
|
||||
if (closePhysicalScannerBtn) {
|
||||
closePhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
|
||||
closePhysicalScannerBtn.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
closePhysicalScannerModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (stopPhysicalScannerBtn) {
|
||||
stopPhysicalScannerBtn.addEventListener('click', closePhysicalScannerModal);
|
||||
}
|
||||
}
|
||||
|
||||
// Run when DOM structure is entirely ready
|
||||
|
||||
+26
-8
@@ -439,7 +439,8 @@ initialize_tenant_database() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker exec -i "$APP_CONTAINER" python3 - "$tenant_id" "$mode" "$admin_password" <<'PY'
|
||||
# Fix: We inject the password securely via a temporary environment variable (-e ADMIN_PASS)
|
||||
docker exec -i -e ADMIN_PASS="$admin_password" "$APP_CONTAINER" python3 - "$tenant_id" "$mode" <<'PY'
|
||||
import sys, os, re, datetime, hashlib
|
||||
sys.path.insert(0, "/app")
|
||||
sys.path.insert(0, "/app/Web")
|
||||
@@ -450,7 +451,12 @@ import Web.modules.database.user as us
|
||||
|
||||
tenant_id = sys.argv[1].lower()
|
||||
mode = sys.argv[2]
|
||||
admin_password = sys.argv[3] if len(sys.argv) > 3 else "admin123"
|
||||
|
||||
# Fix: Read the password directly from the environment variable instead of sys.argv
|
||||
admin_password = os.environ.get("ADMIN_PASS", "admin123")
|
||||
# Failsafe in case of empty string
|
||||
if not admin_password.strip():
|
||||
admin_password = "admin123"
|
||||
|
||||
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
|
||||
db_name = f"inventar_{sanitized}" if sanitized else settings.MONGODB_DB
|
||||
@@ -815,14 +821,26 @@ case "$COMMAND" in
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT_ARG="${3:-}"
|
||||
PASSWORD_ARG="${4:-admin123}" # Optional 4th parameter; defaults to 'admin123' if omitted
|
||||
ARG3="${3:-}"
|
||||
ARG4="${4:-}"
|
||||
|
||||
PORT_ARG=""
|
||||
PASSWORD_ARG="admin123"
|
||||
|
||||
# Check if the 3rd argument is numeric (Port) or text (Password)
|
||||
if [ -n "$ARG3" ]; then
|
||||
if printf '%s\n' "$ARG3" | grep -qE '^[0-9]+$'; then
|
||||
PORT_ARG="$ARG3"
|
||||
if [ -n "$ARG4" ]; then
|
||||
PASSWORD_ARG="$ARG4"
|
||||
fi
|
||||
else
|
||||
# If ARG3 is not numeric, treat it as the password without setting a port
|
||||
PASSWORD_ARG="$ARG3"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$PORT_ARG" ]; then
|
||||
if ! printf '%s\n' "$PORT_ARG" | grep -qE '^[0-9]+$'; then
|
||||
echo "Error: Port must be a numeric value."
|
||||
exit 1
|
||||
fi
|
||||
register_tenant_port "$TENANT_ID" "$PORT_ARG"
|
||||
update_runtime_ports "$PORT_ARG"
|
||||
sync_tenant_port_map
|
||||
|
||||
Reference in New Issue
Block a user