Add in-app help page with user manual (Romanian)

Adds a /help page served from the dashboard navigation, containing the full
Romanian user manual for operators: dashboard, playlist management, adding
media, removing files from playlists, player management, and the advanced
edited-media view.

Implementation notes
- Manual is pre-rendered to HTML with pandoc and shipped as a static asset
  (app/static/help/_manual_body.html) rather than parsed at runtime. This
  keeps the container free of a Markdown dependency and makes the page render
  identically regardless of what is installed.
- /help is login-protected; the sidebar table of contents is derived from the
  level-2 headings at request time.
- Screenshots are served from app/static/help/screenshots/ (32 images).
- Regenerate after editing the manual:
    documentatie/convert_help_page.sh
    docker compose up -d --build

Also adds
- backup_players_playlists.sh / restore_database.sh for DB backup and restore.
- .gitignore rules so local database backups (real production data), generated
  .docx files and duplicate screenshot copies are never committed.
This commit is contained in:
2026-09-15 16:46:16 +03:00
parent b4c3f65636
commit 7177cdb9ab
45 changed files with 3082 additions and 0 deletions
+21
View File
@@ -76,6 +76,27 @@ build/
#data #data
data/ data/
# Local database backups / restore points.
# These contain REAL production data (users, password hashes, player feedback).
# They must never leave the host — keep them on disk only.
backups/
restore-points/
# Generated documents (regenerate with documentatie/convert_manual.sh)
documentatie/*.docx
# Screenshots are shipped with the app under app/static/help/screenshots/.
# The copy in documentatie/ is the editing source; ignore it to avoid committing
# ~4 MB twice.
documentatie/screenshots/
# Local notes / scratch files
inportant info .txt
important info.txt
# Legacy pre-sanitization archive (also covered as docs/old_code_documentation/)
old_code_documentation/
# Local archive / restore-point snapshots. # Local archive / restore-point snapshots.
# Kept on disk for reference (see docs/SANITIZATION-REVIEW.md) but deliberately # Kept on disk for reference (see docs/SANITIZATION-REVIEW.md) but deliberately
# NOT tracked: docs/legacy code/ is a full pre-sanitization repo snapshot and # NOT tracked: docs/legacy code/ is a full pre-sanitization repo snapshot and
+44
View File
@@ -89,6 +89,50 @@ def dashboard():
) )
@main_bp.route('/help')
@login_required
def help_page():
"""User manual / help page.
The manual lives at ``app/static/help/_manual_body.html`` — a pre-rendered
HTML fragment generated from ``documentatie/Manual-Utilizare-DigiServer.md``
with ``pandoc`` (see ``documentatie/convert_help_page.sh``).
It is shipped as a static asset rather than parsed at runtime on purpose:
it keeps the runtime free of a Markdown dependency and means the help page
renders identically regardless of what is installed in the container.
"""
import os
import re
base_dir = os.path.dirname(os.path.dirname(__file__)) # -> app/
manual_path = os.path.join(base_dir, 'static', 'help', '_manual_body.html')
manual_html = ''
toc = []
try:
with open(manual_path, 'r', encoding='utf-8') as f:
manual_html = f.read()
# Build the sidebar from the level-2 headings (`<h2 id="...">`), which
# is exactly the printed table of contents of the manual. Nested <h3>
# entries would make the sidebar unwieldy, so they are left out.
for anchor, title in re.findall(
r'<h2 id="([^"]+)"[^>]*>(.*?)</h2>', manual_html, re.DOTALL
):
clean = re.sub(r'<[^>]+>', '', title).strip()
if clean:
toc.append({'anchor': anchor, 'title': clean})
except FileNotFoundError:
manual_html = (
'<h1>Manual indisponibil</h1>'
'<p>Fișierul manualului nu a fost găsit. '
'Rulați <code>documentatie/convert_help_page.sh</code> pentru a-l genera.</p>'
)
return render_template('help.html', manual_html=manual_html, toc=toc)
@main_bp.route('/health') @main_bp.route('/health')
def health(): def health():
"""Health check endpoint""" """Health check endpoint"""
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>

After

Width:  |  Height:  |  Size: 340 B

+4
View File
@@ -393,6 +393,10 @@
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt=""> <img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="">
Playlists Playlists
</a> </a>
<a href="{{ url_for('main.help_page') }}">
<img src="{{ url_for('static', filename='icons/help.svg') }}" alt="" onerror="this.style.display='none';">
Ajutor
</a>
<a href="{{ url_for('admin.admin_panel') }}">Admin</a> <a href="{{ url_for('admin.admin_panel') }}">Admin</a>
<a href="{{ url_for('auth.logout') }}">Logout ({{ current_user.username }})</a> <a href="{{ url_for('auth.logout') }}">Logout ({{ current_user.username }})</a>
<button class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode"> <button class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode">
+276
View File
@@ -0,0 +1,276 @@
{% extends "base.html" %}
{% block title %}Ajutor Manual de Utilizare{% endblock %}
{% block content %}
<style>
/* ---- Help page: local layout & typography ---- */
.help-wrapper {
display: grid;
grid-template-columns: 280px 1fr;
gap: 2rem;
align-items: start;
}
/* Sidebar cu cuprins */
.help-toc {
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1.25rem;
box-shadow: var(--shadow);
}
.help-toc h3 {
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
.help-toc ul {
list-style: none;
padding: 0;
margin: 0;
}
.help-toc li { margin-bottom: 0.15rem; }
.help-toc a {
display: block;
padding: 0.4rem 0.6rem;
border-radius: 6px;
color: var(--text-color);
text-decoration: none;
font-size: 0.875rem;
line-height: 1.35;
transition: all 0.15s;
}
.help-toc a:hover {
background: var(--bg-color);
color: var(--primary-color);
}
.help-toc a.active {
background: var(--primary-color);
color: #fff;
font-weight: 500;
}
/* Continutul manualului */
.help-content {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 2rem 2.5rem;
box-shadow: var(--shadow);
line-height: 1.7;
overflow-wrap: break-word;
}
.help-content h1 {
font-size: 1.85rem;
margin: 0 0 1rem;
padding-bottom: 0.75rem;
border-bottom: 3px solid var(--primary-color);
}
.help-content h2 {
font-size: 1.4rem;
margin: 2.25rem 0 0.85rem;
padding-top: 1rem;
color: var(--primary-color);
scroll-margin-top: 20px;
}
.help-content h3 {
font-size: 1.1rem;
margin: 1.5rem 0 0.6rem;
scroll-margin-top: 20px;
}
.help-content h4 {
font-size: 1rem;
margin: 1.25rem 0 0.5rem;
}
.help-content p { margin: 0.7rem 0; }
.help-content ul, .help-content ol { margin: 0.7rem 0 0.7rem 1.5rem; }
.help-content li { margin-bottom: 0.35rem; }
.help-content hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 2rem 0;
}
.help-content a { color: var(--primary-color); }
.help-content code {
background: var(--bg-color);
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-size: 0.875em;
font-family: 'SFMono-Regular', Consolas, monospace;
border: 1px solid var(--border-color);
}
.help-content pre {
background: var(--bg-color);
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
border: 1px solid var(--border-color);
margin: 1rem 0;
}
.help-content pre code {
background: none;
border: none;
padding: 0;
}
.help-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
margin: 0.75rem 0;
display: block;
}
.help-content table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
font-size: 0.9rem;
}
.help-content th, .help-content td {
border: 1px solid var(--border-color);
padding: 0.6rem 0.75rem;
text-align: left;
vertical-align: top;
}
.help-content th {
background: var(--bg-color);
font-weight: 600;
}
.help-content tr:nth-child(even) td {
background: color-mix(in srgb, var(--bg-color) 50%, transparent);
}
/* Citat / atentionari */
.help-content blockquote {
border-left: 4px solid var(--primary-color);
background: var(--bg-color);
margin: 1rem 0;
padding: 0.75rem 1rem;
border-radius: 0 8px 8px 0;
}
.help-content blockquote p { margin: 0.3rem 0; }
/* Textul italics al figurilor */
.help-content em { color: var(--text-secondary); font-size: 0.9em; }
/* Back-to-top */
.help-top-btn {
position: fixed;
right: 24px;
bottom: 24px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--primary-color);
color: #fff;
border: none;
cursor: pointer;
font-size: 1.25rem;
box-shadow: var(--shadow-lg);
display: none;
align-items: center;
justify-content: center;
transition: transform 0.2s;
z-index: 500;
}
.help-top-btn:hover { transform: translateY(-3px); }
.help-top-btn.visible { display: flex; }
/* Cauta */
.help-search {
width: 100%;
padding: 0.5rem 0.75rem;
margin-bottom: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-color);
color: var(--text-color);
font-size: 0.875rem;
}
.help-search:focus {
outline: none;
border-color: var(--primary-color);
}
@media (max-width: 900px) {
.help-wrapper { grid-template-columns: 1fr; }
.help-toc { position: static; max-height: none; }
.help-content { padding: 1.25rem; }
.help-content h1 { font-size: 1.5rem; }
.help-content h2 { font-size: 1.2rem; }
}
</style>
<div class="container">
<div class="help-wrapper">
<!-- Cuprins / navigare laterala -->
<aside class="help-toc">
<h3>Cuprins</h3>
<input type="text" id="tocSearch" class="help-search"
placeholder="Caută în cuprins..." autocomplete="off">
<ul id="tocList">
{% for item in toc %}
<li>
<a href="#{{ item.anchor }}" data-anchor="{{ item.anchor }}">
{{ item.title }}
</a>
</li>
{% endfor %}
</ul>
</aside>
<!-- Continutul manualului -->
<main class="help-content" id="helpContent">
{{ manual_html | safe }}
</main>
</div>
</div>
<button class="help-top-btn" id="topBtn" title="Sus" onclick="window.scrollTo({top:0,behavior:'smooth'})"></button>
<script>
(function () {
// Cuprins: evidentiaza sectiunea curenta la scroll
var links = Array.prototype.slice.call(document.querySelectorAll('#tocList a'));
var sections = links.map(function (a) {
return document.getElementById(a.dataset.anchor);
}).filter(Boolean);
function onScroll() {
var btn = document.getElementById('topBtn');
if (window.scrollY > 400) { btn.classList.add('visible'); }
else { btn.classList.remove('visible'); }
var pos = window.scrollY + 120;
var current = null;
for (var i = 0; i < sections.length; i++) {
if (sections[i].offsetTop <= pos) { current = sections[i].id; }
}
links.forEach(function (a) {
a.classList.toggle('active', a.dataset.anchor === current);
});
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
// Filtrare in cuprins
var search = document.getElementById('tocSearch');
search.addEventListener('input', function () {
var q = this.value.toLowerCase().trim();
links.forEach(function (a) {
var li = a.parentElement;
li.style.display = (!q || a.textContent.toLowerCase().indexOf(q) !== -1) ? '' : 'none';
});
});
})();
</script>
{% endblock %}
+240
View File
@@ -0,0 +1,240 @@
#!/bin/bash
# ============================================================================
# DigiServer - Database Backup (restore-ready for Docker image / host upgrades)
# ----------------------------------------------------------------------------
# Produces a timestamped, self-contained bundle under ./backups/<timestamp>/:
#
# dashboard.db.gz - CONSISTENT full snapshot of the SQLite database
# (player, playlist, playlist_content,
# player_feedback and all other tables)
# players_playlists.sql.gz - logical dump of player/playlist tables
# (secondary safety net; human-readable)
# SHA256SUMS - integrity check
# MANIFEST.txt - row counts, sizes, checksums, app version
# RESTORE.md - copy-paste restore instructions for this bundle
#
# The bundle is designed so that after a `docker compose build` + host upgrade
# you can run ./restore_database.sh <timestamp> and be back exactly as before.
#
# Usage:
# ./backup_players_playlists.sh # full restore-ready backup
# ./backup_players_playlists.sh --logical-only # SQL dump only (no 2.2GB copy)
# ============================================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml"
CONTAINER="digiserver-v2"
DB_HOST="$PROJECT_DIR/data/instance/dashboard.db"
DB_CTR="/app/instance/dashboard.db"
SNAP_CTR="/app/instance/__snapshot.db"
BACKUP_ROOT="$PROJECT_DIR/backups"
TS="$(date +%Y%m%d_%H%M%S)"
DEST="$BACKUP_ROOT/$TS"
ALL_DUMP_TABLES=(player playlist playlist_content player_user player_edit content group group_content)
LOGICAL_ONLY=0
[ "${1:-}" = "--logical-only" ] && LOGICAL_ONLY=1
mkdir -p "$DEST"
echo "============================================================"
echo " DigiServer restore-ready backup"
echo " Target: $DEST"
echo "============================================================"
if [ ! -f "$DB_HOST" ]; then
echo "ERROR: database not found at $DB_HOST"
exit 1
fi
# ---------------------------------------------------------------------------
# 1. Consistent snapshot via SQLite online backup API (safe while app runs)
# ---------------------------------------------------------------------------
if [ "$LOGICAL_ONLY" -eq 0 ]; then
echo
echo "[1/5] Creating consistent DB snapshot (online backup API)..."
$COMPOSE exec -T digiserver-app python - <<'PYEOF'
import sqlite3
src = sqlite3.connect('/app/instance/dashboard.db')
dst = sqlite3.connect('/app/instance/__snapshot.db')
with dst:
src.backup(dst)
dst.close(); src.close()
print(" snapshot created")
PYEOF
docker cp "$CONTAINER:$SNAP_CTR" "$DEST/dashboard.db"
$COMPOSE exec -T digiserver-app rm -f "$SNAP_CTR"
echo " -> size: $(du -h "$DEST/dashboard.db" | cut -f1)"
echo
echo "[2/5] Compressing snapshot..."
gzip -f "$DEST/dashboard.db"
echo " -> $(ls -lh "$DEST/dashboard.db.gz" | awk '{print $5}')"
else
echo
echo "[1/5] Skipping full snapshot (--logical-only)"
echo "[2/5] Skipping compression"
fi
# ---------------------------------------------------------------------------
# 3. Logical SQL dump (secondary safety net)
# ---------------------------------------------------------------------------
echo
echo "[3/5] Creating logical SQL dump of player/playlist tables..."
TABLE_LIST=$(printf '"%s",' "${ALL_DUMP_TABLES[@]}")
TABLE_LIST="[${TABLE_LIST%,}]"
$COMPOSE exec -T digiserver-app python - "$TABLE_LIST" > "$DEST/players_playlists.sql" <<'PYEOF'
import sqlite3, sys
tables = eval(sys.argv[1])
con = sqlite3.connect('/app/instance/dashboard.db')
o = sys.stdout
o.write("-- DigiServer logical backup: players & playlists\n")
o.write("-- Tables: %s\n\n" % ", ".join(tables))
o.write("PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n\n")
for t in tables:
row = con.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (t,)).fetchone()
if not row or not row[0]:
continue
o.write("DROP TABLE IF EXISTS \"%s\";\n" % t)
o.write(row[0].rstrip() + ";\n")
cur = con.execute('SELECT * FROM "%s"' % t)
cols = [d[0] for d in cur.description]
collist = ",".join('"%s"' % c for c in cols)
batch = []
def flush(b):
if b:
o.write("INSERT INTO \"%s\" (%s) VALUES\n%s;\n" % (t, collist, ",\n".join(b)))
for rec in cur:
vals = []
for v in rec:
if v is None: vals.append("NULL")
elif isinstance(v, (int, float)): vals.append(str(v))
elif isinstance(v, bytes): vals.append("X'%s'" % v.hex())
else: vals.append("'" + str(v).replace("'", "''") + "'")
batch.append("(%s)" % ",".join(vals))
if len(batch) >= 500:
flush(batch); batch = []
flush(batch)
o.write("\n")
o.write("COMMIT;\n")
PYEOF
gzip -f "$DEST/players_playlists.sql"
echo " -> $(ls -lh "$DEST/players_playlists.sql.gz" | awk '{print $5}')"
# ---------------------------------------------------------------------------
# 4. Checksums
# ---------------------------------------------------------------------------
echo
echo "[4/5] Writing checksums..."
( cd "$DEST" && sha256sum *.gz > SHA256SUMS )
sed 's/^/ /' "$DEST/SHA256SUMS"
# ---------------------------------------------------------------------------
# 5. Manifest + restore instructions
# ---------------------------------------------------------------------------
echo
echo "[5/5] Writing MANIFEST.txt and RESTORE.md..."
{
echo "DigiServer backup manifest"
echo "=========================="
echo "Created: $(date -Iseconds)"
echo "Host: $(hostname)"
echo "Backup ID: $TS"
echo "Source DB: $DB_HOST"
echo "Source DB size: $(du -h "$DB_HOST" | cut -f1)"
echo ""
echo "Row counts at backup time:"
$COMPOSE exec -T digiserver-app python - <<'PYEOF' 2>/dev/null || echo " (unavailable)"
import sqlite3
con = sqlite3.connect('/app/instance/dashboard.db')
for t in ['player','playlist','playlist_content','player_user','player_edit',
'player_feedback','content','group','group_content','user']:
try:
n = con.execute('SELECT COUNT(*) FROM "%s"' % t).fetchone()[0]
print(' %-20s %9d' % (t, n))
except Exception as e:
print(' %-20s ERROR %s' % (t, e))
PYEOF
echo ""
echo "Files:"
( cd "$DEST" && ls -lh | tail -n +2 | sed 's/^/ /' )
} > "$DEST/MANIFEST.txt"
cat > "$DEST/RESTORE.md" <<EOF
# Restore this DigiServer backup
Backup ID: **$TS**
Created: $(date -Iseconds)
## What's in this bundle
| File | Purpose |
|------|---------|
| \`dashboard.db.gz\` | Full consistent snapshot (all tables) - primary restore source |
| \`players_playlists.sql.gz\` | Logical dump of player/playlist tables - secondary source |
| \`SHA256SUMS\` | Integrity check |
| \`MANIFEST.txt\` | Row counts + metadata |
## Restore procedure (after Docker image / host upgrade)
Run from the project root (\`$PROJECT_DIR\`):
\`\`\`bash
./restore_database.sh $TS
\`\`\`
Or do it manually:
\`\`\`bash
# 1. Stop the app so nothing writes to the DB
docker compose stop digiserver-app
# 2. Verify the backup is intact
( cd backups/$TS && sha256sum -c SHA256SUMS )
# 3. Decompress and swap in the database
gunzip -c backups/$TS/dashboard.db.gz > data/instance/dashboard.db
# 4. Fix ownership (container runs as uid 1000)
sudo chown 1000:1000 data/instance/dashboard.db
chmod 644 data/instance/dashboard.db
# 5. Remove any stale journal file
rm -f data/instance/dashboard.db-journal
# 6. Rebuild/start the upgraded image
docker compose up -d --build
# 7. Re-run migrations (new schema may have been added)
docker compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
docker compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
docker compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
docker compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
# 8. Verify
docker compose exec -T digiserver-app python -c "
import sqlite3
c=sqlite3.connect('/app/instance/dashboard.db')
print('players: ', c.execute('SELECT COUNT(*) FROM player').fetchone()[0])
print('playlists:', c.execute('SELECT COUNT(*) FROM playlist').fetchone()[0])
print('items: ', c.execute('SELECT COUNT(*) FROM playlist_content').fetchone()[0])
"
\`\`\`
## Important notes
- The container's **entrypoint only creates a DB if one is missing** - dropping in this
file bypasses re-initialisation, which is what we want.
- Ownership **must be uid/gid 1000** (host \`pi:pi\` == container \`appuser\`).
- If the new app version changed the schema, the migration step (7) is mandatory.
EOF
echo
echo "============================================================"
echo " Backup complete and restore-ready: $DEST"
echo " To restore later: ./restore_database.sh $TS"
echo "============================================================"
+225
View File
@@ -0,0 +1,225 @@
# Cum convertesc acest manual în format .DOCX
Acest fișier explică pașii pentru a transforma `Manual-Utilizare-DigiServer.md` într-un document Word (`.docx`) gata de printat sau distribuit.
---
## Metoda 1 Pandoc (recomandată)
Pandoc este cea mai bună soluție: convertește Markdown în DOCX păstrând titluri, tabele, liste și imagini.
### 1.1. Instalare Pandoc
Pe Debian/Ubuntu (inclusiv pe acest server):
```bash
sudo apt-get update
sudo apt-get install -y pandoc
```
Verificare:
```bash
pandoc --version
```
### 1.2. Conversie simplă
Din folderul `documentatie/`:
```bash
cd /srv/digiserver-v2/documentatie
pandoc Manual-Utilizare-DigiServer.md -o Manual-Utilizare-DigiServer.docx
```
### 1.3. Conversie cu cuprins automat (Table of Contents)
```bash
pandoc Manual-Utilizare-DigiServer.md \
--toc \
--toc-depth=2 \
--number-sections \
-o Manual-Utilizare-DigiServer.docx
```
| Opțiune | Efect |
|---------|-------|
| `--toc` | Adaugă cuprins automat |
| `--toc-depth=2` | Cuprinsul include titluri de nivel 1 și 2 |
| `--number-sections` | Numerotează secțiunile (1, 1.1, 1.2...) |
### 1.4. Conversie cu șablon personalizat (logo, fonturi corporate)
Dacă doriți un aspect personalizat:
1. Generați un șablon de referință:
```bash
pandoc -o template-referinta.docx --print-default-data-file reference.docx
```
2. Deschideți `template-referinta.docx` în Word
3. Modificați stilurile (fonturi, culori, antet, logo)
4. Salvați și folosiți-l la conversie:
```bash
pandoc Manual-Utilizare-DigiServer.md \
--toc --number-sections \
--reference-doc=template-referinta.docx \
-o Manual-Utilizare-DigiServer.docx
```
### 1.5. Conversie cu imagini redimensionate (pentru capturile de ecran)
```bash
pandoc Manual-Utilizare-DigiServer.md \
--toc --number-sections \
--resource-path=screenshots \
-o Manual-Utilizare-DigiServer.docx
```
> **Notă:** Pentru ca imaginile să apară în document, ele trebuie să existe în folderul
> `screenshots/` cu numele exacte din Anexa B și trebuie referite în Markdown.
---
## Metoda 2 VS Code (fără instalări suplimentare)
Dacă editați în VS Code:
1. Instalați extensia **„Markdown PDF"** sau **„Markdown Preview Enhanced"**
2. Deschideți `Manual-Utilizare-DigiServer.md`
3. Click dreapta → **„Markdown: Export to DOCX"** (sau PDF)
4. Salvați documentul rezultat
Alternativ, extensia **„Markdown All in One"** + **„Pandoc"** permite export direct DOCX din Command Palette.
---
## Metoda 3 Online (fără instalare)
Pentru conversie rapidă fără a instala nimic:
1. Accesați un convertor online (ex: `https://www.markdowntodocx.com`)
2. Încărcați sau lipiți conținutul din `Manual-Utilizare-DigiServer.md`
3. Descărcați fișierul `.docx` rezultat
> **Atenție:** Nu folosiți convertizoare online pentru documente cu informații sensibile (adrese IP interne, credențiale). Preferați Metoda 1 sau 2.
---
## Pașii compleți pentru un document final (cu capturi de ecran)
### Pasul 1 Faceți capturile de ecran
Deschideți aplicația în browser și faceți capturi pentru fiecare punct din **Anexa B** a manualului.
**Sfaturi pentru capturi bune:**
- Folosiți rezoluția completă a ferestrei (nu tăiați meniurile)
- Evitați să apară date sensibile (parole, IP-uri reale dacă nu e necesar)
- Salvați în format PNG pentru claritate
- Denumiți exact ca în Anexa B (ex: `01-login.png`)
### Pasul 2 Salvați capturile
Puneți toate fișierele în:
```
/srv/digiserver-v2/documentatie/screenshots/
```
### Pasul 3 Referiți imaginile în Markdown
Înlocuiți liniile de tip:
```markdown
**[SCREENSHOT: 03-dashboard.png Dashboard după autentificare]**
```
cu:
```markdown
![Dashboard după autentificare](screenshots/03-dashboard.png)
*Figura 1 Dashboard-ul principal al aplicației*
```
### Pasul 4 Convertiți în DOCX
```bash
cd /srv/digiserver-v2/documentatie
pandoc Manual-Utilizare-DigiServer.md \
--toc \
--toc-depth=2 \
--number-sections \
--resource-path=screenshots \
-o Manual-Utilizare-DigiServer.docx
```
### Pasul 5 Verificați documentul
Deschideți `Manual-Utilizare-DigiServer.docx` în Word / LibreOffice și verificați:
- [ ] Cuprinsul este corect și complet
- [ ] Toate capturile de ecran apar
- [ ] Tabelele sunt formatate corect
- [ ] Numerotarea secțiunilor este consecventă
- [ ] Textul diacriticilor românești (ă, â, î, ș, ț) se afișează corect
---
## Script automat de conversie
Un script care face totul automat:
```bash
#!/bin/bash
# convert_manual.sh - Converteste manualul in DOCX cu cuprins si imagini
cd "$(dirname "$0")"
echo "Verificare Pandoc..."
if ! command -v pandoc &> /dev/null; then
echo "Pandoc nu este instalat. Rulati: sudo apt-get install -y pandoc"
exit 1
fi
echo "Numar capturi de ecran disponibile:"
ls -1 screenshots/*.png 2>/dev/null | wc -l
echo "Conversie in DOCX..."
pandoc Manual-Utilizare-DigiServer.md \
--toc \
--toc-depth=2 \
--number-sections \
--resource-path=screenshots \
-o Manual-Utilizare-DigiServer.docx
if [ -f Manual-Utilizare-DigiServer.docx ]; then
echo "✓ Document creat: Manual-Utilizare-DigiServer.docx"
ls -lh Manual-Utilizare-DigiServer.docx
else
echo "✗ Eroare la conversie"
exit 1
fi
```
Salvați ca `convert_manual.sh`, apoi:
```bash
chmod +x convert_manual.sh
./convert_manual.sh
```
---
## Rezumat rapid
| Metodă | Necesită | Calitate | Recomandat pentru |
|--------|----------|----------|-------------------|
| **Pandoc** | instalare | ★★★★★ | Document final profesional |
| **VS Code** | extensie | ★★★★ | Utilizatori VS Code |
| **Online** | browser | ★★★ | Conversie rapidă, documente nesensibile |
**Recomandare:** Folosiți **Pandoc cu `--toc` și `--number-sections`** și un șablon de referință personalizat pentru un document final profesionist.
---
*Pentru întrebări despre conversie, consultați documentația Pandoc: https://pandoc.org/MANUAL.html*
+711
View File
@@ -0,0 +1,711 @@
# Manual de Utilizare DigiServer v2
**Versiune document:** 1.0
**Data:** Septembrie 2026
**Aplicație:** DigiServer v2 Sistem de management al conținutului pentru ecrane digitale (Digital Signage)
---
## Cuprins
1. [Introducere](#1-introducere)
2. [Autentificare în aplicație](#2-autentificare-în-aplicație)
3. [Dashboard (Panoul principal)](#3-dashboard-panoul-principal)
4. [Biblioteca de Media](#4-biblioteca-de-media)
5. [Adăugarea de Media în sistem](#5-adăugarea-de-media-în-sistem)
6. [Managementul Playlist-urilor](#6-managementul-playlist-urilor)
7. [Adăugarea de conținut într-un Playlist](#7-adăugarea-de-conținut-într-un-playlist)
8. [Ștergerea unui fișier din Playlist](#8-ștergerea-unui-fișier-din-playlist)
9. [Reordonarea conținutului în Playlist](#9-reordonarea-conținutului-în-playlist)
10. [Managementul Playerelelor](#10-managementul-playerelelor)
11. [Vizualizarea fișierelor editate pe Player (funcție avansată)](#11-vizualizarea-fișierelor-editate-pe-player-funcție-avansată)
12. [Administrare](#12-administrare)
13. [Întrebări frecvente și depanare](#13-întrebări-frecvente-și-depanare)
---
## 1. Introducere
**DigiServer v2** este o aplicație web folosită pentru a gestiona conținutul afișat pe ecranele digitale (playere). Cu ajutorul ei puteți:
- Încărca fișiere media (imagini, videoclipuri, documente PDF, prezentări PowerPoint)
- Crea **playlist-uri** liste ordonate de conținut care se afișează pe ecrane
- Atribui playlist-uri către playere specifice
- Urmări automatizarea și starea playerelor
- Vizualiza și gestiona versiunile editate ale fișierelor de pe playere
### Structura generală a aplicației
```
┌─────────────────────────────────────────────────────────┐
│ UTILIZATOR (browser) │
│ │ │
│ ▼ │
│ DigiServer (interfață web) │
│ │ │
│ ├──► Biblioteca de Media (fișiere) │
│ ├──► Playlist-uri (liste de redare) │
│ └──► Playere (ecrane fizice) │
└─────────────────────────────────────────────────────────┘
```
### Roluri de utilizator
| Rol | Drepturi |
|-----|----------|
| **Admin** | Acces complet: utilizatori, setări sistem, HTTPS, playere, conținut |
| **Editor** | Poate încărca conținut, crea playlist-uri și edita playere |
| **Utilizator** | Poate vizualiza conținut și playlists |
---
## 2. Autentificare în aplicație
### 2.1. Deschiderea aplicației
Deschideți un browser (Chrome, Firefox sau Edge) și accesați una dintre adresele:
- **Prin adresă IP:** `https://10.76.152.164`
- **Prin nume gazdă:** `https://digiserver`
- **Prin domeniu:** `https://digiserver.sibiusb.harting.intra`
> **Notă:** Dacă browserul afișează un avertisment de certificat („Conexiunea nu este privată"), acest lucru este normal pentru rețeaua internă. Apăsați **„Avansat" → „Continuă către..."**. Administratorul de rețea poate instala certificatul intern pentru a elimina acest avertisment.
### 2.2. Ecranul de autentificare
![Ecranul de autentificare](screenshots/01-login.png)
*Figura 1 Ecranul de login*
Introduceți:
1. **Nume utilizator** (username)
2. **Parolă**
3. Apăsați butonul **„Autentificare"** / **„Login"**
> **Important:** Dacă ați uitat parola, contactați administratorul sistemului. Parola poate fi schimbată ulterior din meniul de cont.
### 2.3. Schimbarea parolei
După prima autentificare, se recomandă schimbarea parolei:
1. Accesați **meniul de utilizator** (colț dreapta-sus)
2. Selectați **„Schimbă parola"** / **„Change Password"**
3. Introduceți parola veche, apoi parola nouă (de două ori)
4. Confirmați
![Formularul de schimbare a parolei](screenshots/02-change-password.png)
*Figura 2 Schimbarea parolei*
---
## 3. Dashboard (Panoul principal)
După autentificare, sunteți redirecționat către **Dashboard** pagina principală a aplicației.
![Dashboard după autentificare](screenshots/03-dashboard.png)
*Figura 3 Dashboard-ul principal*
### 3.1. Elementele Dashboard-ului
Dashboard-ul oferă o imagine de ansamblu asupra sistemului:
| Element | Descriere |
|---------|-----------|
| **Statistici** | Număr de playere, playlist-uri, fișiere media |
| **Stare playere** | Care playere sunt online / offline |
| **Acțiuni rapide** | Butoane pentru adăugare rapidă de conținut |
| **Meniu de navigare** | Acces către toate secțiunile aplicației |
### 3.2. Meniul principal de navigare
Meniul din partea stângă (sau de sus) conține:
| Secțiune | Funcție |
|----------|---------|
| **Dashboard** | Pagina principală cu statistici |
| **Media Library** (Biblioteca de Media) | Toate fișierele încărcate |
| **Playlists** | Gestionarea listelor de redare |
| **Players** (Playere) | Gestionarea ecranelor |
| **Admin** | Setări de sistem (doar pentru administratori) |
### 3.3. Cum citiți statisticile
1. **Playere (Players):** Numărul total de ecrane configurate
2. **Playlist-uri:** Numărul de liste de redare existente
3. **Media:** Numărul total de fișiere încărcate
Apăsați pe orice statistică pentru a accesa secțiunea corespunzătoare.
---
## 4. Biblioteca de Media
Biblioteca de Media conține **toate fișierele** încărcate în sistem: imagini, videoclipuri, documente PDF și prezentări.
![Biblioteca de Media](screenshots/04-media-library.png)
*Figura 4 Bibliotecac de Media*
### 4.1. Accesarea bibliotecii
Din meniul principal, selectați **„Media Library"** sau accesați direct `/media-library`.
### 4.2. Informații afișate pentru fiecare fișier
| Coloană / Element | Semnificație |
|-------------------|--------------|
| **Nume fișier** | Numele original al fișierului încărcat |
| **Tip** | Imagine / Video / PDF / Prezentare |
| **Dimensiune** | Mărimea fișierului |
| **Data încărcării** | Când a fost adăugat în sistem |
| **Previzualizare** | Miniatură pentru imagini și videoclipuri |
### 4.3. Filtrarea și căutarea
- Folosiți **bara de căutare** pentru a găsi un fișier după nume
- Folosiți **filtrele** pentru a afișa doar un anumit tip (imagini, videoclipuri etc.)
### 4.4. Ștergerea unui fișier din bibliotecă
> **Atenție:** Ștergerea din bibliotecă este permanentă. Dacă fișierul este folosit într-un playlist, acesta va apărea ca lipsă.
1. Găsiți fișierul în listă
2. Apăsați butonul **Ștergere** (iconiță coș de gunoi / X)
3. Confirmați ștergerea
![Accesarea opțiunii de ștergere](screenshots/05-1-delete-file.png)
*Figura 5 Accesarea butonului de ștergere*
![Confirmarea ștergerii unui fișier](screenshots/05-2-delete-file-confirm.png)
*Figura 6 Confirmarea ștergerii*
---
## 5. Adăugarea de Media în sistem
### 5.1. Deschiderea formularului de încărcare
Din meniul principal selectați **„Upload Media"** sau accesați pagina de încărcare.
![Formularul de încărcare media](screenshots/06-1-upload-media.png)
*Figura 7 Pagina de încărcare media*
### 5.2. Tipuri de fișiere acceptate
| Categorie | Formate acceptate |
|-----------|-------------------|
| **Imagini** | PNG, JPG, JPEG, GIF, BMP |
| **Video** | MP4, AVI, MKV, MOV, WEBM |
| **Documente** | PDF |
| **Prezentări** | PPT, PPTX |
### 5.3. Pașii pentru încărcare
1. **Apăsați zona de selecție** sau butonul **„Alege fișier"**
2. Selectați fișierul (sau mai multe fișiere) din calculator
3. Așteptați finalizarea încărcării
4. Veți primi confirmarea că fișierul a fost adăugat
![Selectarea fișierului de încărcat](screenshots/06-2-upload-media-select.png)
*Figura 8 Selectarea fișierului*
### 5.4. Confirmarea încărcării
După finalizarea încărcării, aplicația afișează un banner verde de confirmare cu mesajul **„Successfully uploaded"**, care indică playlist-ul în care a fost adăugat fișierul.
![Banner de confirmare a încărcării](screenshots/07-upload-success.png)
*Figura 9 Bannerul „Successfully uploaded"*
### 5.5. Adăugarea unui link web (Web Link)
Pe lângă fișiere, puteți adăuga și **adrese web** care să fie afișate pe ecrane.
1. Selectați opțiunea **„Add Web Link"** / **„Adaugă link web"**
2. Introduceți **URL-ul complet** (ex: `https://www.exemplu.ro`)
3. Dați un **titlu** pentru link
4. Salvați
![Adăugarea unui link web](screenshots/08-add-weblink.png)
*Figura 10 Adăugarea unui link web*
> **Sfat:** Limita maximă pentru un fișier este de 2 GB. Pentru fișiere foarte mari, împărțiți-le sau contactați administratorul.
---
## 6. Managementul Playlist-urilor
Un **playlist** este o listă ordonată de conținut care se redă pe un player. Puteți avea mai multe playlist-uri pentru diferite ecrane sau situații.
### 6.1. Vizualizarea playlist-urilor
Din meniul principal selectați **„Playlists"**.
![Lista playlist-urilor](screenshots/09-playlists-list.png)
*Figura 9 Lista playlist-urilor*
Aici vedeți toate playlist-urile existente, cu:
| Element | Descriere |
|---------|-----------|
| **Nume** | Denumirea playlist-ului |
| **Nr. elemente** | Câte fișiere conține |
| **Player atribuit** | Pe ce ecran se redă |
| **Acțiuni** | Editare, gestionare conținut, ștergere |
### 6.2. Crearea unui playlist nou
1. Apăsați butonul **„Creează Playlist"** / **„Create Playlist"**
2. Introduceți **numele playlist-ului** (ex: „Ecran Recepție")
3. Opțional: adăugați o **descriere**
4. Apăsați **„Salvează"**
![Crearea unui playlist nou](screenshots/10-create-playlist.png)
*Figura 10 Crearea unui playlist*
### 6.3. Gestionarea conținutului unui playlist
Pentru a vedea și edita conținutul unui playlist:
1. Găsiți playlist-ul în listă
2. Apăsați butonul **„Gestionare"** / **„Manage"** (sau numele playlist-ului)
3. Se deschide pagina de management a conținutului
![Pagina de management a playlist-ului](screenshots/11-1-manage-playlist.png)
*Figura 12 Accesarea managementului*
![Conținutul playlist-ului](screenshots/11-2-manage-playlist-content.png)
*Figura 13 Conținutul playlist-ului*
### 6.4. Ștergerea unui playlist
1. Găsiți playlist-ul în listă
2. Apăsați butonul **Ștergere**
3. Confirmați
> **Atenție:** Ștergerea unui playlist nu șterge fișierele media. Doar lista de redare este eliminată.
---
## 7. Adăugarea de conținut într-un Playlist
### 7.1. Pași pentru adăugare
1. Deschideți playlist-ul (secțiunea **Management**)
2. Apăsați **„Adaugă conținut"** / **„Add Content"**
3. Se afișează lista cu toate fișierele disponibile
4. **Bifați** fișierele dorite (puteți selecta mai multe)
5. Apăsați **„Adaugă"**
![Selectarea conținutului de adăugat](screenshots/12-add-content-to-playlist.png)
*Figura 12 Adăugarea conținutului*
### 7.2. Opțiuni per element
După adăugare, pentru fiecare element puteți configura:
| Opțiune | Descriere |
|---------|-----------|
| **Durată afișare** | Cât timp se afișează elementul (secunde) |
| **Fără sunet (Muted)** | Dezactivează sunetul pentru acel element |
| **Editare permisă** | Permite player-ului să editeze fișierul local |
![Opțiunile fiecărui element](screenshots/13-content-options.png)
*Figura 13 Opțiunile elementelor*
### 7.3. Adăugarea unui link web direct în playlist
1. În pagina de management a playlist-ului, apăsați **„Adaugă link web"**
2. Introduceți URL-ul și titlul
3. Salvați link-ul apare în listă ca orice alt element
---
## 8. Ștergerea unui fișier din Playlist
### 8.1. Ștergerea unui singur element
1. Deschideți playlist-ul în modul **Management**
2. Găsiți elementul dorit în listă
3. Apăsați iconița **Ștergere** (X / coș de gunoi) de pe rândul respectiv
4. Confirmați dacă vi se cere
![Ștergerea unui element din playlist](screenshots/14-remove-from-playlist.png)
*Figura 14 Ștergerea unui element*
> **Important:** Ștergerea din playlist **nu șterge fișierul** din Biblioteca de Media. Fișierul rămâne disponibil pentru alte playlist-uri.
### 8.2. Ștergerea mai multor elemente simultan (Bulk Remove)
Pentru a șterge mai multe fișiere dintr-un playlist:
1. În pagina de management, **bifați** elementele dorite
2. Apăsați butonul **„Șterge selectate"** / **„Bulk Remove"**
3. Confirmați operațiunea
![Ștergerea în masă](screenshots/15-bulk-remove.png)
*Figura 15 Ștergerea în masă*
### 8.3. Diferența: ștergere playlist vs. ștergere fișier
| Acțiune | Efect |
|---------|-------|
| **Ștergere element din playlist** | Elementul dispare din listă; fișierul rămâne în bibliotecă |
| **Ștergere fișier din bibliotecă** | Fișierul dispare complet; apare ca lipsă în toate playlist-urile |
| **Ștergere playlist** | Lista dispare; toate fișierele rămân în bibliotecă |
---
## 9. Reordonarea conținutului în Playlist
Ordinea elementelor determină ordinea de afișare pe ecran.
### 9.1. Reordonare prin tragere (drag & drop)
1. Deschideți playlist-ul în modul **Management**
2. Apăsați și țineți apăsat pe iconița de **mâner** (⋮⋮) din dreptul unui element
3. Trageți elementul în poziția dorită
4. Ordinea se salvează automat
![Reordonarea elementelor](screenshots/16-reorder-playlist.png)
*Figura 16 Reordonarea elementelor*
### 9.2. Verificarea ordinii
După reordonare, verificați că numerele de ordine (1, 2, 3...) corespund intenției dvs.
---
## 10. Managementul Playerelelor
**Playerele** sunt ecranele fizice care afișează conținutul.
### 10.1. Lista playerelor
Din meniul principal selectați **„Players"**.
![Lista playerelor](screenshots/17-players-list.png)
*Figura 17 Lista playerelor*
| Coloană | Semnificație |
|---------|--------------|
| **Nume** | Denumirea player-ului |
| **Locație** | Unde este amplasat ecranul |
| **Adresă IP** | Adresa de rețea a player-ului |
| **Playlist atribuit** | Ce conținut redă |
| **Stare** | Online / Offline / Stare deploy |
### 10.2. Adăugarea unui player nou
1. Apăsați **„Adaugă Player"** / **„Add Player"**
2. Completați:
- **Nume** (ex: „Ecran Hol Principal")
- **Adresă IP**
- **Locație** (opțional)
3. Salvați
![Adăugarea unui player](screenshots/18-1-add-player.png)
*Figura 20 Formularul de adăugare player*
![Player completat](screenshots/18-2-add-player-filled.png)
*Figura 21 Formularul completat*
### 10.3. Editarea unui player
1. Apăsați **„Editare"** / **„Edit"** pe rândul player-ului
2. Modificați câmpurile dorite
3. Salvați
### 10.4. Atribuirea unui playlist unui player
1. În lista de playere, găsiți player-ul dorit
2. Apăsați **„Atribuie Playlist"** / **„Assign Playlist"**
3. Selectați playlist-ul din listă
4. Confirmați
![Atribuirea unui playlist](screenshots/19-1-assign-playlist.png)
*Figura 22 Selectarea playlist-ului*
![Atribuire confirmată](screenshots/19-2-assign-playlist-done.png)
*Figura 23 Confirmarea atribuirii*
> **Notă:** După atribuire, player-ul va prelua noul conținut la următoarea sincronizare.
### 10.5. Starea de deployment
Fiecare player are o **stare de deployment** care arată dacă conținutul a fost trimis cu succes:
| Stare | Semnificație |
|-------|--------------|
| ✓ **Succes** | Conținutul a fost trimis cu succes |
| ⏳ **În așteptare** | Se așteaptă sincronizarea |
| ✗ **Eroare** | Trimiterea a eșuat verificați mesajul |
### 10.6. Regenerarea cheii de autentificare
Dacă un player nu se mai poate conecta:
1. Găsiți player-ul în listă
2. Apăsați **„Regenerează autentificare"** / **„Regenerate Auth"**
3. Confirmați se va genera o cheie nouă
> **Atenție:** Va trebui să actualizați cheia și pe player-ul fizic.
---
## 11. Vizualizarea fișierelor editate pe Player (funcție avansată)
Această funcție vă permite să vedeți **ce versiuni editate** ale fișierelor există pe fiecare player. Este utilă când un player modifică local fișierele (de exemplu, suprapune informații, ajustează rezoluția etc.).
### 11.1. Accesarea raportului de fișiere editate
1. Accesați secțiunea **Players**
2. Găsiți player-ul dorit
3. Apăsați **„Fișiere editate"** / **„Edited Media"**
![Lista fișierelor editate](screenshots/20-edited-media.png)
*Figura 20 Fișierele editate*
### 11.2. Ce afișează această pagină
| Element | Descriere |
|---------|-----------|
| **Fișier original** | Fișierul din biblioteca centrală |
| **Versiune editată** | Fișierul modificat pe player |
| **Data editării** | Când a fost modificat |
| **Status** | Sincronizat / Diferit / Lipsă |
### 11.3. Raportul detaliat
Pentru o vedere completă:
1. Apăsați **„Raport fișiere editate"** / **„Edited Media Report"**
2. Se generează un raport cu toate diferențele
![Raportul fișierelor editate](screenshots/21-1-edited-media-report.png)
*Figura 26 Raportul fișierelor editate*
![Detaliu raport](screenshots/21-2-edited-media-report-detail.png)
*Figura 27 Detaliu din raport*
### 11.4. Interpretarea statusurilor
| Status | Ce înseamnă | Ce faceți |
|--------|-------------|-----------|
| **Sincronizat** | Fișierul de pe player corespunde cu cel central | Nimic este în regulă |
| **Diferit** | Fișierul a fost editat pe player | Decideți dacă păstrați sau resincronizați |
| **Lipsă** | Fișierul lipsește de pe player | Resincronizați player-ul |
### 11.5. Vizualizare în mod ecran complet (Fullscreen)
Pentru a vedea cum arată conținutul pe player:
1. În lista de playere, apăsați **„Ecran complet"** / **„Fullscreen"**
2. Se deschide o previzualizare la scară
![Previzualizarea fullscreen](screenshots/22-player-fullscreen.png)
*Figura 22 Previzualizarea fullscreen*
---
## 12. Administrare
> Această secțiune este disponibilă **doar pentru administratori**.
### 12.1. Managementul utilizatorilor
**Admin → Users**
- Creați utilizatori noi
- Modificați roluri (Admin / Editor / Utilizator)
- Resetați parole
- Ștergeți utilizatori
![Managementul utilizatorilor](screenshots/23-1-user-management.png)
*Figura 28 Lista utilizatorilor*
![Editarea unui utilizator](screenshots/23-2-user-management-edit.png)
*Figura 29 Editarea unui utilizator*
### 12.2. Setări HTTPS
**Admin → HTTPS Configuration**
Aici configurați certificatul SSL/TLS pentru acces securizat:
1. **Hostname** numele serverului
2. **Adresă IP** IP-ul serverului
3. **Domeniu** domeniul (lăsați gol pentru rețea internă)
4. **Email** pentru notificări certificat
![Configurarea HTTPS](screenshots/24-https-config.png)
*Figura 24 Configurarea HTTPS*
> **Important:** Când mutați serverul în rețeaua de producție, setați IP-ul corect (`10.76.152.164`).
### 12.3. Personalizare logo
**Admin → Customize Logos**
- Încărcați logo pentru antet (header)
- Încărcați logo pentru ecranul de login
### 12.4. Dependențe și unelte
**Admin → Dependencies**
Permite instalarea de unelte opționale:
| Unealtă | Rol |
|---------|-----|
| **LibreOffice** | Conversie automată PPT/PPTX în imagini |
| **Emoji Fonts** | Afișarea corectă a emoji-urilor |
![Instalarea dependențelor](screenshots/25-dependencies.png)
*Figura 25 Instalarea dependențelor*
### 12.5. Media rămasă fără utilizare
**Admin → Leftover Media**
Afișează fișierele care nu mai sunt folosite în niciun playlist. Le puteți șterge pentru a elibera spațiu.
### 12.6. Jurnale de sistem (Logs)
**Admin → System Info / Logs**
Vedeți activitatea sistemului și eventualele erori.
---
## 13. Întrebări frecvente și depanare
### 13.1. Nu mă pot autentifica
**Simptom:** Parola este respinsă.
**Soluție:**
1. Verificați că scrieți corect numele de utilizator și parola (atenție la majuscule)
2. Dacă ați uitat parola, contactați administratorul
3. Verificați că accesați adresa corectă (cu `https://`)
### 13.2. Browserul afișează avertisment de certificat
**Simptom:** „Conexiunea nu este privată" / „Nu este sigur".
**Soluție:** Acest lucru este normal pentru rețeaua internă. Apăsați **„Avansat" → „Continuă către..."**. Administratorul de rețea poate furniza un certificat intern pentru a elimina avertismentul.
### 13.3. Fișierul nu se încarcă
**Simptom:** Încărcarea eșuează sau se blochează.
**Soluție:**
1. Verificați că fișierul este sub limita de **2 GB**
2. Verificați că formatul este acceptat (PNG, JPG, MP4, PDF, PPT etc.)
3. Verificați conexiunea la rețea
4. Încercați din nou; dacă persistă, contactați administratorul
### 13.4. Player-ul nu afișează conținutul nou
**Simptom:** Ați schimbat playlist-ul, dar ecranul afișează vechiul conținut.
**Soluție:**
1. Verificați că playlist-ul este **atribuit** player-ului
2. Verificați **starea de deployment** a player-ului
3. Așteptați următoarea sincronizare (poate dura câteva minute)
4. Dacă persistă, verificați că player-ul este **online** și accesibil în rețea
### 13.5. Am șters un fișier din bibliotecă din greșeală
**Simptom:** Fișierul apare ca „lipsă" în playlist-uri.
**Soluție:** Reîncărcați fișierul în bibliotecă și adăugați-l din nou în playlist. Dacă aveți un backup, contactați administratorul.
### 13.6. Playlist-ul se redă în ordine greșită
**Simptom:** Elementele apar în altă ordine decât ați stabilit.
**Soluție:** Verificați ordinea în pagina de **Management** a playlist-ului și reordonați folosind drag & drop. Confirmați că ordinea s-a salvat.
### 13.7. Fișierele editate nu corespund
**Simptom:** Raportul de fișiere editate arată diferențe neașteptate.
**Soluție:** Consultați secțiunea [11.4](#114-interpretarea-statusurilor) pentru interpretarea statusurilor. Decideți dacă resincronizați player-ul.
---
## Anexă A Glosar de termeni
| Termen | Explicație |
|--------|------------|
| **Media** | Fișier (imagine, video, document) încărcat în sistem |
| **Playlist** | Listă ordonată de conținut care se redă pe un ecran |
| **Player** | Dispozitivul/ecranul fizic care afișează conținutul |
| **Deployment** | Procesul de trimitere a conținutului către player |
| **Dashboard** | Pagina principală cu statistici |
| **Bibliotecă de Media** | Colecția tuturor fișierelor încărcate |
| **Bulk Remove** | Ștergerea mai multor elemente simultan |
| **Edited Media** | Versiunile modificate local ale fișierelor, de pe player |
| **Web Link** | Adresă web afișată ca element în playlist |
---
## Anexă B Lista capturilor de ecran necesare
Salvați capturile în folderul `documentatie/screenshots/` cu numele exacte de mai jos:
| Nr. | Nume fișier | Secțiune | Ce trebuie să conțină |
|-----|-------------|----------|------------------------|
| 1 | `01-login.png` | 2.2 | Ecranul de autentificare |
| 2 | `02-change-password.png` | 2.3 | Formularul de schimbare parolă |
| 3 | `03-dashboard.png` | 3 | Dashboard-ul complet |
| 4 | `04-media-library.png` | 4 | Lista fișierelor media |
| 5 | `05-delete-file.png` | 4.4 | Confirmarea ștergerii |
| 6 | `06-upload-media.png` | 5.1 | Formularul de încărcare |
| 7 | `07-upload-progress.png` | 5.3 | Progresul încărcării |
| 8 | `08-add-weblink.png` | 5.4 | Formularul de link web |
| 9 | `09-playlists-list.png` | 6.1 | Lista playlist-urilor |
| 10 | `10-create-playlist.png` | 6.2 | Crearea unui playlist |
| 11 | `11-manage-playlist.png` | 6.3 | Managementul conținutului |
| 12 | `12-add-content-to-playlist.png` | 7.1 | Selectarea conținutului |
| 13 | `13-content-options.png` | 7.2 | Opțiunile unui element |
| 14 | `14-remove-from-playlist.png` | 8.1 | Ștergerea unui element |
| 15 | `15-bulk-remove.png` | 8.2 | Ștergerea în masă |
| 16 | `16-reorder-playlist.png` | 9.1 | Reordonarea elementelor |
| 17 | `17-players-list.png` | 10.1 | Lista playerelor |
| 18 | `18-add-player.png` | 10.2 | Adăugarea unui player |
| 19 | `19-assign-playlist.png` | 10.4 | Atribuirea unui playlist |
| 20 | `20-edited-media.png` | 11.1 | Fișierele editate |
| 21 | `21-edited-media-report.png` | 11.3 | Raportul detaliat |
| 22 | `22-player-fullscreen.png` | 11.5 | Previzualizarea fullscreen |
| 23 | `23-user-management.png` | 12.1 | Managementul utilizatorilor |
| 24 | `24-https-config.png` | 12.2 | Configurarea HTTPS |
| 25 | `25-dependencies.png` | 12.4 | Instalarea dependențelor |
---
*Sfârșitul documentului*
+167
View File
@@ -0,0 +1,167 @@
#!/bin/bash
# ============================================================================
# capture_screenshots.sh - Helper pentru capturile de ecran ale manualului
# ----------------------------------------------------------------------------
# Ghideaza pas-cu-pas captura celor 25 de ecrane necesare pentru manual.
# Foloseste Flameshot (recomandat) sau scrot.
#
# Utilizare:
# ./capture_screenshots.sh # mod ghidat, pas cu pas
# ./capture_screenshots.sh --list # doar listeaza ce trebuie capturat
# ./capture_screenshots.sh --check # verifica ce capturi exista deja
# ============================================================================
set -u
SCREENSHOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/screenshots"
mkdir -p "$SCREENSHOT_DIR"
# Cele 25 de capturi necesare: nume|descriere|secțiune manual
CAPTURES=(
"01-login|Ecranul de autentificare (login)|2.2"
"02-change-password|Formularul de schimbare parola|2.3"
"03-dashboard|Dashboard-ul complet cu statistici|3"
"04-media-library|Lista fisierelor din Biblioteca de Media|4"
"05-delete-file|Confirmarea stergerii unui fisier|4.4"
"06-upload-media|Formularul de incarcare media|5.1"
"07-upload-progress|Progresul incarcarii unui fisier|5.3"
"08-add-weblink|Formularul de adaugare link web|5.4"
"09-playlists-list|Lista tuturor playlist-urilor|6.1"
"10-create-playlist|Crearea unui playlist nou|6.2"
"11-manage-playlist|Pagina de management a continutului|6.3"
"12-add-content-to-playlist|Selectarea continutului de adaugat|7.1"
"13-content-options|Optiunile per element (durata, muted)|7.2"
"14-remove-from-playlist|Stergerea unui element din playlist|8.1"
"15-bulk-remove|Stergerea in masa a elementelor|8.2"
"16-reorder-playlist|Reordonarea elementelor (drag & drop)|9.1"
"17-players-list|Lista playerelor|10.1"
"18-add-player|Adaugarea unui player nou|10.2"
"19-assign-playlist|Atribuirea unui playlist unui player|10.4"
"20-edited-media|Lista fisierelor editate pe player|11.1"
"21-edited-media-report|Raportul detaliat al fisierelor editate|11.3"
"22-player-fullscreen|Previzualizarea in mod ecran complet|11.5"
"23-user-management|Managementul utilizatorilor|12.1"
"24-https-config|Configurarea HTTPS|12.2"
"25-dependencies|Instalarea dependentelor|12.4"
)
# ---------------------------------------------------------------------------
# Mod --list
# ---------------------------------------------------------------------------
if [ "${1:-}" = "--list" ]; then
echo "============================================================"
echo " Capturi de ecran necesare pentru manual (25 total)"
echo "============================================================"
echo
printf "%-6s %-34s %-36s %s\n" "NR" "FISIER" "CE SA CONTINA" "SECTIUNE"
echo "--------------------------------------------------------------------------"
i=1
for entry in "${CAPTURES[@]}"; do
IFS='|' read -r name desc section <<< "$entry"
printf "%-6s %-34s %-36s %s\n" "$i." "${name}.png" "$desc" "$section"
i=$((i+1))
done
echo
echo "Salveaza toate capturile in:"
echo " $SCREENSHOT_DIR"
exit 0
fi
# ---------------------------------------------------------------------------
# Mod --check
# ---------------------------------------------------------------------------
if [ "${1:-}" = "--check" ]; then
echo "============================================================"
echo " Verificare capturi de ecran existente"
echo "============================================================"
echo
found=0; missing=0
for entry in "${CAPTURES[@]}"; do
IFS='|' read -r name desc section <<< "$entry"
if [ -f "$SCREENSHOT_DIR/${name}.png" ]; then
size=$(du -h "$SCREENSHOT_DIR/${name}.png" | cut -f1)
printf " \033[0;32m✓\033[0m %-32s %s\n" "${name}.png" "$size"
found=$((found+1))
else
printf " \033[0;31m✗\033[0m %-32s LIPSA\n" "${name}.png"
missing=$((missing+1))
fi
done
echo
echo "-------------------------------------------------------------"
echo " Gasite: $found / 25"
echo " Lipsa: $missing / 25"
echo "-------------------------------------------------------------"
if [ "$missing" -eq 0 ]; then
echo
echo " Toate capturile sunt prezente! Puteti rula:"
echo " ./convert_manual.sh"
fi
exit 0
fi
# ---------------------------------------------------------------------------
# Mod ghidat
# ---------------------------------------------------------------------------
echo "============================================================"
echo " Ghid capturi de ecran - Manual DigiServer"
echo "============================================================"
echo
if ! command -v scrot &> /dev/null && ! command -v flameshot &> /dev/null; then
echo "✗ Nu exista tool de capturi. Instalati cu:"
echo " sudo apt-get install -y flameshot scrot"
exit 1
fi
echo "Deschideti aplicatia in browser: https://10.76.152.164"
echo "Autentificati-va ca admin, apoi urmati pasii."
echo
echo "Pentru fiecare ecran vi se va cere sa apasati Enter,"
echo "apoi se face captura. Daca ati facut-o deja, scrieti 's' (skip)."
echo
completed=0
for entry in "${CAPTURES[@]}"; do
IFS='|' read -r name desc section <<< "$entry"
target="$SCREENSHOT_DIR/${name}.png"
echo "-------------------------------------------------------------"
echo " ${name}.png"
echo " Ce: $desc"
echo " Sectiune manual: $section"
if [ -f "$target" ]; then
echo " (exista deja - scrieti 'r' pentru a reface, Enter pentru skip)"
fi
read -r -p " Enter = captura | s = skip | q = iesire: " action
case "$action" in
q|Q) echo; echo "Iesire. Capturi realizate: $completed"; exit 0 ;;
r|R) : ;;
s|S) echo " skipped"; continue ;;
esac
echo " Captura in 3 secunde... (treceti la fereastra dorita)"
sleep 3
if command -v scrot &> /dev/null; then
scrot "$target"
else
flameshot full -p "$target"
fi
if [ -f "$target" ]; then
echo " ✓ Salvat: $target"
completed=$((completed+1))
else
echo " ✗ Captura a esuat"
fi
done
echo
echo "============================================================"
echo " Ghid terminat. Capturi realizate: $completed"
echo "============================================================"
echo
echo "Verificati cu: ./capture_screenshots.sh --check"
echo "Apoi convertiti: ./convert_manual.sh"
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# ============================================================================
# convert_help_page.sh - Regenerates the in-app help page content
# ----------------------------------------------------------------------------
# Converts documentatie/Manual-Utilizare-DigiServer.md into the HTML fragment
# that the Flask app serves at /help, and refreshes the screenshot copies.
#
# Run this whenever the manual is edited, then rebuild the image:
# ./documentatie/convert_help_page.sh
# cd .. && docker compose up -d --build
# ============================================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC_MD="$PROJECT_DIR/documentatie/Manual-Utilizare-DigiServer.md"
SRC_IMG_DIR="$PROJECT_DIR/documentatie/screenshots"
DEST_DIR="$PROJECT_DIR/app/static/help"
DEST_IMG_DIR="$DEST_DIR/screenshots"
DEST_HTML="$DEST_DIR/_manual_body.html"
echo "============================================================"
echo " Regenerare pagina Ajutor (/help)"
echo "============================================================"
echo
# ---------------------------------------------------------------------------
# Checks
# ---------------------------------------------------------------------------
if ! command -v pandoc &> /dev/null; then
echo "✗ Pandoc nu este instalat."
echo " Instalati cu: sudo apt-get install -y pandoc"
exit 1
fi
if [ ! -f "$SRC_MD" ]; then
echo "✗ Nu am gasit manualul: $SRC_MD"
exit 1
fi
mkdir -p "$DEST_IMG_DIR"
# ---------------------------------------------------------------------------
# 1. Convert Markdown -> HTML fragment
# ---------------------------------------------------------------------------
echo "[1/3] Conversie Markdown -> HTML..."
pandoc "$SRC_MD" \
-t html5 \
--no-highlight \
--wrap=none \
-o "$DEST_HTML"
echo "$(basename "$DEST_HTML")"
# ---------------------------------------------------------------------------
# 2. Copy screenshots and rewrite their paths for Flask's static route
# ---------------------------------------------------------------------------
echo
echo "[2/3] Copiere capturi de ecran si rescriere cai..."
cp "$SRC_IMG_DIR"/*.png "$DEST_IMG_DIR"/ 2>/dev/null || {
echo " ⚠ Nicio captura gasita in $SRC_IMG_DIR"
}
COUNT=$(ls -1 "$DEST_IMG_DIR"/*.png 2>/dev/null | wc -l)
echo "$COUNT capturi copiate"
# Markdown references images as "screenshots/x.png"; the app serves them from
# /static/help/screenshots/, so the prefix has to be made explicit.
sed -i 's|src="screenshots/|src="/static/help/screenshots/|g' "$DEST_HTML"
REFS=$(grep -c '/static/help/screenshots/' "$DEST_HTML" || true)
echo "$REFS referinte de imagini rescrise"
# ---------------------------------------------------------------------------
# 3. Sanity check: every referenced image must exist
# ---------------------------------------------------------------------------
echo
echo "[3/3] Verificare consistenta..."
MISSING=0
while IFS= read -r ref; do
[ -z "$ref" ] && continue
if [ ! -f "$DEST_IMG_DIR/$(basename "$ref")" ]; then
echo " ✗ lipsa: $ref"
MISSING=$((MISSING + 1))
fi
done < <(grep -oE '/static/help/screenshots/[^"]+' "$DEST_HTML" | sort -u || true)
if [ "$MISSING" -eq 0 ]; then
echo " ✓ toate imaginile referite exista"
else
echo "$MISSING imagini lipsa"
fi
echo
echo "============================================================"
echo " Gata. Rebuild imaginea pentru a aplica modificarile:"
echo " cd $PROJECT_DIR && docker compose up -d --build"
echo "============================================================"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
# ============================================================================
# convert_manual.sh - Converteste manualul DigiServer in format DOCX
# ----------------------------------------------------------------------------
# Genereaza un document Word cu:
# - Cuprins automat
# - Secțiuni numerotate
# - Capturi de ecran incluse (din folderul screenshots/)
#
# Utilizare:
# ./convert_manual.sh
# ============================================================================
set -e
cd "$(dirname "$0")"
echo "============================================================"
echo " Conversie Manual DigiServer -> DOCX"
echo "============================================================"
echo
# ---------------------------------------------------------------------------
# Verificare Pandoc
# ---------------------------------------------------------------------------
if ! command -v pandoc &> /dev/null; then
echo "✗ Pandoc nu este instalat."
echo
echo " Instalati cu:"
echo " sudo apt-get update && sudo apt-get install -y pandoc"
echo
exit 1
fi
echo "✓ Pandoc detectat: $(pandoc --version | head -1)"
echo
# ---------------------------------------------------------------------------
# Verificare fisiere sursa
# ---------------------------------------------------------------------------
if [ ! -f "Manual-Utilizare-DigiServer.md" ]; then
echo "✗ Fisierul Manual-Utilizare-DigiServer.md nu a fost gasit."
exit 1
fi
echo "✓ Manual gasit"
mkdir -p screenshots
SCREENSHOT_COUNT=$(ls -1 screenshots/*.png screenshots/*.jpg 2>/dev/null | wc -l)
echo "✓ Capturi de ecran disponibile: $SCREENSHOT_COUNT"
if [ "$SCREENSHOT_COUNT" -eq 0 ]; then
echo " (Nu exista capturi - documentul va fi generat doar cu text)"
fi
echo
# ---------------------------------------------------------------------------
# Conversie
# ---------------------------------------------------------------------------
echo "Conversie in curs..."
echo
pandoc Manual-Utilizare-DigiServer.md \
--toc \
--toc-depth=2 \
--number-sections \
--resource-path=screenshots \
--metadata title="Manual de Utilizare DigiServer v2" \
--metadata lang="ro-RO" \
-o Manual-Utilizare-DigiServer.docx
# ---------------------------------------------------------------------------
# Verificare rezultat
# ---------------------------------------------------------------------------
echo
if [ -f "Manual-Utilizare-DigiServer.docx" ]; then
echo "============================================================"
echo " ✓ Document creat cu succes!"
echo "============================================================"
ls -lh Manual-Utilizare-DigiServer.docx
echo
echo "Deschideti cu: libreoffice Manual-Utilizare-DigiServer.docx"
echo " sau deschideti in Microsoft Word."
else
echo "✗ Eroare: documentul nu a fost creat."
exit 1
fi
+156
View File
@@ -0,0 +1,156 @@
#!/bin/bash
# ============================================================================
# DigiServer - Database Restore
# ----------------------------------------------------------------------------
# Restores a database snapshot created by ./backup_players_playlists.sh.
#
# Usage:
# ./restore_database.sh # restore the latest backup
# ./restore_database.sh 20260915_143000 # restore a specific backup ID
# ./restore_database.sh --list # list available backups
# ./restore_database.sh <id> --skip-migrations
#
# Typical use: run AFTER upgrading the Docker image / docker host.
# ============================================================================
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml"
BACKUP_ROOT="$PROJECT_DIR/backups"
DB_HOST="$PROJECT_DIR/data/instance/dashboard.db"
SKIP_MIGRATIONS=0
BACKUP_ID=""
for arg in "$@"; do
case "$arg" in
--list) ls -1 "$BACKUP_ROOT" 2>/dev/null | sort || echo "(no backups found)"; exit 0 ;;
--skip-migrations) SKIP_MIGRATIONS=1 ;;
-*) echo "Unknown option: $arg"; exit 1 ;;
*) BACKUP_ID="$arg" ;;
esac
done
if [ -z "$BACKUP_ID" ]; then
BACKUP_ID="$(ls -1 "$BACKUP_ROOT" 2>/dev/null | sort | tail -1 || true)"
fi
if [ -z "$BACKUP_ID" ]; then
echo "ERROR: no backups found in $BACKUP_ROOT"
exit 1
fi
DEST="$BACKUP_ROOT/$BACKUP_ID"
if [ ! -d "$DEST" ]; then
echo "ERROR: backup '$BACKUP_ID' not found in $BACKUP_ROOT"
echo "Available:"
ls -1 "$BACKUP_ROOT" 2>/dev/null | sed 's/^/ /'
exit 1
fi
echo "============================================================"
echo " DigiServer database restore"
echo " Backup: $BACKUP_ID"
echo "============================================================"
# ---------------------------------------------------------------------------
# 1. Verify integrity
# ---------------------------------------------------------------------------
if [ -f "$DEST/SHA256SUMS" ]; then
echo
echo "[1/6] Verifying checksums..."
( cd "$DEST" && sha256sum -c SHA256SUMS )
else
echo
echo "[1/6] No SHA256SUMS found, skipping verification"
fi
# ---------------------------------------------------------------------------
# 2. Stop the app (so nothing writes during restore)
# ---------------------------------------------------------------------------
echo
echo "[2/6] Stopping application container..."
$COMPOSE stop digiserver-app
# ---------------------------------------------------------------------------
# 3. Back up the CURRENT database before overwriting it
# ---------------------------------------------------------------------------
if [ -f "$DB_HOST" ]; then
SAFETY="$BACKUP_ROOT/pre_restore_$(date +%Y%m%d_%H%M%S)_dashboard.db"
echo
echo "[3/6] Saving current DB as a safety copy -> $(basename "$SAFETY")"
cp "$DB_HOST" "$SAFETY"
else
echo
echo "[3/6] No existing DB to preserve"
fi
# ---------------------------------------------------------------------------
# 4. Restore the snapshot
# ---------------------------------------------------------------------------
echo
echo "[4/6] Restoring database snapshot..."
if [ -f "$DEST/dashboard.db.gz" ]; then
gunzip -c "$DEST/dashboard.db.gz" > "$DB_HOST"
elif [ -f "$DEST/dashboard.db" ]; then
cp "$DEST/dashboard.db" "$DB_HOST"
else
echo "ERROR: no dashboard.db(.gz) in backup $BACKUP_ID"
exit 1
fi
# Remove any stale journal left over from the old DB
rm -f "$DB_HOST-journal" "$DB_HOST-wal" "$DB_HOST-shm"
# Ownership: container runs as uid 1000; host files are pi:pi (1000)
if [ "$(id -u)" -eq 0 ]; then
chown 1000:1000 "$DB_HOST"
else
sudo chown 1000:1000 "$DB_HOST"
fi
chmod 644 "$DB_HOST"
echo " -> restored $(du -h "$DB_HOST" | cut -f1)"
# ---------------------------------------------------------------------------
# 5. Start the (upgraded) app
# ---------------------------------------------------------------------------
echo
echo "[5/6] Starting application..."
$COMPOSE up -d --build
sleep 8
# ---------------------------------------------------------------------------
# 6. Re-run migrations so any new schema is applied to the restored data
# ---------------------------------------------------------------------------
if [ "$SKIP_MIGRATIONS" -eq 0 ]; then
echo
echo "[6/6] Re-running migrations..."
for m in add_https_config_table add_player_user_table add_email_to_https_config migrate_player_user_global; do
if $COMPOSE exec -T digiserver-app test -f "/app/migrations/$m.py"; then
echo " - $m"
$COMPOSE exec -T digiserver-app python "/app/migrations/$m.py" || \
echo " (warning: $m reported an issue - often means it was already applied)"
fi
done
else
echo
echo "[6/6] Migrations skipped (--skip-migrations)"
fi
# ---------------------------------------------------------------------------
# Verify
# ---------------------------------------------------------------------------
echo
echo "Verification:"
$COMPOSE exec -T digiserver-app python -c "
import sqlite3
c = sqlite3.connect('/app/instance/dashboard.db')
for t in ['player','playlist','playlist_content']:
try:
print(' %-18s %d' % (t, c.execute('SELECT COUNT(*) FROM \"%s\"' % t).fetchone()[0]))
except Exception as e:
print(' %-18s ERROR %s' % (t, e))
"
echo
echo "============================================================"
echo " Restore complete: $BACKUP_ID"
echo "============================================================"