diff --git a/assets/css/style.css b/assets/css/style.css
index 2a4c2a7..58b3805 100644
--- a/assets/css/style.css
+++ b/assets/css/style.css
@@ -1,18 +1,760 @@
-html { scroll-behavior: smooth; }
-*{ box-sizing: border-box; margin:0; padding:0;}
+/* ==========================================================================
+ SYSTEM BLOCKNOTES - MAIN CSS (VANILLA CSS WITH HSL THEME VARIABLES)
+ ========================================================================== */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap');
-.main-nav{
- display:flex;
- width:100%;
- gap: 1rem;
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: #0b0f19;
+ color: #f1f5f9;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ line-height: 1.6;
+}
+
+a {
+ color: var(--secCol14, #38bdf8);
+ text-decoration: none;
+ transition: all 0.2s ease;
+}
+
+a:hover {
+ color: var(--secCol18, #bae6fd);
+}
+
+/* --- HEADER & NAVIGATION --- */
+.navbar-header {
+ background: rgba(15, 23, 42, 0.85);
+ backdrop-filter: blur(12px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+}
+
+.nav-container {
+ max-width: 1240px;
+ margin: 0 auto;
+ padding: 0.85rem 1.5rem;
+ display: flex;
align-items: center;
- justify-content: flex-start;
- list-style-type: none;
- padding:1rem;
+ justify-content: space-between;
+}
+
+.brand-logo {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ font-weight: 700;
+ font-size: 1.35rem;
+ color: #ffffff;
+ letter-spacing: -0.02em;
+}
+
+.brand-logo .logo-icon {
+ font-size: 1.6rem;
+}
+
+.main-nav {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ list-style: none;
}
.main-nav a {
+ color: #94a3b8;
+ font-weight: 500;
+ font-size: 0.95rem;
+ padding: 0.5rem 0.9rem;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+}
+
+.main-nav a:hover,
+.main-nav a.active {
+ color: #ffffff;
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.admin-link {
+ color: var(--thiCol15, #c084fc) !important;
+ background: rgba(192, 132, 252, 0.1) !important;
+ border: 1px solid rgba(192, 132, 252, 0.25);
+}
+
+.admin-link:hover,
+.admin-link.active {
+ background: rgba(192, 132, 252, 0.25) !important;
+ color: #ffffff !important;
+}
+
+.user-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.82rem;
+ font-weight: 600;
+ padding: 0.35rem 0.75rem;
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: #e2e8f0;
+}
+
+.badge-admin {
+ background: rgba(234, 179, 8, 0.15);
+ border-color: rgba(234, 179, 8, 0.3);
+ color: #fde047;
+}
+
+.nav-logout-btn {
+ color: #ef4444 !important;
+ background: rgba(239, 68, 68, 0.08) !important;
+}
+
+.nav-logout-btn:hover {
+ background: rgba(239, 68, 68, 0.2) !important;
+ color: #fca5a5 !important;
+}
+
+/* --- MAIN WRAPPER & FOOTER --- */
+.main-wrapper {
+ max-width: 1240px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 2rem 1.5rem;
+ flex: 1;
+}
+
+.main-footer {
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ background: #090d16;
+ padding: 1.5rem 0;
+ margin-top: auto;
+ color: #64748b;
+ font-size: 0.85rem;
+ text-align: center;
+}
+
+.footer-container {
+ max-width: 1240px;
+ margin: 0 auto;
+ padding: 0 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+}
+
+/* --- PAGE HEADERS & CARDS --- */
+.page-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 2rem;
+ flex-wrap: wrap;
+ gap: 1rem;
+}
+
+.page-title-group h1 {
+ font-size: 1.85rem;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 0.3rem;
+}
+
+.page-title-group p {
+ color: #94a3b8;
+ font-size: 0.95rem;
+}
+
+.card {
+ background: #131b2e;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 14px;
+ padding: 1.5rem;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+}
+
+/* --- BUTTONS --- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.65rem 1.2rem;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ font-weight: 600;
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
text-decoration: none;
}
+.btn-sm {
+ padding: 0.4rem 0.75rem;
+ font-size: 0.82rem;
+ border-radius: 6px;
+}
+
+.btn-primary {
+ background: var(--priCol10, #22c55e);
+ color: #042f13;
+}
+
+.btn-primary:hover {
+ background: var(--priCol12, #4ade80);
+ color: #042f13;
+ box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
+}
+
+.btn-secondary {
+ background: var(--secCol8, #0284c7);
+ color: #ffffff;
+}
+
+.btn-secondary:hover {
+ background: var(--secCol10, #0ea5e9);
+ color: #ffffff;
+ box-shadow: 0 0 15px rgba(14, 165, 233, 0.4);
+}
+
+.btn-dark {
+ background: rgba(255, 255, 255, 0.08);
+ color: #e2e8f0;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+}
+
+.btn-dark:hover {
+ background: rgba(255, 255, 255, 0.15);
+ color: #ffffff;
+}
+
+.btn-danger {
+ background: rgba(239, 68, 68, 0.15);
+ color: #f87171;
+ border: 1px solid rgba(239, 68, 68, 0.3);
+}
+
+.btn-danger:hover {
+ background: #ef4444;
+ color: #ffffff;
+}
+
+/* --- BLOCKNOTE GRID & CARDS --- */
+.blocknotes-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 1.5rem;
+}
+
+.blocknote-card {
+ background: #131b2e;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 14px;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
+ position: relative;
+}
+
+.blocknote-card:hover {
+ transform: translateY(-4px);
+ border-color: rgba(255, 255, 255, 0.2);
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.35);
+}
+
+.blocknote-card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+.blocknote-title {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #ffffff;
+ line-height: 1.3;
+}
+
+.blocknote-owner-tag {
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 0.25rem 0.6rem;
+ border-radius: 20px;
+ background: rgba(14, 165, 233, 0.15);
+ color: #7dd3fc;
+ border: 1px solid rgba(14, 165, 233, 0.3);
+ white-space: nowrap;
+}
+
+.blocknote-card-body {
+ font-size: 0.9rem;
+ color: #94a3b8;
+ flex: 1;
+}
+
+.blocknote-meta {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ font-size: 0.8rem;
+ color: #64748b;
+}
+
+.blocknote-card-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+/* --- VIEW BLOCKNOTE DETAILED NOTE BLOCKS --- */
+.notes-container {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.note-block {
+ background: #131b2e;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
+ transition: all 0.25s ease;
+}
+
+.note-header {
+ background: rgba(255, 255, 255, 0.03);
+ padding: 1.1rem 1.5rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.2s ease;
+}
+
+.note-header:hover {
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.note-title-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.collapse-icon {
+ font-size: 0.85rem;
+ color: var(--secCol14, #38bdf8);
+ transition: transform 0.25s ease;
+ display: inline-block;
+}
+
+.note-block.collapsed .collapse-icon {
+ transform: rotate(0deg);
+}
+
+.note-block:not(.collapsed) .collapse-icon {
+ transform: rotate(90deg);
+}
+
+.subnote-count-badge {
+ font-size: 0.78rem;
+ font-weight: 600;
+ padding: 0.2rem 0.6rem;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.07);
+ color: #94a3b8;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.note-block.collapsed .note-header {
+ border-bottom: none;
+}
+
+.note-block.collapsed .note-body {
+ display: none;
+}
+
+.note-description-box {
+ padding: 1rem 1.5rem 0.5rem 1.5rem;
+ font-size: 0.92rem;
+ color: #94a3b8;
+ border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
+}
+
+.note-title-area h3 {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 0;
+}
+
+.note-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-shrink: 0;
+}
+
+.subnotes-list {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1.25rem;
+}
+
+.subnote-card {
+ background: #1a243a;
+ border: 1px solid rgba(255, 255, 255, 0.07);
+ border-radius: 12px;
+ padding: 1.25rem;
+ position: relative;
+}
+
+.subnote-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.85rem;
+}
+
+.subnote-title-group {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.subnote-title {
+ font-size: 1.05rem;
+ font-weight: 600;
+ color: #f1f5f9;
+}
+
+.type-badge {
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ padding: 0.2rem 0.5rem;
+ border-radius: 4px;
+}
+
+.type-badge.text {
+ background: rgba(148, 163, 184, 0.15);
+ color: #cbd5e1;
+}
+
+.type-badge.command {
+ background: rgba(234, 179, 8, 0.15);
+ color: #fde047;
+ border: 1px solid rgba(234, 179, 8, 0.3);
+}
+
+.subnote-content-text {
+ font-size: 0.95rem;
+ color: #cbd5e1;
+ white-space: pre-wrap;
+ background: rgba(0, 0, 0, 0.2);
+ padding: 0.85rem 1rem;
+ border-radius: 8px;
+ border-left: 3px solid var(--secCol10, #0ea5e9);
+}
+
+/* --- COMMAND BLOCK WITH COPY BUTTON --- */
+.command-block-wrapper {
+ position: relative;
+ background: #050811;
+ border: 1px solid rgba(234, 179, 8, 0.35);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), inset 0 0 10px rgba(234, 179, 8, 0.03);
+ border-radius: 10px;
+ overflow: hidden;
+ margin-top: 0.75rem;
+}
+
+.command-block-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: rgba(234, 179, 8, 0.12);
+ padding: 0.45rem 0.9rem;
+ border-bottom: 1px solid rgba(234, 179, 8, 0.2);
+ font-size: 0.8rem;
+ font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
+ color: #fde047;
+ font-weight: 600;
+}
+
+.command-badge-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ font-size: 0.82rem;
+ letter-spacing: 0.3px;
+}
+
+.copy-btn {
+ background: rgba(255, 255, 255, 0.1);
+ color: #ffffff;
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ padding: 0.3rem 0.75rem;
+ border-radius: 6px;
+ font-size: 0.78rem;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
+ transition: all 0.2s ease;
+}
+
+.copy-btn:hover {
+ background: #eab308;
+ color: #000000;
+ border-color: #eab308;
+ box-shadow: 0 0 10px rgba(234, 179, 8, 0.4);
+}
+
+.copy-btn.copied {
+ background: #22c55e !important;
+ color: #042f13 !important;
+ border-color: #22c55e !important;
+}
+
+.command-content-code {
+ font-family: 'Fira Code', 'Cascadia Code', 'Monaco', 'Consolas', monospace;
+ font-size: 0.88rem;
+ line-height: 1.5;
+ color: #4ade80;
+ background: #03060d;
+ padding: 0.85rem 1rem;
+ overflow-x: auto;
+ overflow-y: auto;
+ max-height: 320px;
+ white-space: pre;
+ margin: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.6);
+}
+
+.command-content-code code {
+ font-family: inherit;
+ color: inherit;
+}
+
+/* --- FORMS & INPUTS --- */
+.form-group {
+ margin-bottom: 1.25rem;
+}
+
+.form-group label {
+ display: block;
+ font-size: 0.88rem;
+ font-weight: 600;
+ color: #cbd5e1;
+ margin-bottom: 0.4rem;
+}
+
+.form-control {
+ width: 100%;
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ background: #090d16;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ color: #ffffff;
+ font-size: 0.95rem;
+ font-family: inherit;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.form-control:focus {
+ outline: none;
+ border-color: var(--secCol10, #0ea5e9);
+ box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.2);
+}
+
+textarea.form-control {
+ resize: vertical;
+ min-height: 100px;
+}
+
+select.form-control {
+ cursor: pointer;
+}
+
+/* --- MODAL --- */
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.75);
+ backdrop-filter: blur(6px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.25s ease;
+}
+
+.modal-backdrop.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-card {
+ background: #131b2e;
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 16px;
+ width: 100%;
+ max-width: 540px;
+ padding: 1.75rem;
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
+ transform: translateY(20px);
+ transition: transform 0.25s ease;
+}
+
+.modal-backdrop.active .modal-card {
+ transform: translateY(0);
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 1.25rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.modal-header h3 {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.modal-close-btn {
+ background: transparent;
+ border: none;
+ color: #94a3b8;
+ font-size: 1.5rem;
+ cursor: pointer;
+ line-height: 1;
+}
+
+.modal-close-btn:hover {
+ color: #ffffff;
+}
+
+/* --- ALERTS & NOTIFICATIONS --- */
+.alert {
+ padding: 0.9rem 1.2rem;
+ border-radius: 10px;
+ margin-bottom: 1.5rem;
+ font-size: 0.92rem;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+}
+
+.alert-success {
+ background: rgba(34, 197, 94, 0.15);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ color: #4ade80;
+}
+
+.alert-danger {
+ background: rgba(239, 68, 68, 0.15);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ color: #f87171;
+}
+
+/* --- TABLES (ADMIN) --- */
+.table-responsive {
+ overflow-x: auto;
+}
+
+.custom-table {
+ width: 100%;
+ border-collapse: collapse;
+ text-align: left;
+ font-size: 0.9rem;
+}
+
+.custom-table th {
+ background: rgba(255, 255, 255, 0.04);
+ color: #cbd5e1;
+ font-weight: 600;
+ padding: 0.85rem 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.custom-table td {
+ padding: 0.85rem 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+ color: #e2e8f0;
+}
+
+.custom-table tr:hover td {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+/* --- COLOR PICKER PREVIEW --- */
+.color-preview-box {
+ height: 60px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: #0f172a;
+ margin-top: 0.5rem;
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ padding: 3rem 1rem;
+ background: #131b2e;
+ border: 1px border-dash rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+}
+
+.empty-state-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ opacity: 0.7;
+}
+
+.empty-state h3 {
+ font-size: 1.3rem;
+ font-weight: 700;
+ color: #ffffff;
+ margin-bottom: 0.5rem;
+}
+
+.empty-state p {
+ color: #94a3b8;
+ max-width: 400px;
+ margin: 0 auto 1.5rem auto;
+}
diff --git a/assets/js/main.js b/assets/js/main.js
index 5f8d470..048d4fc 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -1,9 +1,211 @@
-/* @ default js*/
-function yearNow(textlocation) {
- //This is a function to substitutes a value into the current year.
- //Usually good for the footer.
- var data = new Date();
- var year;
- year = data.getFullYear();
- document.querySelector(textlocation).replaceWith(year);
+/**
+ * Blocknotes Application Main JavaScript (Vanilla JS)
+ */
+
+document.addEventListener("DOMContentLoaded", function() {
+
+ // --- 1. MODALS OPEN & CLOSE HANDLERS ---
+ document.querySelectorAll('[data-modal-target]').forEach(function(trigger) {
+ trigger.addEventListener('click', function(e) {
+ e.preventDefault();
+ const modalId = this.getAttribute('data-modal-target');
+ const modal = document.getElementById(modalId);
+ if (modal) {
+ modal.classList.add('active');
+ }
+ });
+ });
+
+ document.querySelectorAll('.modal-close-btn, .modal-close-trigger').forEach(function(closeBtn) {
+ closeBtn.addEventListener('click', function() {
+ const modal = this.closest('.modal-backdrop');
+ if (modal) {
+ modal.classList.remove('active');
+ }
+ });
+ });
+
+ // Close modal when clicking on backdrop outside modal-card
+ document.querySelectorAll('.modal-backdrop').forEach(function(backdrop) {
+ backdrop.addEventListener('click', function(e) {
+ if (e.target === this) {
+ this.classList.remove('active');
+ }
+ });
+ });
+
+ // --- 2. LIVE HSL THEME COLOR PREVIEW (PROFILE PAGE) ---
+ const priInput = document.getElementById('theme_pri');
+ const secInput = document.getElementById('theme_sec');
+ const thiInput = document.getElementById('theme_thi');
+
+ if (priInput && secInput && thiInput) {
+ function updateLiveTheme() {
+ const priVal = priInput.value;
+ const secVal = secInput.value;
+ const thiVal = thiInput.value;
+
+ document.documentElement.style.setProperty('--pri', priVal);
+ document.documentElement.style.setProperty('--sec', secVal);
+ document.documentElement.style.setProperty('--thi', thiVal);
+
+ const priValDisp = document.getElementById('pri_val');
+ const secValDisp = document.getElementById('sec_val');
+ const thiValDisp = document.getElementById('thi_val');
+ if (priValDisp) priValDisp.textContent = priVal + 'ยฐ';
+ if (secValDisp) secValDisp.textContent = secVal + 'ยฐ';
+ if (thiValDisp) thiValDisp.textContent = thiVal + 'ยฐ';
+ }
+
+ priInput.addEventListener('input', updateLiveTheme);
+ secInput.addEventListener('input', updateLiveTheme);
+ thiInput.addEventListener('input', updateLiveTheme);
+ }
+});
+
+/**
+ * Copy text to clipboard with fallback and localized feedback
+ */
+function copyToClipboard(btnElement, textToCopy) {
+ if (navigator.clipboard && window.isSecureContext) {
+ navigator.clipboard.writeText(textToCopy).then(() => {
+ handleCopySuccess(btnElement);
+ }).catch(() => {
+ fallbackCopyText(btnElement, textToCopy);
+ });
+ } else {
+ fallbackCopyText(btnElement, textToCopy);
+ }
+}
+
+function fallbackCopyText(btnElement, textToCopy) {
+ let textArea = document.createElement("textarea");
+ textArea.value = textToCopy;
+ textArea.style.position = "fixed";
+ textArea.style.left = "-999999px";
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ try {
+ document.execCommand('copy');
+ handleCopySuccess(btnElement);
+ } catch (err) {
+ console.error('Fallback copy failed', err);
+ }
+ document.body.removeChild(textArea);
+}
+
+function handleCopySuccess(btnElement) {
+ if (!btnElement) return;
+ const originalHTML = btnElement.innerHTML;
+ const copiedLabel = btnElement.getAttribute('data-copied-text') || 'โ Copiato!';
+ btnElement.innerHTML = copiedLabel;
+ btnElement.classList.add('copied');
+ setTimeout(() => {
+ btnElement.innerHTML = originalHTML;
+ btnElement.classList.remove('copied');
+ }, 2000);
+}
+
+/**
+ * Toggle collapse state for a specific note block
+ */
+function toggleNoteCollapse(noteId) {
+ const noteBlock = document.getElementById('note-block-' + noteId);
+ if (noteBlock) {
+ noteBlock.classList.toggle('collapsed');
+ }
+}
+
+/**
+ * Expand all note blocks on page
+ */
+function expandAllNotes() {
+ document.querySelectorAll('.note-block').forEach(function(nb) {
+ nb.classList.remove('collapsed');
+ });
+}
+
+/**
+ * Collapse all note blocks on page
+ */
+function collapseAllNotes() {
+ document.querySelectorAll('.note-block').forEach(function(nb) {
+ nb.classList.add('collapsed');
+ });
+}
+
+/**
+ * Filter notes and subnotes within the current blocknote in real-time
+ */
+function filterCurrentBlocknoteNotes(query) {
+ const term = query.toLowerCase().trim();
+ const clearBtn = document.getElementById('clear-search-btn');
+ const noResults = document.getElementById('no-search-results');
+ const noteBlocks = document.querySelectorAll('.note-block');
+
+ if (clearBtn) {
+ clearBtn.style.display = term ? 'inline-flex' : 'none';
+ }
+
+ let totalMatches = 0;
+
+ noteBlocks.forEach(function(noteBlock) {
+ if (!term) {
+ // Reset search
+ noteBlock.style.display = '';
+ noteBlock.classList.add('collapsed');
+ noteBlock.querySelectorAll('.subnote-card').forEach(function(sub) {
+ sub.style.display = '';
+ });
+ totalMatches++;
+ return;
+ }
+
+ const noteTitle = (noteBlock.querySelector('.note-title-wrapper h3')?.innerText || '').toLowerCase();
+ const noteDesc = (noteBlock.querySelector('.note-description-box p')?.innerText || '').toLowerCase();
+ const noteTitleMatch = noteTitle.includes(term) || noteDesc.includes(term);
+
+ let subnoteMatchCount = 0;
+ const subnoteCards = noteBlock.querySelectorAll('.subnote-card');
+
+ subnoteCards.forEach(function(subCard) {
+ const subTitle = (subCard.querySelector('.subnote-title')?.innerText || '').toLowerCase();
+ const subContent = (subCard.querySelector('.subnote-content-text, .command-code')?.innerText || '').toLowerCase();
+
+ if (noteTitleMatch || subTitle.includes(term) || subContent.includes(term)) {
+ subCard.style.display = '';
+ subnoteMatchCount++;
+ } else {
+ subCard.style.display = 'none';
+ }
+ });
+
+ if (noteTitleMatch || subnoteMatchCount > 0) {
+ noteBlock.style.display = '';
+ noteBlock.classList.remove('collapsed'); // Auto expand on match
+ totalMatches++;
+ } else {
+ noteBlock.style.display = 'none';
+ }
+ });
+
+ if (noResults) {
+ if (term && totalMatches === 0) {
+ noResults.style.display = 'block';
+ } else {
+ noResults.style.display = 'none';
+ }
+ }
+}
+
+/**
+ * Clear search input and restore view
+ */
+function clearNoteSearch() {
+ const input = document.getElementById('note-search-input');
+ if (input) {
+ input.value = '';
+ filterCurrentBlocknoteNotes('');
+ }
}
\ No newline at end of file
diff --git a/blocknotes/bn_6a8589d13417a.json b/blocknotes/bn_6a8589d13417a.json
new file mode 100644
index 0000000..df89546
--- /dev/null
+++ b/blocknotes/bn_6a8589d13417a.json
@@ -0,0 +1,41 @@
+{
+ "id": "bn_6a8589d13417a",
+ "title": "Comandi Nginx e Server",
+ "username": "admin",
+ "created_at": "2026-08-19 10:47:45",
+ "updated_at": "2026-08-19 10:54:03",
+ "notes": [
+ {
+ "id": "note_6a8589d1343d6",
+ "title": "Configurazione Nginx",
+ "description": "Comandi principali per riavviare e testare Nginx",
+ "subnotes": [
+ {
+ "id": "sub_6a8589d1344ba",
+ "title": "Riavvia Nginx",
+ "type": "command",
+ "content": "sudo systemctl restart nginx"
+ },
+ {
+ "id": "sub_6a8589d1344f5",
+ "title": "Note di Sicurezza",
+ "type": "text",
+ "content": "Assicurarsi che la porta 80 e 443 siano aperte sul firewall UFW."
+ }
+ ]
+ },
+ {
+ "id": "note_6a858a699ba2c",
+ "title": "Test2",
+ "description": "Questa e' una nota di prova .",
+ "subnotes": [
+ {
+ "id": "sub_6a858aede4049",
+ "title": "Prova 2",
+ "type": "command",
+ "content": "Questo e' un comando!"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/blocknotes/bn_positive_internet.json b/blocknotes/bn_positive_internet.json
new file mode 100644
index 0000000..d0246ac
--- /dev/null
+++ b/blocknotes/bn_positive_internet.json
@@ -0,0 +1,444 @@
+{
+ "id": "bn_positive_internet",
+ "title": "Positive Internet",
+ "username": "admin",
+ "created_at": "2026-08-19 10:56:45",
+ "updated_at": "2026-08-19 10:56:45",
+ "notes": [
+ {
+ "id": "note_1_6a858bed6abbd",
+ "title": "Login Egguburt",
+ "description": "Login details",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abbf",
+ "title": "Eggburt",
+ "type": "command",
+ "content": "I want to Admin Positive Servers!"
+ },
+ {
+ "id": "sub_2_6a858bed6abc0",
+ "title": "Timpani",
+ "type": "command",
+ "content": "I want to administer Timpani."
+ },
+ {
+ "id": "sub_3_6a858bed6abc1",
+ "title": "## How to connect in Timpani ##",
+ "type": "command",
+ "content": "From Eggburt: ssh timpani@timpops\r\nPassword: 99flak3\r\ntimpss 'servername'"
+ }
+ ]
+ },
+ {
+ "id": "note_2_6a858bed6abcc",
+ "title": "Varnish",
+ "description": "Varnish collection of commands",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abcd",
+ "title": "Varnish Domain Clean Cache",
+ "type": "command",
+ "content": "varnishadm \"ban req.http.host ~ DOMAIN\""
+ },
+ {
+ "id": "sub_2_6a858bed6abce",
+ "title": "Check the varnish syntax command:",
+ "type": "command",
+ "content": "varnishd -C -f \/etc\/varnish\/default.vcl"
+ }
+ ]
+ },
+ {
+ "id": "note_3_6a858bed6abcf",
+ "title": "Find Spam IP",
+ "description": "findspam commands",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abd0",
+ "title": "Spam in fw-a",
+ "type": "command",
+ "content": "tail -n3000 \/var\/log\/nginx\/access.log | awk '{print $1}' | sort -nr | uniq -c | sort -nr | head -n15"
+ },
+ {
+ "id": "sub_2_6a858bed6abd1",
+ "title": "Spam in Home Dir",
+ "type": "command",
+ "content": "tail -n3000 \/home\/*\/logs\/*access*log | awk '{print $1}' | sort -nr | uniq -c | sort -nr | head -n15"
+ },
+ {
+ "id": "sub_3_6a858bed6abd2",
+ "title": "Spam in App (apache2)",
+ "type": "command",
+ "content": "tail -n3000 \/var\/log\/apache2\/access.log | awk '{print $1}' | sort -nr | uniq -c | sort -nr | head -n15"
+ },
+ {
+ "id": "sub_4_6a858bed6abd3",
+ "title": "Check all kind of logs in nginx",
+ "type": "command",
+ "content": "tail -n3000 \/var\/log\/nginx\/*access*log* | awk '{print $1}' | sort -nr | uniq -c | sort -nr | head -n15"
+ }
+ ]
+ },
+ {
+ "id": "note_4_6a858bed6abd4",
+ "title": "Clean Disks Space",
+ "description": "Cleaning Diskspaces for each kind of VMs",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abd5",
+ "title": "## Usually for App VM ##",
+ "type": "command",
+ "content": "apt clean; journalctl --vacuum-size=8M; logrotate -vf \/etc\/logrotate.d\/rsyslog; logrotate -vf \/etc\/logrotate.d\/apache2; logrotate -vf \/etc\/logrotate.d\/nginx; logrotate -vf \/etc\/logrotate.d\/varnish;"
+ },
+ {
+ "id": "sub_2_6a858bed6abd6",
+ "title": "## Usually for firewall VM ##",
+ "type": "command",
+ "content": "apt clean; journalctl --vacuum-size=8M; logrotate -vf \/etc\/logrotate.d\/rsyslog; logrotate -vf \/etc\/logrotate.d\/nginx; logrotate -vf \/etc\/logrotate.d\/varnish;"
+ },
+ {
+ "id": "sub_3_6a858bed6abd7",
+ "title": "## PHP ##",
+ "type": "command",
+ "content": "logrotate -vf \/etc\/logrotate.d\/php5.6-fpm; logrotate -vf \/etc\/logrotate.d\/php7.3-fpm; logrotate -vf \/etc\/logrotate.d\/php7.4-fpm; logrotate -vf \/etc\/logrotate.d\/php8.1-fpm; logrotate -vf \/etc\/logrotate.d\/php8.2-fpm;"
+ },
+ {
+ "id": "sub_4_6a858bed6abd8",
+ "title": "## ONLY BASIC ##",
+ "type": "command",
+ "content": "apt clean; journalctl --vacuum-size=8M; logrotate -vf \/etc\/logrotate.d\/rsyslog;"
+ },
+ {
+ "id": "sub_5_6a858bed6abd9",
+ "title": "VM + only apache2",
+ "type": "command",
+ "content": "apt clean; journalctl --vacuum-size=8M; logrotate -vf \/etc\/logrotate.d\/rsyslog; logrotate -vf \/etc\/logrotate.d\/apache2;"
+ }
+ ]
+ },
+ {
+ "id": "note_5_6a858bed6abda",
+ "title": "Add findspam Script",
+ "description": "This is the findspam from the repository.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abdb",
+ "title": "Command:",
+ "type": "command",
+ "content": "apt install -y jq; echo \"\" && echo \"Checking the reachability of the server...\"; ping -c 2 -W 2 scripts.svm.picl.co.uk > \/dev\/null 2>&1 && echo \"Server reachable. Proceeding with script update.\" || { echo \"Server unreachable. Update canceled.\"; exit 1; }; for i in updatebspamscripts findspam blockspamip spamwhat nobanip unblockspams; do echo \"Updating script $i...\"; curl -s -o \/usr\/local\/sbin\/$i https:\/\/scripts.svm.picl.co.uk\/scripts\/$i.sh && chmod 700 \/usr\/local\/sbin\/$i && echo \"Script '$i' updated.\" || echo \"Error updating script '$i'.\"; done"
+ }
+ ]
+ },
+ {
+ "id": "note_6_6a858bed6abdc",
+ "title": "Create New Database",
+ "description": "Command to create new database and database use",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abdd",
+ "title": "## Create Databases ##",
+ "type": "command",
+ "content": "CREATE DATABASE unixuser;"
+ },
+ {
+ "id": "sub_2_6a858bed6abde",
+ "title": "# Create User For DB #",
+ "type": "command",
+ "content": "grant all privileges on `unixuser`.* to `unixuser`@'%' identified by 'password database';"
+ }
+ ]
+ },
+ {
+ "id": "note_7_6a858bed6abdf",
+ "title": "Rsync From Server to the other",
+ "description": "Command to rsync between the servers",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abe0",
+ "title": "## Rsync Command ##",
+ "type": "command",
+ "content": "rsync -avzP \/home\/user\/public_html\/ user@10.10.10.17:\/home\/user\/public_html\/"
+ }
+ ]
+ },
+ {
+ "id": "note_8_6a858bed6abe1",
+ "title": "WP-CLI Commands",
+ "description": "Usefully commands for wp-cli. Note that you need to change \"from-website\" and \"to-website\" with the correct domain.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abe2",
+ "title": "## Export DB ##",
+ "type": "command",
+ "content": "wp db export"
+ },
+ {
+ "id": "sub_2_6a858bed6abe3",
+ "title": "## Change Links for wordpress ##",
+ "type": "command",
+ "content": "wp search-replace 'from-website.Com' 'to-website.com' --all-tables"
+ },
+ {
+ "id": "sub_3_6a858bed6abe4",
+ "title": "Add New User (change the '###*' strings with the correct informations)",
+ "type": "command",
+ "content": "wp user create ###USERNAME ###EMAIL@DOMAIN --role=administrator --user_pass=\"###PASSWORD\""
+ },
+ {
+ "id": "sub_4_6a858bed6abe5",
+ "title": "Install WP-CLI (Use this as root user)",
+ "type": "command",
+ "content": "curl -O https:\/\/raw.githubusercontent.com\/wp-cli\/builds\/gh-pages\/phar\/wp-cli.phar; chmod +x wp-cli.phar; sudo mv wp-cli.phar \/usr\/local\/bin\/wp"
+ },
+ {
+ "id": "sub_5_6a858bed6abe6",
+ "title": "Install varnish-cache plugin:",
+ "type": "command",
+ "content": "wp plugin install varnish-http-purge\r\nwp plugin activate varnish-http-purge"
+ },
+ {
+ "id": "sub_6_6a858bed6abe7",
+ "title": "Configuration to add in wp-config.php for the cache plugin",
+ "type": "command",
+ "content": "define('VHP_VARNISH_IP','varnish-vip:6081');"
+ },
+ {
+ "id": "sub_7_6a858bed6abe8",
+ "title": "(Run as Root) Checksums verify WP CLI on all \/home folders",
+ "type": "command",
+ "content": "for u in \/home\/*; do user=$(basename \"$u\"); for d in httpdocs public_html; do if [ -d \"$u\/$d\" ] && [ -f \"$u\/$d\/wp-config.php\" ]; then echo \"=== $user ($d) ===\"; sudo -u \"$user\" -H wp --path=\"$u\/$d\" core verify-checksums; echo; fi; done; done"
+ },
+ {
+ "id": "sub_8_6a858bed6abe9",
+ "title": "verify checksum for a single website",
+ "type": "command",
+ "content": "wp core verify-checksums"
+ },
+ {
+ "id": "sub_9_6a858bed6abea",
+ "title": "restore corefiles",
+ "type": "command",
+ "content": "wp core download --version=$(wp core version) --force"
+ },
+ {
+ "id": "sub_10_6a858bed6abeb",
+ "title": "Create posops user",
+ "type": "command",
+ "content": "wp user create posops good@positive-internet.com --role=administrator --user_pass='P3dsfj7nYLaMxxXk'"
+ },
+ {
+ "id": "sub_11_6a858bed6abec",
+ "title": "Cambiare la password di un utente",
+ "type": "command",
+ "content": "wp user update posops --user_pass='P3dsfj7nYLaMxxXk'"
+ }
+ ]
+ },
+ {
+ "id": "note_9_6a858bed6abed",
+ "title": "Find Killig Process Error",
+ "description": "Command to check what has been killed in the VMs \/ Machine",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abee",
+ "title": "## How to check errors ##",
+ "type": "command",
+ "content": "grep -E '(reaper|oom)' \/var\/log\/syslog"
+ }
+ ]
+ },
+ {
+ "id": "note_10_6a858bed6abef",
+ "title": "Generate SSH Key",
+ "description": "Command to generate ssh key",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abf0",
+ "title": "## ED25519 ##",
+ "type": "command",
+ "content": "ssh-keygen -t ed25519"
+ },
+ {
+ "id": "sub_2_6a858bed6abf1",
+ "title": "## RSA ##",
+ "type": "command",
+ "content": "ssh-keygen -t rsa -b 4096"
+ }
+ ]
+ },
+ {
+ "id": "note_11_6a858bed6abf2",
+ "title": "User Basic Auth",
+ "description": "How to create \/ add user for the basicauth.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abf3",
+ "title": "## CREATE A FILE \/ USER (cancel existing one) ##",
+ "type": "command",
+ "content": "htpasswd -c \/etc\/nginx\/.htpasswd-name nuovo_utente"
+ },
+ {
+ "id": "sub_2_6a858bed6abf4",
+ "title": "## ADD EXTRA USER ##",
+ "type": "command",
+ "content": "htpasswd \/etc\/nginx\/.htpasswd-name username"
+ },
+ {
+ "id": "sub_3_6a858bed6abf5",
+ "title": "Block to add",
+ "type": "command",
+ "content": "auth_basic \"Restricted Access\";\r\n auth_basic_user_file \/etc\/nginx\/.htpasswd;"
+ },
+ {
+ "id": "sub_4_6a858bed6abf6",
+ "title": "Ownership and privileges",
+ "type": "command",
+ "content": "htpassloc=\"etc\/nginx\/.htpasswd\";\r\nchown www-data:www-data $htpassloc;\r\nchmod 644 $htpassloc"
+ }
+ ]
+ },
+ {
+ "id": "note_12_6a858bed6abf7",
+ "title": "Check DB Size",
+ "description": "Command to check the dimension of the databases; (needs to be specified the databases names)",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abf8",
+ "title": "## CHECK DB SIZE ##",
+ "type": "command",
+ "content": "SELECT table_schema AS \"Database\", ROUND(SUM(data_length + index_length) \/ 1024 \/ 1024, 2) AS \"Dimension (MB)\" FROM information_schema.tables WHERE table_schema IN (\"dbname1 \", \"dbname2\", \"dbname3\", \"dbname4\") GROUP BY table_schema;"
+ }
+ ]
+ },
+ {
+ "id": "note_13_6a858bed6abf9",
+ "title": "Remove Files and keep the newest one",
+ "description": "Change the variable keep in base to how much newest file you need to keep!",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abfa",
+ "title": "## REMOVE OLDEST FILES CODE ##",
+ "type": "command",
+ "content": "count=0; keep=4; echo \"### FILE THAT WILL BE REMOVED: ###\"; echo \"-------------------------------\"; for i in $(ls -t); do if [ $count -lt $keep ]; then echo \"Keeping: $i\"; else echo \"Removing $i\"; fi; ((count++)); done; echo \"-------------------------------\"; echo \"Do you wish to continue?\"; echo \"y - Yes\"; echo \"default - no\"; read check; check=${check,,}; if [ \"$check\" == \"y\" ]; then echo \"#### REMOVING FILE ####\"; echo \"-------------------------------\"; count=0; for i in $(ls -t); do if [ $count -lt $keep ]; then echo \"Keeping: $i\"; else echo \"Removed $i\"; rm \"$i\"; fi; ((count++)); done; echo \"-------------------------------\"; else echo \"Not Removed\"; fi\r\n"
+ }
+ ]
+ },
+ {
+ "id": "note_14_6a858bed6abfb",
+ "title": "Add Diskspace Command",
+ "description": "After increased the diskspace of the hard disk on proxmox, we can run this command to add the 100% of the diskspace to the root partition.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abfc",
+ "title": "Command:",
+ "type": "command",
+ "content": "parted \/dev\/vda resizepart 1 100%; pvresize \/dev\/vda1; lvextend -l +100%FREE \/dev\/vgmain\/root; resize2fs \/dev\/vgmain\/root"
+ }
+ ]
+ },
+ {
+ "id": "note_15_6a858bed6abfd",
+ "title": "New Certboot Certificate",
+ "description": "Install new certboot Certificate",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6abfe",
+ "title": "Command:",
+ "type": "command",
+ "content": "certbot certonly --webroot -w \/var\/www\/html -d ###DOMAIN"
+ }
+ ]
+ },
+ {
+ "id": "note_16_6a858bed6abff",
+ "title": "Extract pfx key",
+ "description": "Use this command to extract the key and the pem files from a .pfx file.\r\nChange the variables before to run the command.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac00",
+ "title": "Command:",
+ "type": "command",
+ "content": "PFX=\"FILE_NAME\";\r\nPASS=\"PASSWORD_PFX\";\r\nopenssl pkcs12 -in \"$PFX\" -passin pass:\"$PASS\" -nocerts -nodes -out server.key && \\\r\nopenssl pkcs12 -in \"$PFX\" -passin pass:\"$PASS\" -nokeys | \\\r\nsed -n '\/BEGIN CERTIFICATE\/,\/END CERTIFICATE\/p' > fullchain.pem\r\n"
+ }
+ ]
+ },
+ {
+ "id": "note_17_6a858bed6ac01",
+ "title": "check if the ssh-key is valid",
+ "description": "With this command we can check if a ssh key is valid or not",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac02",
+ "title": "Command:",
+ "type": "command",
+ "content": "FILENAME=\"path\/to\/the\/file\";\r\nssh-keygen -l -f $FILENAME;\r\n"
+ }
+ ]
+ },
+ {
+ "id": "note_18_6a858bed6ac03",
+ "title": "checking valid rules for studiorep-fw",
+ "description": "This command is checking which of the following rules has been accepted for the php-rules",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac04",
+ "title": "Command:",
+ "type": "command",
+ "content": "php-fpm8.3 -tt 2>&1 | grep -A50 \"\\[POOL_NAME\\]\""
+ }
+ ]
+ },
+ {
+ "id": "note_19_6a858bed6ac05",
+ "title": "Add new VM to spa",
+ "description": "Use this to add the new element to the spa command. Specify the Public IP, the port and the hostname",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac06",
+ "title": "Command:",
+ "type": "command",
+ "content": "sudoedit \/home\/posops\/ansible\/hosts"
+ }
+ ]
+ },
+ {
+ "id": "note_20_6a858bed6ac07",
+ "title": "PHP-FPM Scripts",
+ "description": "Script Collection for PHP-FPM",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac08",
+ "title": "Restarting all php-fpm existing",
+ "type": "command",
+ "content": "for i in \/etc\/php\/*; do version=$(basename \"$i\"); service=\"php${version}-fpm\"; if systemctl list-units --type=service --all | grep -q \"$service\"; then echo \"[ $service ]\"; systemctl restart \"$service\"; systemctl status \"$service\" | grep Active; fi; done"
+ },
+ {
+ "id": "sub_2_6a858bed6ac09",
+ "title": "Check the php-version for the vhost",
+ "type": "command",
+ "content": "VERSION=8.3; for i in *; do socket=$(grep -oP 'SetHandler\\s+\"proxy:unix:\\K[^|\"]+' \"$i\"); if [ -n \"$socket\" ]; then domain=$(basename \"$socket\" .sock); match=$(grep -iRl \"$domain\" \/etc\/php\/*\/fpm\/pool.d\/); if [ -n \"$match\" ]; then phpver=$(echo \"$match\" | sed -n 's|\/etc\/php\/\\([^\/]*\\)\/fpm\/.*|\\1|p' | sort -u); if echo \"$phpver\" | grep -q \"^$VERSION$\"; then echo \"[ $domain ]\"; echo \"php-fpm $phpver\"; fi; fi; fi; done"
+ },
+ {
+ "id": "sub_3_6a858bed6ac0a",
+ "title": "Check the php-version for the vhost",
+ "type": "command",
+ "content": "exver=\"7.4\" instver=\"8.4\" ; for i in $(dpkg -l *php* | awk '{print $2}' | grep $exver); do apt install php$instver${i:6} -y; done"
+ }
+ ]
+ },
+ {
+ "id": "note_21_6a858bed6ac0b",
+ "title": "ownership to 755 folder and 644 files",
+ "description": "Changing the ownership from 755 to the folders and 644 to the files (sub-directories included) with find + xarg in the current director.",
+ "subnotes": [
+ {
+ "id": "sub_1_6a858bed6ac0c",
+ "title": "Command",
+ "type": "command",
+ "content": "find . -type d -print0 | xargs -0 chmod 755;\r\nfind . -type f -print0 | xargs -0 chmod 644"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/config/constants.php b/config/constants.php
index bc09841..3b3594a 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -54,8 +54,7 @@
$full_path = $const_before . $const_value;
if (!is_dir($full_path) && !is_file($full_path)) {
- debugStatusMes("The folder/file declared into '$const_name' constant is missing or not accessible. Please check it.", "High");
- die;
+ @mkdir($full_path, 0777, true);
}
if (!defined($const_name)) {
diff --git a/config/db.php b/config/db.php
index 219ad75..9666807 100644
--- a/config/db.php
+++ b/config/db.php
@@ -1,21 +1,104 @@
connect_error) {
+ header("Location: first_start.php");
+ exit;
+ }
+ $test_conn->close();
+ } catch (Exception $ex) {
+ header("Location: first_start.php");
+ exit;
}
-} catch (mysqli_sql_exception $e) {
- // Access Deny
- include(PAGES_FOLD . "db-error.php");
- debugStatusMes("Database connection failed.", "High");
-
+}
- die();
+try {
+ // Create Connection to MySQL host first (in case DB doesn't exist yet)
+ $conn = new mysqli(DB_HOST, DB_USER, DB_PASS);
+
+ // Create Database if not exists
+ $db_name_escaped = str_replace('`', '``', DB_NAME);
+ $conn->query("CREATE DATABASE IF NOT EXISTS `$db_name_escaped` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
+ $conn->select_db(DB_NAME);
+
+ // Ensure BLOCKNOTES_FOLD directory exists
+ if (!is_dir(BLOCKNOTES_FOLD)) {
+ @mkdir(BLOCKNOTES_FOLD, 0777, true);
+ }
+
+ // Auto-create users table if missing (with ENUM containing 'owner')
+ $sql_table = "CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ role ENUM('owner', 'admin', 'user') NOT NULL DEFAULT 'user',
+ theme_pri INT DEFAULT 98,
+ theme_sec INT DEFAULT 207,
+ theme_thi INT DEFAULT 280,
+ language VARCHAR(5) NOT NULL DEFAULT 'it',
+ default_blocknote VARCHAR(100) NOT NULL DEFAULT '',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
+ $conn->query($sql_table);
+
+ // Auto-migrate role column ENUM if missing 'owner'
+ $conn->query("ALTER TABLE users MODIFY COLUMN role ENUM('owner', 'admin', 'user') NOT NULL DEFAULT 'user'");
+
+ // Auto-migrate columns if missing in existing table
+ $check_lang = $conn->query("SHOW COLUMNS FROM users LIKE 'language'");
+ if ($check_lang->num_rows === 0) {
+ $conn->query("ALTER TABLE users ADD COLUMN language VARCHAR(5) NOT NULL DEFAULT 'it'");
+ }
+
+ $check_def_bn = $conn->query("SHOW COLUMNS FROM users LIKE 'default_blocknote'");
+ if ($check_def_bn->num_rows === 0) {
+ $conn->query("ALTER TABLE users ADD COLUMN default_blocknote VARCHAR(100) NOT NULL DEFAULT ''");
+ }
+
+ // Auto-create site_settings table
+ $sql_settings = "CREATE TABLE IF NOT EXISTS site_settings (
+ setting_key VARCHAR(50) PRIMARY KEY,
+ setting_value TEXT
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
+ $conn->query($sql_settings);
+
+ // Seed default site settings if missing
+ $conn->query("INSERT IGNORE INTO site_settings (setting_key, setting_value) VALUES ('site_title', 'Blocknotes')");
+ $conn->query("INSERT IGNORE INTO site_settings (setting_key, setting_value) VALUES ('site_favicon', 'assets/img/favicon.png')");
+
+ // Seed default owner account if table is empty or ensure admin user is owner if no owner exists
+ $res = $conn->query("SELECT COUNT(*) AS total FROM users");
+ $row = $res->fetch_assoc();
+ if ((int)$row['total'] === 0) {
+ $default_pass = password_hash("admin123", PASSWORD_DEFAULT);
+ $admin_user = "admin";
+ $stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, 'owner')");
+ $stmt->bind_param("ss", $admin_user, $default_pass);
+ $stmt->execute();
+ $stmt->close();
+ } else {
+ // Check if an owner exists, if not, promote the first user to owner
+ $check_owner = $conn->query("SELECT COUNT(*) AS total_owners FROM users WHERE role = 'owner'");
+ $owner_row = $check_owner->fetch_assoc();
+ if ((int)$owner_row['total_owners'] === 0) {
+ $conn->query("UPDATE users SET role = 'owner' ORDER BY id ASC LIMIT 1");
+ }
+ }
+
+} catch (mysqli_sql_exception $e) {
+ if (file_exists($first_start_path) && basename($_SERVER['SCRIPT_FILENAME'] ?? '') !== 'first_start.php') {
+ header("Location: first_start.php");
+ exit;
+ }
+ include(PAGES_FOLD . "db-error.php");
+ debugStatusMes("Database setup error: " . $e->getMessage(), "High");
+ die();
}
?>
\ No newline at end of file
diff --git a/config/functions_init.php b/config/functions_init.php
index 3b0a610..8eb752d 100644
--- a/config/functions_init.php
+++ b/config/functions_init.php
@@ -22,7 +22,7 @@ function get_current_page() {
$page = $_GET["page"] ?? "home";
$page = strtolower(trim($page));
- $page = preg_replace("/[^a-z0-9\-]/", "", $page);
+ $page = preg_replace("/[^a-z0-9\-_]/", "", $page);
if ($page === "") {
$page = "home";
diff --git a/config/init.php b/config/init.php
index 47e238b..5714e8f 100644
--- a/config/init.php
+++ b/config/init.php
@@ -5,12 +5,14 @@
switch($env){
/*------ DON'T TOUCH THIS PART ------ */
case "generic":
- define('WEB_ROOT', $_SERVER['DOCUMENT_ROOT']);
- define('DOMAIN', $_SERVER['HTTP_HOST']);
+ $doc_root = !empty($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : dirname(__DIR__);
+ define('WEB_ROOT', $doc_root);
+ $host = !empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
+ define('DOMAIN', $host);
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
- define('FULL_DOMAIN', $protocol . $_SERVER['HTTP_HOST']);
- define("ROOT_URL", "http://" . DOMAIN );
- define("ROOT_DIR", WEB_ROOT . "/");
+ define('FULL_DOMAIN', $protocol . DOMAIN);
+ define("ROOT_URL", $protocol . DOMAIN );
+ define("ROOT_DIR", rtrim(WEB_ROOT, '/') . "/");
break;
/* ----------------------------------- */
default:
diff --git a/config/settings.php b/config/settings.php
index 5a4a6da..2ddd809 100644
--- a/config/settings.php
+++ b/config/settings.php
@@ -1,15 +1,15 @@
connect_error) {
+ $msg_error = "Connessione MySQL fallita: " . $conn->connect_error;
+ } else {
+ // Create database
+ $db_name_escaped = str_replace('`', '``', $db_name);
+ if (!$conn->query("CREATE DATABASE IF NOT EXISTS `$db_name_escaped` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")) {
+ $msg_error = "Impossibile creare il database '$db_name': " . $conn->error;
+ } else {
+ $conn->select_db($db_name);
+
+ // Create users table
+ $sql_users = "CREATE TABLE IF NOT EXISTS users (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ role ENUM('owner', 'admin', 'user') NOT NULL DEFAULT 'user',
+ theme_pri INT DEFAULT 98,
+ theme_sec INT DEFAULT 207,
+ theme_thi INT DEFAULT 280,
+ language VARCHAR(5) NOT NULL DEFAULT 'it',
+ default_blocknote VARCHAR(100) NOT NULL DEFAULT '',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
+ $conn->query($sql_users);
+
+ // Create site_settings table
+ $sql_settings = "CREATE TABLE IF NOT EXISTS site_settings (
+ setting_key VARCHAR(50) PRIMARY KEY,
+ setting_value TEXT
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
+ $conn->query($sql_settings);
+
+ // Insert site_settings default values
+ $conn->query("INSERT IGNORE INTO site_settings (setting_key, setting_value) VALUES ('site_title', 'Blocknotes')");
+ $conn->query("INSERT IGNORE INTO site_settings (setting_key, setting_value) VALUES ('site_favicon', 'assets/img/favicon.png')");
+
+ // Create Owner account
+ $hash_owner = password_hash($owner_pass, PASSWORD_DEFAULT);
+ $stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, 'owner') ON DUPLICATE KEY UPDATE password = VALUES(password), role = 'owner'");
+ $stmt->bind_param("ss", $owner_user, $hash_owner);
+ $stmt->execute();
+ $stmt->close();
+
+ // Create Admin account
+ $hash_admin = password_hash($admin_pass, PASSWORD_DEFAULT);
+ $stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, 'admin') ON DUPLICATE KEY UPDATE password = VALUES(password), role = 'admin'");
+ $stmt->bind_param("ss", $admin_user, $hash_admin);
+ $stmt->execute();
+ $stmt->close();
+
+ // Create blocknotes directory if missing
+ $blocknotes_dir = __DIR__ . "/blocknotes/";
+ if (!is_dir($blocknotes_dir)) {
+ @mkdir($blocknotes_dir, 0777, true);
+ }
+
+ // Update config/settings.php file
+ $settings_file_content = '';
+ file_put_contents(__DIR__ . "/config/settings.php", $settings_file_content);
+
+ // Rename first_start.php to first_start.php.completed to prevent installer re-entry
+ @rename(__FILE__, __DIR__ . "/first_start.php.completed");
+
+ // Redirect to login page
+ header("Location: index.php?page=login&installed=1");
+ exit;
+ }
+ }
+ }
+}
+?>
+
+
+
+
+
+ = __('setup_title') ?> - Blocknotes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ โ ๏ธ = htmlspecialchars($msg_error) ?>
+
+
+
+
+
+
+
diff --git a/functions/auth.php b/functions/auth.php
new file mode 100644
index 0000000..04e74b1
--- /dev/null
+++ b/functions/auth.php
@@ -0,0 +1,314 @@
+ false, 'message' => 'Inserisci sia username che password.'];
+ }
+
+ $stmt = $conn->prepare("SELECT id, username, password, role, theme_pri, theme_sec, theme_thi, language, default_blocknote FROM users WHERE username = ? LIMIT 1");
+ $stmt->bind_param("s", $username);
+ $stmt->execute();
+ $result = $stmt->get_result();
+
+ if ($row = $result->fetch_assoc()) {
+ if (password_verify($password, $row['password'])) {
+ $_SESSION['user'] = [
+ 'id' => $row['id'],
+ 'username' => $row['username'],
+ 'role' => $row['role'],
+ 'theme_pri' => $row['theme_pri'] ?? 98,
+ 'theme_sec' => $row['theme_sec'] ?? 207,
+ 'theme_thi' => $row['theme_thi'] ?? 280,
+ 'language' => $row['language'] ?? 'it',
+ 'default_blocknote' => $row['default_blocknote'] ?? ''
+ ];
+ $stmt->close();
+ return ['success' => true];
+ }
+ }
+ $stmt->close();
+ return ['success' => false, 'message' => 'Username o password errati.'];
+}
+
+/**
+ * Logout current user.
+ */
+function logout_user() {
+ $_SESSION = array();
+ if (ini_get("session.use_cookies")) {
+ $params = session_get_cookie_params();
+ setcookie(session_name(), '', time() - 42000,
+ $params["path"], $params["domain"],
+ $params["secure"], $params["httponly"]
+ );
+ }
+ session_destroy();
+}
+
+/**
+ * Update user password.
+ */
+function update_user_password($user_id, $new_password) {
+ global $conn;
+ if (strlen($new_password) < 4) {
+ return ['success' => false, 'message' => 'La password deve contenere almeno 4 caratteri.'];
+ }
+ $hash = password_hash($new_password, PASSWORD_DEFAULT);
+ $stmt = $conn->prepare("UPDATE users SET password = ? WHERE id = ?");
+ $stmt->bind_param("si", $hash, $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ return $res ? ['success' => true, 'message' => 'Password aggiornata con successo!'] : ['success' => false, 'message' => 'Errore durante il salvataggio della password.'];
+}
+
+/**
+ * Update user language preference.
+ */
+function update_user_language($user_id, $lang) {
+ global $conn;
+ if (!in_array($lang, ['it', 'en', 'es', 'fr'])) {
+ $lang = 'it';
+ }
+
+ $stmt = $conn->prepare("UPDATE users SET language = ? WHERE id = ?");
+ $stmt->bind_param("si", $lang, $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ if ($res) {
+ if (isset($_SESSION['user']) && $_SESSION['user']['id'] == $user_id) {
+ $_SESSION['user']['language'] = $lang;
+ }
+ return ['success' => true, 'message' => 'Lingua aggiornata con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore durante l\'aggiornamento della lingua.'];
+}
+
+/**
+ * Update user default landing blocknote preference.
+ */
+function update_user_default_blocknote($user_id, $bn_id) {
+ global $conn;
+ $bn_id = trim($bn_id);
+
+ $stmt = $conn->prepare("UPDATE users SET default_blocknote = ? WHERE id = ?");
+ $stmt->bind_param("si", $bn_id, $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ if ($res) {
+ if (isset($_SESSION['user']) && $_SESSION['user']['id'] == $user_id) {
+ $_SESSION['user']['default_blocknote'] = $bn_id;
+ }
+ return ['success' => true, 'message' => 'Pagina di avvio aggiornata con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore durante l\'aggiornamento della pagina di avvio.'];
+}
+
+/**
+ * Update user interface theme colors (HSL).
+ */
+function update_user_theme($user_id, $pri, $sec, $thi) {
+ global $conn;
+ $pri = max(0, min(360, (int)$pri));
+ $sec = max(0, min(360, (int)$sec));
+ $thi = max(0, min(360, (int)$thi));
+
+ $stmt = $conn->prepare("UPDATE users SET theme_pri = ?, theme_sec = ?, theme_thi = ? WHERE id = ?");
+ $stmt->bind_param("iiii", $pri, $sec, $thi, $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ if ($res) {
+ if (isset($_SESSION['user']) && $_SESSION['user']['id'] == $user_id) {
+ $_SESSION['user']['theme_pri'] = $pri;
+ $_SESSION['user']['theme_sec'] = $sec;
+ $_SESSION['user']['theme_thi'] = $thi;
+ }
+ return ['success' => true, 'message' => 'Colori dell\'interfaccia aggiornati!'];
+ }
+ return ['success' => false, 'message' => 'Errore durante l\'aggiornamento dei colori.'];
+}
+
+/**
+ * Admin: Get list of all registered users.
+ */
+function get_all_users() {
+ global $conn;
+ $result = $conn->query("SELECT id, username, role, created_at FROM users ORDER BY id ASC");
+ $users = [];
+ while ($row = $result->fetch_assoc()) {
+ $users[] = $row;
+ }
+ return $users;
+}
+
+/**
+ * Admin: Register a new user.
+ */
+function admin_create_user($username, $password, $role = 'user') {
+ global $conn;
+ $username = trim($username);
+ if (empty($username) || empty($password)) {
+ return ['success' => false, 'message' => 'Username e password sono obbligatori.'];
+ }
+ if (!in_array($role, ['user', 'admin', 'owner'])) {
+ $role = 'user';
+ }
+
+ if ($role === 'owner') {
+ $check_owner = $conn->query("SELECT id FROM users WHERE role = 'owner'");
+ if ($check_owner->num_rows > 0) {
+ return ['success' => false, 'message' => 'Puรฒ esistere un solo account Owner nel sistema!'];
+ }
+ }
+
+ // Check if exists
+ $stmt = $conn->prepare("SELECT id FROM users WHERE username = ? LIMIT 1");
+ $stmt->bind_param("s", $username);
+ $stmt->execute();
+ if ($stmt->get_result()->num_rows > 0) {
+ $stmt->close();
+ return ['success' => false, 'message' => 'Un utente con questo username esiste giร .'];
+ }
+ $stmt->close();
+
+ $hash = password_hash($password, PASSWORD_DEFAULT);
+ $stmt = $conn->prepare("INSERT INTO users (username, password, role) VALUES (?, ?, ?)");
+ $stmt->bind_param("sss", $username, $hash, $role);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ return $res ? ['success' => true, 'message' => 'Utente creato con successo!'] : ['success' => false, 'message' => 'Impossibile creare l\'utente.'];
+}
+
+/**
+ * Admin: Delete a user by ID.
+ */
+function admin_delete_user($user_id) {
+ global $conn;
+ $user_id = (int)$user_id;
+
+ // Prevent self-deletion
+ if (current_user() && current_user()['id'] == $user_id) {
+ return ['success' => false, 'message' => 'Non puoi eliminare il tuo stesso account!'];
+ }
+
+ // Check target user role - Owner can NEVER be deleted
+ $stmt = $conn->prepare("SELECT role FROM users WHERE id = ? LIMIT 1");
+ $stmt->bind_param("i", $user_id);
+ $stmt->execute();
+ $target_user = $stmt->get_result()->fetch_assoc();
+ $stmt->close();
+
+ if ($target_user && $target_user['role'] === 'owner') {
+ return ['success' => false, 'message' => 'L\'account Owner non puรฒ essere eliminato da nessuno!'];
+ }
+
+ $stmt = $conn->prepare("DELETE FROM users WHERE id = ?");
+ $stmt->bind_param("i", $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ return $res ? ['success' => true, 'message' => 'Utente eliminato.'] : ['success' => false, 'message' => 'Errore nell\'eliminazione dell\'utente.'];
+}
+
+/**
+ * Admin: Update user role.
+ */
+function admin_update_user_role($user_id, $role) {
+ global $conn;
+ $user_id = (int)$user_id;
+ if (!in_array($role, ['admin', 'user', 'owner'])) return ['success' => false, 'message' => 'Ruolo non valido.'];
+
+ // Check target user role - Owner role cannot be changed
+ $stmt = $conn->prepare("SELECT role FROM users WHERE id = ? LIMIT 1");
+ $stmt->bind_param("i", $user_id);
+ $stmt->execute();
+ $target_user = $stmt->get_result()->fetch_assoc();
+ $stmt->close();
+
+ if ($target_user && $target_user['role'] === 'owner') {
+ return ['success' => false, 'message' => 'Il ruolo dell\'account Owner non puรฒ essere modificato!'];
+ }
+
+ if ($role === 'owner') {
+ $check_owner = $conn->query("SELECT id FROM users WHERE role = 'owner'");
+ if ($check_owner->num_rows > 0) {
+ return ['success' => false, 'message' => 'Puรฒ esistere un solo account Owner nel sistema!'];
+ }
+ }
+
+ $stmt = $conn->prepare("UPDATE users SET role = ? WHERE id = ?");
+ $stmt->bind_param("si", $role, $user_id);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ return $res ? ['success' => true, 'message' => 'Ruolo aggiornato.'] : ['success' => false, 'message' => 'Impossibile aggiornare il ruolo.'];
+}
+
diff --git a/functions/blocknotes.php b/functions/blocknotes.php
new file mode 100644
index 0000000..8a17446
--- /dev/null
+++ b/functions/blocknotes.php
@@ -0,0 +1,421 @@
+ false, 'message' => 'Il titolo del blocknote รจ obbligatorio.'];
+ }
+
+ $id = 'bn_' . uniqid();
+ $data = [
+ 'id' => $id,
+ 'title' => $title,
+ 'username' => $username,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ 'notes' => []
+ ];
+
+ if (save_blocknote($data)) {
+ return ['success' => true, 'id' => $id, 'message' => 'Blocknote creato con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio del file JSON.'];
+}
+
+/**
+ * Import a JSON string (supports standard format and legacy codes format).
+ */
+function import_blocknote_json($json_string, $username, $custom_title = '') {
+ $data = json_decode($json_string, true);
+ if (!is_array($data)) {
+ return ['success' => false, 'message' => 'File JSON non valido o malformato.'];
+ }
+
+ $id = 'bn_' . uniqid();
+ $title = !empty($custom_title) ? trim($custom_title) : 'Blocknote Importato';
+
+ // Case 1: Standard format with 'notes' array
+ if (isset($data['notes']) && is_array($data['notes'])) {
+ $bn = [
+ 'id' => $id,
+ 'title' => !empty($data['title']) ? $data['title'] : $title,
+ 'username' => $username,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ 'notes' => $data['notes']
+ ];
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'id' => $id, 'message' => 'Blocknote importato con successo!'];
+ }
+ }
+
+ // Case 2: Legacy format (array of notes with 'title', 'description', 'codes')
+ if (isset($data[0]) && is_array($data[0])) {
+ $notes = [];
+ foreach ($data as $n_idx => $legacy_note) {
+ $note_id = 'note_' . ($n_idx + 1) . '_' . uniqid();
+ $subnotes = [];
+
+ if (isset($legacy_note['codes']) && is_array($legacy_note['codes'])) {
+ foreach ($legacy_note['codes'] as $s_idx => $legacy_code) {
+ $subnotes[] = [
+ 'id' => 'sub_' . ($s_idx + 1) . '_' . uniqid(),
+ 'title' => $legacy_code['sub_title'] ?? 'Comando',
+ 'type' => 'command',
+ 'content' => $legacy_code['code'] ?? ''
+ ];
+ }
+ }
+
+ $notes[] = [
+ 'id' => $note_id,
+ 'title' => $legacy_note['title'] ?? 'Nota ' . ($n_idx + 1),
+ 'description' => $legacy_note['description'] ?? '',
+ 'subnotes' => $subnotes
+ ];
+ }
+
+ $bn = [
+ 'id' => $id,
+ 'title' => $title,
+ 'username' => $username,
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ 'notes' => $notes
+ ];
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'id' => $id, 'message' => 'Blocknote in formato legacy importato e convertito con successo!'];
+ }
+ }
+
+ return ['success' => false, 'message' => 'Formato JSON non riconosciuto.'];
+}
+
+/**
+ * Check permission on blocknote.
+ */
+function can_access_blocknote($bn, $username, $is_admin) {
+ if (!$bn) return false;
+ if ($is_admin) return true;
+ return strtolower($bn['username'] ?? '') === strtolower($username);
+}
+
+/**
+ * Rename a blocknote.
+ */
+function rename_blocknote($id, $new_title, $username, $is_admin) {
+ $bn = get_blocknote_by_id($id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) {
+ return ['success' => false, 'message' => 'Non hai i permessi per modificare questo blocknote.'];
+ }
+
+ $new_title = trim($new_title);
+ if (empty($new_title)) return ['success' => false, 'message' => 'Il titolo non puรฒ essere vuoto.'];
+
+ $bn['title'] = $new_title;
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'message' => 'Blocknote rinominato con successo.'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio del file JSON.'];
+}
+
+/**
+ * Delete a blocknote.
+ */
+function delete_blocknote($id, $username, $is_admin) {
+ $bn = get_blocknote_by_id($id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) {
+ return ['success' => false, 'message' => 'Non hai i permessi per eliminare questo blocknote.'];
+ }
+
+ $filepath = BLOCKNOTES_FOLD . preg_replace('/[^a-zA-Z0-9_\-]/', '', $bn['id']) . ".json";
+ if (file_exists($filepath)) {
+ @unlink($filepath);
+ }
+ return ['success' => true, 'message' => 'Blocknote eliminato con successo.'];
+}
+
+/**
+ * Add a new Note block to a Blocknote.
+ */
+function add_note($blocknote_id, $title, $description, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $title = trim($title);
+ if (empty($title)) return ['success' => false, 'message' => 'Il titolo della nota รจ obbligatorio.'];
+
+ $note_id = 'note_' . uniqid();
+ $new_note = [
+ 'id' => $note_id,
+ 'title' => $title,
+ 'description' => trim($description),
+ 'subnotes' => []
+ ];
+
+ $bn['notes'][] = $new_note;
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'note_id' => $note_id, 'message' => 'Nota aggiunta con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore durante il salvataggio.'];
+}
+
+/**
+ * Edit an existing Note block.
+ */
+function edit_note($blocknote_id, $note_id, $title, $description, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $title = trim($title);
+ if (empty($title)) return ['success' => false, 'message' => 'Il titolo della nota รจ obbligatorio.'];
+
+ $found = false;
+ foreach ($bn['notes'] as &$note) {
+ if ($note['id'] === $note_id) {
+ $note['title'] = $title;
+ $note['description'] = trim($description);
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) return ['success' => false, 'message' => 'Nota non trovata.'];
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'message' => 'Nota modificata con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio.'];
+}
+
+/**
+ * Delete a Note block.
+ */
+function delete_note($blocknote_id, $note_id, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $initial_count = count($bn['notes']);
+ $bn['notes'] = array_values(array_filter($bn['notes'], function($n) use ($note_id) {
+ return $n['id'] !== $note_id;
+ }));
+
+ if (count($bn['notes']) === $initial_count) {
+ return ['success' => false, 'message' => 'Nota non trovata.'];
+ }
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'message' => 'Nota eliminata con successo.'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio.'];
+}
+
+/**
+ * Add a Subnote to a Note block.
+ */
+function add_subnote($blocknote_id, $note_id, $sub_title, $type, $content, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $sub_title = trim($sub_title);
+ if (empty($sub_title)) return ['success' => false, 'message' => 'Il titolo della sottonota รจ obbligatorio.'];
+
+ $type = ($type === 'command') ? 'command' : 'text';
+ $sub_id = 'sub_' . uniqid();
+
+ $found = false;
+ foreach ($bn['notes'] as &$note) {
+ if ($note['id'] === $note_id) {
+ if (!isset($note['subnotes'])) $note['subnotes'] = [];
+ $note['subnotes'][] = [
+ 'id' => $sub_id,
+ 'title' => $sub_title,
+ 'type' => $type,
+ 'content' => $content
+ ];
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) return ['success' => false, 'message' => 'Nota genitore non trovata.'];
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'subnote_id' => $sub_id, 'message' => 'Sottonota aggiunta con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio.'];
+}
+
+/**
+ * Edit a Subnote.
+ */
+function edit_subnote($blocknote_id, $note_id, $sub_id, $sub_title, $type, $content, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $sub_title = trim($sub_title);
+ if (empty($sub_title)) return ['success' => false, 'message' => 'Il titolo della sottonota รจ obbligatorio.'];
+ $type = ($type === 'command') ? 'command' : 'text';
+
+ $found = false;
+ foreach ($bn['notes'] as &$note) {
+ if ($note['id'] === $note_id) {
+ if (isset($note['subnotes'])) {
+ foreach ($note['subnotes'] as &$sub) {
+ if ($sub['id'] === $sub_id) {
+ $sub['title'] = $sub_title;
+ $sub['type'] = $type;
+ $sub['content'] = $content;
+ $found = true;
+ break 2;
+ }
+ }
+ }
+ }
+ }
+
+ if (!$found) return ['success' => false, 'message' => 'Sottonota non trovata.'];
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'message' => 'Sottonota modificata con successo!'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio.'];
+}
+
+/**
+ * Delete a Subnote.
+ */
+function delete_subnote($blocknote_id, $note_id, $sub_id, $username, $is_admin) {
+ $bn = get_blocknote_by_id($blocknote_id);
+ if (!$bn) return ['success' => false, 'message' => 'Blocknote non trovato.'];
+ if (!can_access_blocknote($bn, $username, $is_admin)) return ['success' => false, 'message' => 'Permesso negato.'];
+
+ $found = false;
+ foreach ($bn['notes'] as &$note) {
+ if ($note['id'] === $note_id && isset($note['subnotes'])) {
+ $initial = count($note['subnotes']);
+ $note['subnotes'] = array_values(array_filter($note['subnotes'], function($s) use ($sub_id) {
+ return $s['id'] !== $sub_id;
+ }));
+ if (count($note['subnotes']) < $initial) {
+ $found = true;
+ break;
+ }
+ }
+ }
+
+ if (!$found) return ['success' => false, 'message' => 'Sottonota non trovata.'];
+
+ if (save_blocknote($bn)) {
+ return ['success' => true, 'message' => 'Sottonota eliminata con successo.'];
+ }
+ return ['success' => false, 'message' => 'Errore nel salvataggio.'];
+}
diff --git a/functions/functions.php b/functions/functions.php
index d36fbca..b4dee53 100644
--- a/functions/functions.php
+++ b/functions/functions.php
@@ -1,13 +1,15 @@
prepare("UPDATE users SET language = ? WHERE id = ?");
+ $stmt->bind_param("si", $lang, $_SESSION['user']['id']);
+ $stmt->execute();
+ $stmt->close();
+ }
+ } else {
+ $_SESSION['guest_language'] = $lang;
+ }
+ }
+}
+
+/**
+ * Translations Dictionary
+ */
+function get_translations() {
+ return [
+ 'it' => [
+ // Login Page
+ 'login_title' => 'Accedi a Blocknotes',
+ 'login_subtitle' => 'Gestisci le tue note e i tuoi comandi',
+ 'label_username' => 'Nome Utente',
+ 'label_password' => 'Password',
+ 'placeholder_username' => 'Inserisci username',
+ 'placeholder_password' => 'Inserisci password',
+ 'btn_submit_login' => '๐ Accedi',
+ 'login_hint_footer' => 'Accredito predefinito per il primo accesso',
+ 'login_installed_success' => '๐ Setup completato! Accedi con le credenziali create.',
+
+ // Navbar
+ 'nav_blocknotes' => '๐ Blocknotes',
+ 'nav_colors_profile' => '๐จ Colori & Profilo',
+ 'nav_admin' => '๐ Gestione Utenti',
+ 'nav_logout' => '๐ช Esci',
+ 'nav_login' => '๐ Accedi',
+ 'nav_language' => '๐ Lingua',
+
+ // Dashboard (home)
+ 'dashboard_title_user' => 'I Miei Blocknotes',
+ 'dashboard_title_admin' => 'Tutti i Blocknotes (Vista Admin)',
+ 'dashboard_desc_user' => 'Crea, organizza e consulta i tuoi blocknotes.',
+ 'dashboard_desc_admin' => 'In qualitร di Admin puoi visualizzare, gestire ed eliminare i blocknotes di tutti gli utenti.',
+ 'btn_new_blocknote' => 'โ Nuovo Blocknote',
+ 'btn_import_json' => '๐ฅ Importa JSON',
+ 'btn_export_json' => '๐พ Esporta JSON',
+ 'btn_open_edit' => '๐๏ธ Apri & Modifica',
+ 'btn_rename' => 'โ๏ธ Rinomina',
+ 'btn_delete' => '๐๏ธ Elimina',
+ 'empty_blocknotes_title' => 'Nessun Blocknote trovato',
+ 'empty_blocknotes_desc_user' => 'Non hai ancora creato nessun blocknote. Inizia subito!',
+ 'empty_blocknotes_desc_admin' => 'Non ci sono ancora blocknotes salvati nel sistema.',
+ 'modal_create_title' => 'โ Crea Nuovo Blocknote',
+ 'modal_rename_title' => 'โ๏ธ Rinomina Blocknote',
+ 'modal_import_title' => '๐ฅ Importa Blocknote (JSON)',
+ 'label_title' => 'Titolo Blocknote',
+ 'label_file_json' => 'Carica File .json',
+ 'label_paste_json' => 'Oppure incolla qui il codice JSON',
+ 'btn_cancel' => 'Annulla',
+ 'btn_save' => 'Salva',
+ 'btn_create' => 'Crea Blocknote',
+ 'btn_import' => 'Importa Blocknote',
+ 'note_single' => 'Nota',
+ 'note_plural' => 'Note',
+
+ // View Blocknote
+ 'btn_back_list' => 'โฌ
๏ธ Torna ai Blocknotes',
+ 'btn_expand_all' => '๐ Espandi Tutte',
+ 'btn_collapse_all' => '๐ Riduci Tutte',
+ 'btn_new_note' => 'โ Nuova Nota',
+ 'btn_add_subnote' => 'โ Sottonota',
+ 'subnote_single' => 'sottonota',
+ 'subnote_plural' => 'sottonote',
+ 'badge_text' => '๐ Testo',
+ 'badge_command' => 'โก Comando',
+ 'btn_copy' => '๐ Copia',
+ 'btn_copied' => 'โ Copiato!',
+ 'search_placeholder' => '๐ Cerca note, sottonote o comandi in questo blocknote...',
+ 'btn_clear_search' => 'โ Annulla Cerca',
+ 'search_no_results' => 'Nessun risultato trovato',
+ 'search_no_results_desc' => 'Nessuna nota o comando corrisponde al termine cercato in questo blocknote.',
+ 'modal_add_note' => 'โ Nuova Nota a Blocchi',
+ 'modal_edit_note' => 'โ๏ธ Modifica Nota',
+ 'modal_add_subnote' => 'โ Aggiungi Sottonota',
+ 'modal_edit_subnote' => 'โ๏ธ Modifica Sottonota',
+ 'label_note_description' => 'Descrizione (opzionale)',
+ 'label_subnote_title' => 'Titolo Sottonota',
+ 'label_subnote_type' => 'Tipo di Sottonota',
+ 'label_subnote_content' => 'Contenuto',
+ 'type_option_text' => '๐ Testo generico',
+ 'type_option_command' => 'โก Comando (con pulsante Copia)',
+
+ // Profile & Settings
+ 'profile_title' => '๐จ Impostazioni Profilo & Tema',
+ 'profile_desc' => 'Personalizza password, colori dell\'interfaccia, lingua e pagina di avvio.',
+ 'card_password_title' => '๐ Cambio Password',
+ 'card_colors_title' => '๐จ Personalizza Colori Interfaccia (HSL)',
+ 'card_lang_title' => '๐ Lingua dell\'Interfaccia',
+ 'card_landing_title' => '๐ Pagina di Primo Accesso (Landing Page)',
+ 'label_new_pass' => 'Nuova Password',
+ 'label_confirm_pass' => 'Conferma Nuova Password',
+ 'label_select_lang' => 'Seleziona Lingua',
+ 'label_select_landing' => 'Seleziona Pagina di Avvio',
+ 'landing_option_dashboard' => '๐ Pagina Principale (Tutti i Blocknotes)',
+ 'landing_option_bn_prefix' => '๐ Apri direttamente: ',
+ 'btn_update_pass' => '๐ Aggiorna Password',
+ 'btn_save_colors' => '๐พ Salva Preferenze Colori',
+ 'btn_save_lang' => '๐ Salva Lingua',
+ 'btn_save_landing' => '๐ Salva Pagina di Avvio',
+
+ // Admin Users
+ 'admin_title' => '๐ Pannello Amministrazione Utenti',
+ 'admin_desc' => 'Crea, gestisci e modifica i ruoli e gli accessi degli utenti.',
+ 'btn_add_user' => 'โ Aggiungi Nuovo Utente',
+ 'col_username' => 'Nome Utente',
+ 'col_role' => 'Ruolo',
+ 'col_bns' => 'Blocknotes Creati',
+ 'col_created' => 'Data Registrazione',
+ 'col_actions' => 'Azioni Admin',
+ 'role_standard' => 'Standard User',
+ 'role_admin' => 'Administrator',
+ 'role_owner' => '๐ Owner (Proprietario)',
+ 'badge_owner' => '๐ Owner',
+ 'btn_reset_pass' => '๐ Reset Pass',
+ 'modal_create_user' => 'โ Registra Nuovo Utente',
+ 'modal_reset_pass' => '๐ Reset Password Utente',
+ // Confirm Dialogs
+ 'confirm_delete_blocknote' => 'Sei sicuro di voler eliminare questo blocknote e tutte le sue note?',
+ 'confirm_delete_note' => 'Sei sicuro di voler eliminare questa nota e tutte le sue sottonote?',
+ 'confirm_delete_subnote' => 'Sei sicuro di voler eliminare questa sottonota?',
+ 'confirm_delete_user' => 'Sei sicuro di voler eliminare questo utente?',
+
+ // Placeholders
+ 'placeholder_bn_title' => 'Es. Comandi Server Linux, Note Progetto PHP...',
+ 'placeholder_rename_title' => 'Nuovo titolo...',
+ 'placeholder_import_title' => 'Es. Positive Internet',
+ 'placeholder_paste_json' => 'Incolla qui il JSON nel vecchio o nuovo formato...',
+ 'placeholder_note_title' => 'Es. Installazione MySQL, Server Setup...',
+ 'placeholder_note_desc' => 'Breve descrizione di questo blocco di note...',
+ 'placeholder_subnote_title' => 'Es. Comando creazione database, Note configurazione...',
+ 'placeholder_subnote_content' => 'Inserisci il testo o le istruzioni del comando...',
+ 'placeholder_site_title' => 'Es. Blocknotes, Enterprise Notes...',
+ 'placeholder_username_new' => 'es. mario, giuseppe...',
+ 'placeholder_password_new' => 'Inserisci nuova password',
+ 'placeholder_password_confirm' => 'Ripeti nuova password',
+
+ // Extra Labels
+ 'label_owner_prefix' => 'Proprietario: ',
+ 'label_updated_prefix' => 'Ultimo aggiornamento: ',
+ 'label_you' => 'Tu',
+ 'label_unknown' => 'Sconosciuto',
+ 'label_current_favicon' => 'Favicon Attuale: ',
+
+ // First Start Installer Setup
+ 'setup_title' => '๐ Setup Iniziale Blocknotes',
+ 'setup_subtitle' => 'Configura il database MySQL e crea i primi utenti privilegiati (Owner & Admin).',
+ 'setup_sec1_title' => '1. Configurazione Database MySQL',
+ 'setup_db_host' => 'Host Database',
+ 'setup_db_name' => 'Nome Database',
+ 'setup_db_user' => 'Utente MySQL',
+ 'setup_db_pass' => 'Password MySQL',
+ 'setup_db_pass_placeholder' => 'Password (lascia vuoto se nessuna)',
+ 'setup_sec2_title' => '2. Account Owner (Proprietario Unico)',
+ 'setup_owner_user' => 'Username Owner',
+ 'setup_owner_pass' => 'Password Owner',
+ 'setup_owner_pass_placeholder' => 'Inserisci password per Owner',
+ 'setup_sec3_title' => '3. Primo Account Amministratore (Admin)',
+ 'setup_admin_user' => 'Username Admin',
+ 'setup_admin_pass' => 'Password Admin',
+ 'setup_admin_pass_placeholder' => 'Inserisci password per Admin',
+ 'setup_btn_submit' => 'โก Completa Installazione & Configura Sistema',
+ 'err_setup_db_fields' => 'Compila tutti i dati di connessione al Database MySQL.',
+ 'err_setup_owner_fields' => 'Inserisci username e password per l\'utente Owner (Proprietario).',
+ 'err_setup_admin_fields' => 'Inserisci username e password per il primo utente Admin.',
+ 'err_setup_same_user' => 'L\'utente Owner e l\'utente Admin devono avere nomi utente differenti.'
+ ],
+
+ 'en' => [
+ // Login Page
+ 'login_title' => 'Login to Blocknotes',
+ 'login_subtitle' => 'Manage your notes and commands',
+ 'label_username' => 'Username',
+ 'label_password' => 'Password',
+ 'placeholder_username' => 'Enter username',
+ 'placeholder_password' => 'Enter password',
+ 'btn_submit_login' => '๐ Login',
+ 'login_hint_footer' => 'Default first login credential',
+ 'login_installed_success' => '๐ Setup completed! Login with your created credentials.',
+
+ // Navbar
+ 'nav_blocknotes' => '๐ Blocknotes',
+ 'nav_colors_profile' => '๐จ Colors & Profile',
+ 'nav_admin' => '๐ User Management',
+ 'nav_logout' => '๐ช Logout',
+ 'nav_login' => '๐ Login',
+ 'nav_language' => '๐ Language',
+
+ // Dashboard (home)
+ 'dashboard_title_user' => 'My Blocknotes',
+ 'dashboard_title_admin' => 'All Blocknotes (Admin View)',
+ 'dashboard_desc_user' => 'Create, organize, and view your blocknotes.',
+ 'dashboard_desc_admin' => 'As an Admin you can view, manage, and delete blocknotes of all users.',
+ 'btn_new_blocknote' => 'โ New Blocknote',
+ 'btn_import_json' => '๐ฅ Import JSON',
+ 'btn_export_json' => '๐พ Export JSON',
+ 'btn_open_edit' => '๐๏ธ Open & Edit',
+ 'btn_rename' => 'โ๏ธ Rename',
+ 'btn_delete' => '๐๏ธ Delete',
+ 'empty_blocknotes_title' => 'No Blocknotes found',
+ 'empty_blocknotes_desc_user' => 'You haven\'t created any blocknote yet. Start now!',
+ 'empty_blocknotes_desc_admin' => 'There are no blocknotes saved in the system yet.',
+ 'modal_create_title' => 'โ Create New Blocknote',
+ 'modal_rename_title' => 'โ๏ธ Rename Blocknote',
+ 'modal_import_title' => '๐ฅ Import Blocknote (JSON)',
+ 'label_title' => 'Blocknote Title',
+ 'label_file_json' => 'Upload .json File',
+ 'label_paste_json' => 'Or paste JSON code here',
+ 'btn_cancel' => 'Cancel',
+ 'btn_save' => 'Save',
+ 'btn_create' => 'Create Blocknote',
+ 'btn_import' => 'Import Blocknote',
+ 'note_single' => 'Note',
+ 'note_plural' => 'Notes',
+
+ // View Blocknote
+ 'btn_back_list' => 'โฌ
๏ธ Back to Blocknotes',
+ 'btn_expand_all' => '๐ Expand All',
+ 'btn_collapse_all' => '๐ Collapse All',
+ 'btn_new_note' => 'โ New Note',
+ 'btn_add_subnote' => 'โ Subnote',
+ 'subnote_single' => 'subnote',
+ 'subnote_plural' => 'subnotes',
+ 'badge_text' => '๐ Text',
+ 'badge_command' => 'โก Command',
+ 'btn_copy' => '๐ Copy',
+ 'btn_copied' => 'โ Copied!',
+ 'search_placeholder' => '๐ Search notes, subnotes, or commands in this blocknote...',
+ 'btn_clear_search' => 'โ Clear Search',
+ 'search_no_results' => 'No results found',
+ 'search_no_results_desc' => 'No notes or commands match your search term in this blocknote.',
+ 'modal_add_note' => 'โ New Block Note',
+ 'modal_edit_note' => 'โ๏ธ Edit Note',
+ 'modal_add_subnote' => 'โ Add Subnote',
+ 'modal_edit_subnote' => 'โ๏ธ Edit Subnote',
+ 'label_note_description' => 'Description (optional)',
+ 'label_subnote_title' => 'Subnote Title',
+ 'label_subnote_type' => 'Subnote Type',
+ 'label_subnote_content' => 'Content',
+ 'type_option_text' => '๐ Generic Text',
+ 'type_option_command' => 'โก Command (with Copy button)',
+
+ // Profile & Settings
+ 'profile_title' => '๐จ Profile & Theme Settings',
+ 'profile_desc' => 'Customize password, UI colors, language, and default landing page.',
+ 'card_password_title' => '๐ Change Password',
+ 'card_colors_title' => '๐จ Interface Colors (HSL)',
+ 'card_lang_title' => '๐ Interface Language',
+ 'card_landing_title' => '๐ Default Landing Page',
+ 'label_new_pass' => 'New Password',
+ 'label_confirm_pass' => 'Confirm New Password',
+ 'label_select_lang' => 'Select Language',
+ 'label_select_landing' => 'Select Landing Page',
+ 'landing_option_dashboard' => '๐ Main Selection Page (All Blocknotes)',
+ 'landing_option_bn_prefix' => '๐ Open directly: ',
+ 'btn_update_pass' => '๐ Update Password',
+ 'btn_save_colors' => '๐พ Save Color Preferences',
+ 'btn_save_lang' => '๐ Save Language',
+ 'btn_save_landing' => '๐ Save Landing Page',
+
+ // Admin Users
+ 'admin_title' => '๐ User Administration Panel',
+ 'admin_desc' => 'Create, manage, and edit user roles and access.',
+ 'btn_add_user' => 'โ Add New User',
+ 'col_username' => 'Username',
+ 'col_role' => 'Role',
+ 'col_bns' => 'Blocknotes Created',
+ 'col_created' => 'Created Date',
+ 'col_actions' => 'Admin Actions',
+ 'role_standard' => 'Standard User',
+ 'role_admin' => 'Administrator',
+ 'role_owner' => '๐ Owner',
+ 'badge_owner' => '๐ Owner',
+ 'btn_reset_pass' => '๐ Reset Pass',
+ 'modal_create_user' => 'โ Register New User',
+ 'modal_reset_pass' => '๐ Reset User Password',
+ 'card_site_settings_title' => 'โ๏ธ Site Settings (Owner Only)',
+ 'label_site_title' => 'Global Application / Site Title',
+ 'label_favicon' => 'Site Favicon (High Resolution, min 64x64px)',
+ 'btn_save_site_title' => '๐พ Save Site Title',
+ 'btn_upload_favicon' => '๐ค Upload New Favicon',
+
+ // Confirm Dialogs
+ 'confirm_delete_blocknote' => 'Are you sure you want to delete this blocknote and all its notes?',
+ 'confirm_delete_note' => 'Are you sure you want to delete this note and all its subnotes?',
+ 'confirm_delete_subnote' => 'Are you sure you want to delete this subnote?',
+ 'confirm_delete_user' => 'Are you sure you want to delete this user?',
+
+ // Placeholders
+ 'placeholder_bn_title' => 'E.g. Linux Server Commands, PHP Notes...',
+ 'placeholder_rename_title' => 'New title...',
+ 'placeholder_import_title' => 'E.g. Positive Internet',
+ 'placeholder_paste_json' => 'Paste JSON here in old or new format...',
+ 'placeholder_note_title' => 'E.g. MySQL Installation, Server Setup...',
+ 'placeholder_note_desc' => 'Short description of this note block...',
+ 'placeholder_subnote_title' => 'E.g. Database creation command...',
+ 'placeholder_subnote_content' => 'Enter text or command instructions...',
+ 'placeholder_site_title' => 'E.g. Blocknotes, Enterprise Notes...',
+ 'placeholder_username_new' => 'e.g. john, mary...',
+ 'placeholder_password_new' => 'Enter new password',
+ 'placeholder_password_confirm' => 'Repeat new password',
+
+ // Extra Labels
+ 'label_owner_prefix' => 'Owner: ',
+ 'label_updated_prefix' => 'Last updated: ',
+ 'label_you' => 'You',
+ 'label_unknown' => 'Unknown',
+ 'label_current_favicon' => 'Current Favicon: ',
+
+ // First Start Installer Setup
+ 'setup_title' => '๐ Blocknotes Initial Setup',
+ 'setup_subtitle' => 'Configure MySQL database and create the initial privileged users (Owner & Admin).',
+ 'setup_sec1_title' => '1. MySQL Database Configuration',
+ 'setup_db_host' => 'Database Host',
+ 'setup_db_name' => 'Database Name',
+ 'setup_db_user' => 'MySQL User',
+ 'setup_db_pass' => 'MySQL Password',
+ 'setup_db_pass_placeholder' => 'Password (leave empty if none)',
+ 'setup_sec2_title' => '2. Owner Account (Single Owner)',
+ 'setup_owner_user' => 'Owner Username',
+ 'setup_owner_pass' => 'Owner Password',
+ 'setup_owner_pass_placeholder' => 'Enter password for Owner',
+ 'setup_sec3_title' => '3. First Admin Account',
+ 'setup_admin_user' => 'Admin Username',
+ 'setup_admin_pass' => 'Admin Password',
+ 'setup_admin_pass_placeholder' => 'Enter password for Admin',
+ 'setup_btn_submit' => 'โก Complete Installation & Configure System',
+ 'err_setup_db_fields' => 'Please fill in all MySQL database connection fields.',
+ 'err_setup_owner_fields' => 'Please enter username and password for the Owner user.',
+ 'err_setup_admin_fields' => 'Please enter username and password for the first Admin user.',
+ 'err_setup_same_user' => 'Owner user and Admin user must have different usernames.'
+ ],
+
+ 'es' => [
+ // Login Page
+ 'login_title' => 'Iniciar sesiรณn en Blocknotes',
+ 'login_subtitle' => 'Gestiona tus notas y comandos',
+ 'label_username' => 'Nombre de Usuario',
+ 'label_password' => 'Contraseรฑa',
+ 'placeholder_username' => 'Introduce nombre de usuario',
+ 'placeholder_password' => 'Introduce contraseรฑa',
+ 'btn_submit_login' => '๐ Iniciar Sesiรณn',
+ 'login_hint_footer' => 'Credencial predeterminada para el primer acceso',
+ 'login_installed_success' => '๐ ยกConfiguraciรณn completada! Inicia sesiรณn con tus credenciales.',
+
+ // Navbar
+ 'nav_blocknotes' => '๐ Blocknotes',
+ 'nav_colors_profile' => '๐จ Colores y Perfil',
+ 'nav_admin' => '๐ Gestiรณn de Usuarios',
+ 'nav_logout' => '๐ช Salir',
+ 'nav_login' => '๐ Iniciar Sesiรณn',
+ 'nav_language' => '๐ Idioma',
+
+ // Dashboard (home)
+ 'dashboard_title_user' => 'Mis Blocknotes',
+ 'dashboard_title_admin' => 'Todos los Blocknotes (Vista Admin)',
+ 'dashboard_desc_user' => 'Crea, organiza y consulta tus blocknotes.',
+ 'dashboard_desc_admin' => 'Como Admin puedes ver, gestionar y eliminar los blocknotes de todos los usuarios.',
+ 'btn_new_blocknote' => 'โ Nuevo Blocknote',
+ 'btn_import_json' => '๐ฅ Importar JSON',
+ 'btn_export_json' => '๐พ Exportar JSON',
+ 'btn_open_edit' => '๐๏ธ Abrir y Editar',
+ 'btn_rename' => 'โ๏ธ Renombrar',
+ 'btn_delete' => '๐๏ธ Eliminar',
+ 'empty_blocknotes_title' => 'No se encontraron blocknotes',
+ 'empty_blocknotes_desc_user' => 'Aรบn no has creado ningรบn blocknote. ยกEmpieza ahora!',
+ 'empty_blocknotes_desc_admin' => 'Aรบn no hay blocknotes guardados en el sistema.',
+ 'modal_create_title' => 'โ Crear Nuevo Blocknote',
+ 'modal_rename_title' => 'โ๏ธ Renombrar Blocknote',
+ 'modal_import_title' => '๐ฅ Importar Blocknote (JSON)',
+ 'label_title' => 'Tรญtulo del Blocknote',
+ 'label_file_json' => 'Subir archivo .json',
+ 'label_paste_json' => 'O pega el cรณdigo JSON aquรญ',
+ 'btn_cancel' => 'Cancelar',
+ 'btn_save' => 'Guardar',
+ 'btn_create' => 'Crear Blocknote',
+ 'btn_import' => 'Importar Blocknote',
+ 'note_single' => 'Nota',
+ 'note_plural' => 'Notas',
+
+ // View Blocknote
+ 'btn_back_list' => 'โฌ
๏ธ Volver a Blocknotes',
+ 'btn_expand_all' => '๐ Expandir Todas',
+ 'btn_collapse_all' => '๐ Contraer Todas',
+ 'btn_new_note' => 'โ Nueva Nota',
+ 'btn_add_subnote' => 'โ Subnota',
+ 'subnote_single' => 'subnota',
+ 'subnote_plural' => 'subnotas',
+ 'badge_text' => '๐ Texto',
+ 'badge_command' => 'โก Comando',
+ 'btn_copy' => '๐ Copiar',
+ 'btn_copied' => 'โ ยกCopiado!',
+ 'search_placeholder' => '๐ Buscar notas, subnotas o comandos...',
+ 'btn_clear_search' => 'โ Cancelar Bรบsqueda',
+ 'search_no_results' => 'No se encontraron resultados',
+ 'search_no_results_desc' => 'Ninguna nota o comando coincide con tu bรบsqueda.',
+ 'modal_add_note' => 'โ Nueva Nota en Bloque',
+ 'modal_edit_note' => 'โ๏ธ Editar Nota',
+ 'modal_add_subnote' => 'โ Aรฑadir Subnota',
+ 'modal_edit_subnote' => 'โ๏ธ Editar Subnota',
+ 'label_note_description' => 'Descripciรณn (opcional)',
+ 'label_subnote_title' => 'Tรญtulo de Subnota',
+ 'label_subnote_type' => 'Tipo de Subnota',
+ 'label_subnote_content' => 'Contenido',
+ 'type_option_text' => '๐ Texto genรฉrico',
+ 'type_option_command' => 'โก Comando (con botรณn Copiar)',
+
+ // Profile & Settings
+ 'profile_title' => '๐จ Ajustes de Perfil y Tema',
+ 'profile_desc' => 'Personaliza contraseรฑa, colores de interfaz, idioma y pรกgina de inicio.',
+ 'card_password_title' => '๐ Cambiar Contraseรฑa',
+ 'card_colors_title' => '๐จ Colores de Interfaz (HSL)',
+ 'card_lang_title' => '๐ Idioma de la Interfaz',
+ 'card_landing_title' => '๐ Pรกgina de Inicio Predeterminada',
+ 'label_new_pass' => 'Nueva Contraseรฑa',
+ 'label_confirm_pass' => 'Confirmar Nueva Contraseรฑa',
+ 'label_select_lang' => 'Seleccionar Idioma',
+ 'label_select_landing' => 'Seleccionar Pรกgina de Inicio',
+ 'landing_option_dashboard' => '๐ Pรกgina Principal (Todos los Blocknotes)',
+ 'landing_option_bn_prefix' => '๐ Abrir directamente: ',
+ 'btn_update_pass' => '๐ Actualizar Contraseรฑa',
+ 'btn_save_colors' => '๐พ Guardar Colores',
+ 'btn_save_lang' => '๐ Guardar Idioma',
+ 'btn_save_landing' => '๐ Guardar Pรกgina de Inicio',
+
+ // Admin Users
+ 'admin_title' => '๐ Panel de Administraciรณn de Usuarios',
+ 'admin_desc' => 'Crea, gestiona y edita roles de usuarios y accesos.',
+ 'btn_add_user' => 'โ Aรฑadir Nuevo Usuario',
+ 'col_username' => 'Nombre de Usuario',
+ 'col_role' => 'Rol',
+ 'col_bns' => 'Blocknotes Creados',
+ 'col_created' => 'Fecha de Creaciรณn',
+ 'col_actions' => 'Acciones de Admin',
+ 'role_standard' => 'Usuario Estรกndar',
+ 'role_admin' => 'Administrador',
+ 'role_owner' => '๐ Propietario (Owner)',
+ 'badge_owner' => '๐ Propietario',
+ 'btn_reset_pass' => '๐ Reset Pass',
+ 'modal_create_user' => 'โ Registrar Nuevo Usuario',
+ 'modal_reset_pass' => '๐ Resetear Contraseรฑa',
+ 'card_site_settings_title' => 'โ๏ธ Ajustes del Sitio (Solo Propietario)',
+ 'label_site_title' => 'Tรญtulo del Sitio / Aplicaciรณn',
+ 'label_favicon' => 'Favicon del Sitio (Alta resoluciรณn, mรญn 64x64px)',
+ 'btn_save_site_title' => '๐พ Guardar Tรญtulo',
+ 'btn_upload_favicon' => '๐ค Subir Nueva Favicon',
+
+ // Confirm Dialogs
+ 'confirm_delete_blocknote' => 'ยฟEstรกs seguro de que quieres eliminar este blocknote y todas sus notas?',
+ 'confirm_delete_note' => 'ยฟEstรกs seguro de que quieres eliminar esta nota y todas sus subnotas?',
+ 'confirm_delete_subnote' => 'ยฟEstรกs seguro de que quieres eliminar esta subnota?',
+ 'confirm_delete_user' => 'ยฟEstรกs seguro de que quieres eliminar este usuario?',
+
+ // Placeholders
+ 'placeholder_bn_title' => 'Ej. Comandos Servidor Linux...',
+ 'placeholder_rename_title' => 'Nuevo tรญtulo...',
+ 'placeholder_import_title' => 'Ej. Positive Internet',
+ 'placeholder_paste_json' => 'Pega el JSON aquรญ en formato antiguo o nuevo...',
+ 'placeholder_note_title' => 'Ej. Instalaciรณn MySQL...',
+ 'placeholder_note_desc' => 'Breve descripciรณn de este bloque de notas...',
+ 'placeholder_subnote_title' => 'Ej. Comando creaciรณn base de datos...',
+ 'placeholder_subnote_content' => 'Introduce el texto o instrucciones del comando...',
+ 'placeholder_site_title' => 'Ej. Blocknotes...',
+ 'placeholder_username_new' => 'ej. mario, juan...',
+ 'placeholder_password_new' => 'Introduce nueva contraseรฑa',
+ 'placeholder_password_confirm' => 'Repite nueva contraseรฑa',
+
+ // Extra Labels
+ 'label_owner_prefix' => 'Propietario: ',
+ 'label_updated_prefix' => 'รltima actualizaciรณn: ',
+ 'label_you' => 'Tรบ',
+ 'label_unknown' => 'Desconocido',
+ 'label_current_favicon' => 'Favicon Actual: ',
+
+ // First Start Installer Setup
+ 'setup_title' => '๐ Configuraciรณn Inicial de Blocknotes',
+ 'setup_subtitle' => 'Configura la base de datos MySQL y crea los primeros usuarios privilegiados (Propietario y Admin).',
+ 'setup_sec1_title' => '1. Configuraciรณn de Base de Datos MySQL',
+ 'setup_db_host' => 'Host de Base de Datos',
+ 'setup_db_name' => 'Nombre de la Base de Datos',
+ 'setup_db_user' => 'Usuario MySQL',
+ 'setup_db_pass' => 'Contraseรฑa MySQL',
+ 'setup_db_pass_placeholder' => 'Contraseรฑa (deja en blanco si no hay)',
+ 'setup_sec2_title' => '2. Cuenta de Propietario (รnico)',
+ 'setup_owner_user' => 'Nombre de Usuario Propietario',
+ 'setup_owner_pass' => 'Contraseรฑa de Propietario',
+ 'setup_owner_pass_placeholder' => 'Introduce contraseรฑa para Propietario',
+ 'setup_sec3_title' => '3. Primera Cuenta de Administrador (Admin)',
+ 'setup_admin_user' => 'Nombre de Usuario Admin',
+ 'setup_admin_pass' => 'Contraseรฑa de Admin',
+ 'setup_admin_pass_placeholder' => 'Introduce contraseรฑa para Admin',
+ 'setup_btn_submit' => 'โก Completar Instalaciรณn y Configurar Sistema',
+ 'err_setup_db_fields' => 'Por favor completa todos los campos de conexiรณn a la base de datos MySQL.',
+ 'err_setup_owner_fields' => 'Por favor introduce nombre de usuario y contraseรฑa para el Propietario.',
+ 'err_setup_admin_fields' => 'Por favor introduce nombre de usuario y contraseรฑa para el primer Admin.',
+ 'err_setup_same_user' => 'El usuario Propietario y el usuario Admin deben tener nombres diferentes.'
+ ],
+
+ 'fr' => [
+ // Login Page
+ 'login_title' => 'Connexion ร Blocknotes',
+ 'login_subtitle' => 'Gรฉrez vos notes et vos commandes',
+ 'label_username' => 'Nom d\'Utilisateur',
+ 'label_password' => 'Mot de Passe',
+ 'placeholder_username' => 'Entrez nom d\'utilisateur',
+ 'placeholder_password' => 'Entrez mot de passe',
+ 'btn_submit_login' => '๐ Connexion',
+ 'login_hint_footer' => 'Identifiant par dรฉfaut pour le premier accรจs',
+ 'login_installed_success' => '๐ Configuration terminรฉe! Connectez-vous avec vos identifiants.',
+
+ // Navbar
+ 'nav_blocknotes' => '๐ Blocknotes',
+ 'nav_colors_profile' => '๐จ Couleurs & Profil',
+ 'nav_admin' => '๐ Gestion des Utilisateurs',
+ 'nav_logout' => '๐ช Dรฉconnexion',
+ 'nav_login' => '๐ Connexion',
+ 'nav_language' => '๐ Langue',
+
+ // Dashboard (home)
+ 'dashboard_title_user' => 'Mes Blocknotes',
+ 'dashboard_title_admin' => 'Tous les Blocknotes (Vue Admin)',
+ 'dashboard_desc_user' => 'Crรฉez, organisez et consultez vos blocknotes.',
+ 'dashboard_desc_admin' => 'En tant qu\'Admin vous pouvez afficher, gรฉrer et supprimer les blocknotes de tous les utilisateurs.',
+ 'btn_new_blocknote' => 'โ Nouveau Blocknote',
+ 'btn_import_json' => '๐ฅ Importer JSON',
+ 'btn_export_json' => '๐พ Exporter JSON',
+ 'btn_open_edit' => '๐๏ธ Ouvrir & Modifier',
+ 'btn_rename' => 'โ๏ธ Renommer',
+ 'btn_delete' => '๐๏ธ Supprimer',
+ 'empty_blocknotes_title' => 'Aucun blocknote trouvรฉ',
+ 'empty_blocknotes_desc_user' => 'Vous n\'avez pas encore crรฉรฉ de blocknote. Commencez dรจs maintenant!',
+ 'empty_blocknotes_desc_admin' => 'Il n\'y a encore aucun blocknote enregistrรฉ dans le systรจme.',
+ 'modal_create_title' => 'โ Crรฉer un Nouveau Blocknote',
+ 'modal_rename_title' => 'โ๏ธ Renommer le Blocknote',
+ 'modal_import_title' => '๐ฅ Importer Blocknote (JSON)',
+ 'label_title' => 'Titre du Blocknote',
+ 'label_file_json' => 'Tรฉlรฉverser le fichier .json',
+ 'label_paste_json' => 'Ou collez le code JSON ici',
+ 'btn_cancel' => 'Annuler',
+ 'btn_save' => 'Enregistrer',
+ 'btn_create' => 'Crรฉer Blocknote',
+ 'btn_import' => 'Importer Blocknote',
+ 'note_single' => 'Note',
+ 'note_plural' => 'Notes',
+
+ // View Blocknote
+ 'btn_back_list' => 'โฌ
๏ธ Retour aux Blocknotes',
+ 'btn_expand_all' => '๐ Tout Dรฉvelopper',
+ 'btn_collapse_all' => '๐ Tout Rรฉduire',
+ 'btn_new_note' => 'โ Nouvelle Note',
+ 'btn_add_subnote' => 'โ Sous-note',
+ 'subnote_single' => 'sous-note',
+ 'subnote_plural' => 'sous-notes',
+ 'badge_text' => '๐ Texte',
+ 'badge_command' => 'โก Commande',
+ 'btn_copy' => '๐ Copier',
+ 'btn_copied' => 'โ Copiรฉ!',
+ 'search_placeholder' => '๐ Rechercher notes, sous-notes ou commandes...',
+ 'btn_clear_search' => 'โ Annuler la Recherche',
+ 'search_no_results' => 'Aucun rรฉsultat trouvรฉ',
+ 'search_no_results_desc' => 'Aucune note ou commande ne correspond ร votre recherche.',
+ 'modal_add_note' => 'โ Nouvelle Note par Bloc',
+ 'modal_edit_note' => 'โ๏ธ Modifier la Note',
+ 'modal_add_subnote' => 'โ Ajouter une Sous-note',
+ 'modal_edit_subnote' => 'โ๏ธ Modifier la Sous-note',
+ 'label_note_description' => 'Description (facultatif)',
+ 'label_subnote_title' => 'Titre de la Sous-note',
+ 'label_subnote_type' => 'Type de Sous-note',
+ 'label_subnote_content' => 'Contenu',
+ 'type_option_text' => '๐ Texte gรฉnรฉrique',
+ 'type_option_command' => 'โก Commande (avec bouton Copier)',
+
+ // Profile & Settings
+ 'profile_title' => '๐จ Paramรจtres Profil & Thรจme',
+ 'profile_desc' => 'Personnalisez votre mot de passe, couleurs, langue et page d\'accueil.',
+ 'card_password_title' => '๐ Changer le Mot de Passe',
+ 'card_colors_title' => '๐จ Couleurs de l\'Interface (HSL)',
+ 'card_lang_title' => '๐ Langue de l\'Interface',
+ 'card_landing_title' => '๐ Page d\'Accueil par Dรฉfaut',
+ 'label_new_pass' => 'Nouveau Mot de Passe',
+ 'label_confirm_pass' => 'Confirmer le Mot de Passe',
+ 'label_select_lang' => 'Sรฉlectionner la Langue',
+ 'label_select_landing' => 'Sรฉlectionner la Page d\'Accueil',
+ 'landing_option_dashboard' => '๐ Page Principale (Tous les Blocknotes)',
+ 'landing_option_bn_prefix' => '๐ Ouvrir directement: ',
+ 'btn_update_pass' => '๐ Mettre ร Jour',
+ 'btn_save_colors' => '๐พ Enregistrer les Couleurs',
+ 'btn_save_lang' => '๐ Enregistrer la Langue',
+ 'btn_save_landing' => '๐ Enregistrer la Page d\'Accueil',
+
+ // Admin Users
+ 'admin_title' => '๐ Panneau d\'Administration Utilisateurs',
+ 'admin_desc' => 'Crรฉez, gรฉrez et modifiez les rรดles et accรจs des utilisateurs.',
+ 'btn_add_user' => 'โ Ajouter un Utilisateur',
+ 'col_username' => 'Nom d\'Utilisateur',
+ 'col_role' => 'Rรดle',
+ 'col_bns' => 'Blocknotes Crรฉรฉs',
+ 'col_created' => 'Date de Crรฉation',
+ 'col_actions' => 'Actions Admin',
+ 'role_standard' => 'Utilisateur Standard',
+ 'role_admin' => 'Administrateur',
+ 'role_owner' => '๐ Propriรฉtaire (Owner)',
+ 'badge_owner' => '๐ Propriรฉtaire',
+ 'btn_reset_pass' => '๐ Rรฉinit Pass',
+ 'modal_create_user' => 'โ Enregistrer un Utilisateur',
+ 'modal_reset_pass' => '๐ Rรฉinitialiser le Mot de Passe',
+ 'card_site_settings_title' => 'โ๏ธ Paramรจtres du Site (Propriรฉtaire Uniquement)',
+ 'label_site_title' => 'Titre Global du Site',
+ 'label_favicon' => 'Favicon du Site (Haute rรฉsolution, min 64x64px)',
+ 'btn_save_site_title' => '๐พ Enregistrer le Titre',
+ 'btn_upload_favicon' => '๐ค Tรฉlรฉverser la Favicon',
+
+ // Confirm Dialogs
+ 'confirm_delete_blocknote' => 'รtes-vous sรปr de vouloir supprimer ce blocknote et toutes ses notes?',
+ 'confirm_delete_note' => 'รtes-vous sรปr de vouloir supprimer cette note et toutes ses sous-notes?',
+ 'confirm_delete_subnote' => 'รtes-vous sรปr de vouloir supprimer cette sous-note?',
+ 'confirm_delete_user' => 'รtes-vous sรปr de vouloir supprimer cet utilisateur?',
+
+ // Placeholders
+ 'placeholder_bn_title' => 'Ex. Commandes Serveur Linux...',
+ 'placeholder_rename_title' => 'Nouveau titre...',
+ 'placeholder_import_title' => 'Ex. Positive Internet',
+ 'placeholder_paste_json' => 'Collez le JSON ici au format ancien ou nouveau...',
+ 'placeholder_note_title' => 'Ex. Installation MySQL...',
+ 'placeholder_note_desc' => 'Brรจve description de ce bloc de notes...',
+ 'placeholder_subnote_title' => 'Ex. Commande crรฉation base de donnรฉes...',
+ 'placeholder_subnote_content' => 'Entrez le texte ou les instructions de la commande...',
+ 'placeholder_site_title' => 'Ex. Blocknotes...',
+ 'placeholder_username_new' => 'ex. pierre, marie...',
+ 'placeholder_password_new' => 'Entrez nouveau mot de passe',
+ 'placeholder_password_confirm' => 'Rรฉpรฉtez le mot de passe',
+
+ // Extra Labels
+ 'label_owner_prefix' => 'Propriรฉtaire: ',
+ 'label_updated_prefix' => 'Derniรจre mise ร jour: ',
+ 'label_you' => 'Vous',
+ 'label_unknown' => 'Inconnu',
+ 'label_current_favicon' => 'Favicon Actuelle: ',
+
+ // First Start Installer Setup
+ 'setup_title' => '๐ Configuration Initiale de Blocknotes',
+ 'setup_subtitle' => 'Configurez la base de donnรฉes MySQL et crรฉez les premiers utilisateurs privilรฉgiรฉs (Propriรฉtaire et Admin).',
+ 'setup_sec1_title' => '1. Configuration de la Base de Donnรฉes MySQL',
+ 'setup_db_host' => 'Hรดte de la Base de Donnรฉes',
+ 'setup_db_name' => 'Nom de la Base de Donnรฉes',
+ 'setup_db_user' => 'Utilisateur MySQL',
+ 'setup_db_pass' => 'Mot de Passe MySQL',
+ 'setup_db_pass_placeholder' => 'Mot de passe (laissez vide si aucun)',
+ 'setup_sec2_title' => '2. Compte Propriรฉtaire (Unique)',
+ 'setup_owner_user' => 'Nom d\'Utilisateur Propriรฉtaire',
+ 'setup_owner_pass' => 'Mot de Passe Propriรฉtaire',
+ 'setup_owner_pass_placeholder' => 'Entrez le mot de passe pour le Propriรฉtaire',
+ 'setup_sec3_title' => '3. Premier Compte Administrateur (Admin)',
+ 'setup_admin_user' => 'Nom d\'Utilisateur Admin',
+ 'setup_admin_pass' => 'Mot de Passe Admin',
+ 'setup_admin_pass_placeholder' => 'Entrez le mot de passe pour Admin',
+ 'setup_btn_submit' => 'โก Terminer l\'Installation et Configurer le Systรจme',
+ 'err_setup_db_fields' => 'Veuillez remplir tous les champs de connexion ร la base de donnรฉes MySQL.',
+ 'err_setup_owner_fields' => 'Veuillez entrer le nom d\'utilisateur et le mot de passe pour le Propriรฉtaire.',
+ 'err_setup_admin_fields' => 'Veuillez entrer le nom d\'utilisateur et le mot de passe pour le premier Admin.',
+ 'err_setup_same_user' => 'L\'utilisateur Propriรฉtaire et l\'utilisateur Admin doivent avoir des noms diffรฉrents.'
+ ]
+ ];
+}
+
+/**
+ * Translate key to current active language string.
+ */
+function __($key, $default = '') {
+ $lang = get_current_lang();
+ $translations = get_translations();
+
+ if (isset($translations[$lang][$key])) {
+ return $translations[$lang][$key];
+ }
+ if (isset($translations['it'][$key])) {
+ return $translations['it'][$key];
+ }
+ if (isset($translations['en'][$key])) {
+ return $translations['en'][$key];
+ }
+ return !empty($default) ? $default : $key;
+}
diff --git a/functions/site_settings.php b/functions/site_settings.php
new file mode 100644
index 0000000..d37456f
--- /dev/null
+++ b/functions/site_settings.php
@@ -0,0 +1,89 @@
+prepare("SELECT setting_value FROM site_settings WHERE setting_key = ? LIMIT 1");
+ $stmt->bind_param("s", $key);
+ $stmt->execute();
+ $res = $stmt->get_result();
+
+ if ($row = $res->fetch_assoc()) {
+ $stmt->close();
+ return $row['setting_value'];
+ }
+ $stmt->close();
+ return $default;
+}
+
+/**
+ * Update or insert site setting.
+ */
+function update_site_setting($key, $value) {
+ global $conn;
+ if (!isset($conn)) return false;
+
+ $stmt = $conn->prepare("INSERT INTO site_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
+ $stmt->bind_param("ss", $key, $value);
+ $res = $stmt->execute();
+ $stmt->close();
+
+ return $res;
+}
+
+/**
+ * Upload and validate high-resolution site Favicon.
+ */
+function upload_site_favicon($file) {
+ if (empty($file) || $file['error'] !== UPLOAD_ERR_OK) {
+ return ['success' => false, 'message' => 'Errore nel caricamento del file favicon.'];
+ }
+
+ $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
+ $allowed_extensions = ['png', 'ico', 'webp', 'svg', 'jpeg', 'jpg'];
+
+ if (!in_array($ext, $allowed_extensions)) {
+ return ['success' => false, 'message' => 'Formato favicon non supportato. Formati ammessi: PNG, ICO, WEBP, SVG, JPG.'];
+ }
+
+ // High resolution dimension check for raster formats (PNG, WEBP, JPG, ICO)
+ if (in_array($ext, ['png', 'webp', 'jpeg', 'jpg', 'ico'])) {
+ $image_info = @getimagesize($file['tmp_name']);
+ if ($image_info) {
+ $width = $image_info[0];
+ $height = $image_info[1];
+
+ // Minimum high resolution requirement: 64x64px
+ if ($width < 64 || $height < 64) {
+ return [
+ 'success' => false,
+ 'message' => "Risoluzione favicon troppo bassa ({$width}x{$height}px). Per garantire la massima qualitร visiva, carica un'immagine ad alta risoluzione (almeno 64x64px)."
+ ];
+ }
+ }
+ }
+
+ // Ensure assets/img directory exists
+ $img_dir = ROOT_DIR . "assets/img/";
+ if (!is_dir($img_dir)) {
+ @mkdir($img_dir, 0777, true);
+ }
+
+ $new_filename = "favicon_" . time() . "." . $ext;
+ $target_file = $img_dir . $new_filename;
+ $relative_path = "assets/img/" . $new_filename;
+
+ if (move_uploaded_file($file['tmp_name'], $target_file)) {
+ update_site_setting('site_favicon', $relative_path);
+ return ['success' => true, 'message' => 'Favicon aggiornata con successo!'];
+ }
+
+ return ['success' => false, 'message' => 'Impossibile salvare il file favicon su disco.'];
+}
diff --git a/pages/admin_users.php b/pages/admin_users.php
new file mode 100644
index 0000000..bd08d11
--- /dev/null
+++ b/pages/admin_users.php
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+ โ
= htmlspecialchars($msg_success) ?>
+
+
+
+
+
+ โ ๏ธ = htmlspecialchars($msg_error) ?>
+
+
+
+
+
+
+
+ ๐
+
= __('card_site_settings_title') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ = __('col_username') ?> |
+ = __('col_role') ?> |
+ = __('col_bns') ?> |
+ = __('col_created') ?> |
+ = __('col_actions') ?> |
+
+
+
+
+
+
+ | #= (int)$u['id'] ?> |
+
+ = htmlspecialchars($u['username']) ?>
+
+ = __('label_you') ?>
+
+ |
+
+
+
+ ๐ = __('badge_owner') ?>
+
+
+
+
+ |
+
+
+ ๐ = $count ?> Blocknotes
+
+ |
+
+ = date('d/m/Y H:i', strtotime($u['created_at'])) ?>
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/export_blocknote.php b/pages/export_blocknote.php
new file mode 100644
index 0000000..ea3f41e
--- /dev/null
+++ b/pages/export_blocknote.php
@@ -0,0 +1,27 @@
+
+
-
- Welcome to the Home Page
- This is the home page.
-
+
+
+
+
+ โ
= htmlspecialchars($msg_success) ?>
+
+
+
+
+
+ โ ๏ธ = htmlspecialchars($msg_error) ?>
+
+
+
+
+
+
๐
+
= __('empty_blocknotes_title') ?>
+
= is_admin() ? __('empty_blocknotes_desc_admin') : __('empty_blocknotes_desc_user') ?>
+
+
+
+
+
+
+
+
+
+
+
+ ๐ = $note_count ?> = ($note_count === 1) ? __('note_single') : __('note_plural') ?>
+ ๐ = date('d/m/Y H:i', strtotime($bn['updated_at'] ?? 'now')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/login.php b/pages/login.php
new file mode 100644
index 0000000..93cf2fc
--- /dev/null
+++ b/pages/login.php
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
๐
+
= __('login_title') ?>
+
= __('login_subtitle') ?>
+
+
+
+
+ โ
= htmlspecialchars($success_msg) ?>
+
+
+
+
+
+ โ ๏ธ = htmlspecialchars($error_msg) ?>
+
+
+
+
+
+
+ = __('login_hint_footer') ?>:
+ = __('label_username') ?>: admin | = __('label_password') ?>: admin123
+
+
+
+
diff --git a/pages/logout.php b/pages/logout.php
new file mode 100644
index 0000000..97ea822
--- /dev/null
+++ b/pages/logout.php
@@ -0,0 +1,4 @@
+
+
+
+
+
+
+
+
+
+
= __('card_lang_title') ?>
+
+
+
โ
= htmlspecialchars($msg_lang_success) ?>
+
+
+
+
โ ๏ธ = htmlspecialchars($msg_lang_error) ?>
+
+
+
+
+
+
+
+
= __('card_landing_title') ?>
+
+
+
โ
= htmlspecialchars($msg_landing_success) ?>
+
+
+
+
โ ๏ธ = htmlspecialchars($msg_landing_error) ?>
+
+
+
+
+
+
+
+
= __('card_password_title') ?>
+
+
+
โ
= htmlspecialchars($msg_password_success) ?>
+
+
+
+
โ ๏ธ = htmlspecialchars($msg_password_error) ?>
+
+
+
+
+
+
+
+
= __('card_colors_title') ?>
+
+
+
โ
= htmlspecialchars($msg_theme_success) ?>
+
+
+
+
โ ๏ธ = htmlspecialchars($msg_theme_error) ?>
+
+
+
+
+
+
+
diff --git a/pages/view_blocknote.php b/pages/view_blocknote.php
new file mode 100644
index 0000000..6a009df
--- /dev/null
+++ b/pages/view_blocknote.php
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
+ โ
= htmlspecialchars($msg_success) ?>
+
+
+
+
+
+ โ ๏ธ = htmlspecialchars($msg_error) ?>
+
+
+
+
+
+
+
+
+
+
๐
+
= __('search_no_results') ?>
+
= __('search_no_results_desc') ?>
+
+
+
+
+
+
+
๐
+
= __('empty_blocknotes_title') ?>
+
= __('empty_blocknotes_desc_user') ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($note['description']) ?>
+
+
+
+
+
+
+ = __('empty_blocknotes_desc_user') ?>
+
+
+
+
+
+
+
+
+
+
+ โก = __('badge_command') ?>
+
+
+
= htmlspecialchars($sub['content']) ?>
+
+
+
+ = nl2br(htmlspecialchars($sub['content'])) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scratch/convert_legacy.php b/scratch/convert_legacy.php
new file mode 100644
index 0000000..fcd38e1
--- /dev/null
+++ b/scratch/convert_legacy.php
@@ -0,0 +1,368 @@
+ /dev/null 2>&1 && echo \"Server reachable. Proceeding with script update.\" || { echo \"Server unreachable. Update canceled.\"; exit 1; }; for i in updatebspamscripts findspam blockspamip spamwhat nobanip unblockspams; do echo \"Updating script $i...\"; curl -s -o /usr/local/sbin/$i https://scripts.svm.picl.co.uk/scripts/$i.sh && chmod 700 /usr/local/sbin/$i && echo \"Script '$i' updated.\" || echo \"Error updating script '$i'.\"; done"
+ }
+ ]
+ },
+ {
+ "title": "Create New Database",
+ "description": "Command to create new database and database use",
+ "codes": [
+ {
+ "sub_title": "## Create Databases ##",
+ "code": "CREATE DATABASE unixuser;"
+ },
+ {
+ "sub_title": "# Create User For DB #",
+ "code": "grant all privileges on `unixuser`.* to `unixuser`@'%' identified by 'password database';"
+ }
+ ]
+ },
+ {
+ "title": "Rsync From Server to the other",
+ "description": "Command to rsync between the servers",
+ "codes": [
+ {
+ "sub_title": "## Rsync Command ##",
+ "code": "rsync -avzP /home/user/public_html/ user@10.10.10.17:/home/user/public_html/"
+ }
+ ]
+ },
+ {
+ "title": "WP-CLI Commands",
+ "description": "Usefully commands for wp-cli. Note that you need to change \"from-website\" and \"to-website\" with the correct domain.",
+ "codes": [
+ {
+ "sub_title": "## Export DB ##",
+ "code": "wp db export"
+ },
+ {
+ "sub_title": "## Change Links for wordpress ##",
+ "code": "wp search-replace 'from-website.Com' 'to-website.com' --all-tables"
+ },
+ {
+ "sub_title": "Add New User (change the '###*' strings with the correct informations)",
+ "code": "wp user create ###USERNAME ###EMAIL@DOMAIN --role=administrator --user_pass=\"###PASSWORD\""
+ },
+ {
+ "sub_title": "Install WP-CLI (Use this as root user)",
+ "code": "curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar; chmod +x wp-cli.phar; sudo mv wp-cli.phar /usr/local/bin/wp"
+ },
+ {
+ "sub_title": "Install varnish-cache plugin:",
+ "code": "wp plugin install varnish-http-purge\r\nwp plugin activate varnish-http-purge"
+ },
+ {
+ "sub_title": "Configuration to add in wp-config.php for the cache plugin",
+ "code": "define('VHP_VARNISH_IP','varnish-vip:6081');"
+ },
+ {
+ "sub_title": "(Run as Root) Checksums verify WP CLI on all /home folders",
+ "code": "for u in /home/*; do user=$(basename \"$u\"); for d in httpdocs public_html; do if [ -d \"$u/$d\" ] && [ -f \"$u/$d/wp-config.php\" ]; then echo \"=== $user ($d) ===\"; sudo -u \"$user\" -H wp --path=\"$u/$d\" core verify-checksums; echo; fi; done; done"
+ },
+ {
+ "sub_title": "verify checksum for a single website",
+ "code": "wp core verify-checksums"
+ },
+ {
+ "sub_title": "restore corefiles",
+ "code": "wp core download --version=$(wp core version) --force"
+ },
+ {
+ "sub_title": "Create posops user",
+ "code": "wp user create posops good@positive-internet.com --role=administrator --user_pass='P3dsfj7nYLaMxxXk'"
+ },
+ {
+ "sub_title": "Cambiare la password di un utente",
+ "code": "wp user update posops --user_pass='P3dsfj7nYLaMxxXk'"
+ }
+ ]
+ },
+ {
+ "title": "Find Killig Process Error",
+ "description": "Command to check what has been killed in the VMs / Machine",
+ "codes": [
+ {
+ "sub_title": "## How to check errors ##",
+ "code": "grep -E '(reaper|oom)' /var/log/syslog"
+ }
+ ]
+ },
+ {
+ "title": "Generate SSH Key",
+ "description": "Command to generate ssh key",
+ "codes": [
+ {
+ "sub_title": "## ED25519 ##",
+ "code": "ssh-keygen -t ed25519"
+ },
+ {
+ "sub_title": "## RSA ##",
+ "code": "ssh-keygen -t rsa -b 4096"
+ }
+ ]
+ },
+ {
+ "title": "User Basic Auth",
+ "description": "How to create / add user for the basicauth.",
+ "codes": [
+ {
+ "sub_title": "## CREATE A FILE / USER (cancel existing one) ##",
+ "code": "htpasswd -c /etc/nginx/.htpasswd-name nuovo_utente"
+ },
+ {
+ "sub_title": "## ADD EXTRA USER ##",
+ "code": "htpasswd /etc/nginx/.htpasswd-name username"
+ },
+ {
+ "sub_title": "Block to add",
+ "code": "auth_basic \"Restricted Access\";\r\n auth_basic_user_file /etc/nginx/.htpasswd;"
+ },
+ {
+ "sub_title": "Ownership and privileges",
+ "code": "htpassloc=\"etc/nginx/.htpasswd\";\r\nchown www-data:www-data $htpassloc;\r\nchmod 644 $htpassloc"
+ }
+ ]
+ },
+ {
+ "title": "Check DB Size",
+ "description": "Command to check the dimension of the databases; (needs to be specified the databases names)",
+ "codes": [
+ {
+ "sub_title": "## CHECK DB SIZE ##",
+ "code": "SELECT table_schema AS \"Database\", ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS \"Dimension (MB)\" FROM information_schema.tables WHERE table_schema IN (\"dbname1 \", \"dbname2\", \"dbname3\", \"dbname4\") GROUP BY table_schema;"
+ }
+ ]
+ },
+ {
+ "title": "Remove Files and keep the newest one",
+ "description": "Change the variable keep in base to how much newest file you need to keep!",
+ "codes": [
+ {
+ "sub_title": "## REMOVE OLDEST FILES CODE ##",
+ "code": "count=0; keep=4; echo \"### FILE THAT WILL BE REMOVED: ###\"; echo \"-------------------------------\"; for i in $(ls -t); do if [ $count -lt $keep ]; then echo \"Keeping: $i\"; else echo \"Removing $i\"; fi; ((count++)); done; echo \"-------------------------------\"; echo \"Do you wish to continue?\"; echo \"y - Yes\"; echo \"default - no\"; read check; check=${check,,}; if [ \"$check\" == \"y\" ]; then echo \"#### REMOVING FILE ####\"; echo \"-------------------------------\"; count=0; for i in $(ls -t); do if [ $count -lt $keep ]; then echo \"Keeping: $i\"; else echo \"Removed $i\"; rm \"$i\"; fi; ((count++)); done; echo \"-------------------------------\"; else echo \"Not Removed\"; fi\r\n"
+ }
+ ]
+ },
+ {
+ "title": "Add Diskspace Command",
+ "description": "After increased the diskspace of the hard disk on proxmox, we can run this command to add the 100% of the diskspace to the root partition.",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "parted /dev/vda resizepart 1 100%; pvresize /dev/vda1; lvextend -l +100%FREE /dev/vgmain/root; resize2fs /dev/vgmain/root"
+ }
+ ]
+ },
+ {
+ "title": "New Certboot Certificate",
+ "description": "Install new certboot Certificate",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "certbot certonly --webroot -w /var/www/html -d ###DOMAIN"
+ }
+ ]
+ },
+ {
+ "title": "Extract pfx key",
+ "description": "Use this command to extract the key and the pem files from a .pfx file.\r\nChange the variables before to run the command.",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "PFX=\"FILE_NAME\";\r\nPASS=\"PASSWORD_PFX\";\r\nopenssl pkcs12 -in \"$PFX\" -passin pass:\"$PASS\" -nocerts -nodes -out server.key && \\\r\nopenssl pkcs12 -in \"$PFX\" -passin pass:\"$PASS\" -nokeys | \\\r\nsed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > fullchain.pem\r\n"
+ }
+ ]
+ },
+ {
+ "title": "check if the ssh-key is valid",
+ "description": "With this command we can check if a ssh key is valid or not",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "FILENAME=\"path/to/the/file\";\r\nssh-keygen -l -f $FILENAME;\r\n"
+ }
+ ]
+ },
+ {
+ "title": "checking valid rules for studiorep-fw",
+ "description": "This command is checking which of the following rules has been accepted for the php-rules",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "php-fpm8.3 -tt 2>&1 | grep -A50 \"\\[POOL_NAME\\]\""
+ }
+ ]
+ },
+ {
+ "title": "Add new VM to spa",
+ "description": "Use this to add the new element to the spa command. Specify the Public IP, the port and the hostname",
+ "codes": [
+ {
+ "sub_title": "Command:",
+ "code": "sudoedit /home/posops/ansible/hosts"
+ }
+ ]
+ },
+ {
+ "title": "PHP-FPM Scripts",
+ "description": "Script Collection for PHP-FPM",
+ "codes": [
+ {
+ "sub_title": "Restarting all php-fpm existing",
+ "code": "for i in /etc/php/*; do version=$(basename \"$i\"); service=\"php${version}-fpm\"; if systemctl list-units --type=service --all | grep -q \"$service\"; then echo \"[ $service ]\"; systemctl restart \"$service\"; systemctl status \"$service\" | grep Active; fi; done"
+ },
+ {
+ "sub_title": "Check the php-version for the vhost",
+ "code": "VERSION=8.3; for i in *; do socket=$(grep -oP 'SetHandler\\s+\"proxy:unix:\\K[^|\"]+' \"$i\"); if [ -n \"$socket\" ]; then domain=$(basename \"$socket\" .sock); match=$(grep -iRl \"$domain\" /etc/php/*/fpm/pool.d/); if [ -n \"$match\" ]; then phpver=$(echo \"$match\" | sed -n 's|/etc/php/\\([^/]*\\)/fpm/.*|\\1|p' | sort -u); if echo \"$phpver\" | grep -q \"^$VERSION$\"; then echo \"[ $domain ]\"; echo \"php-fpm $phpver\"; fi; fi; fi; done"
+ },
+ {
+ "sub_title": "Check the php-version for the vhost",
+ "code": "exver=\"7.4\" instver=\"8.4\" ; for i in $(dpkg -l *php* | awk '{print $2}' | grep $exver); do apt install php$instver${i:6} -y; done"
+ }
+ ]
+ },
+ {
+ "title": "ownership to 755 folder and 644 files",
+ "description": "Changing the ownership from 755 to the folders and 644 to the files (sub-directories included) with find + xarg in the current director.",
+ "codes": [
+ {
+ "sub_title": "Command",
+ "code": "find . -type d -print0 | xargs -0 chmod 755;\r\nfind . -type f -print0 | xargs -0 chmod 644"
+ }
+ ]
+ }
+]
+JSON;
+
+$legacy_data = json_decode($raw_json, true);
+
+if (!is_array($legacy_data)) {
+ echo "ERROR: Failed to decode legacy JSON: " . json_last_error_msg() . "\n";
+ exit(1);
+}
+
+$bn_id = 'bn_positive_internet';
+$new_bn = [
+ 'id' => $bn_id,
+ 'title' => 'Positive Internet',
+ 'username' => 'admin',
+ 'created_at' => date('Y-m-d H:i:s'),
+ 'updated_at' => date('Y-m-d H:i:s'),
+ 'notes' => []
+];
+
+foreach ($legacy_data as $n_idx => $legacy_note) {
+ $note_id = 'note_' . ($n_idx + 1) . '_' . uniqid();
+ $subnotes = [];
+
+ if (isset($legacy_note['codes']) && is_array($legacy_note['codes'])) {
+ foreach ($legacy_note['codes'] as $s_idx => $legacy_code) {
+ $subnotes[] = [
+ 'id' => 'sub_' . ($s_idx + 1) . '_' . uniqid(),
+ 'title' => $legacy_code['sub_title'] ?? 'Comando',
+ 'type' => 'command',
+ 'content' => $legacy_code['code'] ?? ''
+ ];
+ }
+ }
+
+ $new_bn['notes'][] = [
+ 'id' => $note_id,
+ 'title' => $legacy_note['title'] ?? 'Nota',
+ 'description' => $legacy_note['description'] ?? '',
+ 'subnotes' => $subnotes
+ ];
+}
+
+save_blocknote($new_bn);
+echo "SUCCESS! Converted and saved Positive Internet blocknote with " . count($new_bn['notes']) . " notes.\n";
diff --git a/templates/footer.php b/templates/footer.php
index 926b647..a038ece 100644
--- a/templates/footer.php
+++ b/templates/footer.php
@@ -1,9 +1,11 @@
-
-
-
-
+
+
+
+