feat(electron): added key managment ui

This commit is contained in:
2025-08-11 11:26:09 -04:00
parent d7d7615376
commit 268e4fbeae
28 changed files with 2525 additions and 247 deletions

View File

@@ -11,11 +11,10 @@ that now return JSON directly. No more complex stdout mixing or data wrapping.
Key Changes:
- Core functions return JSON-serializable dictionaries directly
- Progress messages go to stderr only (never mixed with JSON output)
- Clean JSON output to stdout for Electron to parse
- Simplified error handling with consistent JSON error format
"""
import sys
import os
import json
import inspect
import traceback
@@ -35,7 +34,7 @@ class FourdstEncoder(json.JSONEncoder):
project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root))
from fourdst.core import bundle
from fourdst.core import bundle, keys
def main():
# Use stderr for all logging to avoid interfering with JSON output on stdout
@@ -62,13 +61,25 @@ def main():
print(f"[BRIDGE_INFO] Parsed kwargs: {kwargs}", file=log_file, flush=True)
# Convert path strings to Path objects where needed
path_params = ['outputDir', 'output_dir', 'keyPath', 'key_path', 'bundlePath', 'bundle_path', 'path']
for key, value in kwargs.items():
if isinstance(value, str) and ('path' in key.lower() or 'key' in key.lower()):
if isinstance(value, str) and key in path_params:
kwargs[key] = Path(value)
elif isinstance(value, list) and 'dirs' in key.lower():
kwargs[key] = [Path(p) for p in value]
func = getattr(bundle, command)
# Route commands to appropriate modules
key_commands = [
'list_keys', 'generate_key', 'add_key', 'remove_key',
'sync_remotes', 'get_remote_sources', 'add_remote_source', 'remove_remote_source'
]
if command in key_commands:
func = getattr(keys, command)
module_name = "keys"
else:
func = getattr(bundle, command)
module_name = "bundle"
# Create progress callback that sends structured progress to stderr
# This keeps progress separate from the final JSON result on stdout
@@ -87,7 +98,9 @@ def main():
if 'progress_callback' in sig.parameters:
kwargs['progress_callback'] = progress_callback
print(f"[BRIDGE_INFO] Calling function `bundle.{command}`...", file=log_file, flush=True)
print(f"[BRIDGE_INFO] Calling function `{module_name}.{command}`...", file=log_file, flush=True)
print(f"[BRIDGE_DEBUG] Function signature: {func.__name__}{inspect.signature(func)}", file=log_file, flush=True)
print(f"[BRIDGE_DEBUG] Kwargs being passed: {kwargs}", file=log_file, flush=True)
result = func(**kwargs)
print(f"[BRIDGE_INFO] Function returned successfully.", file=log_file, flush=True)

View File

@@ -30,6 +30,10 @@
<div class="category-icon" style="background-color: #10b981;">LC</div>
<span class="category-label">libconstants</span>
</div>
<div class="category-item" data-category="keys" title="Key Management">
<div class="category-icon" style="background-color: #8b5cf6;">KM</div>
<span class="category-label">Key Management</span>
</div>
<div class="category-item" data-category="opat" title="OPAT Core">
<div class="category-icon" style="background-color: #f59e0b;">OC</div>
<span class="category-label">OPAT Core</span>
@@ -71,6 +75,19 @@
</div>
</div>
<!-- Key Management content -->
<div class="sidebar-content hidden" data-category="keys">
<div class="sidebar-header">
<h3>Key Management</h3>
</div>
<nav class="sidebar-nav">
<button id="keys-list-btn" class="nav-button active">Trusted Keys</button>
<button id="keys-generate-btn" class="nav-button">Generate Key</button>
<button id="keys-add-btn" class="nav-button">Add Key</button>
<button id="keys-remotes-btn" class="nav-button">Remote Sources</button>
</nav>
</div>
<!-- OPAT Core content -->
<div class="sidebar-content hidden" data-category="opat">
<div class="sidebar-header">
@@ -126,6 +143,14 @@
</div>
</div>
<div class="feature-card">
<div class="feature-icon" style="background-color: #8b5cf6;">KM</div>
<div class="feature-info">
<h3>Key Management</h3>
<p>Manage cryptographic keys for bundle signing and verification.</p>
</div>
</div>
<div class="feature-card">
<div class="feature-icon" style="background-color: #10b981;">LC</div>
<div class="feature-info">
@@ -455,6 +480,118 @@
</div>
</div>
<!-- Key Management Views -->
<div id="keys-view" class="hidden">
<!-- Trusted Keys View -->
<div id="keys-list-view" class="key-management-view">
<div class="keys-header">
<h3>Trusted Keys</h3>
<p>Manage cryptographic keys for bundle signing and verification.</p>
</div>
<div id="keys-list-container">
<div class="empty-state">
<div class="empty-icon">🔑</div>
<h3>Loading Keys...</h3>
<p>Please wait while we load your trusted keys.</p>
</div>
</div>
</div>
<!-- Generate Key View -->
<div id="keys-generate-view" class="key-management-view hidden">
<div class="generate-key-header">
<h3>Generate New Key Pair</h3>
<p>Create a new cryptographic key pair for signing bundles.</p>
</div>
<div class="generate-key-form">
<div class="form-group">
<label for="generate-key-name">Key Name:</label>
<input type="text" id="generate-key-name" placeholder="author_key" value="author_key">
<small>Base name for the generated key files</small>
</div>
<div class="form-group">
<label for="generate-key-type">Key Type:</label>
<select id="generate-key-type">
<option value="ed25519">Ed25519 (Recommended)</option>
<option value="rsa">RSA-2048</option>
</select>
<small>Ed25519 is faster and more secure than RSA</small>
</div>
<div class="form-group">
<label for="generate-output-dir">Output Directory:</label>
<input type="text" id="generate-output-dir" placeholder="." value=".">
<small>Directory where keys will be saved</small>
</div>
<div class="form-actions">
<button id="generate-key-btn" class="action-button primary">Generate Key Pair</button>
</div>
<div class="security-notice">
<div class="warning-banner">
<span class="warning-icon">⚠️</span>
<div class="warning-text">
<strong>Security Notice:</strong> Keep your private key secure and never share it.
Only share the public key (.pub or .pub.pem files) with others who need to verify your bundles.
</div>
</div>
</div>
</div>
</div>
<!-- Add Key View -->
<div id="keys-add-view" class="key-management-view hidden">
<div class="add-key-header">
<h3>Add Trusted Key</h3>
<p>Add a public key to your trust store for bundle verification.</p>
</div>
<div class="add-key-form">
<div class="form-actions">
<button id="add-key-file-btn" class="action-button primary">Select Key File</button>
</div>
<div class="info-notice">
<div class="info-banner">
<span class="info-icon"></span>
<div class="info-text">
<strong>Supported formats:</strong> .pub, .pub.pem, and .pem files containing public keys.
</div>
</div>
</div>
</div>
</div>
<!-- Remote Sources View -->
<div id="keys-remotes-view" class="key-management-view hidden">
<div class="remotes-header">
<h3>Remote Key Sources</h3>
<p>Manage Git repositories that contain trusted public keys.</p>
</div>
<div class="add-remote-form">
<h4>Add New Remote Source</h4>
<div class="form-row">
<div class="form-group">
<label for="remote-name">Name:</label>
<input type="text" id="remote-name" placeholder="my-keys">
</div>
<div class="form-group">
<label for="remote-url">Git Repository URL:</label>
<input type="text" id="remote-url" placeholder="https://github.com/user/keys.git">
</div>
<div class="form-group">
<button id="add-remote-btn" class="action-button primary">Add Remote</button>
</div>
</div>
</div>
<div id="remotes-list-container">
<div class="empty-state">
<div class="empty-icon">🌐</div>
<h3>Loading Remote Sources...</h3>
<p>Please wait while we load your remote key sources.</p>
</div>
</div>
</div>
</div>
<div id="create-bundle-form" class="hidden">
<!-- The create form will be moved into a modal later -->
</div>

View File

@@ -60,7 +60,7 @@
<p>This installer will install the 4DSTAR Bundle Manager, a comprehensive tool for managing 4DSTAR plugin bundles and OPAT data files.</p>
<div class="highlight">
<div>
<strong>What's Included:</strong>
<ul>
<li>4DSTAR Bundle Manager application</li>

View File

@@ -5,7 +5,7 @@
// Import modular components
const { setupAppEventHandlers, setupThemeHandlers } = require('./main/app-lifecycle');
const { setupFileDialogHandlers } = require('./main/file-dialogs');
const { setupBundleIPCHandlers } = require('./main/ipc-handlers');
const { setupBundleIPCHandlers, setupKeyIPCHandlers } = require('./main/ipc-handlers');
// Initialize all modules in the correct order
function initializeMainProcess() {
@@ -21,6 +21,9 @@ function initializeMainProcess() {
// Setup bundle operation IPC handlers
setupBundleIPCHandlers();
// Setup key management IPC handlers
setupKeyIPCHandlers();
console.log('[MAIN_PROCESS] All modules initialized successfully');
}

View File

@@ -10,16 +10,21 @@ function runPythonCommand(command, kwargs, event) {
// Determine executable name based on platform
const executableName = process.platform === 'win32' ? 'fourdst-backend.exe' : 'fourdst-backend';
// Initialize args first
let args = [command, JSON.stringify(kwargs)];
if (app.isPackaged) {
// In packaged app, backend is in resources/backend/ directory
backendPath = path.join(process.resourcesPath, 'backend', executableName);
} else {
// In development, use the meson build output
backendPath = path.join(buildDir, 'electron', 'dist', 'fourdst-backend', executableName);
// In development, use the Python bridge.py directly for faster iteration
backendPath = 'python';
// Update args to include the bridge.py path as the first argument
const bridgePath = path.join(__dirname, '..', 'bridge.py');
args.unshift(bridgePath);
}
console.log(`[MAIN_PROCESS] Spawning backend: ${backendPath}`);
const args = [command, JSON.stringify(kwargs)];
console.log(`[MAIN_PROCESS] With args: [${args.join(', ')}]`);
return new Promise((resolve) => {

View File

@@ -29,6 +29,23 @@ const setupFileDialogHandlers = () => {
return null;
});
// Key file selection dialog
ipcMain.handle('select-key-file', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
title: 'Select Public Key File',
filters: [
{ name: 'Public Key Files', extensions: ['pub', 'pem'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
// Save file dialog
ipcMain.handle('select-save-file', async () => {
const result = await dialog.showSaveDialog({

View File

@@ -3,6 +3,69 @@ const { runPythonCommand } = require('./backend-bridge');
const fs = require('fs-extra');
const path = require('path');
const setupKeyIPCHandlers = () => {
// List keys handler
ipcMain.handle('list-keys', async (event) => {
const kwargs = {};
return runPythonCommand('list_keys', kwargs, event);
});
// Generate key handler
ipcMain.handle('generate-key', async (event, { keyName, keyType, outputDir }) => {
const kwargs = {
key_name: keyName,
key_type: keyType,
output_dir: outputDir
};
return runPythonCommand('generate_key', kwargs, event);
});
// Add key handler
ipcMain.handle('add-key', async (event, keyPath) => {
const kwargs = {
key_path: keyPath
};
return runPythonCommand('add_key', kwargs, event);
});
// Remove key handler
ipcMain.handle('remove-key', async (event, keyIdentifier) => {
const kwargs = {
key_identifier: keyIdentifier
};
return runPythonCommand('remove_key', kwargs, event);
});
// Sync remotes handler
ipcMain.handle('sync-remotes', async (event) => {
const kwargs = {};
return runPythonCommand('sync_remotes', kwargs, event);
});
// Get remote sources handler
ipcMain.handle('get-remote-sources', async (event) => {
const kwargs = {};
return runPythonCommand('get_remote_sources', kwargs, event);
});
// Add remote source handler
ipcMain.handle('add-remote-source', async (event, { name, url }) => {
const kwargs = {
name: name,
url: url
};
return runPythonCommand('add_remote_source', kwargs, event);
});
// Remove remote source handler
ipcMain.handle('remove-remote-source', async (event, name) => {
const kwargs = {
name: name
};
return runPythonCommand('remove_remote_source', kwargs, event);
});
};
const setupBundleIPCHandlers = () => {
// Create bundle handler
ipcMain.handle('create-bundle', async (event, bundleData) => {
@@ -171,5 +234,6 @@ const setupBundleIPCHandlers = () => {
};
module.exports = {
setupBundleIPCHandlers
setupBundleIPCHandlers,
setupKeyIPCHandlers
};

View File

@@ -8,6 +8,7 @@ const { ipcRenderer } = require('electron');
const stateManager = require('./renderer/state-manager');
const domManager = require('./renderer/dom-manager');
const bundleOperations = require('./renderer/bundle-operations');
const keyOperations = require('./renderer/key-operations');
const uiComponents = require('./renderer/ui-components');
const eventHandlers = require('./renderer/event-handlers');
const opatHandler = require('./renderer/opat-handler');
@@ -21,6 +22,7 @@ function initializeModules() {
stateManager,
domManager,
bundleOperations,
keyOperations,
uiComponents,
eventHandlers,
opatHandler,
@@ -30,6 +32,7 @@ function initializeModules() {
// Initialize each module with its dependencies
bundleOperations.initializeDependencies(deps);
keyOperations.initializeDependencies(deps);
uiComponents.initializeDependencies(deps);
eventHandlers.initializeDependencies(deps);
opatHandler.initializeDependencies(deps);
@@ -83,6 +86,7 @@ document.addEventListener('DOMContentLoaded', async () => {
window.stateManager = stateManager;
window.domManager = domManager;
window.bundleOperations = bundleOperations;
window.keyOperations = keyOperations;
window.uiComponents = uiComponents;
window.eventHandlers = eventHandlers;
window.opatHandler = opatHandler;

View File

@@ -2,7 +2,7 @@
// Extracted from renderer.js to centralize DOM element handling and view management
// --- DOM ELEMENTS (will be initialized in initializeDOMElements) ---
let welcomeScreen, bundleView, createBundleForm;
let welcomeScreen, bundleView, keysView, createBundleForm;
let openBundleBtn, createBundleBtn;
let signBundleBtn, validateBundleBtn, clearBundleBtn, saveMetadataBtn;
let saveOptionsModal, overwriteBundleBtn, saveAsNewBtn;
@@ -43,6 +43,7 @@ function initializeDOMElements() {
// Views
welcomeScreen = document.getElementById('welcome-screen');
bundleView = document.getElementById('bundle-view');
keysView = document.getElementById('keys-view');
createBundleForm = document.getElementById('create-bundle-form');
// Sidebar buttons
@@ -89,7 +90,7 @@ function showView(viewId) {
const opatView = document.getElementById('opat-view');
// Hide main content views
[welcomeScreen, bundleView, createBundleForm].forEach(view => {
[welcomeScreen, bundleView, keysView, createBundleForm].forEach(view => {
view.classList.toggle('hidden', view.id !== viewId);
});
@@ -171,6 +172,7 @@ module.exports = {
getElements: () => ({
welcomeScreen,
bundleView,
keysView,
createBundleForm,
openBundleBtn,
createBundleBtn,

View File

@@ -4,7 +4,7 @@
const { ipcRenderer } = require('electron');
// Import dependencies (these will be injected when integrated)
let stateManager, domManager, bundleOperations, fillWorkflow, uiComponents, opatHandler;
let stateManager, domManager, bundleOperations, keyOperations, fillWorkflow, uiComponents, opatHandler;
// --- EVENT LISTENERS SETUP ---
function setupEventListeners() {
@@ -86,6 +86,9 @@ function setupEventListeners() {
elements.overwriteBundleBtn.addEventListener('click', () => handleSaveMetadata(false));
elements.saveAsNewBtn.addEventListener('click', () => handleSaveMetadata(true));
// Key Management event listeners
setupKeyManagementEventListeners();
// Signature warning modal event listeners
elements.signatureWarningCancel.addEventListener('click', () => {
elements.signatureWarningModal.classList.add('hidden');
@@ -207,7 +210,7 @@ function showCategoryHomeScreen(category) {
const views = [
'welcome-screen', 'libplugin-home', 'opat-home',
'libconstants-home', 'serif-home', 'opat-view', 'libplugin-view',
'bundle-view', 'create-bundle-form'
'bundle-view', 'keys-view', 'create-bundle-form'
];
// Hide all views
@@ -222,12 +225,28 @@ function showCategoryHomeScreen(category) {
'libplugin': 'libplugin-home',
'opat': 'opat-home',
'libconstants': 'libconstants-home',
'serif': 'serif-home'
'serif': 'serif-home',
'keys': 'keys-view'
};
const viewId = viewMap[category] || 'welcome-screen';
const view = document.getElementById(viewId);
if (view) view.classList.remove('hidden');
if (view) {
view.classList.remove('hidden');
// Initialize key management when showing keys view
if (category === 'keys' && keyOperations) {
// Show the default keys list view and load keys
showKeyManagementView('keys-list-view');
keyOperations.loadTrustedKeys();
// Set the keys-list-btn as active in sidebar
const keysSidebarButtons = document.querySelectorAll('#keys-list-btn, #keys-generate-btn, #keys-add-btn, #keys-remotes-btn');
keysSidebarButtons.forEach(btn => btn.classList.remove('active'));
const keysListBtn = document.getElementById('keys-list-btn');
if (keysListBtn) keysListBtn.classList.add('active');
}
}
}
// Setup info modal
@@ -411,11 +430,88 @@ async function handleSaveMetadata(saveAsNew = false) {
uiComponents.hideSaveOptionsModal();
}
// Setup key management event listeners
function setupKeyManagementEventListeners() {
// Key management sidebar navigation
const keysSidebarButtons = document.querySelectorAll('#keys-list-btn, #keys-generate-btn, #keys-add-btn, #keys-remotes-btn');
keysSidebarButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
// Update active sidebar button
keysSidebarButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Show appropriate key management view
const viewMap = {
'keys-list-btn': 'keys-list-view',
'keys-generate-btn': 'keys-generate-view',
'keys-add-btn': 'keys-add-view',
'keys-remotes-btn': 'keys-remotes-view'
};
const targetView = viewMap[button.id];
if (targetView) {
showKeyManagementView(targetView);
// Load content for specific views
if (targetView === 'keys-list-view') {
keyOperations.loadTrustedKeys();
} else if (targetView === 'keys-remotes-view') {
keyOperations.loadRemoteSources();
}
}
});
});
// Generate key button
const generateKeyBtn = document.getElementById('generate-key-btn');
if (generateKeyBtn) {
generateKeyBtn.addEventListener('click', keyOperations.handleGenerateKey);
}
// Add remote button
const addRemoteBtn = document.getElementById('add-remote-btn');
if (addRemoteBtn) {
addRemoteBtn.addEventListener('click', keyOperations.handleAddRemote);
}
// Add key file button
const addKeyFileBtn = document.getElementById('add-key-file-btn');
if (addKeyFileBtn) {
addKeyFileBtn.addEventListener('click', keyOperations.handleAddKey);
}
// Dynamic buttons (will be added dynamically to keys list)
document.addEventListener('click', (e) => {
if (e.target.classList.contains('sync-remotes-btn')) {
keyOperations.handleSyncRemotes();
} else if (e.target.classList.contains('remove-remote-btn')) {
const remoteName = e.target.dataset.remoteName;
keyOperations.handleRemoveRemote(remoteName);
}
});
}
// Show specific key management view
function showKeyManagementView(viewId) {
// Hide all key management views
const keyViews = document.querySelectorAll('.key-management-view');
keyViews.forEach(view => view.classList.add('hidden'));
// Show the target view
const targetView = document.getElementById(viewId);
if (targetView) {
targetView.classList.remove('hidden');
}
}
// Initialize dependencies (called when module is loaded)
function initializeDependencies(deps) {
stateManager = deps.stateManager;
domManager = deps.domManager;
bundleOperations = deps.bundleOperations;
keyOperations = deps.keyOperations;
fillWorkflow = deps.fillWorkflow;
uiComponents = deps.uiComponents;
opatHandler = deps.opatHandler;
@@ -424,6 +520,8 @@ function initializeDependencies(deps) {
module.exports = {
initializeDependencies,
setupEventListeners,
setupKeyManagementEventListeners,
showKeyManagementView,
checkSignatureAndWarn,
setupCategoryNavigation,
updateWelcomeScreen,

View File

@@ -0,0 +1,509 @@
// Key operations module for the 4DSTAR Bundle Manager
// Handles all key management operations (list, generate, add, remove, sync)
const { ipcRenderer } = require('electron');
// Dependencies (injected by renderer-refactored.js)
let stateManager, domManager, uiComponents;
// Initialize dependencies
function initializeDependencies(deps) {
stateManager = deps.stateManager;
domManager = deps.domManager;
uiComponents = deps.uiComponents;
console.log('[KEY_OPERATIONS] Dependencies initialized');
}
// === KEY LISTING ===
async function loadKeys() {
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('list-keys');
domManager.hideSpinner();
if (result.success) {
stateManager.setKeysState(result);
displayKeys(result);
return result;
} else {
domManager.showModal('Error', `Failed to load keys: ${result.error}`);
return null;
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to load keys: ${error.message}`);
return null;
}
}
function displayKeys(keysData) {
const keysContainer = document.getElementById('keys-list-container');
if (!keysContainer) return;
if (keysData.total_count === 0) {
keysContainer.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🔑</div>
<h3>No Keys Found</h3>
<p>No trusted keys are currently installed. Generate or add keys to get started.</p>
</div>
`;
return;
}
// Update the main header with count and add action buttons
const mainHeader = document.querySelector('#keys-list-view .keys-header h3');
if (mainHeader) {
mainHeader.textContent = `Trusted Keys (${keysData.total_count})`;
}
// Add action buttons to the header if they don't exist
let keysHeader = document.querySelector('#keys-list-view .keys-header');
if (keysHeader && !keysHeader.querySelector('.keys-actions')) {
const actionsDiv = document.createElement('div');
actionsDiv.className = 'keys-actions';
actionsDiv.innerHTML = `
<button id="refresh-keys-btn" class="action-button secondary">
<span class="icon">🔄</span> Refresh
</button>
<button id="sync-remotes-btn" class="action-button primary">
<span class="icon">🔄</span> Sync Remotes
</button>
`;
keysHeader.appendChild(actionsDiv);
}
let html = '';
for (const [sourceName, keys] of Object.entries(keysData.keys)) {
html += `
<div class="key-source-section">
<div class="source-header">
<h4>${sourceName}</h4>
<span class="key-count">${keys.length} keys</span>
</div>
<div class="keys-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Fingerprint</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
for (const key of keys) {
const shortFingerprint = key.fingerprint.substring(0, 16) + '...';
html += `
<tr data-key-fingerprint="${key.fingerprint}">
<td class="key-name">${key.name}</td>
<td class="key-fingerprint" title="${key.fingerprint}">${shortFingerprint}</td>
<td class="key-size">${key.size_bytes} bytes</td>
<td class="key-actions">
<button class="btn btn-small btn-danger remove-key-btn"
data-key-fingerprint="${key.fingerprint}"
data-key-name="${key.name}">
Remove
</button>
</td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
`;
}
keysContainer.innerHTML = html;
// Add event listeners for dynamically generated content
setupKeyListEventListeners();
}
function setupKeyListEventListeners() {
// Refresh keys button
const refreshBtn = document.getElementById('refresh-keys-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', loadKeys);
}
// Sync remotes button
const syncBtn = document.getElementById('sync-remotes-btn');
if (syncBtn) {
syncBtn.addEventListener('click', handleSyncRemotes);
}
// Remove key buttons - now handled exclusively here (removed from event-handlers.js)
const removeButtons = document.querySelectorAll('.remove-key-btn');
removeButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const fingerprint = e.target.dataset.keyFingerprint;
const keyName = e.target.dataset.keyName;
handleRemoveKey(fingerprint, keyName);
});
});
}
// === KEY GENERATION ===
async function handleGenerateKey() {
const keyName = document.getElementById('generate-key-name')?.value || 'author_key';
const keyType = document.getElementById('generate-key-type')?.value || 'ed25519';
const outputDir = document.getElementById('generate-output-dir')?.value || '.';
if (!keyName.trim()) {
domManager.showModal('Error', 'Please enter a key name.');
return;
}
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('generate-key', {
keyName: keyName.trim(),
keyType: keyType,
outputDir: outputDir
});
domManager.hideSpinner();
if (result.success) {
domManager.showModal('Success', `
<div class="key-generation-success">
<h4>Key Generated Successfully!</h4>
<div class="key-details">
<p><strong>Key Type:</strong> ${result.key_type.toUpperCase()}</p>
<p><strong>Fingerprint:</strong> <code>${result.fingerprint}</code></p>
<p><strong>Private Key:</strong> <code>${result.private_key_path}</code></p>
<p><strong>Public Key:</strong> <code>${result.public_key_path}</code></p>
</div>
<div class="warning">
<strong>⚠️ Important:</strong> Keep your private key secure and never share it!
</div>
</div>
`);
// Clear form
document.getElementById('generate-key-name').value = '';
document.getElementById('generate-output-dir').value = '.';
} else {
domManager.showModal('Error', `Failed to generate key: ${result.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to generate key: ${error.message}`);
}
}
// === KEY ADDITION ===
async function handleAddKey() {
try {
// Use IPC-based file dialog instead of @electron/remote
const keyPath = await ipcRenderer.invoke('select-key-file');
if (!keyPath) {
return; // User canceled dialog
}
domManager.showSpinner();
const addResult = await ipcRenderer.invoke('add-key', keyPath);
domManager.hideSpinner();
if (addResult.success) {
if (addResult.already_existed) {
domManager.showModal('Info', `Key '${addResult.key_name}' already exists in trust store.`);
} else {
domManager.showModal('Success', `
<div class="key-add-success-modern">
<div class="success-header">
<div class="success-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#10b981" stroke="#059669" stroke-width="2"/>
<path d="m9 12 2 2 4-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3>Key Added Successfully!</h3>
<p class="success-subtitle">Your public key has been added to the trust store</p>
</div>
<div class="key-details-card">
<div class="detail-row">
<div class="detail-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Key Name
</div>
<div class="detail-value">${addResult.key_name}</div>
</div>
<div class="detail-row">
<div class="detail-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" stroke="currentColor" stroke-width="2"/>
<path 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" stroke="currentColor" stroke-width="2"/>
</svg>
Fingerprint
</div>
<div class="detail-value fingerprint-value">
<code>${addResult.fingerprint}</code>
<button class="copy-btn" onclick="navigator.clipboard.writeText('${addResult.fingerprint}'); this.innerHTML='✓ Copied'; setTimeout(() => this.innerHTML='Copy', 2000)">Copy</button>
</div>
</div>
</div>
<div class="success-footer">
<div class="info-banner">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="m9 9 1.5 1.5L16 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>This key can now be used to verify signed bundles</span>
</div>
</div>
</div>
`);
}
// Refresh keys list
await loadKeys();
} else {
domManager.showModal('Error', `Failed to add key: ${addResult.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to add key: ${error.message}`);
}
}
// === KEY REMOVAL ===
async function handleRemoveKey(fingerprint, keyName) {
const confirmed = await uiComponents.showConfirmDialog(
'Remove Key',
`Are you sure you want to remove the key "${keyName}"?\n\nThis action cannot be undone.`
);
if (!confirmed) return;
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('remove-key', fingerprint);
domManager.hideSpinner();
if (result.success) {
domManager.showModal('Success', `Removed ${result.removed_count} key(s) successfully.`);
// Refresh keys list
await loadKeys();
} else {
domManager.showModal('Error', `Failed to remove key: ${result.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to remove key: ${error.message}`);
}
}
// === REMOTE SYNC ===
async function handleSyncRemotes() {
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('sync-remotes');
domManager.hideSpinner();
if (result.success) {
const successCount = result.synced_remotes.filter(r => r.status === 'success').length;
const failedCount = result.synced_remotes.filter(r => r.status === 'failed').length;
let message = `
<div class="sync-results">
<h4>Remote Sync Completed</h4>
<p>✅ Successful: ${successCount}</p>
<p>❌ Failed: ${failedCount}</p>
<p>📦 Total keys synced: ${result.total_keys_synced}</p>
</div>
`;
if (result.removed_remotes.length > 0) {
message += `<p><strong>Removed failing remotes:</strong> ${result.removed_remotes.join(', ')}</p>`;
}
domManager.showModal('Sync Results', message);
// Refresh keys list
await loadKeys();
} else {
domManager.showModal('Error', `Failed to sync remotes: ${result.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to sync remotes: ${error.message}`);
}
}
// === REMOTE MANAGEMENT ===
async function loadRemoteSources() {
try {
const result = await ipcRenderer.invoke('get-remote-sources');
if (result.success) {
displayRemoteSources(result.remotes);
return result.remotes;
} else {
domManager.showModal('Error', `Failed to load remote sources: ${result.error}`);
return [];
}
} catch (error) {
domManager.showModal('Error', `Failed to load remote sources: ${error.message}`);
return [];
}
}
function displayRemoteSources(remotes) {
const remotesContainer = document.getElementById('remotes-list-container');
if (!remotesContainer) return;
if (remotes.length === 0) {
remotesContainer.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🌐</div>
<h3>No Remote Sources</h3>
<p>No remote key sources are configured. Add remote repositories to sync keys automatically.</p>
</div>
`;
return;
}
let html = `
<div class="remotes-header">
<h3>Remote Key Sources (${remotes.length})</h3>
</div>
<div class="remotes-table">
<table>
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>URL</th>
<th>Keys</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
for (const remote of remotes) {
const status = remote.exists ? '✅' : '❌';
const statusText = remote.exists ? 'Synced' : 'Not synced';
html += `
<tr>
<td class="remote-status" title="${statusText}">${status}</td>
<td class="remote-name">${remote.name}</td>
<td class="remote-url" title="${remote.url}">${remote.url}</td>
<td class="remote-keys">${remote.keys_count}</td>
<td class="remote-actions">
<button class="btn btn-small btn-danger remove-remote-btn"
data-remote-name="${remote.name}">
Remove
</button>
</td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
`;
remotesContainer.innerHTML = html;
// Add event listeners for remove buttons
const removeButtons = document.querySelectorAll('.remove-remote-btn');
removeButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
const remoteName = e.target.dataset.remoteName;
handleRemoveRemoteSource(remoteName);
});
});
}
async function handleAddRemoteSource() {
const name = document.getElementById('remote-name')?.value;
const url = document.getElementById('remote-url')?.value;
if (!name || !url) {
domManager.showModal('Error', 'Please enter both name and URL for the remote source.');
return;
}
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('add-remote-source', { name: name.trim(), url: url.trim() });
domManager.hideSpinner();
if (result.success) {
domManager.showModal('Success', `Remote source '${result.name}' added successfully.`);
// Clear form
document.getElementById('remote-name').value = '';
document.getElementById('remote-url').value = '';
// Refresh remotes list
await loadRemoteSources();
} else {
domManager.showModal('Error', `Failed to add remote source: ${result.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to add remote source: ${error.message}`);
}
}
async function handleRemoveRemoteSource(remoteName) {
const confirmed = await uiComponents.showConfirmDialog(
'Remove Remote Source',
`Are you sure you want to remove the remote source "${remoteName}"?\n\nThis will also remove all keys synced from this source.`
);
if (!confirmed) return;
try {
domManager.showSpinner();
const result = await ipcRenderer.invoke('remove-remote-source', remoteName);
domManager.hideSpinner();
if (result.success) {
domManager.showModal('Success', `Remote source '${result.name}' removed successfully.`);
// Refresh remotes list and keys list
await loadRemoteSources();
await loadKeys();
} else {
domManager.showModal('Error', `Failed to remove remote source: ${result.error}`);
}
} catch (error) {
domManager.hideSpinner();
domManager.showModal('Error', `Failed to remove remote source: ${error.message}`);
}
}
// Export functions
module.exports = {
initializeDependencies,
loadKeys,
loadTrustedKeys: loadKeys, // Alias for compatibility
handleGenerateKey,
handleAddKey,
handleRemoveKey,
handleSyncRemotes,
loadRemoteSources,
handleAddRemoteSource,
handleRemoveRemoteSource
};

View File

@@ -11,6 +11,10 @@ let pendingOperation = null; // Store the operation to execute after warning con
// Current OPAT file state
let currentOPATFile = null;
// Current key management state
let currentKeys = null;
let keyOperationInProgress = false;
// Bundle state management
const getBundleState = () => ({
currentBundle,
@@ -67,6 +71,27 @@ const clearOPATFile = () => {
currentOPATFile = null;
};
// Key management state functions
const setKeysState = (keysData) => {
currentKeys = keysData;
};
const getKeysState = () => {
return currentKeys;
};
const clearKeysState = () => {
currentKeys = null;
};
const setKeyOperationInProgress = (inProgress) => {
keyOperationInProgress = inProgress;
};
const getKeyOperationInProgress = () => {
return keyOperationInProgress;
};
// Export state management functions
module.exports = {
// Bundle state
@@ -86,6 +111,13 @@ module.exports = {
getOPATFile,
clearOPATFile,
// Key management state
setKeysState,
getKeysState,
clearKeysState,
setKeyOperationInProgress,
getKeyOperationInProgress,
// Direct state access (for compatibility)
getCurrentBundle: () => currentBundle,
getCurrentBundlePath: () => currentBundlePath,

View File

@@ -163,6 +163,14 @@ function hideSaveOptionsModal() {
elements.saveOptionsModal.classList.add('hidden');
}
// Show confirmation dialog
function showConfirmDialog(title, message) {
return new Promise((resolve) => {
const confirmed = confirm(`${title}\n\n${message}`);
resolve(confirmed);
});
}
// Initialize dependencies (called when module is loaded)
function initializeDependencies(deps) {
stateManager = deps.stateManager;
@@ -179,5 +187,6 @@ module.exports = {
checkForChanges,
updateSaveButtonVisibility,
showSaveOptionsModal,
hideSaveOptionsModal
hideSaveOptionsModal,
showConfirmDialog
};

View File

@@ -903,6 +903,711 @@ body.dark-mode .opat-table-container ::-webkit-scrollbar-thumb:hover {
background: #475569;
}
/* ===== KEY MANAGEMENT STYLING ===== */
/* Key Management Views Container */
#keys-view {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: var(--background-color);
}
/* Individual Key Management Views */
.key-management-view {
flex: 1;
overflow-y: auto;
padding: 2rem;
background: var(--background-color);
}
/* Key Management Headers */
.keys-header,
.generate-key-header,
.add-key-header,
.remotes-header {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: flex-end;
flex-wrap: wrap;
gap: 1rem;
}
.keys-header .keys-actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
.keys-header h3,
.generate-key-header h3,
.add-key-header h3,
.remotes-header h3 {
color: var(--text-color);
font-size: 1.8rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
}
.keys-header p,
.generate-key-header p,
.add-key-header p,
.remotes-header p {
color: var(--text-light);
font-size: 1rem;
margin: 0;
line-height: 1.5;
}
/* Form Styling */
.generate-key-form,
.add-key-form {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
background: white;
color: var(--text-color);
transition: all 0.2s ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-group small {
display: block;
color: var(--text-light);
font-size: 0.85rem;
margin-top: 0.25rem;
}
.form-actions {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.form-row {
display: grid;
grid-template-columns: 1fr 2fr auto;
gap: 1rem;
align-items: end;
margin-bottom: 1.5rem;
}
/* Action Buttons */
.action-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.action-button.primary {
background: var(--primary-color);
color: white;
}
.action-button.primary:hover {
background: var(--secondary-color);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
.action-button.secondary {
background: white;
color: var(--primary-color);
border: 1px solid var(--primary-color);
}
.action-button.secondary:hover {
background: var(--primary-color);
color: white;
}
.action-button.danger {
background: #ef4444;
color: white;
}
.action-button.danger:hover {
background: #dc2626;
}
/* Notice Banners */
.security-notice,
.info-notice {
margin-top: 2rem;
}
.warning-banner,
.info-banner {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem 1.25rem;
border-radius: 8px;
border-left: 4px solid;
}
.warning-banner {
background: #fef3c7;
border-left-color: #f59e0b;
}
.info-banner {
background: #dbeafe;
border-left-color: var(--primary-color);
}
.warning-icon,
.info-icon {
font-size: 1.2rem;
flex-shrink: 0;
margin-top: 0.1rem;
}
.warning-text,
.info-text {
flex: 1;
}
.warning-text strong,
.info-text strong {
color: #92400e;
font-weight: 600;
}
.info-text strong {
color: #1e40af;
}
/* Keys List Container */
#keys-list-container {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
}
/* Key Source Sections */
.key-source-section {
border-bottom: 1px solid var(--border-color);
}
.key-source-section:last-child {
border-bottom: none;
}
.source-header {
background: #f8fafc;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.source-header h4 {
margin: 0;
color: var(--text-color);
font-size: 1.1rem;
font-weight: 600;
}
.key-count {
background: var(--primary-color);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.keys-table-wrapper {
overflow-x: auto;
}
.keys-table {
width: 100%;
}
.keys-table table {
width: 100%;
border-collapse: collapse;
}
.keys-table th,
.keys-table td {
padding: 1rem 1.5rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.keys-table th {
background: #f8fafc;
font-weight: 600;
color: var(--text-color);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.keys-table td {
color: var(--text-color);
font-size: 0.95rem;
}
.keys-table tr:last-child td {
border-bottom: none;
}
.keys-table tr:hover {
background: #f8fafc;
}
.key-fingerprint {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.85rem;
color: var(--text-light);
background: #f1f5f9;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.key-actions {
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-small {
padding: 0.375rem 0.75rem;
font-size: 0.8rem;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
transform: translateY(-1px);
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--secondary-color);
transform: translateY(-1px);
}
/* Remote Sources Styling */
.add-remote-form {
background: white;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.add-remote-form h4 {
color: var(--text-color);
font-size: 1.2rem;
font-weight: 600;
margin: 0 0 1.5rem 0;
}
#remotes-list-container {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color);
overflow: hidden;
}
.remotes-table {
width: 100%;
border-collapse: collapse;
}
.remotes-table th,
.remotes-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.remotes-table th {
background: #f8fafc;
font-weight: 600;
color: var(--text-color);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.remotes-table tr:hover {
background: #f8fafc;
}
/* Empty States */
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: var(--text-light);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
color: var(--text-color);
font-size: 1.3rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
}
.empty-state p {
color: var(--text-light);
font-size: 1rem;
margin: 0;
line-height: 1.5;
}
/* Dark Mode Support */
body.dark-mode .key-management-view {
background: var(--bg-color);
}
body.dark-mode .generate-key-form,
body.dark-mode .add-key-form,
body.dark-mode .add-remote-form,
body.dark-mode #keys-list-container,
body.dark-mode #remotes-list-container {
background: #1e293b;
border-color: #334155;
}
body.dark-mode .source-header {
background: #0f172a;
}
body.dark-mode .keys-table th,
body.dark-mode .remotes-table th {
background: #0f172a;
color: #f1f5f9;
}
body.dark-mode .keys-table tr:hover,
body.dark-mode .remotes-table tr:hover {
background: #0f172a;
}
body.dark-mode .form-group input,
body.dark-mode .form-group select {
background: #0f172a;
border-color: #334155;
color: #f1f5f9;
}
body.dark-mode .form-group input:focus,
body.dark-mode .form-group select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
body.dark-mode .key-fingerprint {
background: #0f172a;
color: #94a3b8;
}
body.dark-mode .warning-banner {
background: rgba(245, 158, 11, 0.1);
border-left-color: #f59e0b;
}
body.dark-mode .info-banner {
background: rgba(59, 130, 246, 0.1);
border-left-color: var(--primary-color);
}
body.dark-mode .warning-text strong {
color: #fbbf24;
}
body.dark-mode .info-text strong {
color: #60a5fa;
}
/* Modern Key Add Success Modal */
.key-add-success-modern {
max-width: 500px;
margin: 0 auto;
text-align: center;
}
.success-header {
margin-bottom: 2rem;
}
.success-icon {
margin: 0 auto 1rem auto;
width: 48px;
height: 48px;
}
.success-header h3 {
color: var(--text-color);
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 0.5rem 0;
}
.success-subtitle {
color: var(--text-light);
font-size: 1rem;
margin: 0;
line-height: 1.5;
}
.key-details-card {
background: #f8fafc;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
text-align: left;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.detail-row:last-child {
margin-bottom: 0;
}
.detail-label {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-light);
font-size: 0.9rem;
font-weight: 500;
min-width: 100px;
flex-shrink: 0;
}
.detail-label svg {
color: var(--text-light);
}
.detail-value {
flex: 1;
color: var(--text-color);
font-size: 0.95rem;
word-break: break-all;
}
.fingerprint-value {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
.fingerprint-value code {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.8rem;
background: #e2e8f0;
color: #475569;
padding: 0.25rem 0.5rem;
border-radius: 4px;
flex: 1;
min-width: 0;
}
.copy-btn {
background: var(--primary-color);
color: white;
border: none;
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
flex-shrink: 0;
}
.copy-btn:hover {
background: var(--secondary-color);
transform: translateY(-1px);
}
.success-footer {
margin-top: 1.5rem;
}
.info-banner {
display: flex;
align-items: center;
gap: 0.75rem;
background: #dbeafe;
border: 1px solid #93c5fd;
border-radius: 8px;
padding: 0.75rem 1rem;
color: #1e40af;
font-size: 0.9rem;
text-align: left;
}
.info-banner svg {
color: #3b82f6;
flex-shrink: 0;
}
/* Dark Mode Support for Modern Modal */
body.dark-mode .key-details-card {
background: #1e293b;
border-color: #334155;
}
body.dark-mode .fingerprint-value code {
background: #0f172a;
color: #94a3b8;
}
body.dark-mode .info-banner {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.3);
color: #60a5fa;
}
body.dark-mode .info-banner svg {
color: #60a5fa;
}
/* Responsive Design */
@media (max-width: 1024px) {
.key-management-view {
padding: 1.5rem;
}
.form-row {
grid-template-columns: 1fr;
gap: 1rem;
}
}
@media (max-width: 768px) {
.key-management-view {
padding: 1rem;
}
.generate-key-form,
.add-key-form,
.add-remote-form {
padding: 1.5rem;
}
.keys-table,
.remotes-table {
font-size: 0.9rem;
}
.keys-table th,
.keys-table td,
.remotes-table th,
.remotes-table td {
padding: 0.75rem;
}
.key-actions {
flex-direction: column;
gap: 0.25rem;
}
.key-add-success-modern {
max-width: 100%;
}
.fingerprint-value {
flex-direction: column;
align-items: stretch;
}
.detail-row {
flex-direction: column;
gap: 0.5rem;
}
.detail-label {
min-width: auto;
}
}
#welcome-screen h1 {
font-size: 2rem;
margin-bottom: 0.5rem;

View File

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEINS0UV/JuxLOW7PyegagGJfCP13oVrBXXBhMBRV0YK5+
-----END PRIVATE KEY-----

View File

@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILXntMn/iLZI5hHaU+b9WYzE8E4D04gseoRbexZeIbrU

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAtee0yf+ItkjmEdpT5v1ZjMTwTgPTiCx6hFt7Fl4hutQ=
-----END PUBLIC KEY-----