@@ -0,0 +1,726 @@
<!DOCTYPE html>
<!--
app/ui/index.html
SQS Console — веб - интерфейс для shared - sqs (Nubes branding)
Created: 2026 - 04 - 10
Vanilla HTML/CSS/JS SPA. Встраивается через go:embed.
Режим: Admin (bearer token) — все тенанты, очереди, статистика.
-->
< html lang = "ru" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > SQS Console — Nubes< / title >
< link rel = "icon" href = "https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/favicon.png" >
< style >
/* Nubes palette — идентично iot.kube5s.ru/console */
: root {
--bg-page : #001120 ;
--bg-surface : #001929 ;
--bg-navbar : #001C34 ;
--border : #0b2d50 ;
--accent : #1a7fd4 ;
--accent-hover : #2196f3 ;
--text-primary : #e2ecf6 ;
--text-secondary : #6b8eaa ;
--danger : #e74c3c ;
--success : #27ae60 ;
--warning : #f39c12 ;
}
* , * :: before , * :: after { box-sizing : border-box ; margin : 0 ; padding : 0 ; }
body {
font-family : - apple-system , BlinkMacSystemFont , 'Segoe UI' , Roboto , sans-serif ;
background : var ( - - bg - page ) ;
color : var ( - - text - primary ) ;
min-height : 100 vh ;
}
a { color : var ( - - accent ) ; text-decoration : none ; }
a : hover { color : var ( - - accent - hover ) ; }
/* Navbar */
. navbar {
background : var ( - - bg - navbar ) ;
border-bottom : 1 px solid var ( - - border ) ;
padding : 0 24 px ;
height : 56 px ;
display : flex ;
align-items : center ;
justify-content : space-between ;
}
. navbar-brand {
display : flex ;
align-items : center ;
gap : 12 px ;
}
. navbar-brand img {
height : 28 px ;
filter : brightness ( 0 ) invert ( 1 ) ;
}
. navbar-brand span {
font-size : 16 px ;
font-weight : 700 ;
letter-spacing : 1.5 px ;
color : var ( - - text - primary ) ;
}
. navbar-user {
display : flex ;
align-items : center ;
gap : 16 px ;
font-size : 14 px ;
color : var ( - - text - secondary ) ;
}
. btn-logout {
background : none ;
border : 1 px solid var ( - - border ) ;
color : var ( - - text - secondary ) ;
padding : 6 px 14 px ;
border-radius : 4 px ;
cursor : pointer ;
font-size : 13 px ;
}
. btn-logout : hover { border-color : var ( - - danger ) ; color : var ( - - danger ) ; }
/* Container */
. container { max-width : 1200 px ; margin : 0 auto ; padding : 24 px ; }
/* Cards */
. card {
background : var ( - - bg - surface ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 8 px ;
padding : 20 px ;
margin-bottom : 20 px ;
}
. card-title {
font-size : 14 px ;
font-weight : 600 ;
color : var ( - - text - secondary ) ;
text-transform : uppercase ;
letter-spacing : 1 px ;
margin-bottom : 16 px ;
}
/* Stats grid */
. stats-grid {
display : grid ;
grid-template-columns : repeat ( auto - fit , minmax ( 200 px , 1 fr ) ) ;
gap : 16 px ;
margin-bottom : 24 px ;
}
. stat-card {
background : var ( - - bg - surface ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 8 px ;
padding : 20 px ;
text-align : center ;
}
. stat-value {
font-size : 36 px ;
font-weight : 700 ;
color : var ( - - accent ) ;
}
. stat-label {
font-size : 12 px ;
color : var ( - - text - secondary ) ;
text-transform : uppercase ;
letter-spacing : 1 px ;
margin-top : 4 px ;
}
/* Table */
. table-wrap { overflow-x : auto ; }
table {
width : 100 % ;
border-collapse : collapse ;
font-size : 14 px ;
}
thead th {
text-align : left ;
padding : 12 px 16 px ;
border-bottom : 2 px solid var ( - - border ) ;
color : var ( - - text - secondary ) ;
font-weight : 600 ;
font-size : 12 px ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
tbody td {
padding : 12 px 16 px ;
border-bottom : 1 px solid var ( - - border ) ;
}
tbody tr : hover { background : rgba ( 26 , 127 , 212 , 0.05 ) ; }
tbody tr { cursor : pointer ; }
/* Buttons */
. btn {
display : inline-flex ;
align-items : center ;
gap : 6 px ;
padding : 8 px 20 px ;
border-radius : 4 px ;
font-size : 14 px ;
font-weight : 500 ;
cursor : pointer ;
border : none ;
transition : background 0.15 s ;
}
. btn-primary { background : var ( - - accent ) ; color : #fff ; }
. btn-primary : hover { background : var ( - - accent - hover ) ; }
. btn-danger { background : var ( - - danger ) ; color : #fff ; }
. btn-danger : hover { background : #c0392b ; }
. btn-sm { padding : 4 px 12 px ; font-size : 12 px ; }
/* Forms */
. form-group { margin-bottom : 16 px ; }
. form-group label {
display : block ;
font-size : 13 px ;
color : var ( - - text - secondary ) ;
margin-bottom : 6 px ;
}
. form-group input {
width : 100 % ;
padding : 10 px 14 px ;
background : var ( - - bg - page ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 4 px ;
color : var ( - - text - primary ) ;
font-size : 14 px ;
}
. form-group input : focus {
outline : none ;
border-color : var ( - - accent ) ;
}
/* Login page */
. login-page {
display : flex ;
align-items : center ;
justify-content : center ;
min-height : 100 vh ;
}
. login-box {
background : var ( - - bg - surface ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 12 px ;
padding : 40 px ;
width : 400 px ;
text-align : center ;
}
. login-box img { height : 40 px ; filter : brightness ( 0 ) invert ( 1 ) ; margin-bottom : 12 px ; }
. login-box h1 {
font-size : 18 px ;
letter-spacing : 2 px ;
margin-bottom : 32 px ;
color : var ( - - text - primary ) ;
}
. login-error {
color : var ( - - danger ) ;
font-size : 13 px ;
margin-bottom : 12 px ;
min-height : 20 px ;
}
/* Badges */
. badge {
display : inline-block ;
padding : 2 px 8 px ;
border-radius : 10 px ;
font-size : 11 px ;
font-weight : 600 ;
}
. badge-active { background : rgba ( 39 , 174 , 96 , 0.15 ) ; color : var ( - - success ) ; }
. badge-inactive { background : rgba ( 231 , 76 , 60 , 0.15 ) ; color : var ( - - danger ) ; }
. badge-count {
background : rgba ( 26 , 127 , 212 , 0.15 ) ;
color : var ( - - accent ) ;
min-width : 24 px ;
text-align : center ;
}
/* Breadcrumb */
. breadcrumb {
font-size : 13 px ;
color : var ( - - text - secondary ) ;
margin-bottom : 20 px ;
}
. breadcrumb a { color : var ( - - accent ) ; }
. breadcrumb span { margin : 0 8 px ; }
/* Modal */
. modal-overlay {
position : fixed ;
inset : 0 ;
background : rgba ( 0 , 0 , 0 , 0.6 ) ;
display : flex ;
align-items : center ;
justify-content : center ;
z-index : 1000 ;
}
. modal {
background : var ( - - bg - surface ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 12 px ;
padding : 32 px ;
width : 480 px ;
max-width : 90 vw ;
}
. modal h2 {
font-size : 18 px ;
margin-bottom : 20 px ;
}
. modal-actions {
display : flex ;
gap : 12 px ;
justify-content : flex-end ;
margin-top : 24 px ;
}
/* Hidden helper */
. hidden { display : none !important ; }
/* Responsive */
@ media ( max-width : 640px ) {
. container { padding : 12 px ; }
. login-box { width : 95 vw ; padding : 24 px ; }
. stats-grid { grid-template-columns : 1 fr 1 fr ; }
}
/* Copying animation */
. copy-ok {
color : var ( - - success ) ;
font-size : 12 px ;
margin-left : 8 px ;
animation : fadeout 1.5 s forwards ;
}
@ keyframes fadeout { 0 % { opacity : 1 } 70 % { opacity : 1 } 100 % { opacity : 0 } }
/* Refresh */
. toolbar {
display : flex ;
align-items : center ;
justify-content : space-between ;
margin-bottom : 16 px ;
}
. toolbar h2 {
font-size : 18 px ;
font-weight : 600 ;
}
. auto-refresh {
display : flex ;
align-items : center ;
gap : 8 px ;
font-size : 13 px ;
color : var ( - - text - secondary ) ;
}
< / style >
< / head >
< body >
<!-- ===== LOGIN ===== -->
< div id = "login-page" class = "login-page" >
< div class = "login-box" >
< img src = "https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg" alt = "Nubes" >
< h1 > SQS CONSOLE< / h1 >
< div id = "login-error" class = "login-error" > < / div >
< div class = "form-group" >
< label for = "token-input" > Admin Token< / label >
< input type = "password" id = "token-input" placeholder = "sqs-admin-..." autocomplete = "off" >
< / div >
< button class = "btn btn-primary" style = "width:100%" onclick = "doLogin()" > Войти< / button >
< / div >
< / div >
<!-- ===== APP SHELL ===== -->
< div id = "app" class = "hidden" >
< nav class = "navbar" >
< div class = "navbar-brand" >
< img src = "https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg" alt = "Nubes" >
< span > SQS CONSOLE< / span >
< / div >
< div class = "navbar-user" >
< span > admin< / span >
< button class = "btn-logout" onclick = "doLogout()" > Выход< / button >
< / div >
< / nav >
< div class = "container" >
<!-- Dashboard view -->
< div id = "view-dashboard" > < / div >
<!-- Tenant detail view -->
< div id = "view-tenant" class = "hidden" > < / div >
< / div >
< / div >
<!-- ===== CREATE TENANT MODAL ===== -->
< div id = "modal-create" class = "modal-overlay hidden" onclick = "if(event.target===this)closeModal()" >
< div class = "modal" >
< h2 > Создать тенанта< / h2 >
< div class = "form-group" >
< label for = "ct-name" > Имя< / label >
< input id = "ct-name" placeholder = "my-service" >
< / div >
< div class = "form-group" >
< label for = "ct-queues" > Макс. очередей< / label >
< input id = "ct-queues" type = "number" value = "10" min = "1" max = "1000" >
< / div >
< div id = "ct-error" class = "login-error" > < / div >
< div class = "modal-actions" >
< button class = "btn btn-primary" onclick = "createTenant()" > Создать< / button >
< button class = "btn btn-logout" onclick = "closeModal()" > Отмена< / button >
< / div >
< / div >
< / div >
<!-- ===== CREDENTIALS MODAL ===== -->
< div id = "modal-creds" class = "modal-overlay hidden" onclick = "if(event.target===this)closeCredsModal()" >
< div class = "modal" >
< h2 > Тенант создан< / h2 >
< p style = "color:var(--warning);font-size:13px;margin-bottom:16px" >
⚠ Сохраните credentials — Secret Key показывается только один раз.
< / p >
< div class = "form-group" >
< label > Access Key< / label >
< input id = "creds-ak" readonly onclick = "copyField(this)" >
< / div >
< div class = "form-group" >
< label > Secret Key< / label >
< input id = "creds-sk" readonly onclick = "copyField(this)" >
< / div >
< div class = "modal-actions" >
< button class = "btn btn-primary" onclick = "closeCredsModal()" > Готово< / button >
< / div >
< / div >
< / div >
< script >
// ===== STATE =====
let TOKEN = '' ;
let BASE = '' ;
let refreshTimer = null ;
let currentTenantId = null ;
// ===== AUTH =====
// doLogin — проверяет токен через /admin/health и сохраняет в sessionStorage
function doLogin ( ) {
const token = document . getElementById ( 'token-input' ) . value . trim ( ) ;
if ( ! token ) return ;
// Определяем base URL — тот же origin что и UI
BASE = window . location . origin ;
TOKEN = token ;
api ( '/admin/health' ) . then ( data => {
sessionStorage . setItem ( 'sqs_token' , token ) ;
document . getElementById ( 'login-page' ) . classList . add ( 'hidden' ) ;
document . getElementById ( 'app' ) . classList . remove ( 'hidden' ) ;
showDashboard ( ) ;
} ) . catch ( err => {
document . getElementById ( 'login-error' ) . textContent = 'Неверный токен' ;
TOKEN = '' ;
} ) ;
}
// doLogout — очищает сессию и возвращает на логин
function doLogout ( ) {
TOKEN = '' ;
sessionStorage . removeItem ( 'sqs_token' ) ;
if ( refreshTimer ) clearInterval ( refreshTimer ) ;
document . getElementById ( 'app' ) . classList . add ( 'hidden' ) ;
document . getElementById ( 'login-page' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'token-input' ) . value = '' ;
document . getElementById ( 'login-error' ) . textContent = '' ;
}
// Авто-логин из sessionStorage
( function autoLogin ( ) {
const saved = sessionStorage . getItem ( 'sqs_token' ) ;
if ( saved ) {
document . getElementById ( 'token-input' ) . value = saved ;
doLogin ( ) ;
}
// Enter на поле токена
document . getElementById ( 'token-input' ) . addEventListener ( 'keydown' , e => {
if ( e . key === 'Enter' ) doLogin ( ) ;
} ) ;
} ) ( ) ;
// ===== API HELPER =====
// api — выполняет запрос к admin API с bearer token
function api ( path , opts = { } ) {
return fetch ( BASE + path , {
... opts ,
headers : {
'Authorization' : 'Bearer ' + TOKEN ,
'Content-Type' : 'application/json' ,
... ( opts . headers || { } )
}
} ) . then ( r => {
if ( ! r . ok ) throw new Error ( r . status + ' ' + r . statusText ) ;
if ( r . status === 204 ) return null ;
return r . json ( ) ;
} ) ;
}
// ===== DASHBOARD =====
// showDashboard — загружает health + tenants и рендерит главную страницу
function showDashboard ( ) {
currentTenantId = null ;
document . getElementById ( 'view-tenant' ) . classList . add ( 'hidden' ) ;
document . getElementById ( 'view-dashboard' ) . classList . remove ( 'hidden' ) ;
if ( refreshTimer ) clearInterval ( refreshTimer ) ;
loadDashboard ( ) ;
refreshTimer = setInterval ( loadDashboard , 10000 ) ;
}
// loadDashboard — загружает данные и обновляет DOM
function loadDashboard ( ) {
Promise . all ( [ api ( '/admin/health' ) , api ( '/admin/tenants' ) ] )
. then ( ( [ health , tenants ] ) => renderDashboard ( health , tenants ) )
. catch ( err => console . error ( 'Dashboard load error:' , err ) ) ;
}
// renderDashboard — рендерит статистику и таблицу тенантов
function renderDashboard ( health , tenants ) {
const el = document . getElementById ( 'view-dashboard' ) ;
el . innerHTML = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"> ${ health . tenant _count || 0 } </div>
<div class="stat-label">Тенанты</div>
</div>
<div class="stat-card">
<div class="stat-value"> ${ health . queue _count || 0 } </div>
<div class="stat-label">Очереди</div>
</div>
<div class="stat-card">
<div class="stat-value"> ${ health . message _count || 0 } </div>
<div class="stat-label">Сообщения</div>
</div>
<div class="stat-card">
<div class="stat-value" style="color:var(--success)">●</div>
<div class="stat-label"> ${ health . status || 'ok' } </div>
</div>
</div>
<div class="card">
<div class="toolbar">
<h2>Тенанты</h2>
<div style="display:flex;gap:12px;align-items:center">
<div class="auto-refresh">
<span>⟳ 10с </span>
</div>
<button class="btn btn-primary btn-sm" onclick="openModal()">+ Создать</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Имя</th>
<th>Access Key</th>
<th>Макс. очередей</th>
<th>Статус</th>
<th>Создан</th>
<th></th>
</tr>
</thead>
<tbody>
${ ( tenants || [ ] ) . map ( t => `
<tr onclick="showTenant(' ${ esc ( t . id ) } ')">
<td><strong> ${ esc ( t . name ) } </strong></td>
<td style="font-family:monospace;font-size:12px"> ${ esc ( t . access _key ) } </td>
<td> ${ t . max _queues } </td>
<td> ${ t . active
? '<span class="badge badge-active">active</span>'
: '<span class="badge badge-inactive">inactive</span>' } </td>
<td style="font-size:12px;color:var(--text-secondary)"> ${ fmtDate ( t . created _at ) } </td>
<td>
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deleteTenant(' ${ esc ( t . id ) } ',' ${ esc ( t . name ) } ')">✕</button>
</td>
</tr>
` ) . join ( '' ) }
${ ( ! tenants || tenants . length === 0 ) ? '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:32px">Нет тенантов</td></tr>' : '' }
</tbody>
</table>
</div>
</div>
` ;
}
// ===== TENANT DETAIL =====
// showTenant — переключает вид на детали тенанта и его очереди
function showTenant ( id ) {
currentTenantId = id ;
document . getElementById ( 'view-dashboard' ) . classList . add ( 'hidden' ) ;
document . getElementById ( 'view-tenant' ) . classList . remove ( 'hidden' ) ;
if ( refreshTimer ) clearInterval ( refreshTimer ) ;
loadTenant ( id ) ;
refreshTimer = setInterval ( ( ) => loadTenant ( id ) , 10000 ) ;
}
// loadTenant — загружает тенанта и его очереди
function loadTenant ( id ) {
Promise . all ( [ api ( '/admin/tenants/' + id ) , api ( '/admin/tenants/' + id + '/queues' ) ] )
. then ( ( [ tenant , queues ] ) => renderTenant ( tenant , queues ) )
. catch ( err => {
console . error ( 'Tenant load error:' , err ) ;
showDashboard ( ) ;
} ) ;
}
// renderTenant — рендерит детали тенанта: credentials, список очередей
function renderTenant ( tenant , queues ) {
const el = document . getElementById ( 'view-tenant' ) ;
const totalMsgs = ( queues || [ ] ) . reduce ( ( s , q ) => s + q . messages + q . not _visible , 0 ) ;
el . innerHTML = `
<div class="breadcrumb">
<a href="#" onclick="event.preventDefault();showDashboard()">Тенанты</a>
<span>› </span>
${ esc ( tenant . name ) }
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"> ${ ( queues || [ ] ) . length } </div>
<div class="stat-label">Очереди</div>
</div>
<div class="stat-card">
<div class="stat-value"> ${ totalMsgs } </div>
<div class="stat-label">Сообщения</div>
</div>
<div class="stat-card">
<div class="stat-value"> ${ tenant . max _queues } </div>
<div class="stat-label">Лимит очередей</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size:20px;font-family:monospace"> ${ esc ( tenant . access _key ) } </div>
<div class="stat-label">Access Key</div>
</div>
</div>
<div class="card">
<div class="toolbar">
<h2>Очереди</h2>
<div class="auto-refresh"><span>⟳ 10с </span></div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Имя очереди</th>
<th>Сообщения</th>
<th>In-flight</th>
<th>Visibility Timeout</th>
<th>Max Size</th>
<th>Retention</th>
</tr>
</thead>
<tbody>
${ ( queues || [ ] ) . map ( q => `
<tr>
<td><strong> ${ esc ( q . name ) } </strong></td>
<td><span class="badge badge-count"> ${ q . messages } </span></td>
<td><span class="badge badge-count"> ${ q . not _visible } </span></td>
<td> ${ q . visibility _timeout } с </td>
<td> ${ fmtBytes ( q . max _message _size ) } </td>
<td> ${ fmtDuration ( q . retention _period ) } </td>
</tr>
` ) . join ( '' ) }
${ ( ! queues || queues . length === 0 ) ? '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:32px">Нет очередей — создайте через AWS CLI</td></tr>' : '' }
</tbody>
</table>
</div>
</div>
` ;
}
// ===== TENANT CRUD =====
// openModal — показывает модалку создания тенанта
function openModal ( ) {
document . getElementById ( 'ct-name' ) . value = '' ;
document . getElementById ( 'ct-queues' ) . value = '10' ;
document . getElementById ( 'ct-error' ) . textContent = '' ;
document . getElementById ( 'modal-create' ) . classList . remove ( 'hidden' ) ;
document . getElementById ( 'ct-name' ) . focus ( ) ;
}
function closeModal ( ) {
document . getElementById ( 'modal-create' ) . classList . add ( 'hidden' ) ;
}
// createTenant — POST /admin/tenants, показывает credentials
function createTenant ( ) {
const name = document . getElementById ( 'ct-name' ) . value . trim ( ) ;
const maxQ = parseInt ( document . getElementById ( 'ct-queues' ) . value ) || 10 ;
if ( ! name ) {
document . getElementById ( 'ct-error' ) . textContent = 'Укажите имя' ;
return ;
}
api ( '/admin/tenants' , {
method : 'POST' ,
body : JSON . stringify ( { name : name , max _queues : maxQ } )
} ) . then ( data => {
closeModal ( ) ;
// Показываем credentials
document . getElementById ( 'creds-ak' ) . value = data . access _key || '' ;
document . getElementById ( 'creds-sk' ) . value = data . secret _key || '' ;
document . getElementById ( 'modal-creds' ) . classList . remove ( 'hidden' ) ;
loadDashboard ( ) ;
} ) . catch ( err => {
document . getElementById ( 'ct-error' ) . textContent = 'Ошибка: ' + err . message ;
} ) ;
}
function closeCredsModal ( ) {
document . getElementById ( 'modal-creds' ) . classList . add ( 'hidden' ) ;
}
// deleteTenant — DELETE /admin/tenants/{id} с подтверждением
function deleteTenant ( id , name ) {
if ( ! confirm ( 'Удалить тенанта "' + name + '"?\nВсе его очереди будут удалены.' ) ) return ;
api ( '/admin/tenants/' + id , { method : 'DELETE' } )
. then ( ( ) => loadDashboard ( ) )
. catch ( err => alert ( 'Ошибка удаления: ' + err . message ) ) ;
}
// ===== HELPERS =====
// esc — экранирование HTML для защиты от XSS
function esc ( s ) {
if ( ! s ) return '' ;
const d = document . createElement ( 'div' ) ;
d . textContent = s ;
return d . innerHTML ;
}
// copyField — копирует значение input в буфер обмена
function copyField ( input ) {
navigator . clipboard . writeText ( input . value ) . then ( ( ) => {
const ok = document . createElement ( 'span' ) ;
ok . className = 'copy-ok' ;
ok . textContent = '✓ скопировано' ;
input . parentNode . appendChild ( ok ) ;
setTimeout ( ( ) => ok . remove ( ) , 1500 ) ;
} ) ;
}
// fmtDate — форматирует дату для таблицы
function fmtDate ( s ) {
if ( ! s ) return '—' ;
const d = new Date ( s ) ;
return d . toLocaleDateString ( 'ru-RU' ) + ' ' + d . toLocaleTimeString ( 'ru-RU' , { hour : '2-digit' , minute : '2-digit' } ) ;
}
// fmtBytes — форматирует байты (262144 → 256 KB)
function fmtBytes ( b ) {
if ( ! b ) return '—' ;
if ( b >= 1048576 ) return ( b / 1048576 ) . toFixed ( 0 ) + ' MB' ;
if ( b >= 1024 ) return ( b / 1024 ) . toFixed ( 0 ) + ' KB' ;
return b + ' B' ;
}
// fmtDuration — форматирует секунды (345600 → 4д)
function fmtDuration ( s ) {
if ( ! s ) return '—' ;
if ( s >= 86400 ) return ( s / 86400 ) . toFixed ( 0 ) + 'д' ;
if ( s >= 3600 ) return ( s / 3600 ) . toFixed ( 0 ) + 'ч' ;
if ( s >= 60 ) return ( s / 60 ) . toFixed ( 0 ) + 'м' ;
return s + 'с ' ;
}
< / script >
< / body >
< / html >