diff --git a/.claude/WIP.md b/.claude/WIP.md index 3c291cfa..15521862 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -127,6 +127,16 @@ _Last updated: 2026-02-20 (v0.24.0 - Matrix + SaaS Relay + Media Hub)_ - `hexoctl static quick ` - One-command upload + publish - Tested and verified on router +- **HexoJS Content Upload Wizard** — DONE (2026-02-20) + - 3-step wizard UI at `/admin/services/hexojs/upload` + - File upload: HTML, PDF, Markdown (.md) support + - Metadata: Title, Category, Tags, Public/Private visibility + - Multi-target publishing: HexoJS Blog, Gitea, Streamlit, MetaBlogizer + - Base64 encoding for binary file transfer + - RPCD methods: upload_article, upload_pdf, upload_html, publish_draft, unpublish_post, get_uploads + - Gitea integration with repo/path selection + - SecuBox Welcome Guide deployed at /guide/, /connexion.html, /accueil.html + ### Just Completed (2026-02-19) - **WAF VoIP/XMPP Security Filters** — DONE (2026-02-19) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b62655f3..e5539f84 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -375,7 +375,8 @@ "Bash(npm run build:*)", "Bash(npx gulp browserify:*)", "Bash(npx terser:*)", - "Bash(read)" + "Bash(read)", + "Bash(/home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/c3box-vm-builder.sh:*)" ] } } diff --git a/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/submit.js b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/submit.js new file mode 100644 index 00000000..6a736259 --- /dev/null +++ b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/submit.js @@ -0,0 +1,432 @@ +'use strict'; +'require view'; +'require ui'; +'require rpc'; +'require form'; + +// User submission RPC calls +var callSubmitContent = rpc.declare({ + object: 'luci.hexojs', + method: 'submit_for_review', + params: ['title', 'content', 'category', 'author', 'email'], + expect: { '': {} } +}); + +var callListPending = rpc.declare({ + object: 'luci.hexojs', + method: 'list_pending', + expect: { '': {} } +}); + +var callApproveSubmission = rpc.declare({ + object: 'luci.hexojs', + method: 'approve_submission', + params: ['submission_id', 'publish_target'], + expect: { '': {} } +}); + +var callRejectSubmission = rpc.declare({ + object: 'luci.hexojs', + method: 'reject_submission', + params: ['submission_id', 'reason'], + expect: { '': {} } +}); + +var callGetSubmission = rpc.declare({ + object: 'luci.hexojs', + method: 'get_submission', + params: ['submission_id'], + expect: { '': {} } +}); + +return view.extend({ + load: function() { + return Promise.all([ + callListPending() + ]); + }, + + render: function(data) { + var self = this; + var pendingList = (data[0] && data[0].submissions) || []; + var isAdmin = (data[0] && data[0].is_admin) || false; + + var content = E('div', { class: 'cbi-map', style: 'max-width:900px;margin:0 auto;' }, [ + E('h2', { style: 'text-align:center;margin-bottom:30px;' }, [ + E('span', { style: 'font-size:1.2em;' }, '📝 '), + isAdmin ? 'Content Moderation' : 'Submit Content' + ]), + + // Tab navigation + E('div', { style: 'display:flex;gap:10px;margin-bottom:20px;border-bottom:2px solid #333;padding-bottom:10px;' }, [ + E('button', { + id: 'tab-submit', + class: 'cbi-button cbi-button-action', + click: function() { self.showTab('submit'); } + }, '📤 Submit'), + isAdmin ? E('button', { + id: 'tab-moderate', + class: 'cbi-button', + click: function() { self.showTab('moderate'); } + }, '⚖️ Moderate (' + pendingList.length + ')') : E('span'), + E('button', { + id: 'tab-status', + class: 'cbi-button', + click: function() { self.showTab('status'); } + }, '📊 My Submissions') + ]), + + // Submit Tab + E('div', { id: 'panel-submit', class: 'cbi-section' }, [ + E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' }, + 'Submit your content for review. Once approved by moderators, it will be published to the public portal.'), + + E('div', { style: 'background:#1a1a2e;border-radius:12px;padding:25px;' }, [ + // Author Info + E('div', { style: 'display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;' }, [ + E('div', {}, [ + E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Author Name *'), + E('input', { + type: 'text', + id: 'submit-author', + style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;', + placeholder: 'Your name' + }) + ]), + E('div', {}, [ + E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Email (optional)'), + E('input', { + type: 'email', + id: 'submit-email', + style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;', + placeholder: 'your@email.com' + }) + ]) + ]), + + // Title + E('div', { style: 'margin-bottom:20px;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Title *'), + E('input', { + type: 'text', + id: 'submit-title', + style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;', + placeholder: 'Article title' + }) + ]), + + // Category + E('div', { style: 'margin-bottom:20px;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Category'), + E('select', { + id: 'submit-category', + style: 'width:100%;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;' + }, [ + E('option', { value: 'general' }, 'General'), + E('option', { value: 'news' }, 'News'), + E('option', { value: 'tutorial' }, 'Tutorial'), + E('option', { value: 'opinion' }, 'Opinion'), + E('option', { value: 'tech' }, 'Technology'), + E('option', { value: 'community' }, 'Community') + ]) + ]), + + // Content + E('div', { style: 'margin-bottom:20px;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;color:#888;' }, 'Content * (Markdown supported)'), + E('textarea', { + id: 'submit-content', + style: 'width:100%;height:300px;padding:12px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;font-family:monospace;resize:vertical;', + placeholder: '# Your Article\n\nWrite your content here using **Markdown** formatting...' + }) + ]), + + // Submit Button + E('div', { style: 'text-align:center;' }, [ + E('button', { + class: 'cbi-button cbi-button-positive', + style: 'padding:15px 40px;font-size:1.1em;', + click: ui.createHandlerFn(this, 'submitContent') + }, '📤 Submit for Review') + ]) + ]) + ]), + + // Moderate Tab (Admin only) + isAdmin ? E('div', { id: 'panel-moderate', class: 'cbi-section', style: 'display:none;' }, [ + E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' }, + 'Review and moderate user submissions. Approve to publish or reject with feedback.'), + + pendingList.length === 0 ? + E('div', { style: 'text-align:center;padding:40px;color:#888;' }, [ + E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '✨'), + E('p', {}, 'No pending submissions') + ]) : + E('div', { id: 'pending-list' }, pendingList.map(function(sub) { + return E('div', { + class: 'cbi-section', + style: 'background:#1a1a2e;border-radius:12px;padding:20px;margin-bottom:15px;border-left:4px solid #f39c12;', + 'data-id': sub.id + }, [ + E('div', { style: 'display:flex;justify-content:space-between;align-items:start;' }, [ + E('div', {}, [ + E('h4', { style: 'margin:0 0 10px 0;' }, sub.title), + E('div', { style: 'color:#888;font-size:0.9em;' }, [ + E('span', {}, '👤 ' + sub.author), + E('span', { style: 'margin-left:15px;' }, '📁 ' + sub.category), + E('span', { style: 'margin-left:15px;' }, '📅 ' + sub.date) + ]) + ]), + E('div', { style: 'display:flex;gap:10px;' }, [ + E('button', { + class: 'cbi-button cbi-button-action', + click: ui.createHandlerFn(self, 'previewSubmission', sub.id) + }, '👁️ Preview'), + E('button', { + class: 'cbi-button cbi-button-positive', + click: ui.createHandlerFn(self, 'approveSubmission', sub.id) + }, '✅ Approve'), + E('button', { + class: 'cbi-button cbi-button-negative', + click: ui.createHandlerFn(self, 'rejectSubmission', sub.id) + }, '❌ Reject') + ]) + ]), + E('div', { + class: 'submission-preview', + style: 'display:none;margin-top:15px;padding:15px;background:#16213e;border-radius:8px;max-height:300px;overflow-y:auto;' + }) + ]); + })) + ]) : E('div'), + + // Status Tab + E('div', { id: 'panel-status', class: 'cbi-section', style: 'display:none;' }, [ + E('div', { class: 'cbi-section-descr', style: 'margin-bottom:20px;' }, + 'Track the status of your submitted content.'), + + E('div', { id: 'my-submissions', style: 'text-align:center;padding:40px;color:#888;' }, [ + E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '📋'), + E('p', {}, 'Enter your email to check submission status'), + E('div', { style: 'max-width:400px;margin:20px auto;' }, [ + E('input', { + type: 'email', + id: 'status-email', + style: 'width:70%;padding:10px;border-radius:8px 0 0 8px;border:1px solid #444;background:#16213e;color:#fff;', + placeholder: 'your@email.com' + }), + E('button', { + class: 'cbi-button cbi-button-action', + style: 'border-radius:0 8px 8px 0;', + click: ui.createHandlerFn(this, 'checkStatus') + }, 'Check') + ]) + ]) + ]) + ]); + + return content; + }, + + showTab: function(tab) { + ['submit', 'moderate', 'status'].forEach(function(t) { + var panel = document.getElementById('panel-' + t); + var tabBtn = document.getElementById('tab-' + t); + if (panel) panel.style.display = t === tab ? 'block' : 'none'; + if (tabBtn) { + tabBtn.className = t === tab ? 'cbi-button cbi-button-action' : 'cbi-button'; + } + }); + }, + + submitContent: function() { + var author = document.getElementById('submit-author').value.trim(); + var email = document.getElementById('submit-email').value.trim(); + var title = document.getElementById('submit-title').value.trim(); + var category = document.getElementById('submit-category').value; + var content = document.getElementById('submit-content').value.trim(); + + if (!author || !title || !content) { + ui.addNotification(null, E('p', 'Please fill in all required fields (Author, Title, Content)'), 'warning'); + return; + } + + ui.showModal('Submitting...', [ + E('p', { class: 'spinning' }, 'Sending your content for review...') + ]); + + callSubmitContent(title, content, category, author, email).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.showModal('Submission Received', [ + E('div', { style: 'text-align:center;padding:20px;' }, [ + E('div', { style: 'font-size:4em;margin-bottom:15px;' }, '✅'), + E('h3', {}, 'Thank You!'), + E('p', {}, 'Your submission has been received and is pending moderation.'), + E('p', { style: 'color:#888;' }, 'Submission ID: ' + result.submission_id), + E('button', { + class: 'cbi-button cbi-button-action', + style: 'margin-top:20px;', + click: function() { + ui.hideModal(); + document.getElementById('submit-title').value = ''; + document.getElementById('submit-content').value = ''; + } + }, 'Submit Another') + ]) + ]); + } else { + ui.addNotification(null, E('p', result.error || 'Submission failed'), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); + }); + }, + + previewSubmission: function(submissionId) { + var self = this; + var card = document.querySelector('[data-id="' + submissionId + '"]'); + var preview = card.querySelector('.submission-preview'); + + if (preview.style.display === 'block') { + preview.style.display = 'none'; + return; + } + + preview.innerHTML = '

