homepage/templates/index.html

248 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Домашняя страница</title>
<link rel="icon" href="/static/favicon.ico">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="/static/main.289a6408.css">
<link rel="stylesheet" href="/static/flame.css">
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
</head>
<body style="--color-primary: #FFFDEA; --color-accent: #5c5c5c; --color-background: #1a1a1a;">
<div id="root">
<div class="Layout_Container__HIHX7">
<div style="display:flex; flex-direction:row; align-items:center; gap:18px; margin-bottom:12px; width:100%;">
<form id="google-search" style="display:flex; align-items:center; gap:6px; flex:1 1 0;" onsubmit="event.preventDefault(); window.open('https://www.google.com/search?q='+encodeURIComponent(this.elements.q.value),'_blank');">
<img src="https://www.google.com/favicon.ico" alt="Google" width="24" height="24"/>
<input type="text" name="q" class="SearchBar_SearchBar__MQiwu" placeholder="Поиск Google..." autocomplete="off" style="width:100%; min-width:0; font-size:1.2em;"/>
</form>
<form id="yandex-search" style="display:flex; align-items:center; gap:6px; flex:1 1 0;" onsubmit="event.preventDefault(); window.open('https://yandex.ru/search/?text='+encodeURIComponent(this.elements.q.value),'_blank');">
<img src="https://yandex.ru/favicon.ico" alt="Yandex" width="24" height="24"/>
<input type="text" name="q" class="SearchBar_SearchBar__MQiwu" placeholder="Поиск Яндекс..." autocomplete="off" style="width:100%; min-width:0; font-size:1.2em;"/>
</form>
</div>
<header class="Header_Header__GCJdR">
<span class="Header_HeaderMain__oLoBB">
<div style="display:flex; flex-direction:row; align-items:center; gap:12px;">
<span style="font-size:1.6em; color: var(--color-primary);">{{ current_time }}</span>
<span style="font-size:1.6em; color: var(--color-primary);">{{ now }}</span>
<span id="services-status-indicator" title="Статус сервисов" style="display:inline-block; width:18px; height:18px; border-radius:4px; margin-left:8px; background:#888; border:1.5px solid #444;"></span>
</div>
<div class="WeatherWidget_WeatherWidget__3XlYt" id="weather-area" style="cursor:pointer;">
<div>
{% if weather and weather.icon %}
<img src="{{ weather.icon if not weather.icon.startswith('//') else 'https:' + weather.icon }}" width="50" height="50" alt="icon"/>
{% else %}
<canvas id="weather-icon" width="50" height="50"></canvas>
{% endif %}
</div>
<div class="WeatherWidget_WeatherDetails__1y3CA">
{% if weather %}
<span>{{ weather.temp }}°C</span>
<span>{{ weather.cloud }}% облачность</span>
{% else %}
<span>--°C</span>
<span>--% облачность</span>
{% endif %}
</div>
</div>
</span>
</header>
<a href="/applications"><h2 class="SectionHeadline_SectionHeadline__xa0oJ">Applications</h2></a>
<div class="AppGrid_AppGrid__ZxiaC">
{% for app in applications %}
<a href="{{ app.url }}" target="_blank" rel="noreferrer" class="AppCard_AppCard__NPTM5">
<div class="AppCard_AppCardIcon__ThrUl">
<i data-feather="{{ app.icon_name }}" class="AppCard_CustomIcon__+Tc3I" width="32" height="32"></i>
</div>
<div class="AppCard_AppCardDetails__HgNoY">
<h5>{{ app.name }}</h5>
<span>{{ app.description }}</span>
</div>
</a>
{% endfor %}
</div>
<div class="Home_HomeSpace__--hVj"></div>
<a href="/bookmarks"><h2 class="SectionHeadline_SectionHeadline__xa0oJ">Bookmarks</h2></a>
<div class="BookmarkGrid_BookmarkGrid__i3Tj4">
{% for group in bookmarks %}
<div class="BookmarkCard_BookmarkCard__ooGTe">
<h3 class="">{{ group.group }}</h3>
<div class="BookmarkCard_Bookmarks__alLCe">
{% for link in group.links %}
<a href="{{ link.url }}" target="_blank" rel="noreferrer">
<div class="BookmarkCard_BookmarkIcon__yWkUU">
<i data-feather="{{ link.icon_name }}" class="BookmarkCard_CustomIcon__D7Xys" width="16" height="16"></i>
</div>{{ link.name }}
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<a class="Home_SettingsButton__DrUPz" href="#" id="edit-config-btn">
<svg viewBox="0 0 24 24" role="presentation" class="Icon_Icon__jzcrU"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" style="fill: var(--color-background);"></path></svg>
<span style="margin-left:8px;">Редактировать настройки</span>
</a>
</div>
<div id="config-modal" style="display:none; position:fixed; z-index:1000; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); align-items:center; justify-content:center;">
<div id="config-modal-content" style="background:#fff; color:#222; padding:24px; border-radius:8px; min-width:320px; max-width:90vw; max-height:90vh; display:flex; flex-direction:column; gap:12px; resize:both; overflow:auto; width:60vw; height:60vh;">
<h2>Редактировать config.yaml</h2>
<div style="flex:1 1 auto; display:flex; flex-direction:column; min-height:200px;">
<div id="monaco-editor" style="flex:1 1 auto; min-width:300px; min-height:200px; max-width:100%; max-height:100%; height:100%; border:1px solid #ccc;"></div>
</div>
<div style="display:flex; gap:16px; margin-top:8px;">
<button id="save-config-btn" style="padding:10px 24px; font-size:1.1em;">Сохранить</button>
<button id="cancel-config-btn" style="padding:10px 24px; font-size:1.1em;">Отмена</button>
</div>
<div id="config-error" style="color:red;"></div>
</div>
</div>
<div id="forecast-modal" style="display:none; position:fixed; z-index:2000; left:0; top:0; width:100vw; height:100vh; background:rgba(0,0,0,0.7); align-items:center; justify-content:center;">
<div style="background:#fff; color:#222; padding:24px; border-radius:8px; min-width:320px; max-width:90vw; max-height:90vh; display:flex; flex-direction:column; gap:12px;">
<h2 id="forecast-title">Прогноз</h2>
<div id="forecast-content" style="overflow-y:auto; max-height:60vh;"></div>
<button id="close-forecast-btn" style="padding:10px 24px; font-size:1.1em; align-self:flex-end;">Закрыть</button>
</div>
</div>
<div class="NotificationCenter_NotificationCenter__Tfgzh" style="height: 0px;"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
<script>feather.replace()</script>
<script>
let monacoEditor;
const editBtn = document.getElementById('edit-config-btn');
const modal = document.getElementById('config-modal');
const modalContent = document.getElementById('config-modal-content');
const saveBtn = document.getElementById('save-config-btn');
const cancelBtn = document.getElementById('cancel-config-btn');
const errorDiv = document.getElementById('config-error');
function resizeMonaco() {
if (monacoEditor) {
const editorDiv = document.getElementById('monaco-editor');
const rect = editorDiv.getBoundingClientRect();
monacoEditor.layout({ width: rect.width, height: rect.height });
}
}
// Drag/resize поддерживается через CSS (resize:both)
editBtn.onclick = function(e) {
e.preventDefault();
fetch('/api/config').then(r => r.json()).then(data => {
errorDiv.textContent = '';
modal.style.display = 'flex';
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
require(['vs/editor/editor.main'], function () {
if (monacoEditor) {
monacoEditor.setValue(data.content);
setTimeout(resizeMonaco, 100);
} else {
monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
value: data.content,
language: 'yaml',
theme: 'vs-dark',
automaticLayout: false,
minimap: { enabled: false },
});
setTimeout(resizeMonaco, 100);
}
});
});
};
cancelBtn.onclick = function() {
modal.style.display = 'none';
};
saveBtn.onclick = function() {
const yamlText = monacoEditor.getValue();
if (!window.jsyaml || typeof window.jsyaml.load !== 'function') {
errorDiv.textContent = 'Ошибка: js-yaml не загружен. Проверьте подключение скрипта.';
return;
}
try {
window.jsyaml.load(yamlText); // Проверка синтаксиса
fetch('/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content: yamlText})
}).then(r => r.json().then(data => ({ok: r.ok, data}))).then(res => {
if(res.ok) {
modal.style.display = 'none';
location.reload();
} else {
errorDiv.textContent = res.data.error || 'Ошибка сохранения';
}
});
} catch (e) {
errorDiv.textContent = 'Ошибка YAML: ' + e.message;
}
};
// Следим за изменением размера окна редактора
let resizeObserver = new ResizeObserver(() => {
resizeMonaco();
});
resizeObserver.observe(document.getElementById('config-modal-content'));
const weatherArea = document.getElementById('weather-area');
const forecastModal = document.getElementById('forecast-modal');
const forecastContent = document.getElementById('forecast-content');
const closeForecastBtn = document.getElementById('close-forecast-btn');
const forecastTitle = document.getElementById('forecast-title');
weatherArea.onclick = function() {
forecastContent.innerHTML = 'Загрузка...';
forecastModal.style.display = 'flex';
fetch('/api/weather-forecast').then(r => r.json()).then(data => {
if(data.error) {
forecastContent.innerHTML = '<span style="color:red;">'+data.error+'</span>';
forecastTitle.textContent = 'Прогноз';
return;
}
let days = data.forecast.length;
forecastTitle.textContent = `Прогноз на ${days} ${days === 1 ? 'день' : (days < 5 ? 'дня' : 'дней')}`;
let html = '<table style="width:100%; border-collapse:collapse;">';
html += '<tr><th style="text-align:left; padding:4px;">Дата</th><th style="text-align:left; padding:4px;">Температура</th><th style="text-align:left; padding:4px;">Облачность</th><th style="text-align:left; padding:4px;">Состояние</th><th></th></tr>';
for(const day of data.forecast) {
html += `<tr><td style="padding:4px;">${day.date}</td><td style="padding:4px;">${day.temp}°C</td><td style="padding:4px;">${day.cloud}%</td><td style="padding:4px;">${day.text||''}</td><td>${day.icon ? `<img src='${day.icon.startsWith('//') ? 'https:' + day.icon : day.icon}' width='32' height='32'/>` : ''}</td></tr>`;
}
html += '</table>';
forecastContent.innerHTML = html;
}).catch(() => {
forecastContent.innerHTML = '<span style="color:red;">Ошибка загрузки прогноза</span>';
forecastTitle.textContent = 'Прогноз';
});
};
closeForecastBtn.onclick = function() {
forecastModal.style.display = 'none';
};
window.addEventListener('DOMContentLoaded', function() {
const indicator = document.getElementById('services-status-indicator');
if (indicator) {
fetch('/api/services-status').then(r => r.json()).then(data => {
if (data.all_services_up === true) {
indicator.style.background = '#2ecc40';
indicator.title = 'Все сервисы доступны';
} else if (data.all_services_up === false) {
indicator.style.background = '#ff4136';
indicator.title = 'Недоступен: ' + (data.failed_service || 'неизвестно');
} else {
indicator.style.background = '#888';
indicator.title = 'Статус сервисов неизвестен';
}
}).catch(() => {
indicator.style.background = '#888';
indicator.title = 'Ошибка проверки сервисов';
});
}
});
</script>
</body>
</html>