Add display deletion endpoint and admin UI tweaks

This commit is contained in:
2026-01-24 19:28:25 +01:00
parent 4d4ab086c9
commit a9a1a6cdbe
7 changed files with 201 additions and 12 deletions

View File

@@ -498,3 +498,32 @@ def update_display_name(display_id: int):
db.session.commit()
flash("Display name updated", "success")
return redirect(url_for("admin.company_detail", company_id=display.company_id))
@bp.post("/displays/<int:display_id>/delete")
@login_required
def delete_display(display_id: int):
"""Admin: delete a display."""
admin_required()
display = db.session.get(Display, display_id)
if not display:
abort(404)
company_id = display.company_id
display_name = display.name
# If FK constraints are enabled, delete in a safe order.
# 1) Unassign playlist
display.assigned_playlist_id = None
# 2) Delete active sessions for this display
DisplaySession.query.filter_by(display_id=display.id).delete(synchronize_session=False)
# 3) Delete display
db.session.delete(display)
db.session.commit()
flash(f"Display '{display_name}' deleted.", "success")
return redirect(url_for("admin.company_detail", company_id=company_id))

BIN
app/static/favicon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

139
app/static/logo.svg Normal file
View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="150.74998mm"
height="46.903393mm"
viewBox="0 0 150.74998 46.903393"
version="1.1"
id="svg1"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
sodipodi:docname="opencast logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.78840488"
inkscape:cx="397.00414"
inkscape:cy="561.25984"
inkscape:window-width="1920"
inkscape:window-height="1129"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g35">
<inkscape:page
x="-3.5699601e-22"
y="0"
width="150.74998"
height="46.903393"
id="page2"
margin="0"
bleed="0" />
</sodipodi:namedview>
<defs
id="defs1">
<rect
x="319.63272"
y="230.84586"
width="465.49686"
height="96.397171"
id="rect4" />
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Drop Shadow"
id="filter36"
x="-0.16131238"
y="-0.23838385"
width="1.358472"
height="1.5529181">
<feFlood
result="flood"
in="SourceGraphic"
flood-opacity="0.498039"
flood-color="rgb(0,0,0)"
id="feFlood35" />
<feGaussianBlur
result="blur"
in="SourceGraphic"
stdDeviation="3.000000"
id="feGaussianBlur35" />
<feOffset
result="offset"
in="blur"
dx="1.600000"
dy="2.300000"
id="feOffset35" />
<feComposite
result="comp1"
operator="in"
in="flood"
in2="offset"
id="feComposite35" />
<feComposite
result="comp2"
operator="over"
in="SourceGraphic"
in2="comp1"
id="feComposite36" />
</filter>
</defs>
<g
inkscape:label="Laag 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-19.647457,-50.186441)">
<g
id="g35"
style="filter:url(#filter36)"
inkscape:export-filename="rssfeed\app\static\logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
transform="translate(-1.6779659,-0.67118643)">
<ellipse
style="fill:#ffe511;fill-opacity:1;stroke-width:0.212298"
id="path1"
cx="43.582642"
cy="76.934746"
rx="15.057219"
ry="11.056598"
inkscape:export-filename="rssfeed\app\static\logo.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<ellipse
style="fill:#ffef6e;fill-opacity:1;stroke-width:0.212298"
id="path2"
cx="51.380131"
cy="68.574883"
rx="13.175065"
ry="10.517252" />
<ellipse
style="fill:#fff5a3;fill-opacity:1;stroke-width:0.212298"
id="path3"
cx="57.833221"
cy="78.148277"
rx="15.326097"
ry="10.112741" />
</g>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-4.5436437,1.7684327)"
id="text3"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:74.6667px;font-family:Georgia;-inkscape-font-specification:'Georgia, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect4);display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13386;stroke-dasharray:none;stroke-opacity:1"><tspan
x="319.63281"
y="298.21152"
id="tspan2"><tspan
style="stroke:#000000"
id="tspan1">OpenSlide</tspan></tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -56,6 +56,12 @@ body {
letter-spacing: -0.02em;
}
.brand-logo {
width: 160px;
height: 45px;
display: block;
}
.navbar-brand {
font-weight: 700;
}

View File

@@ -45,7 +45,17 @@
<td class="monospace small">{{ d.token }}</td>
<td class="text-muted">{{ d.assigned_playlist.name if d.assigned_playlist else "(none)" }}</td>
<td class="text-end">
<div class="d-inline-flex gap-2">
<a class="btn btn-outline-ink btn-sm" href="{{ url_for('display.display_player', token=d.token) }}" target="_blank">Open</a>
<form
method="post"
action="{{ url_for('admin.delete_display', display_id=d.id) }}"
data-confirm="Delete display {{ d.name }}? This cannot be undone."
onsubmit="return confirm(this.dataset.confirm);"
>
<button class="btn btn-outline-danger btn-sm" type="submit">Delete</button>
</form>
</div>
</td>
</tr>
{% else %}

View File

@@ -4,6 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title or "Signage" }}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.png') }}" type="image/png" />
<link rel="apple-touch-icon" href="{{ url_for('static', filename='favicon.png') }}" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />
</head>
@@ -11,8 +13,14 @@
<nav class="navbar navbar-expand-lg navbar-light fixed-top app-navbar">
<div class="container">
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<span class="brand-mark" aria-hidden="true">S</span>
<span>Signage</span>
<img
class="brand-logo"
src="{{ url_for('static', filename='logo.svg') }}"
alt="Signage"
width="34"
height="34"
/>
</a>
<button
@@ -31,16 +39,9 @@
<ul class="navbar-nav me-auto">
{% if current_user.is_authenticated %}
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.dashboard') }}">Admin</a>
</li>
{# Dashboard link removed: users can click the logo to go to the dashboard. #}
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('company.dashboard') }}">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('company.my_company') }}">My Company</a>
</li>
{# Dashboard link removed: users can click the logo to go to the dashboard. #}
{% endif %}
{% endif %}
</ul>
@@ -49,6 +50,9 @@
{% if current_user.is_authenticated %}
<div class="small text-muted">{{ current_user.email }}</div>
<a class="btn btn-outline-ink btn-sm" href="{{ url_for('auth.change_password') }}">Change password</a>
{% if not current_user.is_admin %}
<a class="btn btn-outline-ink btn-sm" href="{{ url_for('company.my_company') }}">My company</a>
{% endif %}
{% if session.get('impersonator_admin_id') %}
<a class="btn btn-brand btn-sm" href="{{ url_for('auth.stop_impersonation') }}">Stop impersonation</a>
{% endif %}

View File

@@ -20,6 +20,7 @@ def main():
required = {
"/admin/companies/<int:company_id>/delete",
"/admin/displays/<int:display_id>/delete",
"/admin/displays/<int:display_id>/name",
"/admin/settings",
"/company/displays/<int:display_id>",