feat: improve welcome page ux and remove beta/stable switch commands

- add click-to-toggle password visibility (replaces hold-to-reveal)
- add copy button for username fields
- add "keeping up to date" section with update commands
- remove switch-beta/switch-stable from commands list and docs
This commit is contained in:
Yury Kossakovsky
2025-12-12 08:47:34 -07:00
parent 323d0cb02c
commit e297ff27ef
3 changed files with 93 additions and 30 deletions

View File

@@ -38,9 +38,6 @@ make logs s=<service> # View logs for specific service
make status # Show container status
make monitor # Live CPU/memory monitoring
make restarts # Show restart count per container
make switch-beta # Switch to beta (develop branch)
make switch-stable # Switch to stable (main branch)
```

View File

@@ -238,9 +238,7 @@
{ cmd: 'make doctor', desc: 'Run system diagnostics' },
{ cmd: 'make update', desc: 'Update system and services' },
{ cmd: 'make update-preview', desc: 'Preview available updates' },
{ cmd: 'make clean', desc: 'Remove unused Docker resources' },
{ cmd: 'make switch-beta', desc: 'Switch to beta (develop branch)' },
{ cmd: 'make switch-stable', desc: 'Switch to stable (main branch)' }
{ cmd: 'make clean', desc: 'Remove unused Docker resources' }
];
// DOM Elements
@@ -251,6 +249,41 @@
const errorToast = document.getElementById('error-toast');
const errorMessage = document.getElementById('error-message');
/**
* Create a copy button for any text
*/
function createCopyButton(textToCopy) {
const copyBtn = document.createElement('button');
copyBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50';
copyBtn.innerHTML = `
<svg class="w-4 h-4 text-gray-500 hover:text-brand transition-colors copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
</svg>
<svg class="w-4 h-4 text-brand check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
`;
copyBtn.title = 'Copy to clipboard';
copyBtn.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(textToCopy);
const copyIcon = copyBtn.querySelector('.copy-icon');
const checkIcon = copyBtn.querySelector('.check-icon');
copyIcon.classList.add('hidden');
checkIcon.classList.remove('hidden');
setTimeout(() => {
copyIcon.classList.remove('hidden');
checkIcon.classList.add('hidden');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
});
return copyBtn;
}
/**
* Show error toast
*/
@@ -284,34 +317,34 @@
const toggleBtn = document.createElement('button');
toggleBtn.className = 'p-1.5 rounded-lg hover:bg-surface-400 transition-colors focus:outline-none focus:ring-2 focus:ring-brand/50';
toggleBtn.innerHTML = `
<svg class="w-4 h-4 text-gray-500 hover:text-brand transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-4 h-4 text-gray-500 hover:text-brand transition-colors eye-open" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<svg class="w-4 h-4 text-gray-500 hover:text-brand transition-colors eye-closed hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
</svg>
`;
toggleBtn.title = 'Hold to reveal';
toggleBtn.title = 'Toggle visibility';
// Show password on mouse down, hide on mouse up/leave
const showPassword = () => {
passwordSpan.textContent = passwordSpan.dataset.password;
passwordSpan.dataset.hidden = 'false';
};
const hidePassword = () => {
passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12));
passwordSpan.dataset.hidden = 'true';
};
// Toggle password visibility on click
toggleBtn.addEventListener('click', () => {
const isHidden = passwordSpan.dataset.hidden === 'true';
const eyeOpen = toggleBtn.querySelector('.eye-open');
const eyeClosed = toggleBtn.querySelector('.eye-closed');
toggleBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
showPassword();
if (isHidden) {
passwordSpan.textContent = passwordSpan.dataset.password;
passwordSpan.dataset.hidden = 'false';
eyeOpen.classList.add('hidden');
eyeClosed.classList.remove('hidden');
} else {
passwordSpan.textContent = '*'.repeat(Math.min(password.length, 12));
passwordSpan.dataset.hidden = 'true';
eyeOpen.classList.remove('hidden');
eyeClosed.classList.add('hidden');
}
});
toggleBtn.addEventListener('mouseup', hidePassword);
toggleBtn.addEventListener('mouseleave', hidePassword);
toggleBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
showPassword();
});
toggleBtn.addEventListener('touchend', hidePassword);
// Copy button
const copyBtn = document.createElement('button');
@@ -380,9 +413,11 @@
let fields = [];
if (creds.username) {
fields.push(`
<div class="flex justify-between items-center">
<div class="flex justify-between items-center" id="user-${key}">
<span class="text-gray-500 text-sm">Username:</span>
<span class="font-mono text-sm select-all text-gray-300">${escapeHtml(creds.username)}</span>
<div class="flex items-center gap-1">
<span class="font-mono text-sm select-all text-gray-300">${escapeHtml(creds.username)}</span>
</div>
</div>
`);
}
@@ -458,11 +493,17 @@
${credentialsHtml}
`;
// Add password fields after card is created
// Add password fields and copy buttons after card is created
if (serviceData.credentials) {
const creds = serviceData.credentials;
setTimeout(() => {
if (creds.username) {
const userContainer = card.querySelector(`#user-${key} .flex.items-center`);
if (userContainer) {
userContainer.appendChild(createCopyButton(creds.username));
}
}
if (creds.password) {
const pwdContainer = card.querySelector(`#pwd-${key}`);
if (pwdContainer) {

View File

@@ -122,6 +122,31 @@
</div>
</section>
<!-- Update Section -->
<section class="mb-16">
<div class="flex items-center gap-3 mb-6">
<div class="w-10 h-10 rounded-lg bg-brand/10 border border-brand/20 flex items-center justify-center">
<svg class="w-5 h-5 text-brand" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
</div>
<h2 class="text-2xl font-semibold text-white">Keeping Up to Date</h2>
</div>
<div class="bg-surface-100 rounded-xl border border-surface-400 p-6">
<p class="text-gray-300 mb-4">Keep your services up to date with the latest features and security patches:</p>
<div class="space-y-3">
<div class="flex items-start gap-3">
<code class="text-brand font-mono text-sm bg-surface-200 px-2 py-1 rounded">make update-preview</code>
<span class="text-gray-400 text-sm">Preview available updates before applying</span>
</div>
<div class="flex items-start gap-3">
<code class="text-brand font-mono text-sm bg-surface-200 px-2 py-1 rounded">make update</code>
<span class="text-gray-400 text-sm">Update all services to the latest versions</span>
</div>
</div>
</div>
</section>
<!-- Commands Section -->
<section class="mb-16">
<div class="flex items-center gap-3 mb-6">