daily update 2026-03-29
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user