feat(ui): Phase 8 UI/UX optimization and system documentation update

## Sidebar Enhancement
- Change tabs to icon + small label layout for better space utilization
- Add Teams tab with team collaboration entry point

## Settings Page Improvements
- Connect theme toggle to gatewayStore.saveQuickConfig for persistence
- Remove OpenFang backend download section, simplify UI
- Add time range filter to UsageStats (7d/30d/all)
- Add stat cards with icons (sessions, messages, input/output tokens)
- Add token usage overview bar chart
- Add 8 ZCLAW system skill definitions with categories

## Bug Fixes
- Fix ChannelList duplicate content with deduplication logic
- Integrate CreateTriggerModal in TriggersPanel
- Add independent SecurityStatusPanel with 12 default enabled layers
- Change workflow view to use SchedulerPanel as unified entry

## New Components
- CreateTriggerModal: Event trigger creation modal
- HandApprovalModal: Hand approval workflow dialog
- HandParamsForm: Enhanced Hand parameter form
- SecurityLayersPanel: 16-layer security status display

## Architecture
- Add TOML config parsing support (toml-utils.ts, config-parser.ts)
- Add request timeout and retry mechanism (request-helper.ts)
- Add secure token storage (secure-storage.ts, secure_storage.rs)

## Tests
- Add unit tests for config-parser, toml-utils, request-helper
- Add team-client and teamStore tests

## Documentation
- Update SYSTEM_ANALYSIS.md with Phase 8 completion
- UI completion: 100% (30/30 components)
- API coverage: 93% (63/68 endpoints)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-15 14:12:11 +08:00
parent bf79c06d4a
commit 3e81bd3e50
30 changed files with 8875 additions and 284 deletions

View File

@@ -0,0 +1,181 @@
/**
* ZCLAW Secure Storage
*
* Provides secure credential storage using the OS keyring/keychain.
* Falls back to localStorage when not running in Tauri or if keyring is unavailable.
*
* Platform support:
* - Windows: DPAPI
* - macOS: Keychain
* - Linux: Secret Service API (gnome-keyring, kwallet, etc.)
*/
import { invoke } from '@tauri-apps/api/core';
import { isTauriRuntime } from './tauri-gateway';
// Cache for keyring availability check
let keyringAvailable: boolean | null = null;
/**
* Check if secure storage (keyring) is available
*/
export async function isSecureStorageAvailable(): Promise<boolean> {
if (!isTauriRuntime()) {
return false;
}
// Use cached result if available
if (keyringAvailable !== null) {
return keyringAvailable;
}
try {
keyringAvailable = await invoke<boolean>('secure_store_is_available');
return keyringAvailable;
} catch (error) {
console.warn('[SecureStorage] Keyring not available:', error);
keyringAvailable = false;
return false;
}
}
/**
* Secure storage interface
* Uses OS keyring when available, falls back to localStorage
*/
export const secureStorage = {
/**
* Store a value securely
* @param key - Storage key
* @param value - Value to store
*/
async set(key: string, value: string): Promise<void> {
const trimmedValue = value.trim();
if (await isSecureStorageAvailable()) {
try {
if (trimmedValue) {
await invoke('secure_store_set', { key, value: trimmedValue });
} else {
await invoke('secure_store_delete', { key });
}
// Also write to localStorage as backup/migration support
writeLocalStorageBackup(key, trimmedValue);
return;
} catch (error) {
console.warn('[SecureStorage] Failed to use keyring, falling back to localStorage:', error);
}
}
// Fallback to localStorage
writeLocalStorageBackup(key, trimmedValue);
},
/**
* Retrieve a value from secure storage
* @param key - Storage key
* @returns Stored value or null if not found
*/
async get(key: string): Promise<string | null> {
if (await isSecureStorageAvailable()) {
try {
const value = await invoke<string>('secure_store_get', { key });
if (value !== null && value !== undefined && value !== '') {
return value;
}
// If keyring returned empty, try localStorage fallback for migration
return readLocalStorageBackup(key);
} catch (error) {
console.warn('[SecureStorage] Failed to read from keyring, trying localStorage:', error);
}
}
// Fallback to localStorage
return readLocalStorageBackup(key);
},
/**
* Delete a value from secure storage
* @param key - Storage key
*/
async delete(key: string): Promise<void> {
if (await isSecureStorageAvailable()) {
try {
await invoke('secure_store_delete', { key });
} catch (error) {
console.warn('[SecureStorage] Failed to delete from keyring:', error);
}
}
// Always clear localStorage backup
clearLocalStorageBackup(key);
},
/**
* Check if secure storage is being used (vs localStorage fallback)
*/
async isUsingKeyring(): Promise<boolean> {
return isSecureStorageAvailable();
},
};
/**
* localStorage backup functions for migration and fallback
*/
function writeLocalStorageBackup(key: string, value: string): void {
try {
if (value) {
localStorage.setItem(key, value);
} else {
localStorage.removeItem(key);
}
} catch {
// Ignore localStorage failures
}
}
function readLocalStorageBackup(key: string): string | null {
try {
return localStorage.getItem(key);
} catch {
return null;
}
}
function clearLocalStorageBackup(key: string): void {
try {
localStorage.removeItem(key);
} catch {
// Ignore localStorage failures
}
}
/**
* Synchronous versions for compatibility with existing code
* These use localStorage only and are provided for gradual migration
*/
export const secureStorageSync = {
/**
* Synchronously get a value from localStorage (for migration only)
* @deprecated Use async secureStorage.get() instead
*/
get(key: string): string | null {
return readLocalStorageBackup(key);
},
/**
* Synchronously set a value in localStorage (for migration only)
* @deprecated Use async secureStorage.set() instead
*/
set(key: string, value: string): void {
writeLocalStorageBackup(key, value);
},
/**
* Synchronously delete a value from localStorage (for migration only)
* @deprecated Use async secureStorage.delete() instead
*/
delete(key: string): void {
clearLocalStorageBackup(key);
},
};