Compare commits

...

5 Commits

6 changed files with 304 additions and 34 deletions
+106 -8
View File
@@ -206,6 +206,82 @@
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 */
@media (max-width: 1024px) {
.navbar .container-fluid {
@@ -952,24 +1028,24 @@
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
{% if current_permissions.pages.get('home', 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>
{% 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') }}">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>
{% 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') }}">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>
{% endif %}
{% endif %}
{% if 'username' in session and current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %}
<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>
{% endif %}
<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">
{% 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') }}">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>
{% 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') }}">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>
{% 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') }}">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>
{% 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') }}">📖 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>
{% endif %}
<li class="nav-item" data-nav-fixed="true">
@@ -1999,6 +2075,28 @@
});
});
</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>
</html>
+63
View File
@@ -189,6 +189,38 @@
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 {
display: flex;
gap: 8px;
@@ -230,6 +262,13 @@
<span class="tutorial-pill">Format: Ruhig und geführt</span>
<span class="tutorial-pill">Benutzer: {{ username }}</span>
</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>
<div class="tutorial-grid">
@@ -438,8 +477,32 @@
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();
renderStep();
loadTooltipMode();
})();
</script>
{% endblock %}
+56 -14
View File
@@ -418,25 +418,39 @@ def check_nm_pwd(username, password):
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:
db_name = ctx.db_name or ctx.resolve_tenant()
tenant_db = ctx.db_name or ctx.resolve_tenant()
db_name = tenant_db
except Exception:
pass
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[db_name]
users = db['users']
user = users.find_one({'Username': username}) or users.find_one({'username': username})
client.close()
try:
hashed_password = hashing(password)
if not user:
return None
def find_user_in_db(database_name):
db = client[database_name]
users = db['users']
return users.find_one({'Username': username, 'Password': hashed_password}) or users.find_one({'username': username, 'Password': hashed_password})
user = find_user_in_db(db_name)
if 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()
if user.get('Password') == hashing(password):
return user
return None
@@ -578,11 +592,39 @@ def get_user(username):
dict: User document or None if not found
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
users = db['users']
users_return = users.find_one({'Username': username})
client.close()
return users_return
try:
def find_in_db(database_name):
db = client[database_name]
users = db['users']
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()
def check_admin(username):
+13 -8
View File
@@ -106,11 +106,16 @@ services:
INVENTAR_DELETED_ARCHIVE_FOLDER: /data/deleted-archives
# Performance Tuning
INVENTAR_WORKER_CLASS: gevent
INVENTAR_WORKERS: "4"
INVENTAR_THREADS: "2"
INVENTAR_WORKER_TIMEOUT: "30"
INVENTAR_WORKER_CONNECTIONS: "100"
INVENTAR_WORKER_CLASS: ${INVENTAR_WORKER_CLASS:-gevent}
INVENTAR_WORKERS: "${INVENTAR_WORKERS:-4}"
INVENTAR_THREADS: "${INVENTAR_THREADS:-2}"
INVENTAR_WORKER_TIMEOUT: "${INVENTAR_WORKER_TIMEOUT:-30}"
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
INVENTAR_MULTITENANT_ENABLED: "true"
@@ -131,9 +136,9 @@ services:
# Resource Limits (per instance)
# With 10 instances: each gets ~150MB, totaling ~1.5GB
# Adjust based on server resources
mem_limit: 256m
memswap_limit: 512m
cpus: "1.0"
mem_limit: "${INVENTAR_APP_MEM_LIMIT:-256m}"
memswap_limit: "${INVENTAR_APP_MEM_SWAP_LIMIT:-512m}"
cpus: "${INVENTAR_APP_CPUS:-1.0}"
# Health check for load balancer
healthcheck:
+5 -3
View File
@@ -315,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)
if [ -n "$APP_CONTAINER" ]; then
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.drop_database(f'{settings.MONGODB_DB}_{sys.argv[1]}')
client.drop_database(db_name)
print(f'Database for tenant {sys.argv[1]} dropped.')
" "$TENANT_ID"
echo "Tenant '$TENANT_ID' database removed."
@@ -403,7 +405,7 @@ sys.path.insert(0, '/app/Web')
import settings
from pymongo import MongoClient
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)]
if dbs:
for db in dbs:
+61 -1
View File
@@ -504,12 +504,71 @@ ensure_min_docker_disk_space() {
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() {
cat > "$ENV_FILE" <<EOF
NUITKA_BUILD=$NUITKA_BUILD_VALUE
INVENTAR_HTTP_PORT=$HTTP_PORT_VALUE
INVENTAR_HTTP_PORTS=${HTTP_PORTS_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
}
@@ -518,7 +577,7 @@ write_runtime_compose_override() {
services:
app:
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}
build: null
EOF
@@ -595,6 +654,7 @@ configure_nuitka_mode
resolve_app_image
configure_host_ports
ensure_min_docker_disk_space
detect_server_capacity
ensure_app_image_loaded
write_env_file
write_runtime_compose_override