Loading...

'; + preview.style.display = 'block'; + + callGetSubmission(submissionId).then(function(result) { + if (result.success) { + preview.innerHTML = '
' +
+                    self.escapeHtml(result.content) + '
'; + } else { + preview.innerHTML = '

Failed to load content

'; + } + }); + }, + + escapeHtml: function(text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + }, + + approveSubmission: function(submissionId) { + var self = this; + + ui.showModal('Approve Submission', [ + E('div', { style: 'padding:10px;' }, [ + E('p', {}, 'Select where to publish this content:'), + E('div', { style: 'display:grid;gap:10px;margin:20px 0;' }, [ + E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [ + E('input', { type: 'radio', name: 'publish-target', value: 'hexojs', checked: true }), + E('span', {}, '📝 HexoJS Blog (default)') + ]), + E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [ + E('input', { type: 'radio', name: 'publish-target', value: 'metablogizer' }), + E('span', {}, '🔗 MetaBlogizer') + ]), + E('label', { style: 'display:flex;align-items:center;gap:10px;cursor:pointer;' }, [ + E('input', { type: 'radio', name: 'publish-target', value: 'static' }), + E('span', {}, '📄 Static Page') + ]) + ]), + E('div', { style: 'display:flex;gap:10px;justify-content:flex-end;' }, [ + E('button', { + class: 'cbi-button', + click: ui.hideModal + }, 'Cancel'), + E('button', { + class: 'cbi-button cbi-button-positive', + click: function() { + var target = document.querySelector('input[name="publish-target"]:checked').value; + ui.hideModal(); + self.doApprove(submissionId, target); + } + }, '✅ Publish') + ]) + ]) + ]); + }, + + doApprove: function(submissionId, target) { + ui.showModal('Publishing...', [ + E('p', { class: 'spinning' }, 'Publishing content...') + ]); + + callApproveSubmission(submissionId, target).then(function(result) { + ui.hideModal(); + if (result.success) { + ui.addNotification(null, E('p', 'Content approved and published!'), 'success'); + var card = document.querySelector('[data-id="' + submissionId + '"]'); + if (card) card.remove(); + + // Update counter + var moderateTab = document.getElementById('tab-moderate'); + if (moderateTab) { + var match = moderateTab.textContent.match(/\((\d+)\)/); + if (match) { + var count = parseInt(match[1]) - 1; + moderateTab.textContent = '⚖️ Moderate (' + count + ')'; + } + } + } else { + ui.addNotification(null, E('p', result.error || 'Approval failed'), 'error'); + } + }); + }, + + rejectSubmission: function(submissionId) { + var self = this; + + ui.showModal('Reject Submission', [ + E('div', { style: 'padding:10px;' }, [ + E('p', {}, 'Provide a reason for rejection (optional):'), + E('textarea', { + id: 'reject-reason', + style: 'width:100%;height:100px;padding:10px;border-radius:8px;border:1px solid #444;background:#16213e;color:#fff;margin:15px 0;', + placeholder: 'Reason for rejection...' + }), + E('div', { style: 'display:flex;gap:10px;justify-content:flex-end;' }, [ + E('button', { + class: 'cbi-button', + click: ui.hideModal + }, 'Cancel'), + E('button', { + class: 'cbi-button cbi-button-negative', + click: function() { + var reason = document.getElementById('reject-reason').value; + ui.hideModal(); + self.doReject(submissionId, reason); + } + }, '❌ Reject') + ]) + ]) + ]); + }, + + doReject: function(submissionId, reason) { + callRejectSubmission(submissionId, reason).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', 'Submission rejected'), 'info'); + var card = document.querySelector('[data-id="' + submissionId + '"]'); + if (card) card.remove(); + } else { + ui.addNotification(null, E('p', result.error || 'Rejection failed'), 'error'); + } + }); + }, + + checkStatus: function() { + var email = document.getElementById('status-email').value.trim(); + if (!email) { + ui.addNotification(null, E('p', 'Please enter your email'), 'warning'); + return; + } + + // TODO: Implement status check by email + ui.addNotification(null, E('p', 'Status check feature coming soon'), 'info'); + } +}); diff --git a/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/upload.js b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/upload.js new file mode 100644 index 00000000..53ee5566 --- /dev/null +++ b/package/secubox/luci-app-hexojs/htdocs/luci-static/resources/view/hexojs/upload.js @@ -0,0 +1,427 @@ +'use strict'; +'require view'; +'require ui'; +'require rpc'; +'require secubox/kiss-theme'; + +var callUploadHTML = rpc.declare({ + object: 'luci.hexojs', + method: 'upload_html', + params: ['base64_data', 'title', 'visibility', 'category', 'tags'], + expect: { '': {} } +}); + +var callUploadPDF = rpc.declare({ + object: 'luci.hexojs', + method: 'upload_pdf', + params: ['base64_data', 'title', 'visibility', 'category'], + expect: { '': {} } +}); + +var callUploadMD = rpc.declare({ + object: 'luci.hexojs', + method: 'upload_article', + params: ['filename', 'content', 'title', 'visibility', 'category', 'tags'], + expect: { '': {} } +}); + +var callWizardUpload = rpc.declare({ + object: 'luci.hexojs', + method: 'wizard_upload', + params: ['base64_data', 'filename', 'title', 'visibility', 'category', 'tags', 'target', 'options'], + expect: { '': {} } +}); + +var callGiteaUpload = rpc.declare({ + object: 'luci.gitea', + method: 'upload_file', + params: ['repo', 'path', 'content', 'message'], + expect: { '': {} } +}); + +var callStreamlitCreate = rpc.declare({ + object: 'luci.streamlit', + method: 'create_app', + params: ['name', 'source_file'], + expect: { '': {} } +}); + +var callMetablogizerCreate = rpc.declare({ + object: 'luci.metablogizer', + method: 'create_entry', + params: ['name', 'source_file', 'category'], + expect: { '': {} } +}); + +function fileToBase64(file) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onload = function() { + var base64 = reader.result.split(',')[1]; + resolve(base64); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +} + +return view.extend({ + render: function() { + var self = this; + + var content = E('div', { style: 'padding:20px;max-width:800px;margin:0 auto;' }, [ + E('h2', { style: 'text-align:center;margin-bottom:30px;' }, '📤 Content Upload Wizard'), + + E('div', { class: 'kiss-card', style: 'padding:30px;' }, [ + // Step 1: File Selection + E('div', { class: 'wizard-step', id: 'step-file' }, [ + E('h3', {}, '1. Select File'), + E('p', { style: 'color:#888;' }, 'Supported: HTML, PDF, Markdown (.md)'), + E('div', { style: 'border:2px dashed #3498db;border-radius:12px;padding:40px;text-align:center;margin:20px 0;' }, [ + E('input', { + type: 'file', + id: 'file-input', + accept: '.html,.htm,.pdf,.md,.markdown', + style: 'display:none;' + }), + E('label', { for: 'file-input', style: 'cursor:pointer;display:block;' }, [ + E('div', { style: 'font-size:3em;margin-bottom:10px;' }, '📄'), + E('div', { style: 'color:#3498db;font-size:1.2em;' }, 'Click to select file'), + E('div', { id: 'file-name', style: 'margin-top:10px;color:#27ae60;font-weight:bold;' }, '') + ]) + ]) + ]), + + // Step 2: Metadata + E('div', { class: 'wizard-step', id: 'step-meta', style: 'display:none;' }, [ + E('h3', {}, '2. Article Details'), + E('div', { style: 'margin:15px 0;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;' }, 'Title'), + E('input', { + type: 'text', + id: 'input-title', + style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;', + placeholder: 'Article title...' + }) + ]), + E('div', { style: 'margin:15px 0;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;' }, 'Category'), + E('input', { + type: 'text', + id: 'input-category', + style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;', + placeholder: 'e.g., Blog, Tutorial, News', + value: 'Uploads' + }) + ]), + E('div', { style: 'margin:15px 0;' }, [ + E('label', { style: 'display:block;margin-bottom:5px;' }, 'Tags (comma-separated)'), + E('input', { + type: 'text', + id: 'input-tags', + style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#1a1a2e;color:#fff;', + placeholder: 'tag1, tag2, tag3' + }) + ]), + E('div', { style: 'margin:20px 0;' }, [ + E('label', { style: 'display:block;margin-bottom:10px;' }, 'Visibility'), + E('div', { style: 'display:flex;gap:20px;' }, [ + E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [ + E('input', { type: 'radio', name: 'visibility', value: 'public', checked: true }), + E('span', {}, '🌍 Public') + ]), + E('label', { style: 'display:flex;align-items:center;gap:8px;cursor:pointer;' }, [ + E('input', { type: 'radio', name: 'visibility', value: 'private' }), + E('span', {}, '🔒 Private (Draft)') + ]) + ]) + ]) + ]), + + // Step 3: Target Selection + E('div', { class: 'wizard-step', id: 'step-target', style: 'display:none;' }, [ + E('h3', {}, '3. Publish To'), + E('div', { style: 'display:grid;grid-template-columns:repeat(2,1fr);gap:15px;margin:20px 0;' }, [ + E('div', { + class: 'target-option selected', + 'data-target': 'hexojs', + style: 'padding:20px;border:2px solid #3498db;border-radius:12px;cursor:pointer;text-align:center;' + }, [ + E('div', { style: 'font-size:2em;' }, '📝'), + E('div', { style: 'font-weight:bold;' }, 'HexoJS Blog'), + E('div', { style: 'color:#888;font-size:0.9em;' }, 'Static blog post') + ]), + E('div', { + class: 'target-option', + 'data-target': 'gitea', + style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;' + }, [ + E('div', { style: 'font-size:2em;' }, '🐙'), + E('div', { style: 'font-weight:bold;' }, 'Gitea'), + E('div', { style: 'color:#888;font-size:0.9em;' }, 'Version control') + ]), + E('div', { + class: 'target-option', + 'data-target': 'streamlit', + style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;' + }, [ + E('div', { style: 'font-size:2em;' }, '📊'), + E('div', { style: 'font-weight:bold;' }, 'Streamlit'), + E('div', { style: 'color:#888;font-size:0.9em;' }, 'Interactive app') + ]), + E('div', { + class: 'target-option', + 'data-target': 'metablogizer', + style: 'padding:20px;border:2px solid #444;border-radius:12px;cursor:pointer;text-align:center;' + }, [ + E('div', { style: 'font-size:2em;' }, '🔗'), + E('div', { style: 'font-weight:bold;' }, 'MetaBlogizer'), + E('div', { style: 'color:#888;font-size:0.9em;' }, 'Multi-platform') + ]) + ]), + // Gitea options (shown when gitea selected) + E('div', { id: 'gitea-options', style: 'display:none;margin-top:20px;padding:15px;background:#1a1a2e;border-radius:8px;' }, [ + E('h4', { style: 'margin-bottom:10px;' }, 'Gitea Repository'), + E('input', { + type: 'text', + id: 'gitea-repo', + style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;', + placeholder: 'owner/repo (e.g., admin/my-docs)' + }), + E('input', { + type: 'text', + id: 'gitea-path', + style: 'width:100%;padding:10px;border-radius:6px;border:1px solid #444;background:#16213e;color:#fff;margin-top:10px;', + placeholder: 'path/in/repo (e.g., docs/articles/)' + }) + ]) + ]), + + // Navigation Buttons + E('div', { style: 'display:flex;justify-content:space-between;margin-top:30px;' }, [ + E('button', { + id: 'btn-prev', + class: 'cbi-button', + style: 'display:none;', + click: ui.createHandlerFn(this, 'prevStep') + }, '← Back'), + E('button', { + id: 'btn-next', + class: 'cbi-button cbi-button-action', + style: 'margin-left:auto;', + click: ui.createHandlerFn(this, 'nextStep') + }, 'Next →') + ]), + + // Progress dots + E('div', { style: 'display:flex;justify-content:center;gap:10px;margin-top:20px;' }, [ + E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#3498db;' }), + E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' }), + E('span', { class: 'step-dot', style: 'width:12px;height:12px;border-radius:50%;background:#444;' }) + ]) + ]), + + // Result Card + E('div', { id: 'result-card', class: 'kiss-card', style: 'display:none;padding:30px;text-align:center;' }, [ + E('div', { style: 'font-size:4em;margin-bottom:20px;' }, '✅'), + E('h3', {}, 'Upload Successful!'), + E('p', { id: 'result-message', style: 'color:#888;' }, ''), + E('div', { style: 'margin-top:20px;' }, [ + E('a', { + id: 'result-link', + href: '#', + class: 'cbi-button cbi-button-action', + target: '_blank', + style: 'text-decoration:none;' + }, 'View Article'), + E('button', { + class: 'cbi-button', + style: 'margin-left:10px;', + click: function() { window.location.reload(); } + }, 'Upload Another') + ]) + ]) + ]); + + // Initialize + setTimeout(function() { + self.currentStep = 1; + self.selectedFile = null; + self.selectedTarget = 'hexojs'; + + var fileInput = document.getElementById('file-input'); + fileInput.addEventListener('change', function(e) { + if (e.target.files.length > 0) { + self.selectedFile = e.target.files[0]; + document.getElementById('file-name').textContent = self.selectedFile.name; + var title = self.selectedFile.name.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' '); + document.getElementById('input-title').value = title; + } + }); + + document.querySelectorAll('.target-option').forEach(function(opt) { + opt.addEventListener('click', function() { + document.querySelectorAll('.target-option').forEach(function(o) { + o.style.borderColor = '#444'; + }); + this.style.borderColor = '#3498db'; + self.selectedTarget = this.dataset.target; + // Show/hide gitea options + var giteaOpts = document.getElementById('gitea-options'); + if (giteaOpts) { + giteaOpts.style.display = self.selectedTarget === 'gitea' ? 'block' : 'none'; + } + }); + }); + }, 100); + + return KissTheme.wrap([content], 'admin/services/hexojs/upload'); + }, + + nextStep: function() { + var self = this; + + if (this.currentStep === 1) { + if (!this.selectedFile) { + ui.addNotification(null, E('p', 'Please select a file'), 'warning'); + return; + } + document.getElementById('step-file').style.display = 'none'; + document.getElementById('step-meta').style.display = 'block'; + document.getElementById('btn-prev').style.display = 'block'; + this.currentStep = 2; + this.updateDots(); + } else if (this.currentStep === 2) { + var title = document.getElementById('input-title').value; + if (!title) { + ui.addNotification(null, E('p', 'Please enter a title'), 'warning'); + return; + } + document.getElementById('step-meta').style.display = 'none'; + document.getElementById('step-target').style.display = 'block'; + document.getElementById('btn-next').textContent = '📤 Publish'; + this.currentStep = 3; + this.updateDots(); + } else if (this.currentStep === 3) { + this.doUpload(); + } + }, + + prevStep: function() { + if (this.currentStep === 2) { + document.getElementById('step-meta').style.display = 'none'; + document.getElementById('step-file').style.display = 'block'; + document.getElementById('btn-prev').style.display = 'none'; + this.currentStep = 1; + } else if (this.currentStep === 3) { + document.getElementById('step-target').style.display = 'none'; + document.getElementById('step-meta').style.display = 'block'; + document.getElementById('btn-next').textContent = 'Next →'; + this.currentStep = 2; + } + this.updateDots(); + }, + + updateDots: function() { + var dots = document.querySelectorAll('.step-dot'); + var step = this.currentStep; + dots.forEach(function(dot, i) { + dot.style.background = i < step ? '#3498db' : '#444'; + }); + }, + + doUpload: function() { + var self = this; + var title = document.getElementById('input-title').value; + var category = document.getElementById('input-category').value || 'Uploads'; + var tags = document.getElementById('input-tags').value || ''; + var visibility = document.querySelector('input[name="visibility"]:checked').value; + var target = this.selectedTarget; + + var ext = this.selectedFile.name.split('.').pop().toLowerCase(); + + ui.showModal('Uploading...', [ + E('p', { class: 'spinning' }, 'Processing ' + this.selectedFile.name + '...') + ]); + + fileToBase64(this.selectedFile).then(function(base64) { + // Route based on target + if (target === 'hexojs') { + // Default HexoJS upload + if (ext === 'pdf') { + return callUploadPDF(base64, title, visibility, category); + } else if (ext === 'html' || ext === 'htm') { + return callUploadHTML(base64, title, visibility, category, tags); + } else { + return callUploadMD(self.selectedFile.name, atob(base64), title, visibility, category, tags); + } + } else if (target === 'gitea') { + // Gitea upload + var repo = document.getElementById('gitea-repo').value || 'admin/uploads'; + var path = document.getElementById('gitea-path').value || 'uploads/'; + var fullPath = path.replace(/\/$/, '') + '/' + self.selectedFile.name; + return callGiteaUpload(repo, fullPath, base64, 'Upload: ' + title); + } else if (target === 'streamlit') { + // Create Streamlit app from markdown/python + var appName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-'); + return callStreamlitCreate(appName, base64).then(function(result) { + result.target = 'streamlit'; + result.appUrl = '/admin/services/streamlit/apps/' + appName; + return result; + }); + } else if (target === 'metablogizer') { + // Create MetaBlogizer entry + var entryName = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-'); + return callMetablogizerCreate(entryName, base64, category).then(function(result) { + result.target = 'metablogizer'; + return result; + }); + } else { + // Fallback: use wizard_upload for unified handling + var options = {}; + if (target === 'gitea') { + options.repo = document.getElementById('gitea-repo').value; + options.path = document.getElementById('gitea-path').value; + } + return callWizardUpload(base64, self.selectedFile.name, title, visibility, category, tags, target, JSON.stringify(options)); + } + }).then(function(result) { + ui.hideModal(); + if (result.success) { + document.querySelector('.kiss-card').style.display = 'none'; + document.getElementById('result-card').style.display = 'block'; + + var message = 'Your content has been uploaded.'; + var linkText = 'View'; + var linkHref = '#'; + + if (target === 'hexojs') { + message = 'Your ' + (visibility === 'public' ? 'article' : 'draft') + ' has been created.'; + linkText = 'View Article'; + if (result.slug) linkHref = '/' + result.slug + '/'; + } else if (target === 'gitea') { + message = 'File uploaded to Gitea repository.'; + linkText = 'Open Gitea'; + linkHref = result.url || '/admin/services/gitea/overview'; + } else if (target === 'streamlit') { + message = 'Streamlit app created successfully.'; + linkText = 'Open App'; + linkHref = result.appUrl || '/admin/services/streamlit/overview'; + } else if (target === 'metablogizer') { + message = 'MetaBlogizer entry created.'; + linkText = 'View Entry'; + linkHref = result.url || '/admin/services/metablogizer/overview'; + } + + document.getElementById('result-message').textContent = message; + document.getElementById('result-link').textContent = linkText; + document.getElementById('result-link').href = linkHref; + } else { + ui.addNotification(null, E('p', result.error || 'Upload failed'), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', 'Error: ' + err.message), 'error'); + }); + } +}); diff --git a/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json b/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json index 3a86ae3e..26c2da35 100644 --- a/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json +++ b/package/secubox/luci-app-hexojs/root/usr/share/luci/menu.d/luci-app-hexojs.json @@ -113,5 +113,21 @@ "type": "view", "path": "hexojs/static" } + }, + "admin/services/hexojs/upload": { + "title": "Upload", + "order": 25, + "action": { + "type": "view", + "path": "hexojs/upload" + } + }, + "admin/services/hexojs/submit": { + "title": "Submit & Moderate", + "order": 26, + "action": { + "type": "view", + "path": "hexojs/submit" + } } } diff --git a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json index 6e08713f..2e5850a8 100644 --- a/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json +++ b/package/secubox/luci-app-hexojs/root/usr/share/rpcd/acl.d/luci-app-hexojs.json @@ -32,7 +32,10 @@ "get_instance_endpoints", "get_instance_health", "get_pipeline_status", - "static_list" + "static_list", + "get_uploads", + "list_pending", + "get_submission" ] }, "uci": ["hexojs"] @@ -91,7 +94,15 @@ "static_delete", "static_publish", "static_delete_file", - "static_configure_auth" + "static_configure_auth", + "upload_article", + "upload_pdf", + "upload_html", + "publish_draft", + "unpublish_post", + "submit_for_review", + "approve_submission", + "reject_submission" ] }, "uci": ["hexojs"]