Release 1.0.0 with server info console
This commit is contained in:
107
static/app.js
107
static/app.js
@@ -39,6 +39,7 @@ createApp({
|
||||
isAdmin: false,
|
||||
isMaster: false,
|
||||
authName: "",
|
||||
serverInfo: null,
|
||||
groups: {},
|
||||
devices: [],
|
||||
sliders: {},
|
||||
@@ -86,6 +87,7 @@ createApp({
|
||||
this.isAdmin = false;
|
||||
this.isMaster = false;
|
||||
this.authName = "";
|
||||
this.serverInfo = null;
|
||||
this.groups = {};
|
||||
this.devices = [];
|
||||
this.sliders = {};
|
||||
@@ -115,6 +117,104 @@ createApp({
|
||||
const group = this.groups[targetId];
|
||||
return group ? group.name : null;
|
||||
},
|
||||
serverDisplayName() {
|
||||
if (this.serverInfo?.instance_name) {
|
||||
return this.serverInfo.instance_name;
|
||||
}
|
||||
return this.serverInfo?.app_name || "Ignis Core";
|
||||
},
|
||||
serverDisplaySubtitle() {
|
||||
if (this.serverInfo?.instance_name) {
|
||||
return this.serverInfo.app_name || "Ignis Core";
|
||||
}
|
||||
if (this.serverInfo?.diagnostics_visible) {
|
||||
return "Подключение активно и готово к управлению.";
|
||||
}
|
||||
return "Подключение активно. Операционная диагностика скрыта для гостевого доступа.";
|
||||
},
|
||||
shortSha(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value.length <= 7 ? value : value.slice(0, 7);
|
||||
},
|
||||
formatServerTimestamp(iso) {
|
||||
if (!iso) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const parsed = new Date(iso);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return iso;
|
||||
}
|
||||
|
||||
const pad = (value) => String(value).padStart(2, "0");
|
||||
return `${parsed.getUTCFullYear()}-${pad(parsed.getUTCMonth() + 1)}-${pad(parsed.getUTCDate())} ${pad(parsed.getUTCHours())}:${pad(parsed.getUTCMinutes())} UTC`;
|
||||
},
|
||||
formatServerBuild(build) {
|
||||
if (!build) {
|
||||
return "build info unavailable";
|
||||
}
|
||||
|
||||
const parts = [`v${build.version}`];
|
||||
const shortSha = this.shortSha(build.git_sha);
|
||||
if (shortSha) {
|
||||
parts.push(shortSha);
|
||||
}
|
||||
if (build.build_date) {
|
||||
parts.push(this.formatServerTimestamp(build.build_date));
|
||||
}
|
||||
return parts.join(" · ");
|
||||
},
|
||||
formatDuration(totalSeconds, maxParts = 2) {
|
||||
const seconds = Math.max(Number(totalSeconds || 0), 0);
|
||||
if (seconds < 60) {
|
||||
return "меньше минуты";
|
||||
}
|
||||
|
||||
const units = [
|
||||
{ size: 86400, forms: ["день", "дня", "дней"] },
|
||||
{ size: 3600, forms: ["час", "часа", "часов"] },
|
||||
{ size: 60, forms: ["минута", "минуты", "минут"] },
|
||||
];
|
||||
let remaining = seconds;
|
||||
const parts = [];
|
||||
|
||||
units.forEach((unit) => {
|
||||
if (parts.length >= maxParts) {
|
||||
return;
|
||||
}
|
||||
const value = Math.floor(remaining / unit.size);
|
||||
if (value <= 0) {
|
||||
return;
|
||||
}
|
||||
parts.push(`${value} ${this.pluralRu(value, ...unit.forms)}`);
|
||||
remaining -= value * unit.size;
|
||||
});
|
||||
|
||||
return parts.join(" ");
|
||||
},
|
||||
formatUptime(totalSeconds) {
|
||||
return this.formatDuration(totalSeconds, 2);
|
||||
},
|
||||
formatRelativeUptime(totalSeconds) {
|
||||
const seconds = Math.max(Number(totalSeconds || 0), 0);
|
||||
if (seconds < 60) {
|
||||
return "только что";
|
||||
}
|
||||
return `${this.formatDuration(seconds, 2)} назад`;
|
||||
},
|
||||
pluralRu(count, one, few, many) {
|
||||
const mod10 = count % 10;
|
||||
const mod100 = count % 100;
|
||||
if (mod10 === 1 && mod100 !== 11) {
|
||||
return one;
|
||||
}
|
||||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) {
|
||||
return few;
|
||||
}
|
||||
return many;
|
||||
},
|
||||
async request(path, { method = "GET", query = null, body = null } = {}) {
|
||||
let url = path;
|
||||
if (query) {
|
||||
@@ -185,12 +285,17 @@ createApp({
|
||||
|
||||
this.isFetching = true;
|
||||
try {
|
||||
const [groupsData, devicesData, scenesData] = await Promise.all([
|
||||
const [serverInfoData, groupsData, devicesData, scenesData] = await Promise.all([
|
||||
this.request("/system/info"),
|
||||
this.request("/devices/groups"),
|
||||
this.request("/devices"),
|
||||
this.request("/devices/scenes"),
|
||||
]);
|
||||
|
||||
if (serverInfoData) {
|
||||
this.serverInfo = serverInfoData;
|
||||
}
|
||||
|
||||
if (groupsData) {
|
||||
this.groups = groupsData;
|
||||
Object.keys(this.groups).forEach((id) => {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
</div>
|
||||
<nav class="flex glass p-1 rounded-xl">
|
||||
<button @click="tab = 'control'" :class="tab === 'control' ? 'active-tab text-white' : 'text-slate-500 hover:text-slate-300'" class="px-6 py-2 rounded-lg font-bold text-sm transition-all">ПУЛЬТ</button>
|
||||
<button @click="tab = 'server'" :class="tab === 'server' ? 'active-tab text-white' : 'text-slate-500 hover:text-slate-300'" class="px-6 py-2 rounded-lg font-bold text-sm transition-all">СЕРВЕР</button>
|
||||
<button v-if="isAdmin" @click="tab = 'schedules'" :class="tab === 'schedules' ? 'active-tab text-white' : 'text-slate-500 hover:text-slate-300'" class="px-6 py-2 rounded-lg font-bold text-sm transition-all">ГРАФИК</button>
|
||||
<button v-if="isAdmin" @click="tab = 'admin'" :class="tab === 'admin' ? 'active-tab text-white' : 'text-slate-500 hover:text-slate-300'" class="px-6 py-2 rounded-lg font-bold text-sm transition-all">АДМИНКА</button>
|
||||
<button v-if="isAdmin" @click="tab = 'stats'; fetchStats()" :class="tab === 'stats' ? 'active-tab text-white' : 'text-slate-500 hover:text-slate-300'" class="px-6 py-2 rounded-lg font-bold text-sm transition-all">СТАТА</button>
|
||||
@@ -60,6 +61,96 @@
|
||||
<div class="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full spinner"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="tab === 'server' && !isLoading" class="space-y-8 fade-up">
|
||||
<section v-if="serverInfo" class="glass p-6 rounded-2xl">
|
||||
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.2em] text-slate-600 mb-2">О сервере</div>
|
||||
<h2 class="text-2xl font-black tracking-tight">{{ serverDisplayName() }}</h2>
|
||||
<div class="text-sm text-slate-500 mt-1">{{ serverDisplaySubtitle() }}</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-[10px]">
|
||||
<span class="border px-2.5 py-1 rounded-lg font-bold uppercase text-green-300 border-green-500/30 bg-green-500/10">
|
||||
онлайн
|
||||
</span>
|
||||
<span :class="isAdmin ? 'text-blue-300 border-blue-500/30 bg-blue-500/10' : 'text-slate-300 border-slate-700/60 bg-black/20'" class="border px-2.5 py-1 rounded-lg font-bold uppercase">
|
||||
{{ isAdmin ? 'админ-доступ' : 'гостевой доступ' }}
|
||||
</span>
|
||||
<span v-if="serverInfo.diagnostics_visible" class="border px-2.5 py-1 rounded-lg font-bold uppercase text-orange-300 border-orange-500/30 bg-orange-500/10">
|
||||
расширенная диагностика
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mt-6">
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Статус</div>
|
||||
<div class="text-sm font-bold">Подключено</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">API отвечает и готов к управлению.</div>
|
||||
</div>
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Запущен</div>
|
||||
<div class="text-sm font-bold">{{ formatRelativeUptime(serverInfo.uptime_seconds) }}</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">
|
||||
{{ serverInfo.started_at ? formatServerTimestamp(serverInfo.started_at) : `аптайм ${formatUptime(serverInfo.uptime_seconds)}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Доступ</div>
|
||||
<div class="text-sm font-bold">{{ isMaster ? 'Мастер' : (isAdmin ? 'Администратор' : 'Гостевой') }}</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">
|
||||
{{ isAdmin ? 'Служебная диагностика доступна на этой вкладке.' : 'Служебные параметры и адреса скрыты.' }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="serverInfo.diagnostics_visible" class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Версия</div>
|
||||
<div class="text-sm font-bold">{{ serverInfo.build?.version ? `v${serverInfo.build.version}` : 'не определена' }}</div>
|
||||
<div class="text-[11px] text-slate-500 mt-1">{{ serverInfo.timezone || 'таймзона не указана' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="serverInfo?.diagnostics_visible" class="glass p-6 rounded-2xl">
|
||||
<h2 class="text-lg font-black uppercase mb-5">Диагностика</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Сборка</div>
|
||||
<div class="text-sm font-bold">{{ formatServerBuild(serverInfo.build) }}</div>
|
||||
</div>
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Таймзона</div>
|
||||
<div class="text-sm font-bold">{{ serverInfo.timezone || 'не указана' }}</div>
|
||||
</div>
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Публичный URL</div>
|
||||
<div class="text-xs mono break-all text-slate-300">
|
||||
{{ serverInfo.urls?.effective_public_base_url || 'не определён' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-black/20 border border-slate-800/50 rounded-xl p-4">
|
||||
<div class="text-[10px] uppercase text-slate-600 mb-2">Наблюдаемый URL</div>
|
||||
<div class="text-xs mono break-all text-slate-300">
|
||||
{{ serverInfo.urls?.observed_base_url || 'не определён' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-[10px] mt-5">
|
||||
<span :class="serverInfo.configuration?.configured ? 'text-green-300 border-green-500/30 bg-green-500/10' : 'text-red-300 border-red-500/30 bg-red-500/10'" class="border px-2.5 py-1 rounded-lg font-bold uppercase">
|
||||
{{ serverInfo.configuration?.configured ? 'настроен' : 'требует настройки' }}
|
||||
</span>
|
||||
<span :class="serverInfo.configuration?.public_base_url_configured ? 'text-blue-300 border-blue-500/30 bg-blue-500/10' : 'text-slate-400 border-slate-700/60 bg-black/20'" class="border px-2.5 py-1 rounded-lg font-bold uppercase">
|
||||
{{ serverInfo.configuration?.public_base_url_configured ? 'public url задан' : 'public url не задан' }}
|
||||
</span>
|
||||
<span :class="serverInfo.configuration?.scan_network_configured ? 'text-cyan-300 border-cyan-500/30 bg-cyan-500/10' : 'text-slate-400 border-slate-700/60 bg-black/20'" class="border px-2.5 py-1 rounded-lg font-bold uppercase">
|
||||
{{ serverInfo.configuration?.scan_network_configured ? 'scan network задан' : 'scan network auto' }}
|
||||
</span>
|
||||
<span :class="serverInfo.configuration?.build_metadata_complete ? 'text-orange-300 border-orange-500/30 bg-orange-500/10' : 'text-slate-400 border-slate-700/60 bg-black/20'" class="border px-2.5 py-1 rounded-lg font-bold uppercase">
|
||||
{{ serverInfo.configuration?.build_metadata_complete ? 'build metadata полная' : 'build metadata частичная' }}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ПУЛЬТ -->
|
||||
<div v-if="tab === 'control' && !isLoading" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div v-if="Object.keys(groups).length === 0" class="col-span-full text-center py-20 glass rounded-2xl">
|
||||
|
||||
Reference in New Issue
Block a user