Initial commit: DashCaddy v1.0

Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

278
status/js/license.js Normal file
View File

@@ -0,0 +1,278 @@
// License Management UI
(function() {
// Inject license modal HTML
injectModal('license-modal', `<div id="license-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 420px; max-width: 520px;">
<h3>DashCaddy License</h3>
<p style="font-size: 0.85rem; color: var(--muted); margin: 0 0 16px;">
Activate a license code to unlock premium features.
</p>
<div id="license-status-section" style="margin-bottom: 16px;">
<div id="license-badge" style="display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 20px; font-size: 0.85rem; font-weight: 600; margin-bottom: 12px;">
<span id="license-badge-icon"></span>
<span id="license-badge-text"></span>
</div>
<div id="license-details" style="font-size: 0.85rem; color: var(--muted); line-height: 1.6;"></div>
</div>
<div id="license-activate-section">
<label class="form-label-bold">License Code:</label>
<input type="text" id="license-code-input" placeholder="DC-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
maxlength="35" spellcheck="false" autocomplete="off"
style="width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 4px; background: var(--card-bg); color: var(--fg); font-size: 1rem; font-family: monospace; letter-spacing: 1px;" />
<p class="tiny-hint">Enter your license code to activate premium features</p>
</div>
<div id="license-features" style="margin-top: 16px;">
<label class="form-label-bold" style="margin-bottom: 8px; display: block;">Premium Features:</label>
<div id="license-feature-list" style="display: grid; gap: 8px;"></div>
</div>
<div id="license-error" style="display: none; margin-top: 12px; padding: 10px; border-radius: 4px; background: rgba(231,76,60,0.15); color: var(--bad-fg); font-size: 0.85rem;"></div>
<div id="license-success" style="display: none; margin-top: 12px; padding: 10px; border-radius: 4px; background: rgba(46,204,113,0.15); color: var(--ok-fg); font-size: 0.85rem;"></div>
<div class="weather-modal-buttons" style="margin-top: 16px;">
<button id="license-cancel">Close</button>
<button id="license-deactivate" class="btn-accent" style="display: none; background: var(--bad-bg); border-color: var(--bad-fg); color: var(--bad-fg);">Deactivate</button>
<button id="license-activate" class="btn-accent">Activate</button>
</div>
</div>
</div>`);
const modal = document.getElementById('license-modal');
const codeInput = document.getElementById('license-code-input');
const activateBtn = document.getElementById('license-activate');
const deactivateBtn = document.getElementById('license-deactivate');
const errorEl = document.getElementById('license-error');
const successEl = document.getElementById('license-success');
const badgeIcon = document.getElementById('license-badge-icon');
const badgeText = document.getElementById('license-badge-text');
const badge = document.getElementById('license-badge');
const detailsEl = document.getElementById('license-details');
const featureList = document.getElementById('license-feature-list');
const activateSection = document.getElementById('license-activate-section');
let currentStatus = null;
function hideMessages() {
errorEl.style.display = 'none';
successEl.style.display = 'none';
}
function showError(msg) {
hideMessages();
errorEl.textContent = msg;
errorEl.style.display = 'block';
}
function showSuccess(msg) {
hideMessages();
successEl.textContent = msg;
successEl.style.display = 'block';
}
function renderStatus(status) {
currentStatus = status;
if (status.active) {
badge.style.background = 'rgba(46,204,113,0.15)';
badge.style.color = 'var(--ok-fg)';
badgeIcon.textContent = '\u2605';
badgeText.textContent = 'Premium Active';
const expiryLine = status.lifetime
? '<div>License: <strong>LIFETIME</strong></div>'
: `<div>Expires: <strong>${new Date(status.expiresAt).toLocaleDateString()}</strong> (${status.daysRemaining} days remaining)</div>`;
detailsEl.innerHTML = `
<div>Code: <code style="font-family: monospace;">${status.code || '***'}</code></div>
${expiryLine}
`;
activateSection.style.display = 'none';
activateBtn.style.display = 'none';
deactivateBtn.style.display = '';
} else {
badge.style.background = 'rgba(149,165,166,0.15)';
badge.style.color = 'var(--muted)';
badgeIcon.textContent = '\u2606';
badgeText.textContent = status.expired ? 'License Expired' : 'Free Tier';
detailsEl.innerHTML = status.expired
? '<div>Your license has expired. Enter a new code to renew.</div>'
: '<div>Enter a license code to unlock premium features.</div>';
activateSection.style.display = '';
activateBtn.style.display = '';
deactivateBtn.style.display = 'none';
}
// Render feature list
const features = status.premiumFeatures || {};
const activeFeatures = new Set(status.features || []);
featureList.innerHTML = Object.entries(features).map(([key, info]) => {
const active = activeFeatures.has(key);
return `<div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; background: var(--card-bg); border: 1px solid var(--border);">
<span style="font-size: 1.1rem;">${active ? '\u2705' : '\uD83D\uDD12'}</span>
<div>
<div style="font-weight: 600; font-size: 0.9rem;">${info.name}</div>
<div style="font-size: 0.78rem; color: var(--muted);">${info.description}</div>
</div>
</div>`;
}).join('');
}
async function loadStatus() {
try {
const resp = await fetch('/api/v1/license/status');
const data = await resp.json();
if (data.success) {
renderStatus(data.license);
updateHeaderBadge(data.license);
}
} catch (e) {
console.warn('Failed to load license status:', e.message);
}
}
async function activateLicense() {
const code = codeInput.value.trim();
if (!code) {
showError('Please enter a license code.');
return;
}
hideMessages();
activateBtn.disabled = true;
activateBtn.textContent = 'Activating...';
try {
const resp = await secureFetch('/api/v1/license/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await resp.json();
if (data.success) {
showSuccess(data.message);
codeInput.value = '';
renderStatus(data.license);
showNotification('License activated! Premium features unlocked.', 'success', 5000);
updateHeaderBadge(data.license);
} else {
showError(data.error || 'Activation failed');
}
} catch (e) {
showError('Network error: ' + e.message);
} finally {
activateBtn.disabled = false;
activateBtn.textContent = 'Activate';
}
}
async function deactivateLicense() {
if (!confirm('Deactivate your license? You can reuse the code on another machine.')) return;
deactivateBtn.disabled = true;
deactivateBtn.textContent = 'Deactivating...';
try {
const resp = await secureFetch('/api/v1/license/deactivate', { method: 'POST' });
const data = await resp.json();
if (data.success) {
showSuccess(data.message);
await loadStatus();
showNotification('License deactivated.', 'info', 3000);
updateHeaderBadge({ active: false });
} else {
showError(data.error || 'Deactivation failed');
}
} catch (e) {
showError('Network error: ' + e.message);
} finally {
deactivateBtn.disabled = false;
deactivateBtn.textContent = 'Deactivate';
}
}
function updateHeaderBadge(status) {
const topbar = document.getElementById('license-status-topbar');
const iconEl = document.getElementById('license-topbar-icon');
const textEl = document.getElementById('license-topbar-text');
const timeEl = document.getElementById('license-topbar-time');
if (!topbar) return;
topbar.className = 'license-status-topbar ' + (status.active ? 'premium' : 'free');
if (status.active) {
iconEl.textContent = '\u2605';
textEl.textContent = 'PREMIUM';
if (status.lifetime) {
timeEl.textContent = '\u00b7 LIFETIME';
} else {
const days = status.daysRemaining;
timeEl.textContent = days != null ? '\u00b7 ' + days + 'd remaining' : '';
}
} else {
iconEl.textContent = '\u2606';
textEl.textContent = status.expired ? 'EXPIRED' : 'FREE TIER';
timeEl.textContent = '';
}
}
function openLicenseModal() {
hideMessages();
loadStatus();
modal.classList.add('show');
}
// Format code input as user types (auto-add dashes)
codeInput.addEventListener('input', function() {
let val = this.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
// Don't auto-format if user is deleting
if (val.length > this._prevLength) {
val = val.replace(/-/g, '');
if (val.length > 2 && !val.startsWith('DC')) {
val = 'DC' + val;
}
// Add dashes after DC and every 5 chars
if (val.startsWith('DC') && val.length > 2) {
const parts = ['DC'];
const rest = val.substring(2);
for (let i = 0; i < rest.length; i += 5) {
parts.push(rest.substring(i, i + 5));
}
val = parts.join('-');
}
}
this._prevLength = val.length;
this.value = val;
});
// Wire up events
activateBtn.addEventListener('click', activateLicense);
deactivateBtn.addEventListener('click', deactivateLicense);
codeInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') activateLicense();
});
wireModal(modal, document.getElementById('license-cancel'));
const topbarEl = document.getElementById('license-status-topbar');
if (topbarEl) topbarEl.addEventListener('click', () => window.openLicenseModal && window.openLicenseModal());
// Expose for other modules to open
window.openLicenseModal = openLicenseModal;
// Expose feature check for frontend gating
window.checkPremiumFeature = async function(feature) {
try {
const resp = await fetch(`/api/v1/license/feature/${feature}`);
const data = await resp.json();
return data.available;
} catch {
return false;
}
};
// Load status on page load
loadStatus().then(status => {
if (currentStatus) updateHeaderBadge(currentStatus);
});
})();