1023 lines
44 KiB
HTML
1023 lines
44 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
|
||
<meta name="googlebot" content="noindex, nofollow">
|
||
<title>24mycloud</title>
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0%25' stop-color='%235856d6'/%3E%3Cstop offset='100%25' stop-color='%23818cf8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='64' height='64' rx='14' fill='url(%23g)'/%3E%3Cpath d='M44 28h-1.1A10.5 10.5 0 0 0 23 24a10.5 10.5 0 0 0 .5 3.2A7.5 7.5 0 0 0 16.5 35 7.5 7.5 0 0 0 24 42.5h20a6.5 6.5 0 0 0 0-13z' fill='none' stroke='white' stroke-width='2.5' stroke-linejoin='round'/%3E%3C/svg%3E">
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg: #f5f5f7;
|
||
--nav: #ffffff;
|
||
--card: #ffffff;
|
||
--card-hover: #fafafa;
|
||
--border: #e5e5ea;
|
||
--border-hover: #d1d1d6;
|
||
--text: #1d1d1f;
|
||
--text-2: #48484a;
|
||
--text-3: #8e8e93;
|
||
--accent: #5856d6;
|
||
--accent-light: #ededfc;
|
||
--green: #30b455;
|
||
--green-bg: #e8f8ee;
|
||
--orange: #e68a1a;
|
||
--orange-bg: #fef3e2;
|
||
--red: #e5383b;
|
||
--red-bg: #fde8e8;
|
||
--blue: #3a7bd5;
|
||
--blue-bg: #e8f0fb;
|
||
--purple: #8b5cf6;
|
||
--purple-bg: #f0ebfe;
|
||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.04);
|
||
--shadow-lg: 0 4px 16px rgba(0,0,0,0.08);
|
||
--r: 8px;
|
||
--r-lg: 12px;
|
||
}
|
||
|
||
html[data-theme="dark"] {
|
||
--bg: #0f0f13;
|
||
--nav: #18181e;
|
||
--card: #1e1e26;
|
||
--card-hover: #26262f;
|
||
--border: #2c2c36;
|
||
--border-hover: #3c3c48;
|
||
--text: #ececf0;
|
||
--text-2: #a0a0ac;
|
||
--text-3: #5c5c6a;
|
||
--accent: #7c7af7;
|
||
--accent-light: #252540;
|
||
--green: #3dd68c;
|
||
--green-bg: #152e20;
|
||
--orange: #f5a623;
|
||
--orange-bg: #2e2210;
|
||
--red: #f06c6e;
|
||
--red-bg: #2e1414;
|
||
--blue: #5a9cf5;
|
||
--blue-bg: #14233a;
|
||
--purple: #9d7ffa;
|
||
--purple-bg: #221a38;
|
||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
|
||
--shadow: 0 1px 3px rgba(0,0,0,0.3), 0 2px 8px rgba(0,0,0,0.2);
|
||
--shadow-lg: 0 4px 16px rgba(0,0,0,0.4);
|
||
}
|
||
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
}
|
||
|
||
/* ===== AUTH ===== */
|
||
#authScreen {
|
||
position: fixed; inset: 0; z-index: 9999;
|
||
background: #0c0c14;
|
||
display: flex; align-items: center; justify-content: center;
|
||
overflow: hidden;
|
||
transition: opacity 0.5s, transform 0.5s;
|
||
}
|
||
|
||
#authScreen.hidden { opacity: 0; pointer-events: none; transform: scale(1.05); }
|
||
|
||
.auth-bg {
|
||
position: absolute; inset: 0;
|
||
background:
|
||
radial-gradient(ellipse 600px 400px at 30% 20%, rgba(88,86,214,0.12) 0%, transparent 70%),
|
||
radial-gradient(ellipse 500px 350px at 70% 80%, rgba(59,130,246,0.08) 0%, transparent 70%);
|
||
}
|
||
|
||
.auth-grid {
|
||
position: absolute; inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||
background-size: 60px 60px;
|
||
animation: gridMove 20s linear infinite;
|
||
}
|
||
|
||
@keyframes gridMove {
|
||
0% { transform: translate(0, 0); }
|
||
100% { transform: translate(60px, 60px); }
|
||
}
|
||
|
||
.auth-particles {
|
||
position: absolute; inset: 0; overflow: hidden;
|
||
}
|
||
|
||
.auth-particle {
|
||
position: absolute;
|
||
width: 2px; height: 2px;
|
||
background: rgba(99,102,241,0.4);
|
||
border-radius: 50%;
|
||
animation: float linear infinite;
|
||
}
|
||
|
||
@keyframes float {
|
||
0% { transform: translateY(100vh) scale(0); opacity: 0; }
|
||
10% { opacity: 1; }
|
||
90% { opacity: 1; }
|
||
100% { transform: translateY(-10vh) scale(1); opacity: 0; }
|
||
}
|
||
|
||
.auth-box {
|
||
position: relative;
|
||
background: rgba(24,24,30,0.8);
|
||
backdrop-filter: blur(20px);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 16px;
|
||
padding: 48px 40px;
|
||
width: 380px;
|
||
text-align: center;
|
||
animation: authIn 0.6s ease-out;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(99,102,241,0.05);
|
||
}
|
||
|
||
@keyframes authIn {
|
||
0% { opacity: 0; transform: translateY(20px) scale(0.96); }
|
||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||
}
|
||
|
||
.auth-logo {
|
||
width: 56px; height: 56px;
|
||
background: linear-gradient(135deg, #5856d6, #818cf8);
|
||
border-radius: 14px;
|
||
display: inline-flex; align-items: center; justify-content: center;
|
||
color: #fff;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 4px 20px rgba(88,86,214,0.3);
|
||
animation: logoPulse 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes logoPulse {
|
||
0%, 100% { box-shadow: 0 4px 20px rgba(88,86,214,0.3); }
|
||
50% { box-shadow: 0 4px 30px rgba(88,86,214,0.5); }
|
||
}
|
||
|
||
.auth-title {
|
||
font-size: 22px; font-weight: 700; color: #ececf0;
|
||
margin-bottom: 4px; letter-spacing: -0.3px;
|
||
}
|
||
|
||
.auth-sub {
|
||
font-size: 13px; color: #5c5c6a;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.auth-field {
|
||
position: relative;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.auth-input {
|
||
width: 100%; padding: 12px 16px 12px 42px;
|
||
background: rgba(255,255,255,0.04);
|
||
border: 1px solid rgba(255,255,255,0.08);
|
||
border-radius: 10px; color: #ececf0;
|
||
font-size: 14px; font-family: inherit;
|
||
outline: none; transition: all 0.2s;
|
||
}
|
||
|
||
.auth-input::placeholder { color: #4a4a5a; }
|
||
.auth-input:focus { border-color: var(--accent); background: rgba(255,255,255,0.06); box-shadow: 0 0 0 3px rgba(88,86,214,0.15); }
|
||
|
||
.auth-input-icon {
|
||
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
|
||
color: #5c5c6a;
|
||
}
|
||
|
||
.auth-btn {
|
||
width: 100%; padding: 12px;
|
||
background: linear-gradient(135deg, #5856d6, #6e6ce8);
|
||
border: none;
|
||
border-radius: 10px; color: #fff;
|
||
font-size: 14px; font-weight: 600; font-family: inherit;
|
||
cursor: pointer; margin-top: 14px;
|
||
transition: all 0.2s;
|
||
position: relative; overflow: hidden;
|
||
}
|
||
|
||
.auth-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(88,86,214,0.4); }
|
||
.auth-btn:active { transform: translateY(0); }
|
||
|
||
.auth-btn-shine {
|
||
position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
|
||
animation: shine 3s infinite;
|
||
}
|
||
|
||
@keyframes shine {
|
||
0% { left: -100%; }
|
||
50%,100% { left: 100%; }
|
||
}
|
||
|
||
.auth-error {
|
||
color: #f06c6e; font-size: 12px;
|
||
margin-top: 12px; min-height: 18px;
|
||
}
|
||
|
||
.auth-error.shake { animation: shake 0.4s ease; }
|
||
|
||
@keyframes shake {
|
||
0%,100% { transform: translateX(0); }
|
||
25% { transform: translateX(-8px); }
|
||
75% { transform: translateX(8px); }
|
||
}
|
||
|
||
.auth-footer {
|
||
margin-top: 28px; font-size: 11px; color: #3a3a48;
|
||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||
}
|
||
|
||
/* ===== NAV ===== */
|
||
.nav {
|
||
width: 220px; min-height: 100vh;
|
||
background: var(--nav);
|
||
border-right: 1px solid var(--border);
|
||
display: flex; flex-direction: column;
|
||
position: fixed; left: 0; top: 0; bottom: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.nav-head {
|
||
padding: 20px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
|
||
.nav-logo {
|
||
width: 32px; height: 32px;
|
||
background: linear-gradient(135deg, var(--accent), #818cf8);
|
||
border-radius: 8px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
color: #fff;
|
||
}
|
||
|
||
.nav-name { font-size: 14px; font-weight: 700; }
|
||
.nav-sub { font-size: 11px; color: var(--text-3); }
|
||
|
||
.nav-group { padding: 14px 10px 6px; }
|
||
.nav-group-label {
|
||
font-size: 10px; font-weight: 600;
|
||
text-transform: uppercase; letter-spacing: 1.2px;
|
||
color: var(--text-3); padding: 0 6px; margin-bottom: 4px;
|
||
}
|
||
|
||
.nav-links { display: flex; flex-direction: column; gap: 1px; }
|
||
|
||
.nav-link {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 8px 10px; border-radius: var(--r);
|
||
font-size: 13px; font-weight: 500;
|
||
color: var(--text-2); cursor: pointer;
|
||
transition: all 0.12s; border: 1px solid transparent;
|
||
}
|
||
|
||
.nav-link:hover { background: var(--bg); color: var(--text); }
|
||
.nav-link.on { background: var(--accent-light); color: var(--accent); }
|
||
|
||
.nav-link svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||
|
||
.nav-link .cnt {
|
||
margin-left: auto; font-size: 10px; font-weight: 600;
|
||
padding: 0 6px; border-radius: 8px;
|
||
background: var(--bg); color: var(--text-3);
|
||
line-height: 18px;
|
||
}
|
||
|
||
.nav-link.on .cnt { background: var(--accent); color: #fff; }
|
||
|
||
.nav-foot {
|
||
margin-top: auto; padding: 14px 16px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
}
|
||
|
||
.nav-live {
|
||
display: flex; align-items: center; gap: 6px;
|
||
font-size: 11px; color: var(--text-3);
|
||
}
|
||
|
||
.live-dot {
|
||
width: 6px; height: 6px; border-radius: 50%;
|
||
background: var(--green);
|
||
box-shadow: 0 0 6px var(--green);
|
||
animation: blink 2s infinite;
|
||
}
|
||
|
||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||
|
||
.theme-btn {
|
||
width: 28px; height: 28px; border-radius: 6px;
|
||
background: var(--bg); border: 1px solid var(--border);
|
||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||
font-size: 14px; transition: all 0.15s;
|
||
}
|
||
|
||
.theme-btn:hover { border-color: var(--border-hover); }
|
||
|
||
/* ===== MAIN ===== */
|
||
.main { margin-left: 220px; flex: 1; min-height: 100vh; }
|
||
|
||
.top {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 14px 28px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: var(--nav);
|
||
position: sticky; top: 0; z-index: 50;
|
||
}
|
||
|
||
.top h2 { font-size: 15px; font-weight: 700; }
|
||
|
||
.top-right { display: flex; align-items: center; gap: 10px; }
|
||
|
||
.top-btn {
|
||
padding: 5px 10px; border-radius: var(--r);
|
||
background: var(--bg); border: 1px solid var(--border);
|
||
color: var(--text-2); font-size: 12px; font-family: inherit;
|
||
cursor: pointer; font-weight: 500; transition: all 0.12s;
|
||
}
|
||
|
||
.top-btn:hover { border-color: var(--border-hover); color: var(--text); }
|
||
|
||
.top-clock {
|
||
font-size: 12px; color: var(--text-3);
|
||
font-family: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
.pg { display: none; padding: 24px 28px; }
|
||
.pg.on { display: block; }
|
||
|
||
/* ===== STATS ===== */
|
||
.stats { display: grid; grid-template-columns: repeat(4,1fr); gap: 14px; margin-bottom: 28px; }
|
||
|
||
.st {
|
||
background: var(--card); border: 1px solid var(--border);
|
||
border-radius: var(--r-lg); padding: 18px 20px;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.st:hover { box-shadow: var(--shadow); transform: translateY(-1px); }
|
||
|
||
.st-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||
|
||
.st-icon {
|
||
width: 32px; height: 32px; border-radius: var(--r);
|
||
display: flex; align-items: center; justify-content: center; font-size: 15px;
|
||
}
|
||
|
||
.st-tag {
|
||
font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 10px;
|
||
}
|
||
|
||
.st-val { font-size: 28px; font-weight: 700; letter-spacing: -1px; line-height: 1; }
|
||
.st-lbl { font-size: 12px; color: var(--text-3); margin-top: 3px; }
|
||
|
||
/* ===== TWO COL ===== */
|
||
.cols { display: grid; grid-template-columns: 1fr 340px; gap: 20px; }
|
||
.sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||
.sec-t { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-3); }
|
||
.sec-a { font-size: 12px; color: var(--accent); cursor: pointer; font-weight: 500; }
|
||
.sec-a:hover { text-decoration: underline; }
|
||
|
||
/* ===== COMMITS ===== */
|
||
.feed { display: flex; flex-direction: column; gap: 6px; }
|
||
.feed-day { font-size: 11px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: .8px; padding: 10px 0 2px; }
|
||
|
||
.cm {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 10px 14px; background: var(--card);
|
||
border: 1px solid var(--border); border-radius: var(--r);
|
||
transition: all 0.12s; cursor: default;
|
||
}
|
||
|
||
.cm:hover { border-color: var(--border-hover); background: var(--card-hover); }
|
||
|
||
.cm-dot {
|
||
width: 8px; height: 8px; border-radius: 50%;
|
||
margin-top: 5px; flex-shrink: 0;
|
||
}
|
||
|
||
.cm-body { flex: 1; min-width: 0; }
|
||
.cm-msg { font-size: 13px; font-weight: 500; line-height: 1.35; margin-bottom: 3px; word-break: break-word; }
|
||
.cm-meta { display: flex; gap: 8px; font-size: 11px; color: var(--text-3); align-items: center; }
|
||
.cm-repo { color: var(--accent); font-weight: 600; }
|
||
.cm-sha { font-family: 'JetBrains Mono', monospace; }
|
||
.cm-t { font-size: 12px; color: var(--text-3); font-family: 'JetBrains Mono', monospace; flex-shrink: 0; }
|
||
|
||
/* ===== SIDE PANELS ===== */
|
||
.side { display: flex; flex-direction: column; gap: 16px; }
|
||
|
||
.box {
|
||
background: var(--card); border: 1px solid var(--border);
|
||
border-radius: var(--r-lg); padding: 16px 18px;
|
||
}
|
||
|
||
.box-t { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-3); margin-bottom: 12px; }
|
||
|
||
.sv { display: flex; align-items: center; gap: 8px; padding: 7px 0; }
|
||
.sv-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
||
.sv-info { flex: 1; }
|
||
.sv-name { font-size: 13px; font-weight: 500; }
|
||
.sv-url { font-size: 10px; color: var(--text-3); }
|
||
.sv-st { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 8px; }
|
||
|
||
.bar-wrap { margin-bottom: 8px; }
|
||
.bar-head { display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 4px; }
|
||
.bar-head span:first-child { font-weight: 500; color: var(--text-2); }
|
||
.bar-head span:last-child { color: var(--text-3); }
|
||
.bar-track { height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; }
|
||
.bar-fill { height: 100%; border-radius: 3px; }
|
||
|
||
.ct { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 12px; border-bottom: 1px solid var(--border); }
|
||
.ct:last-child { border-bottom: none; }
|
||
.ct-n { font-weight: 500; }
|
||
.ct-d { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 8px; }
|
||
|
||
/* ===== PROJECTS ===== */
|
||
.pj-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
|
||
|
||
.pj {
|
||
background: var(--card); border: 1px solid var(--border);
|
||
border-radius: var(--r-lg); padding: 22px;
|
||
transition: all 0.15s; cursor: pointer;
|
||
}
|
||
|
||
.pj:hover { border-color: var(--accent); box-shadow: var(--shadow-lg); transform: translateY(-2px); }
|
||
|
||
.pj-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||
.pj-name { font-size: 17px; font-weight: 700; letter-spacing: -0.2px; }
|
||
.pj-lang { font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 12px; }
|
||
.pj-desc { font-size: 13px; color: var(--text-2); line-height: 1.45; margin-bottom: 14px; }
|
||
|
||
.pj-info { display: flex; gap: 14px; padding-top: 14px; border-top: 1px solid var(--border); font-size: 12px; color: var(--text-3); }
|
||
.pj-info b { color: var(--text-2); font-weight: 600; }
|
||
|
||
.pj-cms { margin-top: 14px; display: flex; flex-direction: column; gap: 4px; }
|
||
|
||
.pj-cm {
|
||
display: flex; align-items: center; gap: 8px;
|
||
padding: 6px 10px; background: var(--bg);
|
||
border-radius: var(--r); font-size: 12px;
|
||
}
|
||
|
||
.pj-cm-sha { font-family: 'JetBrains Mono', monospace; color: var(--accent); font-size: 11px; flex-shrink: 0; }
|
||
.pj-cm-msg { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-2); }
|
||
.pj-cm-t { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text-3); flex-shrink: 0; }
|
||
|
||
/* ===== ISSUES ===== */
|
||
.iss-list { display: flex; flex-direction: column; gap: 6px; }
|
||
|
||
.iss {
|
||
display: flex; align-items: flex-start; gap: 10px;
|
||
padding: 14px 18px; background: var(--card);
|
||
border: 1px solid var(--border); border-radius: var(--r);
|
||
transition: all 0.12s;
|
||
}
|
||
|
||
.iss:hover { border-color: var(--border-hover); }
|
||
|
||
.iss-ico {
|
||
width: 18px; height: 18px; border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
flex-shrink: 0; margin-top: 2px; font-size: 10px; font-weight: 700;
|
||
}
|
||
|
||
.iss-body { flex: 1; }
|
||
.iss-title { font-size: 14px; font-weight: 500; margin-bottom: 3px; }
|
||
.iss-meta { font-size: 11px; color: var(--text-3); display: flex; gap: 8px; }
|
||
.iss-lbl { font-size: 10px; font-weight: 600; padding: 1px 7px; border-radius: 8px; flex-shrink: 0; }
|
||
|
||
.empty { text-align: center; padding: 36px; color: var(--text-3); font-size: 13px; }
|
||
|
||
/* Animations */
|
||
.st-val {
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.st-val.updating {
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.cm, .pj, .iss {
|
||
animation: fadeSlideIn 0.35s ease-out both;
|
||
}
|
||
|
||
@keyframes fadeSlideIn {
|
||
0% { opacity: 0; transform: translateY(8px); }
|
||
100% { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.cm:nth-child(1) { animation-delay: 0s; }
|
||
.cm:nth-child(2) { animation-delay: 0.03s; }
|
||
.cm:nth-child(3) { animation-delay: 0.06s; }
|
||
.cm:nth-child(4) { animation-delay: 0.09s; }
|
||
.cm:nth-child(5) { animation-delay: 0.12s; }
|
||
.cm:nth-child(6) { animation-delay: 0.15s; }
|
||
.cm:nth-child(7) { animation-delay: 0.18s; }
|
||
.cm:nth-child(8) { animation-delay: 0.21s; }
|
||
.cm:nth-child(9) { animation-delay: 0.24s; }
|
||
.cm:nth-child(10) { animation-delay: 0.27s; }
|
||
|
||
.pj:nth-child(1) { animation-delay: 0s; }
|
||
.pj:nth-child(2) { animation-delay: 0.05s; }
|
||
.pj:nth-child(3) { animation-delay: 0.1s; }
|
||
.pj:nth-child(4) { animation-delay: 0.15s; }
|
||
.pj:nth-child(5) { animation-delay: 0.2s; }
|
||
.pj:nth-child(6) { animation-delay: 0.25s; }
|
||
|
||
.iss:nth-child(1) { animation-delay: 0s; }
|
||
.iss:nth-child(2) { animation-delay: 0.04s; }
|
||
.iss:nth-child(3) { animation-delay: 0.08s; }
|
||
.iss:nth-child(4) { animation-delay: 0.12s; }
|
||
.iss:nth-child(5) { animation-delay: 0.16s; }
|
||
.iss:nth-child(6) { animation-delay: 0.2s; }
|
||
|
||
.cnt-anim {
|
||
display: inline-block;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.cnt-anim.bump {
|
||
transform: scale(1.3);
|
||
}
|
||
|
||
.flash-new {
|
||
animation: flashHighlight 1.5s ease-out;
|
||
}
|
||
|
||
@keyframes flashHighlight {
|
||
0% { background: var(--accent-light); }
|
||
100% { background: var(--card); }
|
||
}
|
||
|
||
.bar-fill {
|
||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
.sv-st, .ct-d {
|
||
transition: color 0.3s, background 0.3s;
|
||
}
|
||
|
||
@media (max-width: 1100px) { .cols { grid-template-columns: 1fr; } .stats { grid-template-columns: repeat(2,1fr); } }
|
||
@media (max-width: 700px) { .nav { display: none; } .main { margin-left: 0; } .pj-grid { grid-template-columns: 1fr; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- AUTH -->
|
||
<div id="authScreen">
|
||
<div class="auth-bg"></div>
|
||
<div class="auth-grid"></div>
|
||
<div class="auth-particles" id="authParticles"></div>
|
||
<div class="auth-box">
|
||
<div class="auth-logo">
|
||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>
|
||
</div>
|
||
<div class="auth-title">24mycloud</div>
|
||
<div class="auth-sub">Панель управления инфраструктурой</div>
|
||
<div class="auth-field">
|
||
<svg class="auth-input-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||
<input class="auth-input" id="authPass" type="password" placeholder="Введите пароль" autofocus
|
||
onkeydown="if(event.key==='Enter')doAuth()">
|
||
</div>
|
||
<button class="auth-btn" onclick="doAuth()">
|
||
<span class="auth-btn-shine"></span>
|
||
Войти
|
||
</button>
|
||
<div class="auth-error" id="authErr"></div>
|
||
<div class="auth-footer">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||
Защищённое соединение
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- NAV -->
|
||
<nav class="nav">
|
||
<div class="nav-head">
|
||
<div class="nav-logo"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg></div>
|
||
<div><div class="nav-name">mycloud</div><div class="nav-sub">панель управления</div></div>
|
||
</div>
|
||
<div class="nav-group">
|
||
<div class="nav-group-label">Разделы</div>
|
||
<div class="nav-links">
|
||
<div class="nav-link on" data-p="overview">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
|
||
Обзор
|
||
</div>
|
||
<div class="nav-link" data-p="projects">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||
Проекты <span class="cnt" id="nRC">—</span>
|
||
</div>
|
||
<div class="nav-link" data-p="activity">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
||
Активность
|
||
</div>
|
||
<div class="nav-link" data-p="issues">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||
Задачи <span class="cnt" id="nIC">—</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="nav-group">
|
||
<div class="nav-group-label">Сервисы</div>
|
||
<div class="nav-links" id="nSvc"></div>
|
||
</div>
|
||
<div class="nav-foot">
|
||
<div class="nav-live"><div class="live-dot"></div> Все системы ОК</div>
|
||
<button class="theme-btn" id="themeBtn" onclick="toggleTheme()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- MAIN -->
|
||
<div class="main">
|
||
<div class="top">
|
||
<h2 id="pgT">Обзор</h2>
|
||
<div class="top-right">
|
||
<button class="top-btn" onclick="loadData()">Обновить</button>
|
||
<div class="top-clock" id="clk"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pg on" id="pg-overview">
|
||
<div class="stats" id="stG"></div>
|
||
<div class="cols">
|
||
<div>
|
||
<div class="sec-head">
|
||
<div class="sec-t">Последние коммиты</div>
|
||
<div class="sec-a" onclick="go('activity')">Все</div>
|
||
</div>
|
||
<div class="feed" id="oFeed"></div>
|
||
</div>
|
||
<div class="side">
|
||
<div class="box"><div class="box-t">Сервисы</div><div id="sSvc"></div></div>
|
||
<div class="box"><div class="box-t">Хранилище — RAID1</div><div id="sStor"></div></div>
|
||
<div class="box"><div class="box-t">SSL сертификаты</div><div id="sCert"></div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pg" id="pg-projects"><div class="pj-grid" id="pjG"></div></div>
|
||
<div class="pg" id="pg-activity"><div class="feed" id="aFeed"></div></div>
|
||
<div class="pg" id="pg-issues"><div class="iss-list" id="iList"></div></div>
|
||
</div>
|
||
|
||
<script>
|
||
// ===== CONFIG =====
|
||
const C = {
|
||
api: 'https://git.24mycloud.ru',
|
||
token: '157fcfc85bba2e9f6ce01c0327ecf15d7d10ab56',
|
||
pass: '5X4vTwEhhwUNT6ecmkgk9v2J',
|
||
refresh: 30000,
|
||
svcs: [
|
||
{ n:'Gitea', u:'https://git.24mycloud.ru', c:'var(--green)' },
|
||
{ n:'Immich', u:'https://photos.24mycloud.ru', c:'var(--blue)' },
|
||
{ n:'NPM', u:'http://192.168.0.97:81', c:'var(--orange)', lan:1 },
|
||
],
|
||
certs: [
|
||
{ d:'git.24mycloud.ru', e:'2026-07-19' },
|
||
{ d:'photos.24mycloud.ru', e:'2026-07-19' },
|
||
],
|
||
stor: { used:280, total:7475 },
|
||
};
|
||
|
||
// ===== MOCK =====
|
||
const MOCK = [
|
||
{ name:'CoffeeOs', full_name:'maksim/CoffeeOs', language:'TypeScript', private:true,
|
||
description:'POS-система Coffee Like — точки, маржинальность, KPI, аналитика продаж',
|
||
default_branch:'main',
|
||
commits: [
|
||
{sha:'5af055a92f',commit:{message:'chore: удалён устаревший GitHub Actions workflow',author:{name:'Maksim Grachev',date:ago(1)}}},
|
||
{sha:'1d675f3864',commit:{message:'fix(ui): убрана рамка accent у карточки маржи KPI',author:{name:'Maksim Grachev',date:ago(1.5)}}},
|
||
{sha:'be475a5acc',commit:{message:'feat(ui): улучшен дизайн KPI-карточек в блоке точной маржи',author:{name:'Maksim Grachev',date:ago(2)}}},
|
||
{sha:'f52904f94a',commit:{message:'test: health endpoint — источник деплоя для webhook Gitea',author:{name:'Maksim Grachev',date:ago(2.5)}}},
|
||
{sha:'da01c05214',commit:{message:'feat(ui): favicon заменён на лого Coffee Like',author:{name:'Maksim Grachev',date:ago(12)}}},
|
||
{sha:'76851ab6ef',commit:{message:'docs: лог сессии + active-work + changelog оптимизации 20.04',author:{name:'Maksim Grachev',date:ago(13)}}},
|
||
{sha:'3971d82df0',commit:{message:'feat(db): автообслуживание в ночной синхронизации + очистка индексов',author:{name:'Maksim Grachev',date:ago(14)}}},
|
||
],
|
||
issues: [
|
||
{number:12,title:'KPI: фильтр по периоду (неделя/месяц/квартал)',state:'open',created_at:ago(24),user:{login:'maksim'},labels:[{name:'улучшение',color:'84b6eb'}]},
|
||
{number:10,title:'Маржинальность неверно считается при скидке > 30%',state:'open',created_at:ago(48),user:{login:'maksim'},labels:[{name:'баг',color:'e11d48'}]},
|
||
{number:8,title:'Интеграция с iiko: синхронизация меню и стоп-листа',state:'open',created_at:ago(72),user:{login:'maksim'},labels:[{name:'фича',color:'22c55e'}]},
|
||
],
|
||
},
|
||
{ name:'Remington-CRM', full_name:'maksim/Remington-CRM', language:'Python', private:true,
|
||
description:'CRM + модерация Ozon/WB — фильтры отзывов, стоп-слова, автоответы, кэш',
|
||
default_branch:'main',
|
||
commits: [
|
||
{sha:'b0174270ab',commit:{message:'test: повторная проверка webhook автодеплоя',author:{name:'Vlad (Povelitel)',date:ago(0.5)}}},
|
||
{sha:'732541cbfc',commit:{message:'test: webhook автодеплой через Gitea',author:{name:'Vlad (Povelitel)',date:ago(0.8)}}},
|
||
{sha:'83c9a4a08f',commit:{message:'feat(ozon): стоп-слова + текстовый фильтр + exclude_words',author:{name:'Vlad (Povelitel)',date:ago(1.7)}}},
|
||
{sha:'9f43d61599',commit:{message:'fix(ozon): date picker с пресетами, исправлен fmtDateShort',author:{name:'Vlad (Povelitel)',date:ago(10)}}},
|
||
{sha:'33c763ed46',commit:{message:'fix(cache): сериализация Pydantic через model_dump перед кэшированием',author:{name:'Vlad (Povelitel)',date:ago(11)}}},
|
||
{sha:'4887635dc4',commit:{message:'feat(ozon): рейтинг, диапазон дат, фильтр matched-only',author:{name:'Vlad (Povelitel)',date:ago(12)}}},
|
||
],
|
||
issues: [
|
||
{number:5,title:'Ozon: экспорт отфильтрованных отзывов в Excel',state:'open',created_at:ago(12),user:{login:'maksim'},labels:[{name:'фича',color:'22c55e'}]},
|
||
{number:3,title:'Инвалидация кэша при обновлении стоп-слов',state:'open',created_at:ago(48),user:{login:'maksim'},labels:[{name:'баг',color:'e11d48'}]},
|
||
],
|
||
},
|
||
{ name:'BREVIO', full_name:'maksim/BREVIO', language:'TypeScript', private:true,
|
||
description:'AI-платформа кратких резюме встреч — транскрипция, суммаризация, action items',
|
||
default_branch:'main',
|
||
commits: [
|
||
{sha:'a1b2c3d4e5',commit:{message:'feat(ai): интеграция Whisper v3 для русской транскрипции',author:{name:'Maksim Grachev',date:ago(24)}}},
|
||
{sha:'f6g7h8i9j0',commit:{message:'fix(auth): race condition при обновлении токена',author:{name:'Maksim Grachev',date:ago(36)}}},
|
||
{sha:'k1l2m3n4o5',commit:{message:'refactor(api): генерация саммари вынесена в фоновую очередь',author:{name:'Maksim Grachev',date:ago(48)}}},
|
||
],
|
||
issues: [
|
||
{number:22,title:'Качество саммари: контекст из предыдущих встреч',state:'open',created_at:ago(24),user:{login:'maksim'},labels:[{name:'улучшение',color:'84b6eb'}]},
|
||
],
|
||
},
|
||
{ name:'NAS-Ugreen', full_name:'maksim/NAS-Ugreen', language:null, private:true,
|
||
description:'Конфигурации NAS UGREEN — Docker, Gitea, Immich, NPM, rclone, домен',
|
||
default_branch:'main',
|
||
commits: [
|
||
{sha:'p6q7r8s9t0',commit:{message:'docs: NPM + домен + обновлена сетевая схема',author:{name:'Maksim Grachev',date:ago(2)}}},
|
||
{sha:'u1v2w3x4y5',commit:{message:'feat: docker-compose NPM + SSL конфиг',author:{name:'Maksim Grachev',date:ago(3)}}},
|
||
],
|
||
issues: [],
|
||
},
|
||
{ name:'Coffee-Like-Auto', full_name:'maksim/Coffee-Like-Auto', language:'Python', private:true,
|
||
description:'Автоматизация Coffee Like — телеграм-боты, отчёты, интеграция 1С',
|
||
default_branch:'main',
|
||
commits: [
|
||
{sha:'z1a2b3c4d5',commit:{message:'feat(bot): ежедневный отчёт продаж с разбивкой по точкам',author:{name:'Vlad (Povelitel)',date:ago(72)}}},
|
||
{sha:'e6f7g8h9i0',commit:{message:'fix(1c): таймаут синхронизации большого каталога',author:{name:'Vlad (Povelitel)',date:ago(96)}}},
|
||
],
|
||
issues: [],
|
||
},
|
||
];
|
||
|
||
function ago(h) { return new Date(Date.now() - h*3600000).toISOString(); }
|
||
|
||
let D = { repos: [] };
|
||
let prevState = { commits: new Set(), stats: {} };
|
||
|
||
function animateValue(el, newVal) {
|
||
const old = el.textContent;
|
||
if (old === String(newVal)) return;
|
||
el.classList.add('updating');
|
||
setTimeout(() => {
|
||
el.textContent = newVal;
|
||
el.classList.remove('updating');
|
||
}, 150);
|
||
}
|
||
|
||
function bumpBadge(id) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.classList.remove('bump');
|
||
void el.offsetWidth;
|
||
el.classList.add('bump');
|
||
setTimeout(() => el.classList.remove('bump'), 300);
|
||
}
|
||
|
||
// ===== PARTICLES =====
|
||
(function(){
|
||
const cont = document.getElementById('authParticles');
|
||
for(let i=0;i<30;i++){
|
||
const p = document.createElement('div');
|
||
p.className = 'auth-particle';
|
||
p.style.left = Math.random()*100+'%';
|
||
p.style.animationDuration = (6+Math.random()*10)+'s';
|
||
p.style.animationDelay = Math.random()*8+'s';
|
||
p.style.width = p.style.height = (1+Math.random()*2)+'px';
|
||
if(Math.random()>0.5) p.style.background = 'rgba(59,130,246,0.3)';
|
||
cont.appendChild(p);
|
||
}
|
||
})();
|
||
|
||
// ===== AUTH =====
|
||
function doAuth() {
|
||
const v = document.getElementById('authPass').value;
|
||
const err = document.getElementById('authErr');
|
||
if (v === C.pass) {
|
||
document.getElementById('authScreen').classList.add('hidden');
|
||
sessionStorage.setItem('authed','1');
|
||
loadData();
|
||
} else {
|
||
err.textContent = 'Неверный пароль';
|
||
err.classList.remove('shake');
|
||
void err.offsetWidth;
|
||
err.classList.add('shake');
|
||
document.getElementById('authPass').value = '';
|
||
document.getElementById('authPass').focus();
|
||
}
|
||
}
|
||
|
||
if (sessionStorage.getItem('authed') === '1') {
|
||
document.getElementById('authScreen').classList.add('hidden');
|
||
loadData();
|
||
}
|
||
|
||
// ===== THEME =====
|
||
function toggleTheme() {
|
||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||
document.documentElement.setAttribute('data-theme', dark ? '' : 'dark');
|
||
document.getElementById('themeBtn').innerHTML = dark ? '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>' : '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
||
localStorage.setItem('theme', dark ? 'light' : 'dark');
|
||
}
|
||
|
||
if (localStorage.getItem('theme') === 'dark') {
|
||
document.documentElement.setAttribute('data-theme', 'dark');
|
||
document.getElementById('themeBtn').innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
|
||
}
|
||
|
||
// ===== NAV =====
|
||
document.querySelectorAll('.nav-link[data-p]').forEach(el => {
|
||
el.addEventListener('click', () => go(el.dataset.p));
|
||
});
|
||
|
||
function go(id) {
|
||
document.querySelectorAll('.nav-link[data-p]').forEach(n => n.classList.remove('on'));
|
||
document.querySelectorAll('.pg').forEach(p => p.classList.remove('on'));
|
||
document.querySelector(`[data-p="${id}"]`)?.classList.add('on');
|
||
document.getElementById(`pg-${id}`)?.classList.add('on');
|
||
const t = {overview:'Обзор',projects:'Проекты',activity:'Активность',issues:'Задачи'};
|
||
document.getElementById('pgT').textContent = t[id]||id;
|
||
}
|
||
|
||
// ===== CLOCK =====
|
||
setInterval(() => {
|
||
document.getElementById('clk').textContent =
|
||
new Date().toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||
}, 1000);
|
||
|
||
// ===== UTILS =====
|
||
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
||
function tAgo(d){const s=(Date.now()-new Date(d).getTime())/1000;if(s<60)return Math.floor(s)+'с';if(s<3600)return Math.floor(s/60)+'м';if(s<86400)return Math.floor(s/3600)+'ч';return Math.floor(s/86400)+'д';}
|
||
function hm(d){return new Date(d).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'});}
|
||
function isT(d){return new Date(d).toDateString()===new Date().toDateString();}
|
||
function cType(m){const x=m.match(/^(\w+)[\(:]/);return x?x[1].toLowerCase():'other';}
|
||
function dotC(t){return{feat:'var(--green)',fix:'var(--orange)',test:'var(--blue)',docs:'var(--purple)',chore:'var(--text-3)',refactor:'var(--accent)'}[t]||'var(--text-3)';}
|
||
|
||
// ===== API =====
|
||
async function loadData() {
|
||
try {
|
||
const h = {Accept:'application/json'};
|
||
if(C.token) h.Authorization = `token ${C.token}`;
|
||
const r = await fetch(`${C.api}/api/v1/repos/search?limit=50`,{headers:h});
|
||
if(!r.ok) throw new Error(r.status);
|
||
const repos = (await r.json()).data || [];
|
||
D.repos = await Promise.all(repos.map(async rp => {
|
||
const [cm,is] = await Promise.all([
|
||
fetch(`${C.api}/api/v1/repos/${rp.full_name}/commits?limit=15&sha=${rp.default_branch}`,{headers:h}).then(r=>r.json()).catch(()=>[]),
|
||
fetch(`${C.api}/api/v1/repos/${rp.full_name}/issues?state=open&limit=50&type=issues`,{headers:h}).then(r=>r.json()).catch(()=>[]),
|
||
]);
|
||
return {...rp, commits:cm, issues:is};
|
||
}));
|
||
render();
|
||
} catch(e) {
|
||
D.repos = MOCK;
|
||
render();
|
||
}
|
||
}
|
||
|
||
// ===== RENDER =====
|
||
let isFirstRender = true;
|
||
|
||
function render() {
|
||
rStats(); rFeed(); rProjects(); rActivity(); rIssues(); rSvcs(); rStor(); rCerts(); rNavSvc();
|
||
|
||
const rc = D.repos.length;
|
||
const ti = D.repos.reduce((s,r)=>s+(r.issues?.length||0),0);
|
||
|
||
const nRC = document.getElementById('nRC');
|
||
const nIC = document.getElementById('nIC');
|
||
if (nRC.textContent !== String(rc)) { nRC.textContent = rc; bumpBadge('nRC'); }
|
||
if (nIC.textContent !== String(ti||'0')) { nIC.textContent = ti||'0'; bumpBadge('nIC'); }
|
||
|
||
// Track commits for next diff
|
||
const newSet = new Set();
|
||
D.repos.forEach(r => r.commits.forEach(c => newSet.add(c.sha)));
|
||
prevState.commits = newSet;
|
||
isFirstRender = false;
|
||
}
|
||
|
||
function rStats() {
|
||
let tc=0,lp=null,lpN='',ar=0;
|
||
D.repos.forEach(r=>{
|
||
tc+=r.commits.filter(c=>isT(c.commit.author.date)).length;
|
||
if(r.commits[0]){
|
||
const t=new Date(r.commits[0].commit.author.date).getTime();
|
||
if(!lp||t>lp){lp=t;lpN=r.name;}
|
||
if(Date.now()-t<86400000)ar++;
|
||
}
|
||
});
|
||
|
||
// Animate existing stat values if they exist
|
||
const existing = document.querySelectorAll('.st-val[data-key]');
|
||
if (existing.length && !isFirstRender) {
|
||
const vals = { repos: D.repos.length, commits: tc, push: lp?tAgo(new Date(lp).toISOString()):'—', active: ar };
|
||
existing.forEach(el => {
|
||
const k = el.dataset.key;
|
||
if (vals[k] !== undefined && el.textContent !== String(vals[k])) {
|
||
animateValue(el, vals[k]);
|
||
}
|
||
});
|
||
// Update push label
|
||
const lbl = document.getElementById('pushLabel');
|
||
if (lbl) lbl.textContent = `Последний push · ${lpN}`;
|
||
return;
|
||
}
|
||
|
||
document.getElementById('stG').innerHTML = `
|
||
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--accent-light);color:var(--accent)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></div></div><div class="st-val" data-key="repos">${D.repos.length}</div><div class="st-lbl">Репозиториев</div></div>
|
||
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--green-bg);color:var(--green)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>${tc?'<span class="st-tag" style="background:var(--green-bg);color:var(--green)">сегодня</span>':''}</div><div class="st-val" data-key="commits">${tc}</div><div class="st-lbl">Коммитов сегодня</div></div>
|
||
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--orange-bg);color:var(--orange)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div></div><div class="st-val" data-key="push">${lp?tAgo(new Date(lp).toISOString()):'—'}</div><div class="st-lbl" id="pushLabel">Последний push · ${lpN}</div></div>
|
||
<div class="st"><div class="st-top"><div class="st-icon" style="background:var(--blue-bg);color:var(--blue)"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></div></div><div class="st-val" data-key="active">${ar}</div><div class="st-lbl">Активных (24ч)</div></div>`;
|
||
}
|
||
|
||
function cmRow(c,rn,sr){
|
||
const m=c.commit.message.split('\n')[0],t=cType(m);
|
||
const isNew = !isFirstRender && !prevState.commits.has(c.sha);
|
||
const cls = isNew ? 'cm flash-new' : 'cm';
|
||
return `<div class="${cls}"><div class="cm-dot" style="background:${dotC(t)}"></div><div class="cm-body"><div class="cm-msg">${esc(m)}</div><div class="cm-meta">${sr?`<span class="cm-repo">${rn}</span>`:''} <span class="cm-sha">${c.sha.slice(0,7)}</span> <span>${c.commit.author.name}</span></div></div><div class="cm-t">${hm(c.commit.author.date)}</div></div>`;
|
||
}
|
||
|
||
function rFeed(){
|
||
const a=[];
|
||
D.repos.forEach(r=>r.commits.slice(0,6).forEach(c=>a.push({rn:r.name,c})));
|
||
a.sort((x,y)=>new Date(y.c.commit.author.date)-new Date(x.c.commit.author.date));
|
||
document.getElementById('oFeed').innerHTML=a.slice(0,10).map(({rn,c})=>cmRow(c,rn,true)).join('');
|
||
}
|
||
|
||
function rProjects(){
|
||
const s=[...D.repos].sort((a,b)=>{
|
||
const at=a.commits[0]?new Date(a.commits[0].commit.author.date):0;
|
||
const bt=b.commits[0]?new Date(b.commits[0].commit.author.date):0;
|
||
return bt-at;
|
||
});
|
||
const L={TypeScript:{bg:'var(--blue-bg)',c:'var(--blue)'},Python:{bg:'var(--green-bg)',c:'var(--green)'},JavaScript:{bg:'var(--orange-bg)',c:'var(--orange)'}};
|
||
document.getElementById('pjG').innerHTML=s.map(r=>{
|
||
const l=L[r.language]||{bg:'var(--bg)',c:'var(--text-3)'};
|
||
const tc=r.commits.filter(c=>isT(c.commit.author.date)).length;
|
||
const la=r.commits[0]?tAgo(r.commits[0].commit.author.date)+' назад':'нет активности';
|
||
return `<div class="pj" onclick="window.open('${C.api}/${r.full_name}','_blank')">
|
||
<div class="pj-top"><div class="pj-name">${esc(r.name)}</div><div class="pj-lang" style="background:${l.bg};color:${l.c}">${r.language||'docs'}</div></div>
|
||
<div class="pj-desc">${esc(r.description||'Нет описания')}</div>
|
||
<div class="pj-info"><span><b>${tc}</b> сегодня</span><span><b>${r.issues?.length||0}</b> задач</span><span style="margin-left:auto">${la}</span></div>
|
||
<div class="pj-cms">${r.commits.slice(0,3).map(c=>`<div class="pj-cm"><span class="pj-cm-sha">${c.sha.slice(0,7)}</span><span class="pj-cm-msg">${esc(c.commit.message.split('\n')[0])}</span><span class="pj-cm-t">${hm(c.commit.author.date)}</span></div>`).join('')}</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function rActivity(){
|
||
const a=[];
|
||
D.repos.forEach(r=>r.commits.forEach(c=>a.push({rn:r.name,c})));
|
||
a.sort((x,y)=>new Date(y.c.commit.author.date)-new Date(x.c.commit.author.date));
|
||
let h='',ld='';
|
||
a.slice(0,30).forEach(({rn,c})=>{
|
||
const d=new Date(c.commit.author.date).toLocaleDateString('ru-RU',{weekday:'long',day:'numeric',month:'long'});
|
||
if(d!==ld){h+=`<div class="feed-day">${d}</div>`;ld=d;}
|
||
h+=cmRow(c,rn,true);
|
||
});
|
||
document.getElementById('aFeed').innerHTML=h||'<div class="empty">Нет активности</div>';
|
||
}
|
||
|
||
function rIssues(){
|
||
const a=[];
|
||
D.repos.forEach(r=>(r.issues||[]).forEach(i=>a.push({rn:r.name,i})));
|
||
a.sort((x,y)=>new Date(y.i.created_at)-new Date(x.i.created_at));
|
||
if(!a.length){document.getElementById('iList').innerHTML='<div class="empty">Нет открытых задач</div>';return;}
|
||
document.getElementById('iList').innerHTML=a.map(({rn,i})=>`
|
||
<div class="iss">
|
||
<div class="iss-ico" style="background:var(--green-bg);color:var(--green);border:1px solid color-mix(in srgb, var(--green) 20%, transparent)">!</div>
|
||
<div class="iss-body"><div class="iss-title">${esc(i.title)}</div><div class="iss-meta"><span style="color:var(--accent);font-weight:600">${rn}</span><span>#${i.number}</span><span>${tAgo(i.created_at)} назад</span></div></div>
|
||
${(i.labels||[]).map(l=>`<span class="iss-lbl" style="background:${l.color?'#'+l.color+'18':'var(--bg)'};color:${l.color?'#'+l.color:'var(--text-3)'}">${esc(l.name)}</span>`).join('')}
|
||
</div>`).join('');
|
||
}
|
||
|
||
function rSvcs(){
|
||
document.getElementById('sSvc').innerHTML=C.svcs.map(s=>`<div class="sv"><div class="sv-dot" style="background:${s.c}"></div><div class="sv-info"><div class="sv-name">${s.n}</div><div class="sv-url">${s.u.replace(/https?:\/\//,'')}</div></div><span class="sv-st" style="background:var(--green-bg);color:var(--green)">${s.lan?'LAN':'онлайн'}</span></div>`).join('');
|
||
}
|
||
|
||
function rNavSvc(){
|
||
document.getElementById('nSvc').innerHTML=C.svcs.map(s=>`<div class="nav-link" onclick="window.open('${s.u}','_blank')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/></svg>${s.n}</div>`).join('');
|
||
}
|
||
|
||
function rStor(){
|
||
const p=((C.stor.used/C.stor.total)*100).toFixed(1);
|
||
document.getElementById('sStor').innerHTML=`<div class="bar-wrap"><div class="bar-head"><span>${C.stor.used} GiB занято</span><span>${(C.stor.total/1024).toFixed(1)} TiB · ${p}%</span></div><div class="bar-track"><div class="bar-fill" style="width:${p}%;background:linear-gradient(90deg,var(--accent),var(--blue))"></div></div></div>`;
|
||
}
|
||
|
||
function rCerts(){
|
||
const n=Date.now();
|
||
document.getElementById('sCert').innerHTML=C.certs.map(c=>{
|
||
const d=Math.floor((new Date(c.e)-n)/86400000);
|
||
const s=d>30?'background:var(--green-bg);color:var(--green)':d>7?'background:var(--orange-bg);color:var(--orange)':'background:var(--red-bg);color:var(--red)';
|
||
return `<div class="ct"><span class="ct-n">${c.d}</span><span class="ct-d" style="${s}">${d}д</span></div>`;
|
||
}).join('');
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|