Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 857f1e7299 | |||
| 3458f9e8f3 | |||
| 4ad5b3e5a7 | |||
| 1b1acff6b2 | |||
| 5d44c15a4e | |||
| 8d29f2d925 | |||
| 982efa9efe | |||
| 6079f5bc3e | |||
| 629ac13a25 | |||
| 2b582c8700 | |||
| fd55fa9703 | |||
| 08e13d4f23 | |||
| 7c3eadc8cf | |||
| e7f8b8d76a | |||
| d6ac8a9e16 | |||
| c7cfb0e411 | |||
| 6c73f79866 | |||
| 5cb59f4b63 | |||
| 326fc4ef91 | |||
| 12cd365f36 | |||
| b06828cc7e | |||
| 79f07d8631 |
@@ -1,3 +1,12 @@
|
|||||||
NUITKA_BUILD=0
|
NUITKA_BUILD=0
|
||||||
INVENTAR_HTTP_PORT=10000
|
INVENTAR_HTTP_PORT=10000
|
||||||
|
INVENTAR_HTTP_PORTS=10000
|
||||||
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
||||||
|
INVENTAR_APP_CPUS=1.0
|
||||||
|
INVENTAR_APP_MEM_LIMIT=384m
|
||||||
|
INVENTAR_APP_MEM_SWAP_LIMIT=768m
|
||||||
|
INVENTAR_WORKERS=4
|
||||||
|
INVENTAR_THREADS=2
|
||||||
|
INVENTAR_WORKER_TIMEOUT=30
|
||||||
|
INVENTAR_WORKER_CONNECTIONS=100
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
working_dir: /app/Web
|
working_dir: /app/Web
|
||||||
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "30", "--graceful-timeout", "20", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
command: ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "--timeout", "30", "--graceful-timeout", "20", "--worker-connections", "100", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
image: ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
image: ghcr.io/aiirondev/legendary-octo-garbanzo:latest
|
||||||
build: null
|
build: null
|
||||||
|
ports:
|
||||||
|
- "10000:8000"
|
||||||
|
|||||||
+73
-6
@@ -81,12 +81,12 @@ from tenant import get_tenant_context
|
|||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
||||||
app.logger.setLevel(logging.INFO)
|
app.logger.setLevel(logging.DEBUG)
|
||||||
if not os.path.exists(cfg.LOGS_FOLDER):
|
if not os.path.exists(cfg.LOGS_FOLDER):
|
||||||
os.makedirs(cfg.LOGS_FOLDER, exist_ok=True)
|
os.makedirs(cfg.LOGS_FOLDER, exist_ok=True)
|
||||||
log_file_path = os.path.join(cfg.LOGS_FOLDER, 'application.log')
|
log_file_path = os.path.join(cfg.LOGS_FOLDER, 'application.log')
|
||||||
file_handler = RotatingFileHandler(log_file_path, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
|
file_handler = RotatingFileHandler(log_file_path, maxBytes=10 * 1024 * 1024, backupCount=5, encoding='utf-8')
|
||||||
file_handler.setLevel(logging.INFO)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s'))
|
file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s'))
|
||||||
app.logger.handlers = []
|
app.logger.handlers = []
|
||||||
app.logger.addHandler(file_handler)
|
app.logger.addHandler(file_handler)
|
||||||
@@ -401,12 +401,60 @@ def _is_library_module_path(path):
|
|||||||
)
|
)
|
||||||
return path.startswith(library_prefixes)
|
return path.startswith(library_prefixes)
|
||||||
|
|
||||||
|
def _is_inventory_module_path(path):
|
||||||
|
"""Return True when the current request path belongs to the inventory module."""
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if path == '/' or path.startswith('/home'):
|
||||||
|
return True
|
||||||
|
|
||||||
|
inventory_prefixes = (
|
||||||
|
'/scanner',
|
||||||
|
'/inventory',
|
||||||
|
'/upload_admin',
|
||||||
|
'/manage_filters',
|
||||||
|
'/manage_locations',
|
||||||
|
'/admin_borrowings',
|
||||||
|
'/admin_damaged_items',
|
||||||
|
'/admin/borrowings',
|
||||||
|
'/admin/damaged_items',
|
||||||
|
'/terminplan',
|
||||||
|
)
|
||||||
|
return path.startswith(inventory_prefixes)
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def _enforce_module_access():
|
||||||
|
endpoint = request.endpoint or ''
|
||||||
|
if endpoint == 'static' or endpoint.startswith('static') or request.path.startswith('/api/'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(request.path):
|
||||||
|
return "Bibliotheks-Modul ist deaktiviert.", 403
|
||||||
|
|
||||||
|
if not cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(request.path):
|
||||||
|
return "Inventar-Modul ist deaktiviert.", 403
|
||||||
|
|
||||||
def _get_current_module(path):
|
def _get_current_module(path):
|
||||||
"""Resolve the active UI module for navbar separation."""
|
"""Resolve the active UI module for navbar separation."""
|
||||||
|
|
||||||
if cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(path):
|
if cfg.LIBRARY_MODULE_ENABLED and _is_library_module_path(path):
|
||||||
|
session['last_module'] = 'library'
|
||||||
return 'library'
|
return 'library'
|
||||||
return 'inventory'
|
if cfg.INVENTORY_MODULE_ENABLED and _is_inventory_module_path(path):
|
||||||
|
session['last_module'] = 'inventory'
|
||||||
|
return 'inventory'
|
||||||
|
|
||||||
|
last_module = session.get('last_module')
|
||||||
|
if last_module == 'library' and cfg.LIBRARY_MODULE_ENABLED:
|
||||||
|
return 'library'
|
||||||
|
if last_module == 'inventory' and cfg.INVENTORY_MODULE_ENABLED:
|
||||||
|
return 'inventory'
|
||||||
|
|
||||||
|
# Default fallback: prefer inventory if enabled, otherwise library, else inventory
|
||||||
|
if cfg.INVENTORY_MODULE_ENABLED:
|
||||||
|
return 'inventory'
|
||||||
|
return 'library' if cfg.LIBRARY_MODULE_ENABLED else 'inventory'
|
||||||
|
|
||||||
|
|
||||||
def _get_current_user_permissions():
|
def _get_current_user_permissions():
|
||||||
@@ -1118,6 +1166,7 @@ def inject_version():
|
|||||||
'csrf_token': csrf_token,
|
'csrf_token': csrf_token,
|
||||||
'CURRENT_MODULE': current_module,
|
'CURRENT_MODULE': current_module,
|
||||||
'school_periods': SCHOOL_PERIODS,
|
'school_periods': SCHOOL_PERIODS,
|
||||||
|
'inventory_module_enabled': cfg.INVENTORY_MODULE_ENABLED,
|
||||||
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
|
'library_module_enabled': cfg.LIBRARY_MODULE_ENABLED,
|
||||||
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
|
'student_cards_module_enabled': cfg.STUDENT_CARDS_MODULE_ENABLED,
|
||||||
'is_admin': is_admin,
|
'is_admin': is_admin,
|
||||||
@@ -2741,6 +2790,13 @@ def home():
|
|||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
flash('Bitte mit registriertem Konto anmelden!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if not cfg.INVENTORY_MODULE_ENABLED:
|
||||||
|
if cfg.LIBRARY_MODULE_ENABLED:
|
||||||
|
return redirect(url_for('library_view'))
|
||||||
|
else:
|
||||||
|
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
||||||
|
|
||||||
elif not us.check_admin(session['username']):
|
elif not us.check_admin(session['username']):
|
||||||
return render_template(
|
return render_template(
|
||||||
'main.html',
|
'main.html',
|
||||||
@@ -2773,6 +2829,13 @@ def home_admin():
|
|||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
|
if not cfg.INVENTORY_MODULE_ENABLED:
|
||||||
|
if cfg.LIBRARY_MODULE_ENABLED:
|
||||||
|
return redirect(url_for('library_admin'))
|
||||||
|
else:
|
||||||
|
return "Weder Inventar- noch Bibliotheks-Modul sind aktiviert.", 403
|
||||||
|
|
||||||
if not us.check_admin(session['username']):
|
if not us.check_admin(session['username']):
|
||||||
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
@@ -6618,11 +6681,15 @@ def zurueckgeben(id):
|
|||||||
def get_filter():
|
def get_filter():
|
||||||
"""
|
"""
|
||||||
API endpoint to retrieve available item filters/categories.
|
API endpoint to retrieve available item filters/categories.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Dictionary of available filters
|
dict: Dictionary of available filters by filter field
|
||||||
"""
|
"""
|
||||||
return it.get_filters()
|
return jsonify({
|
||||||
|
'filter1': it.get_primary_filters(),
|
||||||
|
'filter2': it.get_secondary_filters(),
|
||||||
|
'filter3': it.get_tertiary_filters(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/get_ausleihung_by_item/<id>')
|
@app.route('/get_ausleihung_by_item/<id>')
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ DEFAULTS = {
|
|||||||
"10": {"start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)"}
|
"10": {"start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)"}
|
||||||
},
|
},
|
||||||
'modules': {
|
'modules': {
|
||||||
|
'inventory': {
|
||||||
|
'enabled': True
|
||||||
|
},
|
||||||
'library': {
|
'library': {
|
||||||
'enabled': False
|
'enabled': False
|
||||||
},
|
},
|
||||||
@@ -190,6 +193,7 @@ class _TenantAwareBool:
|
|||||||
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
||||||
|
|
||||||
|
|
||||||
|
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
||||||
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
||||||
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
||||||
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
+268
-221
@@ -192,6 +192,26 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loginNavbar .navbar-brand {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginNavbar .navbar-brand::before {
|
||||||
|
content: none;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginNavbar .invario-logo {
|
||||||
|
display: block;
|
||||||
|
height: 94px;
|
||||||
|
width: auto;
|
||||||
|
max-width: min(75vw, 460px);
|
||||||
|
object-fit: contain;
|
||||||
|
filter: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-text.tenant-badge {
|
.navbar-text.tenant-badge {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
color: rgba(255,255,255,0.9);
|
color: rgba(255,255,255,0.9);
|
||||||
@@ -337,6 +357,13 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loginNavbar .invario-logo {
|
||||||
|
height: 74px;
|
||||||
|
max-width: 80vw;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.nav-compact .navbar-brand {
|
.navbar.nav-compact .navbar-brand {
|
||||||
@@ -1010,14 +1037,18 @@
|
|||||||
<div class="module-selector-bar" id="moduleBar">
|
<div class="module-selector-bar" id="moduleBar">
|
||||||
<span class="module-label">Module:</span>
|
<span class="module-label">Module:</span>
|
||||||
<div class="module-tabs">
|
<div class="module-tabs">
|
||||||
|
{% if inventory_module_enabled %}
|
||||||
<a href="{{ url_for('home') }}"
|
<a href="{{ url_for('home') }}"
|
||||||
class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}"
|
class="module-tab {% if CURRENT_MODULE == 'inventory' %}active{% endif %}"
|
||||||
id="inventoryModule"
|
id="inventoryModule"
|
||||||
data-module="inventory">
|
data-module="inventory">
|
||||||
📦 Inventarsystem
|
📦 Inventarsystem
|
||||||
</a>
|
</a>
|
||||||
{% if library_module_enabled %}
|
{% endif %}
|
||||||
|
{% if inventory_module_enabled and library_module_enabled %}
|
||||||
<div class="module-separator"></div>
|
<div class="module-separator"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if library_module_enabled %}
|
||||||
<a href="{{ url_for('library_view') }}"
|
<a href="{{ url_for('library_view') }}"
|
||||||
class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}"
|
class="module-tab {% if CURRENT_MODULE == 'library' %}active{% endif %}"
|
||||||
id="libraryModule"
|
id="libraryModule"
|
||||||
@@ -1028,234 +1059,250 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if CURRENT_MODULE != 'library' %}
|
{% if 'username' in session %}
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
|
{% if CURRENT_MODULE != 'library' and inventory_module_enabled %}
|
||||||
<div class="container-fluid">
|
<nav class="navbar navbar-expand-lg navbar-dark" id="inventoryNavbar">
|
||||||
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
|
<div class="container-fluid">
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#inventoryNavContent">
|
<a class="navbar-brand" href="{{ url_for('home') }}" data-icon="📦">Inventarsystem</a>
|
||||||
<span class="navbar-toggler-icon"></span>
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#inventoryNavContent">
|
||||||
</button>
|
<span class="navbar-toggler-icon"></span>
|
||||||
<div class="collapse navbar-collapse" id="inventoryNavContent">
|
</button>
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
<div class="collapse navbar-collapse" id="inventoryNavContent">
|
||||||
{% if current_permissions.pages.get('home', True) %}
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
{% if current_permissions.pages.get('home', True) %}
|
||||||
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a>
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
</li>
|
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a>
|
||||||
{% endif %}
|
</li>
|
||||||
{% if 'username' in session %}
|
{% endif %}
|
||||||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
{% if 'username' in session %}
|
||||||
<li class="nav-item">
|
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||||||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a>
|
<li class="nav-item">
|
||||||
</li>
|
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a>
|
||||||
{% endif %}
|
</li>
|
||||||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
{% endif %}
|
||||||
<li class="nav-item">
|
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||||||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Öffnen Sie das Tutorial, um das System Schritt für Schritt kennenzulernen.">Tutorial</a>
|
<li class="nav-item">
|
||||||
</li>
|
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Öffnen Sie das Tutorial, um das System Schritt für Schritt kennenzulernen.">Tutorial</a>
|
||||||
{% endif %}
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
{% endif %}
|
||||||
<li class="nav-item">
|
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
||||||
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}" data-tutorial-tip="Hier können Sie neue Artikel ins Inventar einpflegen.">➕ Hochladen</a>
|
<li class="nav-item">
|
||||||
</li>
|
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}" data-tutorial-tip="Hier können Sie neue Artikel ins Inventar einpflegen.">➕ Hochladen</a>
|
||||||
{% endif %}
|
</li>
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
{% endif %}
|
||||||
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
||||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||||
</button>
|
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item dropdown ms-lg-auto">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
|
||||||
Mehr Optionen
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
|
|
||||||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
|
||||||
<li><h6 class="dropdown-header">Verwaltung</h6></li>
|
|
||||||
{% if current_permissions.pages.get('manage_filters', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('manage_locations', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
|
||||||
<li><h6 class="dropdown-header">System</h6></li>
|
|
||||||
{% if current_permissions.pages.get('user_del', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('register', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item d-none" id="inv-nav-overflow-anchor" aria-hidden="true"></li>
|
|
||||||
</ul>
|
|
||||||
<div class="d-flex">
|
|
||||||
{% if 'username' in session %}
|
|
||||||
<div class="function-search-wrap">
|
|
||||||
<form class="function-search-form" data-function-search="true">
|
|
||||||
<input
|
|
||||||
class="function-search-input"
|
|
||||||
type="search"
|
|
||||||
name="function_search"
|
|
||||||
placeholder="Funktion suchen..."
|
|
||||||
list="function-search-options"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<button class="function-search-btn" type="submit">Los</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% if current_tenant_db %}
|
|
||||||
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
|
|
||||||
{% endif %}
|
|
||||||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
|
||||||
<div class="dropdown me-2 user-menu-wrap">
|
|
||||||
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
|
||||||
👤
|
|
||||||
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
|
</li>
|
||||||
{% if current_permissions.pages.get('notifications_view', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
<li class="nav-item dropdown ms-lg-auto">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||||
|
Mehr Optionen
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
|
||||||
|
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
||||||
|
<li><h6 class="dropdown-header">Verwaltung</h6></li>
|
||||||
|
{% if current_permissions.pages.get('manage_filters', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('manage_locations', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('admin_borrowings', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||||||
|
<li><h6 class="dropdown-header">System</h6></li>
|
||||||
|
{% if current_permissions.pages.get('user_del', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('register', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
||||||
|
{% endif %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</li>
|
||||||
{% endif %}
|
<li class="nav-item d-none" id="inv-nav-overflow-anchor" aria-hidden="true"></li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
<div class="d-flex">
|
||||||
</div>
|
{% if 'username' in session %}
|
||||||
</nav>
|
<div class="function-search-wrap">
|
||||||
{% endif %}
|
<form class="function-search-form" data-function-search="true">
|
||||||
|
<input
|
||||||
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
class="function-search-input"
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
type="search"
|
||||||
<div class="container-fluid">
|
name="function_search"
|
||||||
<a class="navbar-brand" href="{{ url_for('library_view') }}" data-icon="📚">Bibliothek</a>
|
placeholder="Funktion suchen..."
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNavContent">
|
list="function-search-options"
|
||||||
<span class="navbar-toggler-icon"></span>
|
autocomplete="off"
|
||||||
</button>
|
>
|
||||||
<div class="collapse navbar-collapse" id="libraryNavContent">
|
<button class="function-search-btn" type="submit">Los</button>
|
||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
</form>
|
||||||
{% if current_permissions.pages.get('library_view', True) %}
|
</div>
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
{% if current_tenant_db %}
|
||||||
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
|
<span class="navbar-text tenant-badge" title="Aktive Tenant-Datenbank">{{ current_tenant_db }}</span>
|
||||||
</li>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if 'username' in session %}
|
|
||||||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
|
||||||
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
|
||||||
<span class="theme-icon-light" style="display: none;">☀️</span>
|
|
||||||
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="nav-item dropdown ms-lg-auto">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
|
||||||
Mehr Optionen
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
|
|
||||||
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
|
||||||
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
|
||||||
{% if current_permissions.pages.get('library_loans_admin', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if student_cards_module_enabled %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
{% if current_permissions.actions.get('can_manage_users', True) %}
|
|
||||||
<li><h6 class="dropdown-header">System</h6></li>
|
|
||||||
{% if current_permissions.pages.get('user_del', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
|
||||||
{% endif %}
|
|
||||||
{% if current_permissions.pages.get('register', True) %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
|
||||||
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item d-none" id="lib-nav-overflow-anchor" aria-hidden="true"></li>
|
|
||||||
</ul>
|
|
||||||
<div class="d-flex">
|
|
||||||
{% if 'username' in session %}
|
|
||||||
<div class="function-search-wrap">
|
|
||||||
<form class="function-search-form" data-function-search="true">
|
|
||||||
<input
|
|
||||||
class="function-search-input"
|
|
||||||
type="search"
|
|
||||||
name="function_search"
|
|
||||||
placeholder="Funktion suchen..."
|
|
||||||
list="function-search-options"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<button class="function-search-btn" type="submit">Los</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||||||
<div class="dropdown me-2 user-menu-wrap">
|
<div class="dropdown me-2 user-menu-wrap">
|
||||||
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="invUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
||||||
👤
|
👤
|
||||||
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
|
||||||
{% if current_permissions.pages.get('notifications_view', True) %}
|
{% if current_permissions.pages.get('notifications_view', True) %}
|
||||||
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if library_module_enabled and CURRENT_MODULE == 'library' %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark" id="libraryNavbar">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="{{ url_for('library_view') }}" data-icon="📚">Bibliothek</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#libraryNavContent">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="libraryNavContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||||||
|
{% if current_permissions.pages.get('library_view', True) %}
|
||||||
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
|
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if 'username' in session %}
|
||||||
|
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Hier sehen Sie Ihre bibliotheksbezogenen Ausleihen.">Meine Medien</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
|
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
|
||||||
|
<span class="theme-icon-light" style="display: none;">☀️</span>
|
||||||
|
<span class="theme-icon-dark" style="display: none;">🌙</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown ms-lg-auto">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
|
||||||
|
Mehr Optionen
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
|
||||||
|
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %}
|
||||||
|
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
|
||||||
|
{% if current_permissions.pages.get('library_loans_admin', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('admin_damaged_items', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if student_cards_module_enabled %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{% if current_permissions.actions.get('can_manage_users', True) %}
|
||||||
|
<li><h6 class="dropdown-header">System</h6></li>
|
||||||
|
{% if current_permissions.pages.get('user_del', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_permissions.pages.get('register', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('impressum') }}">Impressum</a></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('license') }}">Lizenz</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none" id="lib-nav-overflow-anchor" aria-hidden="true"></li>
|
||||||
|
</ul>
|
||||||
|
<div class="d-flex">
|
||||||
|
{% if 'username' in session %}
|
||||||
|
<div class="function-search-wrap">
|
||||||
|
<form class="function-search-form" data-function-search="true">
|
||||||
|
<input
|
||||||
|
class="function-search-input"
|
||||||
|
type="search"
|
||||||
|
name="function_search"
|
||||||
|
placeholder="Funktion suchen..."
|
||||||
|
list="function-search-options"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button class="function-search-btn" type="submit">Los</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<span class="navbar-text text-light me-3">{{ session['username'] }}</span>
|
||||||
|
<div class="dropdown me-2 user-menu-wrap">
|
||||||
|
<button class="btn btn-secondary dropdown-toggle user-menu-btn" type="button" id="libUserMenuDropdown" data-bs-toggle="dropdown" aria-expanded="false" data-notification-button="true">
|
||||||
|
👤
|
||||||
|
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
|
||||||
|
{% if current_permissions.pages.get('notifications_view', True) %}
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('change_password') }}">Passwort ändern</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark" id="loginNavbar">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand py-0" href="{{ url_for('home') }}">
|
||||||
|
<img src="{{ url_for('static', filename='img/invario-logo.png') }}" alt="Invario" class="invario-logo">
|
||||||
|
</a>
|
||||||
|
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ url_for('impressum') }}">Impressum</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
+26
-10
@@ -665,18 +665,30 @@
|
|||||||
// Set up schedule form submission
|
// Set up schedule form submission
|
||||||
setupScheduleFormSubmission();
|
setupScheduleFormSubmission();
|
||||||
|
|
||||||
// Fetch user status to get current username
|
// Fetch user status to get current username and full filter lists
|
||||||
fetch('/user_status')
|
Promise.all([
|
||||||
.then(response => response.json())
|
fetch('/user_status').then(response => response.json()),
|
||||||
.then(data => {
|
fetch('/get_filter').then(response => response.json()),
|
||||||
currentUsername = data.username;
|
])
|
||||||
|
.then(([userData, filterData]) => {
|
||||||
|
currentUsername = userData.username;
|
||||||
|
if (filterData.filter1) {
|
||||||
|
allFilter1Values = new Set(filterData.filter1.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
if (filterData.filter2) {
|
||||||
|
allFilter2Values = new Set(filterData.filter2.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
if (filterData.filter3) {
|
||||||
|
allFilter3Values = new Set(filterData.filter3.filter(value => value && typeof value === 'string' && value.trim() !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
// Now load items with username information
|
// Now load items with username information
|
||||||
loadItems();
|
loadItems();
|
||||||
// Setup card images display after a short delay to ensure items are loaded
|
// Setup card images display after a short delay to ensure items are loaded
|
||||||
setTimeout(setupCardImagesDisplay, 500);
|
setTimeout(setupCardImagesDisplay, 500);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error fetching user status:', error);
|
console.error('Error fetching user status or filters:', error);
|
||||||
loadItems(); // Load items anyway in case of error
|
loadItems(); // Load items anyway in case of error
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -721,6 +733,10 @@
|
|||||||
filter3: []
|
filter3: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let allFilter1Values = new Set();
|
||||||
|
let allFilter2Values = new Set();
|
||||||
|
let allFilter3Values = new Set();
|
||||||
|
|
||||||
// Add this line to define allItems globally
|
// Add this line to define allItems globally
|
||||||
let allItems = [];
|
let allItems = [];
|
||||||
|
|
||||||
@@ -1083,10 +1099,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Populate filter options
|
// Populate filter options using server-provided full filter lists when available.
|
||||||
populateFilterOptions('filter1-options', [...filter1Values].sort(), 1);
|
populateFilterOptions('filter1-options', allFilter1Values.size ? [...allFilter1Values].sort() : [...filter1Values].sort(), 1);
|
||||||
populateFilterOptions('filter2-options', [...filter2Values].sort(), 2);
|
populateFilterOptions('filter2-options', allFilter2Values.size ? [...allFilter2Values].sort() : [...filter2Values].sort(), 2);
|
||||||
populateFilterOptions('filter3-options', [...filter3Values].sort(), 3);
|
populateFilterOptions('filter3-options', allFilter3Values.size ? [...allFilter3Values].sort() : [...filter3Values].sort(), 3);
|
||||||
|
|
||||||
// Start display from leftmost position on full reload only.
|
// Start display from leftmost position on full reload only.
|
||||||
if (!append) {
|
if (!append) {
|
||||||
|
|||||||
+74
-4
@@ -13,6 +13,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import settings as cfg
|
import settings as cfg
|
||||||
|
from settings import MongoClient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -32,6 +33,21 @@ def _get_nested_value(source, path, default=None):
|
|||||||
return current
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tenant_db_map():
|
||||||
|
mapping = {}
|
||||||
|
raw_map = os.getenv('INVENTAR_TENANT_DB_MAP', '').strip()
|
||||||
|
if not raw_map:
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
for mapping_pair in re.split(r'[;,\s]+', raw_map):
|
||||||
|
if '=' not in mapping_pair:
|
||||||
|
continue
|
||||||
|
key, value = mapping_pair.split('=', 1)
|
||||||
|
mapping[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
def _parse_port_from_host(host):
|
def _parse_port_from_host(host):
|
||||||
"""Parse host header and return (hostname, port) when a numeric port is present."""
|
"""Parse host header and return (hostname, port) when a numeric port is present."""
|
||||||
if host.startswith('['):
|
if host.startswith('['):
|
||||||
@@ -84,6 +100,52 @@ def get_tenant_config(tenant_id=None):
|
|||||||
return TENANT_REGISTRY.get('default', {}) or {}
|
return TENANT_REGISTRY.get('default', {}) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_db_name(db_name):
|
||||||
|
if not db_name:
|
||||||
|
return None
|
||||||
|
db_name = str(db_name).strip()
|
||||||
|
if db_name and not db_name.startswith('inventar_'):
|
||||||
|
db_name = f'inventar_{db_name}'
|
||||||
|
return db_name
|
||||||
|
|
||||||
|
|
||||||
|
def _tenant_db_aliases(tenant_id):
|
||||||
|
aliases = []
|
||||||
|
env_map = _parse_tenant_db_map()
|
||||||
|
if tenant_id in env_map:
|
||||||
|
aliases.append(env_map[tenant_id])
|
||||||
|
|
||||||
|
if tenant_id.lower().startswith('schule'):
|
||||||
|
aliases.append(tenant_id.lower().replace('schule', 'school', 1))
|
||||||
|
elif tenant_id.lower().startswith('school'):
|
||||||
|
aliases.append(tenant_id.lower().replace('school', 'schule', 1))
|
||||||
|
|
||||||
|
return [alias for alias in aliases if alias]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_db_alias(tenant_id, db_name):
|
||||||
|
try:
|
||||||
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
|
available_databases = client.list_database_names()
|
||||||
|
if db_name in available_databases:
|
||||||
|
return db_name
|
||||||
|
|
||||||
|
for alias in _tenant_db_aliases(tenant_id):
|
||||||
|
candidate = _normalize_db_name(alias)
|
||||||
|
if candidate in available_databases:
|
||||||
|
logger.warning(
|
||||||
|
"Tenant DB fallback applied for tenant %r: %r -> %r",
|
||||||
|
tenant_id,
|
||||||
|
db_name,
|
||||||
|
candidate,
|
||||||
|
)
|
||||||
|
return candidate
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Tenant DB alias resolution failed for %r: %s", tenant_id, exc)
|
||||||
|
|
||||||
|
return db_name
|
||||||
|
|
||||||
|
|
||||||
def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
|
def is_tenant_module_enabled(module_name, tenant_id=None, default=False):
|
||||||
"""Resolve whether a feature module is enabled for the current tenant."""
|
"""Resolve whether a feature module is enabled for the current tenant."""
|
||||||
config = get_tenant_config(tenant_id)
|
config = get_tenant_config(tenant_id)
|
||||||
@@ -170,11 +232,19 @@ class TenantContext:
|
|||||||
def _get_db_name(self, tenant_id):
|
def _get_db_name(self, tenant_id):
|
||||||
"""
|
"""
|
||||||
Get MongoDB database name for tenant.
|
Get MongoDB database name for tenant.
|
||||||
Format: inventar_<tenant_id>
|
Format: inventar_<tenant_id> unless tenant config overrides it.
|
||||||
"""
|
"""
|
||||||
# Sanitize tenant_id for MongoDB database name
|
explicit_db = None
|
||||||
sanitized = ''.join(c if c.isalnum() or c == '_' else '' for c in tenant_id.lower())
|
if isinstance(self.config, dict):
|
||||||
db_name = f"inventar_{sanitized}"
|
explicit_db = self.config.get('db') or self.config.get('db_name')
|
||||||
|
|
||||||
|
if explicit_db:
|
||||||
|
db_name = _normalize_db_name(explicit_db)
|
||||||
|
else:
|
||||||
|
sanitized = ''.join(c if c.isalnum() or c == '_' else '' for c in tenant_id.lower())
|
||||||
|
db_name = f"inventar_{sanitized}"
|
||||||
|
|
||||||
|
db_name = _resolve_db_alias(tenant_id, db_name)
|
||||||
self.db_name = db_name
|
self.db_name = db_name
|
||||||
return db_name
|
return db_name
|
||||||
|
|
||||||
|
|||||||
+24
-12
@@ -20,7 +20,9 @@ from bson.objectid import ObjectId
|
|||||||
import settings as cfg
|
import settings as cfg
|
||||||
from settings import MongoClient
|
from settings import MongoClient
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger('app')
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.propagate = True
|
||||||
|
|
||||||
|
|
||||||
def normalize_student_card_id(card_id):
|
def normalize_student_card_id(card_id):
|
||||||
@@ -448,22 +450,32 @@ def check_nm_pwd(username, password):
|
|||||||
try:
|
try:
|
||||||
hashed_password = hashing(password)
|
hashed_password = hashing(password)
|
||||||
logger.info("check_nm_pwd password hash for username=%r: %s", username, hashed_password)
|
logger.info("check_nm_pwd password hash for username=%r: %s", username, hashed_password)
|
||||||
|
available_dbs = []
|
||||||
|
try:
|
||||||
|
available_dbs = client.list_database_names()
|
||||||
|
logger.debug("MongoDB connected. Available databases=%s", available_dbs)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Unable to list MongoDB databases: %s", exc)
|
||||||
|
|
||||||
db = client[db_name]
|
db = client[db_name]
|
||||||
|
try:
|
||||||
|
existing_collections = db.list_collection_names()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Unable to list collections for db=%r: %s", db_name, exc)
|
||||||
|
existing_collections = []
|
||||||
|
logger.debug("Tenant db=%r collections=%s", db_name, existing_collections)
|
||||||
|
|
||||||
users = db['users']
|
users = db['users']
|
||||||
user_record = users.find_one({'$or': [{'Username': username}, {'username': username}]})
|
query = {'$or': [{'Username': username}, {'username': username}]}
|
||||||
|
logger.debug("Running user lookup on %r: %s", db_name, query)
|
||||||
|
user_record = users.find_one(query)
|
||||||
|
|
||||||
if user_record is None:
|
if user_record is None:
|
||||||
try:
|
logger.warning("No user document found in db=%r for username=%r", db_name, username)
|
||||||
available_dbs = client.list_database_names()
|
if db_name not in available_dbs:
|
||||||
logger.warning(
|
logger.warning("Tenant database %r is missing from available MongoDB databases", db_name)
|
||||||
"No user document in tenant database %r for username=%r. Available databases=%s",
|
if 'users' not in existing_collections:
|
||||||
db_name,
|
logger.warning("Tenant database %r has no users collection", db_name)
|
||||||
username,
|
|
||||||
available_dbs,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.exception("Could not list databases during failed login check: %s", exc)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.info("Found user document for username=%r in db=%r: %s", username, db_name, user_record)
|
logger.info("Found user document for username=%r in db=%r: %s", username, db_name, user_record)
|
||||||
|
|||||||
+87
-29
@@ -4,42 +4,44 @@
|
|||||||
"ver": "0.6.44",
|
"ver": "0.6.44",
|
||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 8000,
|
"port": 8000,
|
||||||
|
|
||||||
"mongodb": {
|
"mongodb": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 27017,
|
"port": 27017,
|
||||||
"db": "Inventarsystem"
|
"db": "Inventarsystem"
|
||||||
},
|
},
|
||||||
|
|
||||||
"scheduler": {
|
"scheduler": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"interval_minutes": 1,
|
"interval_minutes": 1,
|
||||||
"backup_interval_hours": 24
|
"backup_interval_hours": 24
|
||||||
},
|
},
|
||||||
|
|
||||||
"ssl": {
|
"ssl": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"cert": "Web/certs/cert.pem",
|
"cert": "Web/certs/cert.pem",
|
||||||
"key": "Web/certs/key.pem"
|
"key": "Web/certs/key.pem"
|
||||||
},
|
},
|
||||||
|
|
||||||
"images": {
|
"images": {
|
||||||
"thumbnail_size": [150, 150],
|
"thumbnail_size": [
|
||||||
"preview_size": [400, 400]
|
150,
|
||||||
|
150
|
||||||
|
],
|
||||||
|
"preview_size": [
|
||||||
|
400,
|
||||||
|
400
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"upload": {
|
"upload": {
|
||||||
"max_size_mb": 10,
|
"max_size_mb": 10,
|
||||||
"image_max_size_mb": 15,
|
"image_max_size_mb": 15,
|
||||||
"video_max_size_mb": 100
|
"video_max_size_mb": 100
|
||||||
},
|
},
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"backups": "backups",
|
"backups": "backups",
|
||||||
"logs": "logs"
|
"logs": "logs"
|
||||||
},
|
},
|
||||||
|
|
||||||
"modules": {
|
"modules": {
|
||||||
|
"inventory": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
@@ -49,27 +51,83 @@
|
|||||||
"max_borrow_days": 365
|
"max_borrow_days": 365
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tenants": {
|
||||||
"tenants": {},
|
},
|
||||||
|
|
||||||
"allowed_extensions": [
|
"allowed_extensions": [
|
||||||
"png", "jpg", "jpeg", "gif",
|
"png",
|
||||||
"hevc", "heif",
|
"jpg",
|
||||||
"mp4", "mov", "avi", "mkv", "webm",
|
"jpeg",
|
||||||
"mp3", "wav", "ogg", "flac", "aac", "m4a", "opus",
|
"gif",
|
||||||
"pdf", "docx", "xlsx", "pptx", "txt"
|
"hevc",
|
||||||
|
"heif",
|
||||||
|
"mp4",
|
||||||
|
"mov",
|
||||||
|
"avi",
|
||||||
|
"mkv",
|
||||||
|
"webm",
|
||||||
|
"mp3",
|
||||||
|
"wav",
|
||||||
|
"ogg",
|
||||||
|
"flac",
|
||||||
|
"aac",
|
||||||
|
"m4a",
|
||||||
|
"opus",
|
||||||
|
"pdf",
|
||||||
|
"docx",
|
||||||
|
"xlsx",
|
||||||
|
"pptx",
|
||||||
|
"txt"
|
||||||
],
|
],
|
||||||
|
|
||||||
"schoolPeriods": {
|
"schoolPeriods": {
|
||||||
"1": { "start": "08:00", "end": "08:45", "label": "1. Stunde (08:00 - 08:45)" },
|
"1": {
|
||||||
"2": { "start": "08:45", "end": "09:30", "label": "2. Stunde (08:45 - 09:30)" },
|
"start": "08:00",
|
||||||
"3": { "start": "09:45", "end": "10:30", "label": "3. Stunde (09:45 - 10:30)" },
|
"end": "08:45",
|
||||||
"4": { "start": "10:30", "end": "11:15", "label": "4. Stunde (10:30 - 11:15)" },
|
"label": "1. Stunde (08:00 - 08:45)"
|
||||||
"5": { "start": "11:30", "end": "12:15", "label": "5. Stunde (11:30 - 12:15)" },
|
},
|
||||||
"6": { "start": "12:15", "end": "13:00", "label": "6. Stunde (12:15 - 13:00)" },
|
"2": {
|
||||||
"7": { "start": "13:30", "end": "14:15", "label": "7. Stunde (13:30 - 14:15)" },
|
"start": "08:45",
|
||||||
"8": { "start": "14:15", "end": "15:00", "label": "8. Stunde (14:15 - 15:00)" },
|
"end": "09:30",
|
||||||
"9": { "start": "15:15", "end": "16:00", "label": "9. Stunde (15:15 - 16:00)" },
|
"label": "2. Stunde (08:45 - 09:30)"
|
||||||
"10": { "start": "16:00", "end": "16:45", "label": "10. Stunde (16:00 - 16:45)" }
|
},
|
||||||
|
"3": {
|
||||||
|
"start": "09:45",
|
||||||
|
"end": "10:30",
|
||||||
|
"label": "3. Stunde (09:45 - 10:30)"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"start": "10:30",
|
||||||
|
"end": "11:15",
|
||||||
|
"label": "4. Stunde (10:30 - 11:15)"
|
||||||
|
},
|
||||||
|
"5": {
|
||||||
|
"start": "11:30",
|
||||||
|
"end": "12:15",
|
||||||
|
"label": "5. Stunde (11:30 - 12:15)"
|
||||||
|
},
|
||||||
|
"6": {
|
||||||
|
"start": "12:15",
|
||||||
|
"end": "13:00",
|
||||||
|
"label": "6. Stunde (12:15 - 13:00)"
|
||||||
|
},
|
||||||
|
"7": {
|
||||||
|
"start": "13:30",
|
||||||
|
"end": "14:15",
|
||||||
|
"label": "7. Stunde (13:30 - 14:15)"
|
||||||
|
},
|
||||||
|
"8": {
|
||||||
|
"start": "14:15",
|
||||||
|
"end": "15:00",
|
||||||
|
"label": "8. Stunde (14:15 - 15:00)"
|
||||||
|
},
|
||||||
|
"9": {
|
||||||
|
"start": "15:15",
|
||||||
|
"end": "16:00",
|
||||||
|
"label": "9. Stunde (15:15 - 16:00)"
|
||||||
|
},
|
||||||
|
"10": {
|
||||||
|
"start": "16:00",
|
||||||
|
"end": "16:45",
|
||||||
|
"label": "10. Stunde (16:00 - 16:45)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
services:
|
services:
|
||||||
# Management Container for multi-tenant scripts
|
# Management Container for multi-tenant scripts
|
||||||
tenant-manager:
|
tenant-manager:
|
||||||
image: docker:cli
|
image: python:3.12-alpine
|
||||||
container_name: tenant-manager
|
container_name: tenant-manager
|
||||||
restart: "no"
|
restart: "no"
|
||||||
profiles:
|
profiles:
|
||||||
@@ -19,7 +19,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- .:/workspace
|
- .:/workspace
|
||||||
entrypoint: ["sh", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"]
|
entrypoint: >
|
||||||
|
sh -c "apk add --no-cache bash docker-cli docker-cli-compose && cd /workspace && ./manage-tenant.sh \"$$@\"" --
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
|
|||||||
+79
-6
@@ -20,11 +20,13 @@ show_help() {
|
|||||||
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
echo " restart-tenant <id> 'Restart' a single tenant (clears cache/sessions)"
|
||||||
echo " restart-all Restart all application containers (zero-downtime reload)"
|
echo " restart-all Restart all application containers (zero-downtime reload)"
|
||||||
echo " list List active tenants"
|
echo " list List active tenants"
|
||||||
|
echo " module <tenant_id> <module>=<on|off>... Enable/disable modules for a tenant"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " ./manage-tenant.sh add school_a 10001"
|
echo " ./manage-tenant.sh add school_a 10001"
|
||||||
echo " ./manage-tenant.sh remove test_tenant"
|
echo " ./manage-tenant.sh remove test_tenant"
|
||||||
|
echo " ./manage-tenant.sh module school_a inventory=off library=on"
|
||||||
echo " ./manage-tenant.sh restart-all"
|
echo " ./manage-tenant.sh restart-all"
|
||||||
echo " ./manage-tenant.sh -h"
|
echo " ./manage-tenant.sh -h"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -173,13 +175,31 @@ EOF
|
|||||||
|
|
||||||
restart_app_container() {
|
restart_app_container() {
|
||||||
local env_file="$PWD/.docker-build.env"
|
local env_file="$PWD/.docker-build.env"
|
||||||
local compose_args=( -f "$PWD/docker-compose-multitenant.yml" )
|
local compose_args=()
|
||||||
|
|
||||||
if [ -f "$PWD/.docker-compose.runtime.override.yml" ]; then
|
# If HOST_WORKDIR is set (called from container), use absolute paths so docker daemon resolves them correctly
|
||||||
compose_args+=( -f "$PWD/.docker-compose.runtime.override.yml" )
|
if [ -n "$HOST_WORKDIR" ]; then
|
||||||
|
compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/docker-compose-multitenant.yml")" )
|
||||||
|
if [ -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml" ]; then
|
||||||
|
compose_args+=( -f "$(readlink -f "$HOST_WORKDIR/.docker-compose.runtime.override.yml")" )
|
||||||
|
fi
|
||||||
|
if [ -f "$HOST_WORKDIR/.docker-build.env" ]; then
|
||||||
|
compose_args+=( --env-file "$(readlink -f "$HOST_WORKDIR/.docker-build.env")" )
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Normal case: called directly from host
|
||||||
|
compose_args+=( -f "$PWD/docker-compose-multitenant.yml" )
|
||||||
|
if [ -f "$PWD/.docker-compose.runtime.override.yml" ]; then
|
||||||
|
compose_args+=( -f "$PWD/.docker-compose.runtime.override.yml" )
|
||||||
|
fi
|
||||||
|
if [ -f "$env_file" ]; then
|
||||||
|
compose_args+=( --env-file "$env_file" )
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -f "$env_file" ]; then
|
|
||||||
compose_args+=( --env-file "$env_file" )
|
# Pass along COMPOSE_PROJECT_NAME if set so the internal docker-compose sees it
|
||||||
|
if [ -n "$COMPOSE_PROJECT_NAME" ]; then
|
||||||
|
compose_args=( -p "$COMPOSE_PROJECT_NAME" "${compose_args[@]}" )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Restarting app container to apply tenant configuration changes..."
|
echo "Restarting app container to apply tenant configuration changes..."
|
||||||
@@ -492,6 +512,59 @@ PY
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
module)
|
||||||
|
if [ -z "$TENANT_ID" ] || [ -z "${3:-}" ]; then
|
||||||
|
echo "Error: Usage: manage-tenant.sh module <tenant_id> <module_name>=<on|off> [...]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass all remaining arguments to python script
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
if python3 - "$CONFIG_FILE" "$TENANT_ID" "$@" <<'PY'
|
||||||
|
import json, sys, os
|
||||||
|
path = sys.argv[1]
|
||||||
|
tenant_id = sys.argv[2]
|
||||||
|
module_args = sys.argv[3:]
|
||||||
|
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
print(f"Error: config file not found: {path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
|
||||||
|
tenants = cfg.setdefault('tenants', {})
|
||||||
|
tenant_cfg = tenants.setdefault(tenant_id, {})
|
||||||
|
if 'port' not in tenant_cfg:
|
||||||
|
print(f"Warning: Tenant {tenant_id} doesn't have a port mapping in config.json.", file=sys.stderr)
|
||||||
|
|
||||||
|
modules = tenant_cfg.setdefault('modules', {})
|
||||||
|
|
||||||
|
for arg in module_args:
|
||||||
|
if '=' not in arg:
|
||||||
|
print(f"Warning: Ignoring invalid argument format '{arg}'. Expected name=on|off.", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
mod_name, state_str = arg.split('=', 1)
|
||||||
|
state = str(state_str).lower() in ('on', '1', 'true', 'yes')
|
||||||
|
module_cfg = modules.setdefault(mod_name, {})
|
||||||
|
module_cfg['enabled'] = state
|
||||||
|
print(f"Module '{mod_name}' set to '{'on' if state else 'off'}' for tenant '{tenant_id}'.")
|
||||||
|
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(cfg, f, indent=4, ensure_ascii=False)
|
||||||
|
PY
|
||||||
|
then
|
||||||
|
echo "Module configurations updated successfully."
|
||||||
|
if [ -n "$(docker ps -qf 'name=app' | head -n 1)" ]; then
|
||||||
|
restart_app_container
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Failed to update module configuration."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
echo "Unknown command: $COMMAND"
|
echo "Unknown command: $COMMAND"
|
||||||
show_help
|
show_help
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Wrapper to run tenant management fully containerized via docker-compose
|
# Wrapper to run tenant management fully containerized via docker-compose
|
||||||
docker compose -f docker-compose-multitenant.yml --profile tools run --rm tenant-manager "$@"
|
PROJECT_NAME=${COMPOSE_PROJECT_NAME:-$(basename "$PWD" | tr '[:upper:]' '[:lower:]')}
|
||||||
|
# Pass the absolute working directory to the container so docker compose can resolve paths correctly
|
||||||
|
docker compose -f docker-compose-multitenant.yml --profile tools run --rm -e COMPOSE_PROJECT_NAME="$PROJECT_NAME" -e HOST_WORKDIR="$PWD" tenant-manager "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user