daily update 2026-03-29

This commit is contained in:
Агальцов Антон
2026-03-30 00:02:10 +03:00
commit 690abf379c
145 changed files with 14399 additions and 0 deletions
+413
View File
@@ -0,0 +1,413 @@
:root {
--warm-bg: #f5f0e8;
--warm-primary: #c4623a;
--warm-text: #3d3530;
--warm-gray: #6b6560;
--warm-accent: #d4a853;
--warm-white: #fffdf9;
}
body {
background-color: var(--warm-bg);
color: var(--warm-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.site-header {
background-color: var(--warm-primary);
padding: 0.5rem 0;
}
.site-header .navbar-brand {
color: var(--warm-white);
font-weight: 600;
}
.site-header .nav-link {
color: rgba(255, 255, 255, 0.85);
padding: 0.5rem 1rem;
transition: color 0.2s;
}
.site-header .nav-link:hover,
.site-header .nav-link.active {
color: var(--warm-white);
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.navbar-toggler {
border-color: rgba(255, 255, 255, 0.5);
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
.search-form input {
background-color: rgba(255, 255, 255, 0.9);
border: none;
}
.search-form input:focus {
background-color: #fff;
box-shadow: 0 0 0 2px var(--warm-accent);
}
/* Search Results */
.search-results {
position: absolute;
top: 100%;
right: 0;
left: 0;
background: var(--warm-white);
border-top: 1px solid var(--warm-gray);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
max-height: 400px;
overflow-y: auto;
}
.search-results-content {
padding: 1rem;
max-width: 600px;
margin: 0 auto;
}
.search-results ul {
list-style: none;
padding: 0;
margin: 0;
}
.search-results li {
padding: 0.5rem 0;
border-bottom: 1px solid var(--warm-bg);
}
.search-results li:last-child {
border-bottom: none;
}
.search-results a {
color: var(--warm-text);
text-decoration: none;
}
.search-results a:hover {
color: var(--warm-primary);
}
/* Sidebar */
.sidebar {
background-color: var(--warm-white);
border-right: 1px solid #e0d8cc;
min-height: calc(100vh - 60px);
padding: 1.5rem 1rem;
}
.sidebar-content h6 {
color: var(--warm-primary);
font-weight: 600;
margin-bottom: 0.75rem;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sidebar-content .nav-link {
color: var(--warm-text);
padding: 0.375rem 0.75rem;
border-radius: 4px;
font-size: 0.9rem;
}
.sidebar-content .nav-link:hover {
background-color: var(--warm-bg);
}
/* Main Content */
.main-content {
padding: 2rem;
flex: 1;
}
/* Cards */
.card {
background-color: var(--warm-white);
border: 1px solid #e0d8cc;
border-radius: 8px;
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.card-title a {
color: var(--warm-text);
text-decoration: none;
}
.card-title a:hover {
color: var(--warm-primary);
}
/* Plan Cards */
.plan-card {
margin-bottom: 1rem;
}
.plan-card .status-badge {
font-size: 0.875rem;
}
.plan-card.planned {
border-left: 4px solid var(--warm-accent);
}
.plan-card.in-progress {
border-left: 4px solid #e67e22;
}
.plan-card.done {
border-left: 4px solid #27ae60;
opacity: 0.8;
}
.plan-card.cancelled {
border-left: 4px solid #e74c3c;
opacity: 0.6;
}
/* Diary Timeline */
.diary-timeline {
position: relative;
padding-left: 2rem;
}
.diary-timeline::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 2px;
background-color: var(--warm-primary);
}
.diary-entry {
position: relative;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #e0d8cc;
}
.diary-entry:last-child {
border-bottom: none;
}
.diary-entry::before {
content: '';
position: absolute;
left: -2rem;
top: 0.5rem;
width: 12px;
height: 12px;
background-color: var(--warm-primary);
border-radius: 50%;
transform: translateX(-5px);
}
.diary-entry .time {
color: var(--warm-gray);
font-size: 0.875rem;
}
.diary-date {
color: var(--warm-primary);
font-weight: 600;
}
/* Credentials */
.creds-card {
position: relative;
}
.creds-card .encrypted-data {
font-family: monospace;
background: var(--warm-bg);
padding: 1rem;
border-radius: 4px;
word-break: break-all;
font-size: 0.75rem;
}
.creds-card .decrypted-fields {
background: var(--warm-bg);
padding: 1rem;
border-radius: 4px;
}
.creds-card .field-row {
display: flex;
margin-bottom: 0.5rem;
}
.creds-card .field-label {
font-weight: 600;
min-width: 100px;
color: var(--warm-gray);
}
.creds-card .field-value {
flex: 1;
font-family: monospace;
}
/* Article styles */
.article-content {
background-color: var(--warm-white);
padding: 2rem;
border-radius: 8px;
border: 1px solid #e0d8cc;
}
.article-content h1,
.article-content h2,
.article-content h3 {
color: var(--warm-text);
margin-top: 1.5rem;
}
.article-content h1 {
margin-top: 0;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--warm-primary);
}
.article-content table {
width: 100%;
margin-bottom: 1rem;
background: var(--warm-white);
}
.article-content th {
background-color: var(--warm-primary);
color: white;
font-weight: 600;
}
.article-content th,
.article-content td {
padding: 0.75rem;
border: 1px solid #e0d8cc;
}
.article-content code {
background-color: var(--warm-bg);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}
.article-content pre {
background-color: var(--warm-text);
color: var(--warm-bg);
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
.article-content pre code {
background: none;
padding: 0;
}
.article-content a {
color: var(--warm-primary);
}
.article-content a:hover {
color: var(--warm-accent);
}
/* Tags */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.tag {
background-color: var(--warm-bg);
color: var(--warm-gray);
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
text-decoration: none;
}
.tag:hover {
background-color: var(--warm-primary);
color: white;
}
/* Footer */
.site-footer {
background-color: var(--warm-text);
color: var(--warm-bg);
padding: 1rem 0;
margin-top: auto;
}
/* Responsive */
@media (max-width: 767.98px) {
.sidebar {
position: fixed;
top: 60px;
left: 0;
bottom: 0;
z-index: 1000;
width: 280px;
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.show {
transform: translateX(0);
}
.main-content {
width: 100%;
padding: 1rem;
}
.search-form {
width: 100%;
margin-top: 0.5rem;
}
}
/* Context Quote */
.context-quote {
background-color: var(--warm-bg);
border-left: 4px solid var(--warm-accent);
padding: 1rem;
margin-top: 1rem;
font-style: italic;
}
.context-quote cite {
display: block;
text-align: right;
margin-top: 0.5rem;
font-style: normal;
color: var(--warm-gray);
}
+145
View File
@@ -0,0 +1,145 @@
// Credentials encryption/decryption using Web Crypto API
document.addEventListener('DOMContentLoaded', function() {
const decryptButtons = document.querySelectorAll('.decrypt-btn');
decryptButtons.forEach(button => {
button.addEventListener('click', async function() {
const id = this.dataset.id;
const encryptedDiv = document.getElementById('encrypted-' + id);
const decryptedDiv = document.getElementById('decrypted-' + id);
// Get master password
const password = prompt('Введите мастер-пароль:');
if (!password) return;
try {
const encryptedBase64 = encryptedDiv.textContent.trim();
const decrypted = await decrypt(encryptedBase64, password);
// Parse and display decrypted content
const data = JSON.parse(decrypted);
let html = '';
for (const [key, value] of Object.entries(data)) {
const label = formatLabel(key);
html += `<div class="field-row">
<span class="field-label">${label}:</span>
<span class="field-value">${value}</span>
</div>`;
}
decryptedDiv.innerHTML = html;
decryptedDiv.style.display = 'block';
encryptedDiv.style.display = 'none';
this.textContent = 'Скрыть';
this.classList.remove('btn-primary');
this.classList.add('btn-secondary');
// Change button to toggle
this.onclick = function() {
if (decryptedDiv.style.display === 'none') {
decryptedDiv.style.display = 'block';
encryptedDiv.style.display = 'none';
this.textContent = 'Скрыть';
} else {
decryptedDiv.style.display = 'none';
encryptedDiv.style.display = 'block';
this.textContent = 'Расшифровать';
}
};
} catch (error) {
console.error('Decryption error:', error);
alert('Ошибка расшифровки. Проверьте мастер-пароль.');
}
});
});
});
function formatLabel(key) {
const labels = {
'user': 'Пользователь',
'password': 'Пароль',
'host': 'Хост',
'port': 'Порт',
'database': 'База данных',
'notes': 'Заметки',
'ip': 'IP',
'auth': 'Авторизация',
'vpn': 'VPN'
};
return labels[key] || key;
}
// Derive encryption key from password using PBKDF2
async function deriveKey(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
// Decrypt data
async function decrypt(encryptedBase64, password) {
const encryptedData = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
// Extract salt (first 16 bytes), IV (next 12 bytes), and ciphertext
const salt = encryptedData.slice(0, 16);
const iv = encryptedData.slice(16, 28);
const ciphertext = encryptedData.slice(28);
const key = await deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
}
// Encrypt data (for creating new credentials)
async function encrypt(plaintext, password) {
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encoder.encode(plaintext)
);
// Combine salt + iv + ciphertext
const combined = new Uint8Array(16 + 12 + ciphertext.byteLength);
combined.set(salt, 0);
combined.set(iv, 16);
combined.set(new Uint8Array(ciphertext), 28);
return btoa(String.fromCharCode(...combined));
}
// Utility function for encrypting credentials (can be used in console)
window.encryptCreds = async function(data, password) {
const json = JSON.stringify(data);
return await encrypt(json, password);
};
+90
View File
@@ -0,0 +1,90 @@
// Search functionality using Fuse.js
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('search-input');
const searchForm = document.querySelector('.search-form');
const searchResults = document.getElementById('search-results');
const resultsList = document.getElementById('results-list');
const closeSearch = document.getElementById('close-search');
if (!searchInput) return;
let fuse;
// Load search index
fetch('/index.json')
.then(response => response.json())
.then(data => {
fuse = new Fuse(data, {
keys: ['title', 'content', 'tags'],
includeScore: true,
threshold: 0.3,
ignoreLocation: true,
minMatchCharLength: 2
});
})
.catch(err => console.error('Failed to load search index:', err));
function performSearch(query) {
if (!fuse || !query.trim()) {
searchResults.style.display = 'none';
return;
}
const results = fuse.search(query, { limit: 10 });
if (results.length === 0) {
resultsList.innerHTML = '<li>Ничего не найдено</li>';
} else {
resultsList.innerHTML = results.map(result => {
const item = result.item;
const sectionLabels = {
'diary': 'Дневник',
'kb': 'База знаний',
'plans': 'Планы',
'guides': 'Инструкции',
'creds': 'Креды'
};
const label = sectionLabels[item.section] || item.section;
return `<li>
<a href="${item.url}">
<strong>${item.title}</strong>
<small class="text-muted d-block">${label}</small>
</a>
</li>`;
}).join('');
}
searchResults.style.display = 'block';
}
// Debounce function
let debounceTimer;
function debounce(func, delay) {
return function() {
const context = this;
const args = arguments;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => func.apply(context, args), delay);
};
}
searchInput.addEventListener('input', debounce(function(e) {
performSearch(e.target.value);
}, 200));
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
performSearch(searchInput.value);
});
closeSearch.addEventListener('click', function() {
searchResults.style.display = 'none';
});
// Close search when clicking outside
document.addEventListener('click', function(e) {
if (!searchForm.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
});