Initial commit
This commit is contained in:
875
app/templates/admin.html
Normal file
875
app/templates/admin.html
Normal file
File diff suppressed because one or more lines are too long
22
app/templates/empty.html
Normal file
22
app/templates/empty.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Setup</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<section class="hero is-fullheight is-info">
|
||||
<div class="hero-body">
|
||||
<div class="">
|
||||
<p class="title">
|
||||
Willkommen!
|
||||
</p>
|
||||
<p class="subtitle">
|
||||
Dein Dashboard ist noch leer.
|
||||
</p>
|
||||
<a href="/admin" class="button is-white">Zum Admin-Bereich</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
642
app/templates/index.html
Normal file
642
app/templates/index.html
Normal file
@@ -0,0 +1,642 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ data.settings.title }}</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--theme-color: {{ data.settings.theme_color }};
|
||||
--header-font-color: {{ data.settings.header_font_color|default('#FFFFFF') }};
|
||||
--header-inactive-font-color: {{ data.settings.header_inactive_font_color|default('#C0C0C0') }};
|
||||
--group-title-font-color: {{ data.settings.group_title_font_color|default('#FFFFFF') }};
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #333; min-height: 100vh;
|
||||
{% if data.settings.bg_image %}
|
||||
background-image: url('/static/uploads/{{ data.settings.bg_image }}');
|
||||
background-size: cover; background-position: center; background-attachment: fixed;
|
||||
{% endif %}
|
||||
}
|
||||
.body-overlay { background-color: rgba(255, 255, 255, {{ data.settings.overlay_opacity }}); min-height: 100vh; }
|
||||
.navbar.custom-header { background-color: {{ data.settings.header_color }}; box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
|
||||
.navbar.custom-header .navbar-item,
|
||||
.navbar.custom-header strong {
|
||||
color: var(--header-font-color);
|
||||
}
|
||||
.navbar.custom-header .navbar-item.has-text-grey-light {
|
||||
color: var(--header-inactive-font-color);
|
||||
}
|
||||
.navbar.custom-header .navbar-start {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.logo-img { max-height: 2.5rem !important; margin-right: 15px; }
|
||||
.navbar-item.is-active {
|
||||
background-color: var(--theme-color) !important;
|
||||
color: var(--header-font-color) !important;
|
||||
padding: 5px 10px !important;
|
||||
margin: 5px 0 !important;
|
||||
}
|
||||
a.navbar-item:hover { background-color: rgba(255,255,255,0.1); color: var(--header-font-color); }
|
||||
|
||||
.navbar-start .navbar-item {
|
||||
min-width: 150px;
|
||||
justify-content: center;
|
||||
padding: 5px 10px !important;
|
||||
margin: 5px 0 !important;
|
||||
}
|
||||
|
||||
#search-dropdown .dropdown-menu {
|
||||
right: 0;
|
||||
left: auto;
|
||||
min-width: 25rem;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: var(--group-title-font-color);
|
||||
text-shadow: 1px 1px 3px rgba(0,0,0,0.8);
|
||||
margin-bottom: 1rem !important;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container { /* Added by Gemini */
|
||||
max-width: 80% !important;
|
||||
width: 80% !important;
|
||||
}
|
||||
|
||||
.group-column {
|
||||
padding: 0.75rem !important; /* Reduced from 1.5rem to fit 3 columns */
|
||||
margin-bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
{% if data.settings.overlay_opacity|float > 0.6 %}
|
||||
.group-title { color: #363636; text-shadow: none; }
|
||||
{% endif %}
|
||||
|
||||
.card {
|
||||
border-radius: 16px; /* More rounded */
|
||||
border: 1px solid rgba(255, 255, 255, 0.25); /* Iced edge */
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.5); /* Highlight top edge */
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
background-color: rgba({{ data.settings.group_bg_color_rgb | default('255,255,255') }}, {{ data.settings.box_glassiness | default(0.5) }});
|
||||
backdrop-filter: blur(15px); /* Stronger blur */
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease; /* Smooth animation */
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.15);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Widget Styles */
|
||||
.widget {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Apply glass effect to widget notifications too if they are used */
|
||||
.widget .notification {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
|
||||
.widgets-sidebar {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.link-item {
|
||||
position: relative;
|
||||
padding: 12px 15px; /* More breathing room */
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05); /* Subtler separator */
|
||||
transition: background-color 0.2s;
|
||||
display: flex; align-items: center; color: #4a4a4a;
|
||||
height: 85px !important;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.link-item:hover {
|
||||
background-color: rgba(255,255,255,0.4) !important; /* Lighter hover for glass feel */
|
||||
color: var(--theme-color);
|
||||
}
|
||||
.link-item:last-child { border-bottom: none; }
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #ccc;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.status-indicator.is-online {
|
||||
background-color: #48c774;
|
||||
}
|
||||
.status-indicator.is-offline {
|
||||
background-color: #f14668;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.8rem; margin-right: 15px; color: var(--theme-color); flex-shrink: 0;
|
||||
}
|
||||
.icon-container img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
||||
|
||||
.link-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
.link-content p.title,
|
||||
.link-content p.subtitle {
|
||||
margin: 0;
|
||||
}
|
||||
.link-content p.title {
|
||||
font-size: 1.1rem; /* Increased font size */
|
||||
color: #363636;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.link-content p.subtitle {
|
||||
font-size: 0.9rem; /* Increased font size */
|
||||
color: #888;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
#clock {
|
||||
font-size: 1.1rem; /* Increased font size */
|
||||
color: var(--header-font-color); /* Ensure it uses the header font color */
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.link-item .link-content p.subtitle {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="body-overlay">
|
||||
<nav class="navbar custom-header" role="navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item" href="#">
|
||||
{% if data.settings.logo_image %}
|
||||
<img src="/static/uploads/{{ data.settings.logo_image }}" class="logo-img">
|
||||
{% endif %}
|
||||
<strong class="is-size-4 has-text-white">{{ data.settings.title }}</strong>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu is-active" style="background: transparent;">
|
||||
<div class="navbar-start">
|
||||
{% if data.pages %}
|
||||
{% for page in data.pages %}
|
||||
<a class="navbar-item {% if loop.index0 == active_index %}is-active has-text-white{% else %}has-text-grey-light{% endif %}"
|
||||
href="/?page={{ loop.index0 }}">
|
||||
{% if page.icon %}
|
||||
{% if '.' in page.icon or '/' in page.icon %}
|
||||
<img src="{{ '/static/uploads/' + page.icon if not 'http' in page.icon else page.icon }}" style="width:20px; margin-right:8px;">
|
||||
{% else %}
|
||||
<i class="{{ page.icon }}" style="width:20px; margin-right:8px; text-align:center;"></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ page.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<span class="navbar-item" id="clock"></span>
|
||||
<div class="navbar-item">
|
||||
<div class="dropdown" id="search-dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<div class="control has-icons-left">
|
||||
<input class="input is-small" type="text" id="search-input" placeholder="Suchen..." aria-haspopup="true" aria-controls="search-results">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="search-results" role="menu">
|
||||
<div class="dropdown-content">
|
||||
<!-- JS will populate this -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a class="navbar-item has-text-white" href="/admin"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container p-4">
|
||||
{% if active_index == 0 %}
|
||||
<div class="columns is-centered mb-5">
|
||||
<div class="column is-half">
|
||||
<form action="https://www.google.com/search" method="GET">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<input class="input is-medium" type="text" name="q" placeholder="Google Suche..." style="border-radius: 20px 0 0 20px; border: 1px solid rgba(255,255,255,0.3); border-right: none; box-shadow: 0 4px 15px rgba(0,0,0,0.1); background-color: rgba(255,255,255,0.7); backdrop-filter: blur(10px); color: #333;">
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-medium" type="submit" style="border-radius: 0 20px 20px 0; border: 1px solid rgba(255,255,255,0.3); border-left: none; box-shadow: 0 4px 15px rgba(0,0,0,0.1); background-color: var(--theme-color); color: white; opacity: 0.9;">
|
||||
<span class="icon"><i class="fas fa-search"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if active_page %}
|
||||
{# Dynamic Layout Logic #}
|
||||
{% set has_widgets = active_page.widgets|length > 0 %}
|
||||
{% set user_cols = data.settings.columns %}
|
||||
|
||||
{# Defaults (No Widgets) #}
|
||||
{% set main_col_width = 'is-12' %}
|
||||
{% set sidebar_width = '' %}
|
||||
{% set item_col_width = user_cols %}
|
||||
|
||||
{% if has_widgets %}
|
||||
{% if user_cols == 'is-one-third-desktop' %} {# 3 Cols -> 2 + 1 #}
|
||||
{% set main_col_width = 'is-two-thirds' %} {# 66% #}
|
||||
{% set sidebar_width = 'is-one-third' %} {# 33% #}
|
||||
{% set item_col_width = 'is-half-desktop' %} {# 50% of 66% = 33% total #}
|
||||
|
||||
{% elif user_cols == 'is-one-quarter-desktop' %} {# 4 Cols -> 3 + 1 #}
|
||||
{% set main_col_width = 'is-three-quarters' %} {# 75% #}
|
||||
{% set sidebar_width = 'is-one-quarter' %} {# 25% #}
|
||||
{% set item_col_width = 'is-one-third-desktop' %}{# 33% of 75% = 25% total #}
|
||||
|
||||
{% elif user_cols == 'is-one-fifth-desktop' %} {# 5 Cols -> 4 + 1 #}
|
||||
{% set main_col_width = 'is-four-fifths' %} {# 80% #}
|
||||
{% set sidebar_width = 'is-one-fifth' %} {# 20% #}
|
||||
{% set item_col_width = 'is-one-quarter-desktop' %}{# 25% of 80% = 20% total #}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="columns is-variable is-4"> <!-- Main Grid -->
|
||||
|
||||
<!-- Link Groups Column -->
|
||||
<div class="column {{ main_col_width }}">
|
||||
<div class="columns is-multiline is-variable is-4">
|
||||
{% for group in active_page.groups %}
|
||||
<div class="column {{ item_col_width }} is-full-mobile group-column">
|
||||
<!-- Remove height:100% to allow auto-height based on content -->
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<h3 class="title is-5 group-title">
|
||||
{% if group.icon %}
|
||||
{% if '.' in group.icon or '/' in group.icon %}
|
||||
<img src="{{ '/static/uploads/' + group.icon if not 'http' in group.icon else group.icon }}" style="height:24px; margin-right:10px;">
|
||||
{% else %}
|
||||
<i class="{{ group.icon }}" style="margin-right:10px;"></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ group.name }}
|
||||
</h3>
|
||||
|
||||
<div class="card" style="flex-grow: 1;">
|
||||
<div class="card-content p-0">
|
||||
{% for link in group.links %}
|
||||
<a href="{{ link.url }}" target="{{ link.target|default('_self') }}" class="link-item">
|
||||
<span class="status-indicator"></span>
|
||||
<div class="icon-container">
|
||||
{% if '.' in link.icon or '/' in link.icon %}
|
||||
<img src="{{ '/static/uploads/' + link.icon if not 'http' in link.icon else link.icon }}">
|
||||
{% else %}
|
||||
<i class="{{ link.icon }}"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="link-content">
|
||||
<p class="title">{{ link.name }}</p>
|
||||
{% if link.subtitle %}
|
||||
<p class="subtitle">{{ link.subtitle }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Widgets Section -->
|
||||
{% if has_widgets %}
|
||||
<div class="column {{ sidebar_width }}">
|
||||
<div style="margin-top: 2.5rem;"> <!-- Align with cards (offsetting the title) -->
|
||||
{% for widget in active_page.widgets %}
|
||||
<div class="widget mb-5" data-widget-id="{{ widget.id }}" data-widget-type="{{ widget.type }}">
|
||||
<div class="notification is-light">
|
||||
Lade Widget...
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Search functionality
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchDropdown = document.getElementById('search-dropdown');
|
||||
const searchResultsContainer = document.querySelector('#search-results .dropdown-content');
|
||||
|
||||
if (searchInput && searchDropdown && searchResultsContainer) {
|
||||
searchInput.addEventListener('keyup', async function(event) {
|
||||
const searchTerm = event.target.value.toLowerCase();
|
||||
|
||||
if (searchTerm.length < 2) {
|
||||
searchDropdown.classList.remove('is-active');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/search?q=${encodeURIComponent(searchTerm)}`);
|
||||
const results = await response.json();
|
||||
|
||||
searchResultsContainer.innerHTML = '';
|
||||
|
||||
if (results.length > 0) {
|
||||
results.forEach(result => {
|
||||
const resultElement = document.createElement('a');
|
||||
resultElement.href = result.url;
|
||||
resultElement.target = '_blank';
|
||||
resultElement.classList.add('dropdown-item');
|
||||
|
||||
let iconHtml = '';
|
||||
if (result.icon) {
|
||||
if (result.icon.includes('.') || result.icon.includes('/')) {
|
||||
const src = result.icon.startsWith('http') ? result.icon : `/static/uploads/${result.icon}`;
|
||||
iconHtml = `<img src="${src}" style="height:24px; width: 24px; margin-right:10px; object-fit: contain;">`;
|
||||
} else {
|
||||
iconHtml = `<i class="${result.icon}" style="width: 24px; margin-right:10px; text-align: center;"></i>`;
|
||||
}
|
||||
} else {
|
||||
iconHtml = `<span style="height:24px; width: 24px; margin-right:10px;"></span>`;
|
||||
}
|
||||
|
||||
resultElement.innerHTML = `
|
||||
<div style="display: flex; align-items: center;">
|
||||
${iconHtml}
|
||||
<div>
|
||||
<p style="font-weight: 600;">${result.name}</p>
|
||||
<p style="font-size: 0.8rem; color: #888;">${result.subtitle || ''}</p>
|
||||
<p style="font-size: 0.7rem; color: #aaa;">Seite: ${result.page_name} / Gruppe: ${result.group_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
searchResultsContainer.appendChild(resultElement);
|
||||
});
|
||||
} else {
|
||||
const noResult = document.createElement('div');
|
||||
noResult.classList.add('dropdown-item');
|
||||
noResult.textContent = 'Keine Ergebnisse gefunden.';
|
||||
searchResultsContainer.appendChild(noResult);
|
||||
}
|
||||
searchDropdown.classList.add('is-active');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!searchDropdown.contains(event.target)) {
|
||||
searchDropdown.classList.remove('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Link Status Checker
|
||||
const allLinks = document.querySelectorAll('.link-item');
|
||||
|
||||
allLinks.forEach(link => {
|
||||
const url = link.href;
|
||||
const indicator = link.querySelector('.status-indicator');
|
||||
if (!indicator) return;
|
||||
|
||||
if (!url || !url.startsWith('http')) {
|
||||
indicator.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/check_url?url=${encodeURIComponent(url)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'online') {
|
||||
indicator.classList.add('is-online');
|
||||
} else {
|
||||
indicator.classList.add('is-offline');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
indicator.classList.add('is-offline');
|
||||
});
|
||||
});
|
||||
|
||||
// Clock
|
||||
const clockElement = document.getElementById('clock');
|
||||
function updateClock() {
|
||||
if (clockElement) {
|
||||
const now = new Date();
|
||||
clockElement.textContent = now.toLocaleTimeString('de-DE');
|
||||
}
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock(); // initial call
|
||||
|
||||
|
||||
// --- Widget Rendering ---
|
||||
const renderPiholeStatus = (widgetElement, data) => {
|
||||
if (data.error) {
|
||||
widgetElement.innerHTML = `<div class="notification is-danger"><strong>Pi-hole Error:</strong> ${data.error}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const statusClass = data.status === 'enabled' ? 'is-success' : 'is-warning';
|
||||
|
||||
widgetElement.innerHTML = `
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
<span class="icon"><i class="fas fa-chart-bar"></i></span>
|
||||
Pi-hole Status
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Status</p>
|
||||
<p class="title is-4"><span class="tag ${statusClass} is-medium">${data.status}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Blocked</p>
|
||||
<p class="title is-4">${parseFloat(data.ads_percentage_today || 0).toLocaleString('de-DE')}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Queries</p>
|
||||
<p class="title is-5">${parseInt(data.dns_queries_today || 0).toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading">Blocklist</p>
|
||||
<p class="title is-5">${parseInt(data.domains_being_blocked || 0).toLocaleString('de-DE')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderOpenMeteoWeather = (widgetElement, data) => {
|
||||
if (data.error) {
|
||||
widgetElement.innerHTML = `<div class="notification is-danger"><strong>Weather Error:</strong> ${data.error}</div>`;
|
||||
return;
|
||||
}
|
||||
const current = data.current || {};
|
||||
const daily = data.daily || {};
|
||||
const units = data.units || {};
|
||||
|
||||
const temp = current.temperature_2m;
|
||||
const wind = current.wind_speed_10m;
|
||||
const code = current.weather_code;
|
||||
|
||||
// Simple icon mapping (very basic)
|
||||
let iconClass = 'fa-sun';
|
||||
if (code > 3) iconClass = 'fa-cloud-sun';
|
||||
if (code > 45) iconClass = 'fa-cloud-rain';
|
||||
if (code > 70) iconClass = 'fa-snowflake';
|
||||
if (code > 95) iconClass = 'fa-bolt';
|
||||
|
||||
const sunrise = daily.sunrise && daily.sunrise[0] ? daily.sunrise[0].split('T')[1] : '--:--';
|
||||
const sunset = daily.sunset && daily.sunset[0] ? daily.sunset[0].split('T')[1] : '--:--';
|
||||
|
||||
widgetElement.innerHTML = `
|
||||
<div class="card"> <!-- Use standard glass card -->
|
||||
<div class="card-content">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<p class="heading" style="color: #888;">Wetter</p>
|
||||
<p class="title is-3" style="color: var(--theme-color);">${temp} ${units.temperature_2m}</p>
|
||||
<p class="subtitle is-6" style="color: #666;"><i class="fas fa-wind mr-1"></i> ${wind} ${units.wind_speed_10m}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right has-text-centered">
|
||||
<div>
|
||||
<i class="fas ${iconClass} fa-3x" style="color: var(--theme-color); opacity: 0.8;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level is-mobile mt-2" style="border-top: 1px solid rgba(0,0,0,0.05); padding-top: 10px;">
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading" style="color: #aaa; margin-bottom: 0;">Aufgang</p>
|
||||
<p class="is-size-6" style="color: #555;"><i class="fas fa-sun mr-1" style="color: #FFD700;"></i> ${sunrise}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-item has-text-centered">
|
||||
<div>
|
||||
<p class="heading" style="color: #aaa; margin-bottom: 0;">Untergang</p>
|
||||
<p class="is-size-6" style="color: #555;"><i class="fas fa-moon mr-1" style="color: #666;"></i> ${sunset}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderIframeWidget = (widgetElement, data) => {
|
||||
const height = data.height || '300';
|
||||
widgetElement.innerHTML = `
|
||||
<div class="card" style="overflow: hidden;">
|
||||
<iframe src="${data.url}" style="width: 100%; height: ${height}px; border: none;"></iframe>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const WIDGET_RENDERERS = {
|
||||
'pihole_status': renderPiholeStatus,
|
||||
'openmeteo_weather': renderOpenMeteoWeather,
|
||||
'iframe_widget': renderIframeWidget
|
||||
};
|
||||
|
||||
// Fetch and render widgets
|
||||
document.querySelectorAll('.widget').forEach(widgetElement => {
|
||||
const widgetId = widgetElement.dataset.widgetId;
|
||||
const widgetType = widgetElement.dataset.widgetType;
|
||||
const renderer = WIDGET_RENDERERS[widgetType];
|
||||
|
||||
if (!widgetId || !renderer) {
|
||||
widgetElement.innerHTML = '<div class="notification is-danger">Widget-Konfigurationsfehler.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/widget_data/${widgetId}`)
|
||||
.then(response => {
|
||||
// Always parse the JSON to get access to the body, regardless of status
|
||||
return response.json().then(data => {
|
||||
if (!response.ok) {
|
||||
// If response is not ok, pass the JSON data to the catch block
|
||||
return Promise.reject(data);
|
||||
}
|
||||
// If response is ok, pass the JSON data to the next .then()
|
||||
return data;
|
||||
});
|
||||
})
|
||||
.then(data => {
|
||||
// This is the success handler
|
||||
renderer(widgetElement, data);
|
||||
})
|
||||
.catch(errorData => {
|
||||
// errorData is now the JSON object from the server
|
||||
console.error('Error fetching widget data:', errorData);
|
||||
|
||||
let errorMessage = 'Ein unbekannter Fehler ist aufgetreten.';
|
||||
let debugInfo = '';
|
||||
|
||||
if (errorData && errorData.error) {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
|
||||
if (errorData && errorData.debug) {
|
||||
debugInfo = `<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow-y: auto; margin-top: 10px; background-color: #f5f5f5; padding: 5px; color: #333; font-size: 0.75rem;">${errorData.debug.join('\n')}</pre>`;
|
||||
}
|
||||
|
||||
widgetElement.innerHTML = `<div class="notification is-danger"><strong>Fehler:</strong> ${errorMessage}${debugInfo}</div>`;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
52
app/templates/login.html
Normal file
52
app/templates/login.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
<h1 class="title">Admin Login</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="notification is-{{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('login', next=request.args.get('next')) }}">
|
||||
<div class="field">
|
||||
<label class="label">Benutzername</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="username" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Passwort</label>
|
||||
<div class="control">
|
||||
<input class="input" type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button class="button is-primary" type="submit">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
23
app/templates/media.html
Normal file
23
app/templates/media.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<tbody id="mediaTableBody">
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td style="width: 50px;">
|
||||
{% if file.type == 'Icon' or file.type == 'Hintergrund' or file.type == 'Logo' %}
|
||||
<img src="/static/uploads/{{ file.name }}" style="max-height: 30px;">
|
||||
{% else %}
|
||||
<i class="fas fa-file"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ file.name }}</td>
|
||||
<td>{{ file.size }}</td>
|
||||
<td>{{ file.type }}</td>
|
||||
<td>
|
||||
<a href="/admin/delete_media/{{ file.name }}" class="button is-danger is-small" onclick="return confirm('Sicher?')">Löschen</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="has-text-centered">Keine Dateien vorhanden.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
Reference in New Issue
Block a user