Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 678cc61cef | |||
| f26ab5c5fb | |||
| abb09fbdb5 | |||
| 96f5b9e3cd | |||
| 3ab2b075da | |||
| 821d4d0ad8 |
+106
-8
@@ -206,6 +206,82 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.tutorial-tooltips-active [data-tutorial-tip] {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 10px;
|
||||||
|
outline: 1px dashed rgba(59, 130, 246, 0.35);
|
||||||
|
outline-offset: 2px;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.tutorial-tooltips-active [data-tutorial-tip]::before {
|
||||||
|
content: '💡';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -26px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.tutorial-tooltips-active [data-tutorial-tip]::after {
|
||||||
|
content: attr(data-tutorial-tip);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 10px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(6px);
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 280px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.92);
|
||||||
|
color: #f8fafc;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: left;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.22);
|
||||||
|
z-index: 9999;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.tutorial-tooltips-active [data-tutorial-tip]:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateX(-50%) translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-indicator {
|
||||||
|
position: fixed;
|
||||||
|
top: 92px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 2000;
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(56, 189, 248, 0.12);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.tutorial-tooltips-active .tutorial-tooltip-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
/* iPad/Tablet responsive adjustments */
|
/* iPad/Tablet responsive adjustments */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.navbar .container-fluid {
|
.navbar .container-fluid {
|
||||||
@@ -952,24 +1028,24 @@
|
|||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
|
||||||
{% if current_permissions.pages.get('home', True) %}
|
{% if current_permissions.pages.get('home', True) %}
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}">Artikel</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||||||
<li class="nav-item">
|
<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') }}">Meine Ausleihen</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||||||
<li class="nav-item">
|
<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') }}">Tutorial</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}">➕ Hochladen</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
@@ -1071,24 +1147,24 @@
|
|||||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
|
||||||
{% if current_permissions.pages.get('library_view', True) %}
|
{% if current_permissions.pages.get('library_view', True) %}
|
||||||
<li class="nav-item" data-nav-fixed="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') }}">Medien</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'username' in session %}
|
{% if 'username' in session %}
|
||||||
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
{% if current_permissions.pages.get('my_borrowed_items', True) %}
|
||||||
<li class="nav-item">
|
<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') }}">Meine Medien</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_permissions.pages.get('tutorial_page', True) %}
|
{% if current_permissions.pages.get('tutorial_page', True) %}
|
||||||
<li class="nav-item">
|
<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') }}">Tutorial</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %}
|
||||||
<li class="nav-item">
|
<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') }}">📖 Hochladen</a>
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li class="nav-item" data-nav-fixed="true">
|
<li class="nav-item" data-nav-fixed="true">
|
||||||
@@ -1999,6 +2075,28 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const username = {{ (session['username'] if 'username' in session else '')|tojson }};
|
||||||
|
const tooltipModeKey = username ? ('inventarsystem_tutorial_tooltips_enabled_' + username) : 'inventarsystem_tutorial_tooltips_enabled';
|
||||||
|
const enabled = localStorage.getItem(tooltipModeKey) === '1';
|
||||||
|
|
||||||
|
function applyTooltipMode(active) {
|
||||||
|
document.body.classList.toggle('tutorial-tooltips-active', active);
|
||||||
|
let indicator = document.getElementById('tutorialTooltipIndicator');
|
||||||
|
if (!indicator) {
|
||||||
|
indicator = document.createElement('div');
|
||||||
|
indicator.id = 'tutorialTooltipIndicator';
|
||||||
|
indicator.className = 'tutorial-tooltip-indicator';
|
||||||
|
indicator.textContent = 'Tutorial-Modus aktiv: Tooltips sind eingeschaltet';
|
||||||
|
document.body.appendChild(indicator);
|
||||||
|
}
|
||||||
|
indicator.style.display = active ? 'inline-flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTooltipMode(enabled);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -189,6 +189,38 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(224, 242, 254, 0.9);
|
||||||
|
border: 1px solid #bae6fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-toggle label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-toggle input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
accent-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-description {
|
||||||
|
margin: 0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-controls {
|
.workflow-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -230,6 +262,13 @@
|
|||||||
<span class="tutorial-pill">Format: Ruhig und geführt</span>
|
<span class="tutorial-pill">Format: Ruhig und geführt</span>
|
||||||
<span class="tutorial-pill">Benutzer: {{ username }}</span>
|
<span class="tutorial-pill">Benutzer: {{ username }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tutorial-tooltip-toggle">
|
||||||
|
<label for="toggleTutorialTooltips">
|
||||||
|
<input type="checkbox" id="toggleTutorialTooltips" />
|
||||||
|
Tutorial-Tooltips für das gesamte System aktivieren
|
||||||
|
</label>
|
||||||
|
<p class="tooltip-description">Wenn aktiviert, erscheinen auf allen Seiten erklärende Hinweise und Icons für die wichtigsten Navigationselemente.</p>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="tutorial-grid">
|
<div class="tutorial-grid">
|
||||||
@@ -438,8 +477,32 @@
|
|||||||
renderStep();
|
renderStep();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tooltipToggle = document.getElementById('toggleTutorialTooltips');
|
||||||
|
const tooltipModeKey = 'inventarsystem_tutorial_tooltips_enabled_{{ username }}';
|
||||||
|
|
||||||
|
function setTooltipMode(enabled) {
|
||||||
|
localStorage.setItem(tooltipModeKey, enabled ? '1' : '0');
|
||||||
|
document.body.classList.toggle('tutorial-tooltips-active', enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTooltipMode() {
|
||||||
|
const saved = localStorage.getItem(tooltipModeKey);
|
||||||
|
const enabled = saved === '1';
|
||||||
|
if (tooltipToggle) {
|
||||||
|
tooltipToggle.checked = enabled;
|
||||||
|
}
|
||||||
|
setTooltipMode(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tooltipToggle) {
|
||||||
|
tooltipToggle.addEventListener('change', function () {
|
||||||
|
setTooltipMode(this.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
loadState();
|
loadState();
|
||||||
renderStep();
|
renderStep();
|
||||||
|
loadTooltipMode();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+64
-8
@@ -417,14 +417,42 @@ def check_nm_pwd(username, password):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: User document if credentials are valid, None otherwise
|
dict: User document if credentials are valid, None otherwise
|
||||||
"""
|
"""
|
||||||
|
db_name = cfg.MONGODB_DB
|
||||||
|
tenant_db = None
|
||||||
|
try:
|
||||||
|
from tenant import get_tenant_context
|
||||||
|
ctx = get_tenant_context()
|
||||||
|
if ctx and ctx.tenant_id:
|
||||||
|
tenant_db = ctx.db_name or ctx.resolve_tenant()
|
||||||
|
db_name = tenant_db
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
try:
|
||||||
|
hashed_password = hashing(password)
|
||||||
|
|
||||||
|
def find_user_in_db(database_name):
|
||||||
|
db = client[database_name]
|
||||||
users = db['users']
|
users = db['users']
|
||||||
hashed_password = hashlib.sha512(password.encode()).hexdigest()
|
return users.find_one({'Username': username, 'Password': hashed_password}) or users.find_one({'username': username, 'Password': hashed_password})
|
||||||
user = users.find_one({'Username': username, 'Password': hashed_password})
|
|
||||||
client.close()
|
user = find_user_in_db(db_name)
|
||||||
|
if user:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
# Fallback: tenant context may not be available yet, so search all tenant databases.
|
||||||
|
for database_name in client.list_database_names():
|
||||||
|
if database_name == db_name or not database_name.startswith('inventar_'):
|
||||||
|
continue
|
||||||
|
user = find_user_in_db(database_name)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def add_user(
|
def add_user(
|
||||||
username,
|
username,
|
||||||
@@ -471,7 +499,7 @@ def add_user(
|
|||||||
'Admin': False,
|
'Admin': False,
|
||||||
'active_ausleihung': None,
|
'active_ausleihung': None,
|
||||||
'name': name_alias,
|
'name': name_alias,
|
||||||
'last_name': '',
|
'last_name': last_name.strip() if last_name else '',
|
||||||
'IsStudent': bool(is_student),
|
'IsStudent': bool(is_student),
|
||||||
'PermissionPreset': permission_defaults['preset'],
|
'PermissionPreset': permission_defaults['preset'],
|
||||||
'ActionPermissions': permission_defaults['actions'],
|
'ActionPermissions': permission_defaults['actions'],
|
||||||
@@ -564,11 +592,39 @@ def get_user(username):
|
|||||||
dict: User document or None if not found
|
dict: User document or None if not found
|
||||||
"""
|
"""
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
try:
|
||||||
|
def find_in_db(database_name):
|
||||||
|
db = client[database_name]
|
||||||
users = db['users']
|
users = db['users']
|
||||||
users_return = users.find_one({'Username': username})
|
return users.find_one({'Username': username}) or users.find_one({'username': username})
|
||||||
|
|
||||||
|
# Try current tenant first when available
|
||||||
|
try:
|
||||||
|
from tenant import get_tenant_context
|
||||||
|
ctx = get_tenant_context()
|
||||||
|
if ctx and ctx.db_name:
|
||||||
|
user = find_in_db(ctx.db_name)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback to default configured database
|
||||||
|
user = find_in_db(cfg.MONGODB_DB)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
|
# Last fallback: search all tenant databases if the user belongs to a tenant-specific DB
|
||||||
|
for database_name in client.list_database_names():
|
||||||
|
if database_name == cfg.MONGODB_DB or not database_name.startswith('inventar_'):
|
||||||
|
continue
|
||||||
|
user = find_in_db(database_name)
|
||||||
|
if user:
|
||||||
|
return user
|
||||||
|
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
client.close()
|
client.close()
|
||||||
return users_return
|
|
||||||
|
|
||||||
|
|
||||||
def check_admin(username):
|
def check_admin(username):
|
||||||
|
|||||||
@@ -106,11 +106,16 @@ services:
|
|||||||
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
|
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
|
||||||
|
|
||||||
# Performance Tuning
|
# Performance Tuning
|
||||||
INVENTAR_WORKER_CLASS: gevent
|
INVENTAR_WORKER_CLASS: ${INVENTAR_WORKER_CLASS:-gevent}
|
||||||
INVENTAR_WORKERS: "4"
|
INVENTAR_WORKERS: "${INVENTAR_WORKERS:-4}"
|
||||||
INVENTAR_THREADS: "2"
|
INVENTAR_THREADS: "${INVENTAR_THREADS:-2}"
|
||||||
INVENTAR_WORKER_TIMEOUT: "30"
|
INVENTAR_WORKER_TIMEOUT: "${INVENTAR_WORKER_TIMEOUT:-30}"
|
||||||
INVENTAR_WORKER_CONNECTIONS: "100"
|
INVENTAR_WORKER_CONNECTIONS: "${INVENTAR_WORKER_CONNECTIONS:-100}"
|
||||||
|
|
||||||
|
# Auto-scaled runtime settings
|
||||||
|
INVENTAR_APP_CPUS: ${INVENTAR_APP_CPUS:-1.0}
|
||||||
|
INVENTAR_APP_MEM_LIMIT: ${INVENTAR_APP_MEM_LIMIT:-256m}
|
||||||
|
INVENTAR_APP_MEM_SWAP_LIMIT: ${INVENTAR_APP_MEM_SWAP_LIMIT:-512m}
|
||||||
|
|
||||||
# Multi-Tenant
|
# Multi-Tenant
|
||||||
INVENTAR_MULTITENANT_ENABLED: "true"
|
INVENTAR_MULTITENANT_ENABLED: "true"
|
||||||
@@ -131,9 +136,9 @@ services:
|
|||||||
# Resource Limits (per instance)
|
# Resource Limits (per instance)
|
||||||
# With 10 instances: each gets ~150MB, totaling ~1.5GB
|
# With 10 instances: each gets ~150MB, totaling ~1.5GB
|
||||||
# Adjust based on server resources
|
# Adjust based on server resources
|
||||||
mem_limit: 256m
|
mem_limit: "${INVENTAR_APP_MEM_LIMIT:-256m}"
|
||||||
memswap_limit: 512m
|
memswap_limit: "${INVENTAR_APP_MEM_SWAP_LIMIT:-512m}"
|
||||||
cpus: "1.0"
|
cpus: "${INVENTAR_APP_CPUS:-1.0}"
|
||||||
|
|
||||||
# Health check for load balancer
|
# Health check for load balancer
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
+50
-6
@@ -247,11 +247,53 @@ case "$COMMAND" in
|
|||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
if [ -n "$APP_CONTAINER" ]; then
|
||||||
docker exec $APP_CONTAINER python3 -c "
|
docker exec $APP_CONTAINER python3 -c "
|
||||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient; import hashlib
|
import sys, re; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient; import hashlib
|
||||||
|
tenant_id = sys.argv[1].lower()
|
||||||
|
sanitized = ''.join(c for c in tenant_id if c.isalnum() or c == '_')
|
||||||
|
db_name = f'inventar_{sanitized}'
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
|
db = client[db_name]
|
||||||
hashed_pw = hashlib.sha512('admin123'.encode()).hexdigest()
|
hashed_pw = hashlib.sha512('admin123'.encode()).hexdigest()
|
||||||
db.users.insert_one({'Username': 'admin', 'Password': hashed_pw, 'Role': 'admin', 'Name': 'Admin', 'Nachname': 'User'})
|
if db.users.count_documents({'Username': 'admin'}) == 0:
|
||||||
|
db.users.insert_one({
|
||||||
|
'Username': 'admin',
|
||||||
|
'Password': hashed_pw,
|
||||||
|
'Admin': True,
|
||||||
|
'active_ausleihung': None,
|
||||||
|
'name': 'Admin',
|
||||||
|
'last_name': 'User',
|
||||||
|
'IsStudent': False,
|
||||||
|
'PermissionPreset': 'full_access',
|
||||||
|
'ActionPermissions': {
|
||||||
|
'can_borrow': True,
|
||||||
|
'can_insert': True,
|
||||||
|
'can_edit': True,
|
||||||
|
'can_delete': True,
|
||||||
|
'can_manage_users': True,
|
||||||
|
'can_manage_settings': True,
|
||||||
|
'can_view_logs': True,
|
||||||
|
},
|
||||||
|
'PagePermissions': {
|
||||||
|
'home': True,
|
||||||
|
'tutorial_page': True,
|
||||||
|
'my_borrowed_items': True,
|
||||||
|
'notifications_view': True,
|
||||||
|
'impressum': True,
|
||||||
|
'license': True,
|
||||||
|
'library_view': True,
|
||||||
|
'terminplan': True,
|
||||||
|
'home_admin': True,
|
||||||
|
'upload_admin': True,
|
||||||
|
'library_admin': True,
|
||||||
|
'admin_borrowings': True,
|
||||||
|
'library_loans_admin': True,
|
||||||
|
'admin_damaged_items': True,
|
||||||
|
'admin_audit_dashboard': True,
|
||||||
|
'logs': True,
|
||||||
|
'manage_filters': True,
|
||||||
|
'manage_locations': True,
|
||||||
|
},
|
||||||
|
})
|
||||||
print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123')
|
print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123')
|
||||||
" "$TENANT_ID"
|
" "$TENANT_ID"
|
||||||
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
|
||||||
@@ -273,9 +315,11 @@ print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin1
|
|||||||
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
|
||||||
if [ -n "$APP_CONTAINER" ]; then
|
if [ -n "$APP_CONTAINER" ]; then
|
||||||
docker exec $APP_CONTAINER python3 -c "
|
docker exec $APP_CONTAINER python3 -c "
|
||||||
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
|
import sys; sys.path.insert(0, '/app/Web'); from tenant import TenantContext; import settings; from pymongo import MongoClient
|
||||||
|
ctx = TenantContext()
|
||||||
|
db_name = ctx._get_db_name(sys.argv[1])
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
client.drop_database(f'{settings.MONGODB_DB}_{sys.argv[1]}')
|
client.drop_database(db_name)
|
||||||
print(f'Database for tenant {sys.argv[1]} dropped.')
|
print(f'Database for tenant {sys.argv[1]} dropped.')
|
||||||
" "$TENANT_ID"
|
" "$TENANT_ID"
|
||||||
echo "Tenant '$TENANT_ID' database removed."
|
echo "Tenant '$TENANT_ID' database removed."
|
||||||
@@ -361,7 +405,7 @@ sys.path.insert(0, '/app/Web')
|
|||||||
import settings
|
import settings
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
|
||||||
prefix = f'{settings.MONGODB_DB}_'
|
prefix = 'inventar_'
|
||||||
dbs = [d for d in client.list_database_names() if d.startswith(prefix)]
|
dbs = [d for d in client.list_database_names() if d.startswith(prefix)]
|
||||||
if dbs:
|
if dbs:
|
||||||
for db in dbs:
|
for db in dbs:
|
||||||
|
|||||||
@@ -504,12 +504,71 @@ ensure_min_docker_disk_space() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
detect_server_capacity() {
|
||||||
|
local cpus mem_kb mem_gb app_workers app_cpus app_mem app_mem_swap
|
||||||
|
|
||||||
|
cpus="$(nproc 2>/dev/null || echo 1)"
|
||||||
|
mem_kb="$(awk '/MemAvailable:/ {print $2; exit}' /proc/meminfo 2>/dev/null || awk '/MemTotal:/ {print $2; exit}' /proc/meminfo || echo 0)"
|
||||||
|
mem_gb=$(( (mem_kb + 1024*1024 - 1) / (1024*1024) ))
|
||||||
|
|
||||||
|
if [ "$cpus" -ge 8 ] && [ "$mem_gb" -ge 16 ]; then
|
||||||
|
app_workers=8
|
||||||
|
app_cpus="2.0"
|
||||||
|
app_mem="512m"
|
||||||
|
app_mem_swap="1024m"
|
||||||
|
elif [ "$cpus" -ge 4 ] && [ "$mem_gb" -ge 8 ]; then
|
||||||
|
app_workers=4
|
||||||
|
app_cpus="1.0"
|
||||||
|
app_mem="384m"
|
||||||
|
app_mem_swap="768m"
|
||||||
|
elif [ "$cpus" -ge 2 ] && [ "$mem_gb" -ge 4 ]; then
|
||||||
|
app_workers=2
|
||||||
|
app_cpus="0.75"
|
||||||
|
app_mem="320m"
|
||||||
|
app_mem_swap="640m"
|
||||||
|
else
|
||||||
|
app_workers=1
|
||||||
|
app_cpus="0.5"
|
||||||
|
app_mem="256m"
|
||||||
|
app_mem_swap="512m"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$ENV_FILE" ]; then
|
||||||
|
local env_app_cpus env_app_mem env_app_mem_swap env_workers
|
||||||
|
env_app_cpus="$(awk -F= '/^INVENTAR_APP_CPUS=/{print $2; exit}' "$ENV_FILE" | tr -d ' ' || true)"
|
||||||
|
env_app_mem="$(awk -F= '/^INVENTAR_APP_MEM_LIMIT=/{print $2; exit}' "$ENV_FILE" | tr -d ' ' || true)"
|
||||||
|
env_app_mem_swap="$(awk -F= '/^INVENTAR_APP_MEM_SWAP_LIMIT=/{print $2; exit}' "$ENV_FILE" | tr -d ' ' || true)"
|
||||||
|
env_workers="$(awk -F= '/^INVENTAR_WORKERS=/{print $2; exit}' "$ENV_FILE" | tr -d ' ' || true)"
|
||||||
|
|
||||||
|
[ -n "$env_app_cpus" ] && app_cpus="$env_app_cpus"
|
||||||
|
[ -n "$env_app_mem" ] && app_mem="$env_app_mem"
|
||||||
|
[ -n "$env_app_mem_swap" ] && app_mem_swap="$env_app_mem_swap"
|
||||||
|
[ -n "$env_workers" ] && app_workers="$env_workers"
|
||||||
|
fi
|
||||||
|
|
||||||
|
INVENTAR_APP_CPUS="${INVENTAR_APP_CPUS:-$app_cpus}"
|
||||||
|
INVENTAR_APP_MEM_LIMIT="${INVENTAR_APP_MEM_LIMIT:-$app_mem}"
|
||||||
|
INVENTAR_APP_MEM_SWAP_LIMIT="${INVENTAR_APP_MEM_SWAP_LIMIT:-$app_mem_swap}"
|
||||||
|
INVENTAR_WORKERS="${INVENTAR_WORKERS:-$app_workers}"
|
||||||
|
INVENTAR_THREADS="${INVENTAR_THREADS:-2}"
|
||||||
|
|
||||||
|
echo "Detected host capacity: CPUs=$cpus, RAM=${mem_gb}GB"
|
||||||
|
echo "Configured app runtime: INVENTAR_WORKERS=$INVENTAR_WORKERS, INVENTAR_APP_CPUS=$INVENTAR_APP_CPUS, INVENTAR_APP_MEM_LIMIT=$INVENTAR_APP_MEM_LIMIT"
|
||||||
|
}
|
||||||
|
|
||||||
write_env_file() {
|
write_env_file() {
|
||||||
cat > "$ENV_FILE" <<EOF
|
cat > "$ENV_FILE" <<EOF
|
||||||
NUITKA_BUILD=$NUITKA_BUILD_VALUE
|
NUITKA_BUILD=$NUITKA_BUILD_VALUE
|
||||||
INVENTAR_HTTP_PORT=$HTTP_PORT_VALUE
|
INVENTAR_HTTP_PORT=$HTTP_PORT_VALUE
|
||||||
INVENTAR_HTTP_PORTS=${HTTP_PORTS_VALUE// /,}
|
INVENTAR_HTTP_PORTS=${HTTP_PORTS_VALUE// /,}
|
||||||
INVENTAR_APP_IMAGE=$APP_IMAGE_VALUE
|
INVENTAR_APP_IMAGE=$APP_IMAGE_VALUE
|
||||||
|
INVENTAR_APP_CPUS=$INVENTAR_APP_CPUS
|
||||||
|
INVENTAR_APP_MEM_LIMIT=$INVENTAR_APP_MEM_LIMIT
|
||||||
|
INVENTAR_APP_MEM_SWAP_LIMIT=$INVENTAR_APP_MEM_SWAP_LIMIT
|
||||||
|
INVENTAR_WORKERS=$INVENTAR_WORKERS
|
||||||
|
INVENTAR_THREADS=$INVENTAR_THREADS
|
||||||
|
INVENTAR_WORKER_TIMEOUT=${INVENTAR_WORKER_TIMEOUT:-30}
|
||||||
|
INVENTAR_WORKER_CONNECTIONS=${INVENTAR_WORKER_CONNECTIONS:-100}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,7 +577,7 @@ write_runtime_compose_override() {
|
|||||||
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", "${INVENTAR_WORKERS:-2}", "--threads", "${INVENTAR_THREADS:-2}", "--timeout", "${INVENTAR_WORKER_TIMEOUT:-30}", "--graceful-timeout", "20", "--worker-connections", "${INVENTAR_WORKER_CONNECTIONS:-100}", "--max-requests", "200", "--max-requests-jitter", "50", "--log-level", "info", "--access-logfile", "-", "--error-logfile", "-"]
|
||||||
image: ${APP_IMAGE_VALUE}
|
image: ${APP_IMAGE_VALUE}
|
||||||
build: null
|
build: null
|
||||||
EOF
|
EOF
|
||||||
@@ -595,6 +654,7 @@ configure_nuitka_mode
|
|||||||
resolve_app_image
|
resolve_app_image
|
||||||
configure_host_ports
|
configure_host_ports
|
||||||
ensure_min_docker_disk_space
|
ensure_min_docker_disk_space
|
||||||
|
detect_server_capacity
|
||||||
ensure_app_image_loaded
|
ensure_app_image_loaded
|
||||||
write_env_file
|
write_env_file
|
||||||
write_runtime_compose_override
|
write_runtime_compose_override
|
||||||
|
|||||||
Reference in New Issue
Block a user