Добавлена поддержка WebSocket
Этот коммит содержится в:
родитель
98d90b1cf0
Коммит
cce8c81e29
@ -17,6 +17,13 @@ server {
|
|||||||
proxy_pass http://cit.csasq.ru;
|
proxy_pass http://cit.csasq.ru;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /ws {
|
||||||
|
include /opt/cit-is-bot/nginx/secure-headers.conf;
|
||||||
|
include ./conf.d/ws-proxy.conf;
|
||||||
|
|
||||||
|
proxy_pass http://cit.csasq.ru;
|
||||||
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
include /opt/cit-is-bot/nginx/secure-headers.conf;
|
include /opt/cit-is-bot/nginx/secure-headers.conf;
|
||||||
include ./conf.d/gzip.conf;
|
include ./conf.d/gzip.conf;
|
||||||
|
@ -6,3 +6,4 @@ psycopg[binary]
|
|||||||
pydantic
|
pydantic
|
||||||
redis
|
redis
|
||||||
uvicorn
|
uvicorn
|
||||||
|
websockets
|
||||||
|
@ -1,42 +1,97 @@
|
|||||||
class Menu {
|
// class Menu {
|
||||||
#application;
|
// #application;
|
||||||
#root;
|
// #root;
|
||||||
#items;
|
// #items;
|
||||||
|
//
|
||||||
|
// constructor(application) {
|
||||||
|
// this.#application = application;
|
||||||
|
// this.#root = document.querySelector('#menu');
|
||||||
|
// this.#items = this.#root.querySelectorAll('input[data-page]');
|
||||||
|
// this.#items.forEach(item => {
|
||||||
|
// item.addEventListener('click', () => this.#application.open(item.dataset.page));
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// setActive(pageId) {
|
||||||
|
// this.#items.forEach(item => item.classList.remove('active'));
|
||||||
|
// this.#items.forEach(item => item.dataset.page === pageId ? item.classList.add('active') : null);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// class Application {
|
||||||
|
// static DEFAULT_PAGE = 'dashboard';
|
||||||
|
//
|
||||||
|
// #inputContainer;
|
||||||
|
// #outputContainer;
|
||||||
|
// #menu;
|
||||||
|
// #pageId;
|
||||||
|
//
|
||||||
|
// constructor() {
|
||||||
|
// this.#inputContainer = document.querySelector('#input');
|
||||||
|
// this.#outputContainer = document.querySelector('#output');
|
||||||
|
// this.#menu = new Menu(this);
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// open(pageId) {
|
||||||
|
// this.#pageId = pageId;
|
||||||
|
// document.querySelectorAll('#input [data-page]').forEach(page => page.classList.remove('active'));
|
||||||
|
// document.querySelector(`#input [data-page="${pageId}"]`).classList.add('active');
|
||||||
|
// document.querySelectorAll('#output [data-page]').forEach(page => page.classList.remove('active'));
|
||||||
|
// this.#menu.setActive(pageId);
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
constructor(application) {
|
// const mutableStateOf = {
|
||||||
this.#application = application;
|
// _value: null,
|
||||||
this.#root = document.querySelector('#menu');
|
// constructor(value) {
|
||||||
this.#items = this.#root.querySelectorAll('input[data-page]');
|
// this._value = value
|
||||||
this.#items.forEach(item => {
|
// },
|
||||||
item.addEventListener('click', () => this.#application.open(item.dataset.page));
|
// get value() {
|
||||||
});
|
// return this._value
|
||||||
};
|
// },
|
||||||
|
// set value(value) {
|
||||||
|
// this._value = value
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
const connectWebSocket = (url) => {
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
|
||||||
setActive(pageId) {
|
|
||||||
this.#items.forEach(item => item.classList.remove('active'));
|
|
||||||
this.#items.forEach(item => item.dataset.page === pageId ? item.classList.add('active') : null);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Application {
|
ws.onmessage = (event) => {
|
||||||
static DEFAULT_PAGE = 'dashboard';
|
const data = JSON.parse(event.data)
|
||||||
|
document.querySelectorAll(`#${data.id}`).forEach(element => element.toggleAttribute('selected', data.value))
|
||||||
#inputContainer;
|
|
||||||
#outputContainer;
|
|
||||||
#menu;
|
|
||||||
#pageId;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.#inputContainer = document.querySelector('#input');
|
|
||||||
this.#outputContainer = document.querySelector('#output');
|
|
||||||
this.#menu = new Menu(this);
|
|
||||||
};
|
|
||||||
|
|
||||||
open(pageId) {
|
|
||||||
this.#pageId = pageId;
|
|
||||||
document.querySelectorAll('#input [data-page]').forEach(page => page.classList.remove('active'));
|
|
||||||
document.querySelector(`#input [data-page="${pageId}"]`).classList.add('active');
|
|
||||||
document.querySelectorAll('#output [data-page]').forEach(page => page.classList.remove('active'));
|
|
||||||
this.#menu.setActive(pageId);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = new URL(location)
|
||||||
|
wsUrl.protocol = 'wss'
|
||||||
|
wsUrl.pathname = '/ws'
|
||||||
|
const ws = connectWebSocket(wsUrl)
|
||||||
|
|
||||||
|
function getSwitch(id) {
|
||||||
|
const element = document.createElement('md-switch')
|
||||||
|
element.id = id
|
||||||
|
element.onclick = (event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
const data = JSON.stringify({
|
||||||
|
id: id,
|
||||||
|
value: !event.currentTarget.hasAttribute('selected'),
|
||||||
|
})
|
||||||
|
ws.send(data)
|
||||||
|
}
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.body.querySelector('main').append(getSwitch('qwerty'))
|
||||||
|
})
|
||||||
|
32
static/styles/dark-theme.css
Обычный файл
32
static/styles/dark-theme.css
Обычный файл
@ -0,0 +1,32 @@
|
|||||||
|
:root {
|
||||||
|
--md-sys-color-primary: #D0BCFF;
|
||||||
|
--md-sys-color-on-primary: #381E72;
|
||||||
|
--md-sys-color-primary-container: #4F378B;
|
||||||
|
--md-sys-color-on-primary-container: #EADDFF;
|
||||||
|
--md-sys-color-secondary: #CCC2DC;
|
||||||
|
--md-sys-color-on-secondary: #332D41;
|
||||||
|
--md-sys-color-secondary-container: #4A4458;
|
||||||
|
--md-sys-color-on-secondary-container: #E8DEF8;
|
||||||
|
--md-sys-color-tertiary: #EFB8C8;
|
||||||
|
--md-sys-color-on-tertiary: #492532;
|
||||||
|
--md-sys-color-tertiary-container: #633B48;
|
||||||
|
--md-sys-color-on-tertiary-container: #FFD8E4;
|
||||||
|
--md-sys-color-error: #F2B8B5;
|
||||||
|
--md-sys-color-on-error: #601410;
|
||||||
|
--md-sys-color-error-container: #8C1D18;
|
||||||
|
--md-sys-color-on-error-container: #F9DEDC;
|
||||||
|
--md-sys-color-surface: #141218;
|
||||||
|
--md-sys-color-on-surface: #E6E0E9;
|
||||||
|
--md-sys-color-surface-variant: #49454F;
|
||||||
|
--md-sys-color-on-surface-variant: #CAC4D0;
|
||||||
|
--md-sys-color-surface-container-highest: #36343B;
|
||||||
|
--md-sys-color-surface-container-high: #2B2930;
|
||||||
|
--md-sys-color-surface-container: #211F26;
|
||||||
|
--md-sys-color-surface-container-low: #1D1B20;
|
||||||
|
--md-sys-color-surface-container-lowest: #0F0D13;
|
||||||
|
--md-sys-color-inverse-surface: #E6E0E9;
|
||||||
|
--md-sys-color-inverse-on-surface: #322F35;
|
||||||
|
--md-sys-color-surface-tint: #D0BCFF;
|
||||||
|
--md-sys-color-outline: #938F99;
|
||||||
|
--md-sys-color-outline-variant: #49454F;
|
||||||
|
}
|
32
static/styles/light-theme.css
Обычный файл
32
static/styles/light-theme.css
Обычный файл
@ -0,0 +1,32 @@
|
|||||||
|
:root {
|
||||||
|
--md-sys-color-primary: #6750A4;
|
||||||
|
--md-sys-color-on-primary: #FFFFFF;
|
||||||
|
--md-sys-color-primary-container: #EADDFF;
|
||||||
|
--md-sys-color-on-primary-container: #4F378B;
|
||||||
|
--md-sys-color-secondary: #625B71;
|
||||||
|
--md-sys-color-on-secondary: #FFFFFF;
|
||||||
|
--md-sys-color-secondary-container: #E8DEF8;
|
||||||
|
--md-sys-color-on-secondary-container: #4A4458;
|
||||||
|
--md-sys-color-tertiary: #7D5260;
|
||||||
|
--md-sys-color-on-tertiary: #FFFFFF;
|
||||||
|
--md-sys-color-tertiary-container: #FFD8E4;
|
||||||
|
--md-sys-color-on-tertiary-container: #633B48;
|
||||||
|
--md-sys-color-error: #B3261E;
|
||||||
|
--md-sys-color-on-error: #FFFFFF;
|
||||||
|
--md-sys-color-error-container: #F9DEDC;
|
||||||
|
--md-sys-color-on-error-container: #8C1D18;
|
||||||
|
--md-sys-color-surface: #FEF7FF;
|
||||||
|
--md-sys-color-on-surface: #1D1B20;
|
||||||
|
--md-sys-color-surface-variant: #E7E0EC;
|
||||||
|
--md-sys-color-on-surface-variant: #49454F;
|
||||||
|
--md-sys-color-surface-container-highest: #E6E0E9;
|
||||||
|
--md-sys-color-surface-container-high: #ECE6F0;
|
||||||
|
--md-sys-color-surface-container: #F3EDF7;
|
||||||
|
--md-sys-color-surface-container-low: #F7F2FA;
|
||||||
|
--md-sys-color-surface-container-lowest: #FFFFFF;
|
||||||
|
--md-sys-color-inverse-surface: #322F35;
|
||||||
|
--md-sys-color-inverse-on-surface: #F5EFF7;
|
||||||
|
--md-sys-color-surface-tint: #6750A4;
|
||||||
|
--md-sys-color-outline: #79747E;
|
||||||
|
--md-sys-color-outline-variant: #CAC4D0;
|
||||||
|
}
|
@ -1,307 +1,48 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 900;
|
|
||||||
src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic-ext.woff2') format('woff2');
|
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 900;
|
|
||||||
src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic.woff2') format('woff2');
|
|
||||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 900;
|
|
||||||
src: url('https://cdn.csasq.ru/common/fonts/exo-2/vietnamese.woff2') format('woff2');
|
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 900;
|
|
||||||
src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin-ext.woff2') format('woff2');
|
|
||||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 100 900;
|
|
||||||
src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin.woff2') format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--color-1: #2E436E;
|
|
||||||
--color-2: #1E1F22;
|
|
||||||
--color-3: #2B2D30;
|
|
||||||
--color-4: #43454A;
|
|
||||||
--color-5: #575757;
|
|
||||||
--color-6: #CED0D6;
|
|
||||||
--color-7: #569ACC;
|
|
||||||
--color-8: #3574F0;
|
|
||||||
--color-9: #528D58;
|
|
||||||
--color-10: #C94F4F;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
input,
|
|
||||||
button,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
font-family: 'Exo 2', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
height: 100%;
|
|
||||||
color: var(--color-6);
|
|
||||||
background-color: var(--color-2);
|
|
||||||
color-scheme: dark light;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"] {
|
|
||||||
color: var(--color-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-7);
|
|
||||||
text-decoration: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover,
|
|
||||||
a:focus {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#top-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
margin: 0;
|
||||||
padding: .5rem;
|
background-color: var(--md-sys-color-surface-container-high);
|
||||||
background-color: var(--color-3);
|
scroll-behavior: smooth;
|
||||||
border-bottom: solid 2px var(--color-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
main {
|
||||||
display: flex;
|
padding: 1rem;
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#icon {
|
main > * {
|
||||||
width: 1.5rem;
|
width: 100%;
|
||||||
height: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#title {
|
nav {
|
||||||
margin-left: .5rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#actions button {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
padding: .4rem .6rem;
|
|
||||||
color: var(--color-6);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: .25rem;
|
|
||||||
outline: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#actions button:hover,
|
|
||||||
#actions button:focus {
|
|
||||||
background: var(--color-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#actions .icon {
|
|
||||||
width: 1.2rem;
|
|
||||||
height: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
position: sticky;
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: stretch;
|
background-color: var(--md-sys-color-surface-container);
|
||||||
min-width: 12rem;
|
|
||||||
max-width: 16rem;
|
|
||||||
background-color: var(--color-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu .header {
|
nav ul {
|
||||||
padding: .75rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu .item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu input[type="button"] {
|
|
||||||
padding: .5rem .75rem;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--color-6);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
text-align: start;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu input[type="button"]:hover,
|
|
||||||
#menu input[type="button"]:focus {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu input[type="button"].active {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu .marker {
|
|
||||||
width: 3px;
|
|
||||||
background: none;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#menu input[type="button"].active + .marker {
|
|
||||||
background: var(--color-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main {
|
|
||||||
position: sticky;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
padding: .75rem;
|
|
||||||
min-width: 24rem;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#input [data-page]:not(.active),
|
|
||||||
#output [data-page]:not(.active) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input {
|
|
||||||
width: 10rem;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: solid 1px var(--color-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input:focus {
|
|
||||||
outline: solid 2px var(--color-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input[type="button"] {
|
|
||||||
padding: .25rem 1.5rem;
|
|
||||||
width: 100%;
|
|
||||||
color: white;
|
|
||||||
border-radius: .25rem;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input[type="button"].primary {
|
|
||||||
padding: .25rem 1.5rem;
|
|
||||||
background: var(--color-8);
|
|
||||||
border: solid 1px var(--color-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input[type="button"].secondary {
|
|
||||||
padding: .25rem 1.5rem;
|
|
||||||
background: none;
|
|
||||||
border: solid 1px var(--color-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main input[type="button"]:focus {
|
|
||||||
outline: solid 2px var(--color-8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#output {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
padding: .75rem;
|
|
||||||
border-left: solid 1px var(--color-3);
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#bottom-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: end;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 0;
|
||||||
padding: .5rem;
|
padding: .5rem;
|
||||||
background-color: var(--color-3);
|
list-style: none;
|
||||||
border-top: solid .15rem var(--color-2);
|
|
||||||
font-size: .8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.properties th {
|
nav li {
|
||||||
font-weight: normal;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.properties th[colspan="2"] {
|
|
||||||
padding: 1rem 0 .5rem 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.properties td:nth-child(2) {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.properties td[colspan="2"] {
|
|
||||||
padding: .5rem 0 .5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.properties td[colspan="2"] > div {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.properties td[colspan="2"] > div > :not(:first-child) {
|
nav md-icon-button {
|
||||||
margin-left: .5rem;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
table.matrix input {
|
|
||||||
width: 3rem !important;
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@
|
|||||||
<title>Настройки</title>
|
<title>Настройки</title>
|
||||||
<link rel="icon" href="/static/icons/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/static/icons/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" type="text/css" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" type="text/css" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined" type="text/css" />
|
||||||
|
<link rel="stylesheet" href="/static/styles/main.css" type="text/css" />
|
||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
@ -21,14 +23,57 @@
|
|||||||
|
|
||||||
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
|
document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
|
||||||
</script>
|
</script>
|
||||||
<script src="https://telegram.org/js/telegram-web-app.js"></script>
|
<script src="https://telegram.org/js/telegram-web-app.js" type="text/javascript"></script>
|
||||||
<style>
|
<script src="/static/scripts/vendor.js" type="text/javascript"></script>
|
||||||
body > * {
|
<script>
|
||||||
width: 100%;
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
const link = document.createElement('link')
|
||||||
</style>
|
const href = new URL(location)
|
||||||
|
href.pathname = `/static/styles/${window.Telegram.WebApp.colorScheme}-theme.css`
|
||||||
|
link.rel = 'stylesheet'
|
||||||
|
link.href = href.toString()
|
||||||
|
link.type = 'text/css'
|
||||||
|
document.head.append(link)
|
||||||
|
|
||||||
|
window.Telegram.WebApp.ready()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<main>
|
||||||
<md-outlined-text-field label="Favorite color"></md-outlined-text-field>
|
<md-outlined-text-field label="Favorite color"></md-outlined-text-field>
|
||||||
|
<md-switch id="qwerty"></md-switch>
|
||||||
|
<md-switch id="qwerty"></md-switch>
|
||||||
|
<md-switch id="qwerty"></md-switch>
|
||||||
|
<md-switch id="qwerty"></md-switch>
|
||||||
|
</main>
|
||||||
|
<nav class="md-typescale-body-small">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<md-icon-button href="/">
|
||||||
|
<md-icon>dashboard</md-icon>
|
||||||
|
</md-icon-button>
|
||||||
|
Дашборд
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<md-icon-button href="/">
|
||||||
|
<md-icon>ballot</md-icon>
|
||||||
|
</md-icon-button>
|
||||||
|
Опросы
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<md-icon-button href="/">
|
||||||
|
<md-icon>chat</md-icon>
|
||||||
|
</md-icon-button>
|
||||||
|
Чаты
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<md-icon-button href="/">
|
||||||
|
<md-icon>group</md-icon>
|
||||||
|
</md-icon-button>
|
||||||
|
Пользователи
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
51
web/main.py
51
web/main.py
@ -1,12 +1,49 @@
|
|||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.responses import HTMLResponse
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.websockets import WebSocket, WebSocketDisconnect
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
connections: list[WebSocket]
|
||||||
|
|
||||||
|
class StateError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
self.connections = []
|
||||||
|
|
||||||
|
async def connect(
|
||||||
|
self,
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
await websocket.accept()
|
||||||
|
self.connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(
|
||||||
|
self,
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
self.connections.remove(websocket)
|
||||||
|
|
||||||
|
async def broadcast(
|
||||||
|
self,
|
||||||
|
data: dict,
|
||||||
|
):
|
||||||
|
for connection in self.connections:
|
||||||
|
asyncio.ensure_future(connection.send_json(data))
|
||||||
|
|
||||||
|
|
||||||
|
connection_manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title=config.Main.app_name,
|
title=config.Main.app_name,
|
||||||
)
|
)
|
||||||
@ -33,3 +70,17 @@ async def _():
|
|||||||
title='Дашборд',
|
title='Дашборд',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket(
|
||||||
|
path='/ws',
|
||||||
|
)
|
||||||
|
async def _(
|
||||||
|
websocket: WebSocket,
|
||||||
|
):
|
||||||
|
await connection_manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await connection_manager.broadcast(await websocket.receive_json())
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
connection_manager.disconnect(websocket)
|
||||||
|
Загрузка…
Ссылка в новой задаче
Block a user