diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index b9655cf..adefee2 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -95,6 +95,7 @@ const workflowsRoutes = require('../routes/workflows'); const dependenciesRoutes = require('../routes/dependencies'); const securityRoutes = require('../routes/security'); const diskSettingsRoutes = require('../routes/disk-settings'); +const aiIntentRoutes = require('../routes/ai-intent'); const logInsightsRoutes = require('../routes/log-insights'); const billingRoutes = require('../routes/billing'); const DependencyManager = require('./managers/dependency-manager'); @@ -759,6 +760,7 @@ async function createApp() { // Log Insights โ€” plain English activity summary + safe log disposal apiRouter.use('/disk-settings', diskSettingsRoutes); + apiRouter.use(aiIntentRoutes({ asyncHandler: ctx.asyncHandler })); apiRouter.use(logInsightsRoutes({ asyncHandler: ctx.asyncHandler, ok: ctx.ok, diff --git a/dashcaddy-api/src/utilities/i18n.js b/dashcaddy-api/src/utilities/i18n.js index 2390f6c..ad73096 100644 --- a/dashcaddy-api/src/utilities/i18n.js +++ b/dashcaddy-api/src/utilities/i18n.js @@ -483,7 +483,7 @@ const DEFAULT_LANGUAGE = 'en'; // Language metadata for UI dropdowns const LANGUAGE_META = { - en: { name: 'English', flag: '๐Ÿ‡ฌ๐Ÿ‡ง', rtl: false }, + en: { name: 'English', flag: '๐Ÿ‡บ๐Ÿ‡ธ', rtl: false }, ar: { name: 'ุงู„ุนุฑุจูŠุฉ', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ', rtl: true }, bn: { name: 'เฆฌเฆพเฆ‚เฆฒเฆพ', flag: '๐Ÿ‡ง๐Ÿ‡ฉ', rtl: false }, cs: { name: 'ฤŒeลกtina', flag: '๐Ÿ‡จ๐Ÿ‡ฟ', rtl: false }, diff --git a/dashcaddy-api/src/utilities/nesting-guard.js b/dashcaddy-api/src/utilities/nesting-guard.js index 74d523c..a18f1bd 100644 --- a/dashcaddy-api/src/utilities/nesting-guard.js +++ b/dashcaddy-api/src/utilities/nesting-guard.js @@ -10,11 +10,11 @@ const fs = require('fs'); const path = require('path'); -const log = require('./logging'); module.exports = function nestingGuard() { try { - const dataDir = require('../config/paths').dataDir; + const paths = require('../config/paths'); + const dataDir = paths.dataDir; const dataDataPath = path.join(dataDir, 'data'); // If data/data exists, it's a recursive duplicate โ€” remove it @@ -25,17 +25,14 @@ module.exports = function nestingGuard() { const markerFile = path.join(dataDataPath, 'config.json'); const parentMarker = path.join(dataDir, 'config.json'); if (fs.existsSync(markerFile) && fs.existsSync(parentMarker)) { - const size = require('child_process') - .execSync(`du -sh '${dataDataPath}' 2>/dev/null | cut -f1`) - .toString().trim(); - log.warn('startup', `Removing recursive data nesting: ${dataDataPath} (${size})`); + console.log('[nesting-guard] Removing recursive data nesting: ' + dataDataPath); fs.rmSync(dataDataPath, { recursive: true, force: true }); - log.info('startup', 'Recursive nesting removed'); + console.log('[nesting-guard] Recursive nesting removed'); } } } } catch (e) { // Non-fatal โ€” don't crash startup over cleanup - log.warn('startup', `Nesting guard skipped: ${e.message}`); + console.warn('[nesting-guard] Skipped: ' + e.message); } }; diff --git a/status/index.html b/status/index.html index 58a060b..2e9f31f 100644 --- a/status/index.html +++ b/status/index.html @@ -954,6 +954,7 @@ + @@ -962,6 +963,8 @@ + + diff --git a/status/js/ai-chat.js b/status/js/ai-chat.js new file mode 100644 index 0000000..aeb078c --- /dev/null +++ b/status/js/ai-chat.js @@ -0,0 +1,167 @@ +// AI Chat Panel โ€” DashCaddy AI Intent Router frontend +(function() { + let chatMessages = []; + let chatHistory = []; + + function createChatPanel() { + const existing = document.getElementById('ai-chat-panel'); + if (existing) existing.remove(); + + const panel = document.createElement('div'); + panel.id = 'ai-chat-panel'; + panel.style.cssText = ` + position: fixed; bottom: 80px; right: 20px; width: 400px; max-width: 95vw; + height: 500px; max-height: 70vh; background: var(--bg-card, #1a1a2e); + border: 1px solid var(--border, #333); border-radius: 12px; + display: none; flex-direction: column; z-index: 9999; + box-shadow: 0 8px 32px rgba(0,0,0,0.4); + `; + + panel.innerHTML = ` +
+ ๐Ÿค– +
+
AI Assistant
+
Pattern matching โ€ข No key needed
+
+ +
+
+
+ ๐Ÿ‘‹ Hi! I can help you deploy apps, diagnose issues, and manage your server. Try: +
+ + + + +
+
+
+
+
+ + +
+
+ `; + document.body.appendChild(panel); + } + + function toggle() { + let panel = document.getElementById('ai-chat-panel'); + if (!panel) { createChatPanel(); panel = document.getElementById('ai-chat-panel'); } + panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; + if (panel.style.display === 'flex') document.getElementById('ai-chat-input').focus(); + } + + function addMessage(role, text, extra) { + const container = document.getElementById('ai-chat-messages'); + if (!container) return; + const div = document.createElement('div'); + const isUser = role === 'user'; + div.style.cssText = isUser + ? 'align-self:flex-end; background:rgba(99,102,241,0.15); border-radius:10px 10px 2px 10px; padding:10px 14px; max-width:85%; font-size:0.85rem;' + : 'align-self:flex-start; background:rgba(255,255,255,0.05); border-radius:10px 10px 10px 2px; padding:10px 14px; max-width:85%; font-size:0.85rem;'; + div.textContent = text; + container.appendChild(div); + + // Extra content (recommendations, action buttons) + if (extra) { + const extraDiv = document.createElement('div'); + extraDiv.style.cssText = 'align-self:flex-start; max-width:85%; margin-top:4px;'; + if (extra.recommendations) { + extra.recommendations.forEach(rec => { + const btn = document.createElement('button'); + btn.textContent = `๐Ÿ“ฆ Deploy ${rec.app}`; + btn.style.cssText = 'display:block; margin:4px 0; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#4ade80;'; + btn.onclick = () => window.aiChat.send('Deploy ' + rec.app); + extraDiv.appendChild(btn); + }); + } + if (extra.appId) { + const btn = document.createElement('button'); + btn.textContent = `๐Ÿš€ Deploy ${extra.appId}`; + btn.style.cssText = 'display:block; margin:4px 0; background:rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#4ade80;'; + btn.onclick = () => window.aiChat.deploy(extra.appId); + extraDiv.appendChild(btn); + } + if (extra.suggestions) { + const list = document.createElement('div'); + list.style.cssText = 'font-size:0.78rem; color:var(--text-muted,#888); padding:4px 0;'; + list.innerHTML = extra.suggestions.map(s => 'โ€ข ' + s).join('
'); + extraDiv.appendChild(list); + } + if (extra.action === 'diagnose') { + const btn = document.createElement('button'); + btn.textContent = '๐Ÿ” Run Diagnostics'; + btn.style.cssText = 'display:block; margin:4px 0; background:rgba(251,191,36,0.12); border:1px solid rgba(251,191,36,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#fbbf24;'; + btn.onclick = () => window.location.href = '#health'; + extraDiv.appendChild(btn); + } + if (extra.action === 'backup') { + const btn = document.createElement('button'); + btn.textContent = '๐Ÿ’พ Create Backup'; + btn.style.cssText = 'display:block; margin:4px 0; background:rgba(99,102,241,0.12); border:1px solid rgba(99,102,241,0.3); border-radius:8px; padding:8px 12px; cursor:pointer; font-size:0.8rem; color:#a5b4fc;'; + btn.onclick = () => { if (window.createBackup) window.createBackup(); }; + extraDiv.appendChild(btn); + } + container.appendChild(extraDiv); + } + container.scrollTop = container.scrollHeight; + } + + async function send(prefilled) { + const input = document.getElementById('ai-chat-input'); + const message = prefilled || input.value.trim(); + if (!message) return; + input.value = ''; + + addMessage('user', message); + + // Typing indicator + const container = document.getElementById('ai-chat-messages'); + const typing = document.createElement('div'); + typing.id = 'ai-typing'; + typing.style.cssText = 'align-self:flex-start; color:var(--text-muted,#666); font-size:0.8rem; padding:8px 14px;'; + typing.textContent = '๐Ÿค– Thinking...'; + container.appendChild(typing); + container.scrollTop = container.scrollHeight; + + try { + const res = await secureFetch('/api/v1/ai/intent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ message, context: {} }) + }); + const data = await res.json(); + typing.remove(); + + if (data.success !== false && data.data) { + const r = data.data; + const response = r.response || {}; + addMessage('ai', response.message || r.response?.message || 'Action: ' + (r.action || r.intent), response); + } else { + addMessage('ai', data.error || 'Sorry, something went wrong.'); + } + } catch (e) { + typing.remove(); + addMessage('ai', 'Connection error: ' + e.message); + } + } + + async function deploy(appId) { + addMessage('user', 'Deploy ' + appId); + addMessage('ai', `Launching ${appId} from the catalog...`); + // Trigger the app selector with this app + if (window.openAppSelector) { + window.openAppSelector(appId); + } else { + addMessage('ai', `Search for ${appId} in the App Catalog to deploy it.`); + } + } + + window.aiChat = { send, deploy, toggle }; + window.toggleAIChat = toggle; +})(); diff --git a/status/js/globals.js b/status/js/globals.js index 1e0a72e..9dbba55 100644 --- a/status/js/globals.js +++ b/status/js/globals.js @@ -197,6 +197,7 @@ async function secureFetch(url, options = {}) { options = { ...options, signal: AbortSignal.timeout(15000) }; } + options.credentials = options.credentials || 'same-origin'; const response = await fetch(url, options); // Auto-retry once on CSRF failure (token may have been rotated by TOTP re-auth) diff --git a/status/js/language-selector.js b/status/js/language-selector.js index 23d70d8..9cb91c6 100644 --- a/status/js/language-selector.js +++ b/status/js/language-selector.js @@ -18,7 +18,7 @@ const CONFIG_ENDPOINT = '/api/v1/config'; const LANGUAGES = [ - { code: 'en', flag: '๐Ÿ‡ฌ๐Ÿ‡ง', label: 'English' }, + { code: 'en', flag: '๐Ÿ‡บ๐Ÿ‡ธ', label: 'English' }, { code: 'es', flag: '๐Ÿ‡ช๐Ÿ‡ธ', label: 'Espaรฑol' }, { code: 'zh', flag: '๐Ÿ‡จ๐Ÿ‡ณ', label: 'ไธญๆ–‡' }, { code: 'ar', flag: '๐Ÿ‡ธ๐Ÿ‡ฆ', label: 'ุงู„ุนุฑุจูŠุฉ' },