chore: V2 planning docs + template discovery

This commit is contained in:
swanadiva
2026-07-07 11:53:16 +07:00
parent 4a8b4a2bc4
commit 8ebdebedc8
31 changed files with 9304 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"
+8
View File
@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example
+75
View File
@@ -0,0 +1,75 @@
# HostKeeper | Developer & AI System Brief
> **MANDATORY INSTRUCTION FOR DOWNSTREAM AI AGENTS:**
> Before proposing or implementing any changes, code edits, or new features in this workspace, you **MUST** read this instruction set fully. Every visual and architectural decision must align strictly with the rules defined below.
---
## 1. Visual Identity & Design System (Lumina System)
Unlike standard, heavy, dark-themed hacker dashboards, HostKeeper uses the **Lumina System**—a premium, "bright-tech" glassmorphic developer workspace featuring clean off-whites, slate cards, vibrant accent highlights, and a high-clarity layout.
### A. Color Palette
Maintain the exact CSS variables and color definitions declared in `/src/index.css`:
* **Primary (Lumina Blue):** `#0050cb` (Focus boundaries, active selections, primary indicators)
* **Secondary (Neon Green):** `#006e2f` (Active/Online indicators, progress bars, successful transactions)
* **Tertiary (Tech Purple):** `#7e23cc` (File directories, custom command responses, highlight badges)
* **Surface Base Background:** `#f7f9fb`
* **Containers & Cards Lowest:** `#ffffff` (High-contrast pure white cards for depth separation)
* **Text/On-Surface Dark Slate:** `#191c1e` (Main UI text)
* **Subtle Labels/On-Surface Variant:** `#424656`
### B. Grid, Depth & Glassmorphic Accents
* **Dot Grid Texture:** The background uses a responsive 16px repeating radial dot pattern:
`radial-gradient(#e2e8f0 1.5px, transparent 1.5px)` with a size of `16px 16px`.
* **Frosted Glass Sidebars & Overlays:** Use `backdrop-blur-md` and semi-transparent white backgrounds (`rgba(255, 255, 255, 0.7)`) to show subtle grid depth underneath overlays.
* **Elevation & Shadows:** Focus containers should never have heavy dark borders. Instead, apply thin borders (`border-outline-variant/30`) paired with subtle, color-matched glow shadows (`shadow-primary/10`).
### C. Typography Pairing
* **Interface UI Font:** **Inter** (weights `300` to `900`). Used for titles, descriptive labels, tables, navigation buttons, and settings forms.
* **Technical / Data Font:** **JetBrains Mono** (weights `300` to `700`). Used exclusively for file paths, IP addresses, terminal prompt commands, and cataloged snippet blocks.
---
## 2. File & Component Architecture
Keep the project highly modular. Do not merge separate view components into a single file to avoid hitting token limit cutoffs.
* `/src/types.ts`: Holds shared global interfaces (e.g., `Host`, `FileItem`, `Snippet`, `KeychainItem`, `Device`, `BackgroundTransfer`).
* `/src/App.tsx`: The primary state router. Houses active navigation, sidebar frames, quick-action overlays, and global mock state stores.
* `/src/components/DashboardView.tsx`: Hosts list, connection triggers, and background file-transfer progress bars.
* `/src/components/SftpView.tsx`: Dual-pane local-to-remote secure file transmission and folder generation lists.
* `/src/components/TerminalView.tsx`: Interactive multi-tab mock SSH shell supporting customized terminal parser actions.
* `/src/components/SnippetsView.tsx`: DevOps script store with full search index, copy utility, and custom snippet registration.
* `/src/components/KeychainView.tsx`: Secure local keystore sandbox with passphrase disclosure controls, strength validators, and SHA256 fingerprints.
* `/src/components/SettingsView.tsx`: Profile configuration forms, feature toggle switches, and local storage state resets.
* `/src/components/BriefOverlay.tsx`: In-app system documentation and layout specifications viewer.
---
## 3. Core Functional & Interaction Guidelines
When expanding HostKeeper features, future agents must adhere to the following implementation behaviors:
1. **Icon Guidelines:**
* All icons **MUST** be imported strictly from `lucide-react`.
* Do **NOT** write custom inline SVGs.
2. **Terminal Simulation Parser:**
* Inside `TerminalView.tsx`, the interactive shell handles input commands.
* Always extend the mock commands list (`help`, `ls`, `docker ps`, `uname -a`, `ping 8.8.8.8`, `cat server.js`, `keychain`, `clear`) rather than falling back to blank prompts.
3. **Real-time SFTP Actions:**
* SFTP View simulates file transmission. Clicking a file or the upload/download button triggers path transfers with real-time UI feedback. Keep the interactive toast banners operational.
4. **No Unsolicited SDKs or Servers:**
* Keep the app fully client-side and snappy unless requested otherwise.
* Avoid creating unnecessary external backends or adding database service modules unless explicitly specified.
---
## 4. Development Verification Steps
Before finishing any task, run these commands sequentially to guarantee app integrity:
1. **Check Linter Rules:**
`npm run lint` or call the `lint_applet` tool to identify imports or compilation issues early.
2. **Verify Production Build Compilation:**
`npm run build` or call the `compile_applet` tool to ensure TypeScript checks and asset optimization pass without errors.
+20
View File
@@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/fdc890ff-7c2d-48ba-a909-0c5c4e05cd73
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en" class="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HostKeeper | Bright-Tech SSH, SFTP, and Keychain Manager</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
<!-- Google Material Symbols Outlined -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1@100..700,0..1&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
{
"name": "HostKeeper",
"description": "A beautiful, premium, bright-tech terminal, SFTP, and Keychain manager with glassmorphic visuals and comprehensive developer workflows.",
"requestFramePermissions": [],
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
}
+35
View File
@@ -0,0 +1,35 @@
{
"name": "react-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"clean": "rm -rf dist server.js",
"lint": "tsc --noEmit"
},
"dependencies": {
"@google/genai": "^2.4.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"lucide-react": "^0.546.0",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"vite": "^6.2.3",
"express": "^4.21.2",
"dotenv": "^17.2.3",
"motion": "^12.23.24"
},
"devDependencies": {
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
"tsx": "^4.21.0",
"typescript": "~5.8.2",
"vite": "^6.2.3",
"@types/express": "^4.17.21"
}
}
+261
View File
@@ -0,0 +1,261 @@
import React, { useState } from 'react';
import { ViewType, Host } from './types';
import DashboardView from './components/DashboardView';
import SftpView from './components/SftpView';
import TerminalView from './components/TerminalView';
import SnippetsView from './components/SnippetsView';
import KeychainView from './components/KeychainView';
import SettingsView from './components/SettingsView';
import BriefOverlay from './components/BriefOverlay';
// Lucide icons
import {
Terminal,
FolderSync,
TerminalSquare,
Code2,
KeyRound,
Settings2,
FileQuestion,
Cpu,
ChevronRight,
Layers,
ArrowRightLeft,
Crown
} from 'lucide-react';
export default function App() {
const [currentView, setCurrentView] = useState<ViewType>('hosts');
const [isBriefOpen, setIsBriefOpen] = useState(false);
// Connection target host (instantiated via dashboard "Connect" clicks)
const [connectedHost, setConnectedHost] = useState<string | null>(null);
// Hosts collection
const [hosts, setHosts] = useState<Host[]>([
{
id: '1',
name: 'production-api-01',
ip: '10.0.1.201',
os: 'Ubuntu 22.04',
provider: 'AWS US-East',
status: 'active',
lastSeen: '2 hours ago',
type: 'api'
},
{
id: '2',
name: 'db-cluster-main',
ip: '10.0.2.14',
os: 'Debian 11',
provider: 'GCP Cloud',
status: 'active',
lastSeen: '5 hours ago',
type: 'db'
},
{
id: '3',
name: 'edge-node-berlin',
ip: '45.132.89.12',
os: 'Alpine Linux',
provider: 'Hetzner',
status: 'active',
lastSeen: '1 day ago',
type: 'edge'
},
{
id: '4',
name: 'web-frontend-02',
ip: '75.101.44.8',
os: 'Ubuntu 22.04',
provider: 'AWS US-East',
status: 'offline',
lastSeen: '3 days ago',
type: 'web'
}
]);
const handleConnectHost = (hostName: string) => {
setConnectedHost(hostName);
setCurrentView('terminal');
};
const handleAddHost = (newHost: Host) => {
setHosts(prev => [newHost, ...prev]);
};
// Nav items helper
const navItems = [
{ id: 'hosts', label: 'Hosts Manager', icon: Cpu, desc: 'Active clusters & networks' },
{ id: 'sftp', label: 'SFTP File Sync', icon: FolderSync, desc: 'Secure asset pipeline' },
{ id: 'terminal', label: 'SSH Shell client', icon: TerminalSquare, desc: 'Interactive remote command shell' },
{ id: 'snippets', label: 'Snippets Library', icon: Code2, desc: 'Reusable DevOps commands' },
{ id: 'keychain', label: 'Keys & Keychain', icon: KeyRound, desc: 'SSH identity credentials' },
{ id: 'settings', label: 'Preferences', icon: Settings2, desc: 'Workspace settings & account' }
];
// Render view router helper
const renderView = () => {
switch (currentView) {
case 'hosts':
return (
<DashboardView
onConnectHost={handleConnectHost}
hosts={hosts}
onAddHost={handleAddHost}
/>
);
case 'sftp':
return <SftpView />;
case 'terminal':
return <TerminalView />;
case 'snippets':
return <SnippetsView />;
case 'keychain':
return <KeychainView />;
case 'settings':
return <SettingsView />;
default:
return (
<DashboardView
onConnectHost={handleConnectHost}
hosts={hosts}
onAddHost={handleAddHost}
/>
);
}
};
return (
<div className="min-h-screen bg-surface dot-grid flex overflow-hidden">
{/* Sidebar Frame */}
<aside className="w-[260px] border-r border-outline-variant bg-white/75 backdrop-blur-md hidden md:flex flex-col justify-between shrink-0 z-10">
{/* Top Header Logo */}
<div className="p-6 border-b border-outline-variant/30 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-primary flex items-center justify-center text-white shadow-lg shadow-primary/20">
<Terminal className="w-5 h-5 animate-pulse" />
</div>
<div>
<h1 className="font-extrabold text-on-surface text-sm tracking-tight leading-none uppercase">HostKeeper</h1>
<span className="text-[10px] text-outline font-bold tracking-widest mt-1 block leading-none">LUMINA TECH</span>
</div>
</div>
<button
onClick={() => setIsBriefOpen(true)}
className="p-1 hover:bg-primary-container/15 hover:text-primary rounded text-outline transition-colors cursor-pointer"
title="Review System Brief"
>
<FileQuestion className="w-4 h-4" />
</button>
</div>
{/* Navigation list */}
<nav className="flex-1 px-4 py-6 space-y-1 overflow-y-auto no-scrollbar">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = currentView === item.id;
return (
<button
key={item.id}
onClick={() => setCurrentView(item.id as ViewType)}
className={`w-full flex items-center gap-3.5 px-4 py-3 rounded-xl transition-all text-left border cursor-pointer ${isActive ? 'bg-primary text-white border-primary shadow-lg shadow-primary/10' : 'bg-transparent text-on-surface-variant hover:bg-surface-container-low border-transparent'}`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-white' : 'text-outline-variant group-hover:text-primary'}`} />
<div className="min-w-0 flex-1">
<p className="text-xs font-bold leading-tight">{item.label}</p>
<p className={`text-[9px] truncate mt-0.5 leading-none ${isActive ? 'text-primary-fixed' : 'text-outline'}`}>
{item.desc}
</p>
</div>
<ChevronRight className={`w-3.5 h-3.5 ${isActive ? 'opacity-100 text-white' : 'opacity-0'}`} />
</button>
);
})}
</nav>
{/* User profile footer drawer badge */}
<div className="p-4 border-t border-outline-variant/40 bg-surface-container-low/30">
<div className="flex items-center justify-between p-2 rounded-xl bg-white border border-outline-variant/50">
<div className="flex items-center gap-2.5 min-w-0">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center font-black text-primary text-xs relative shrink-0">
AR
<span className="absolute right-0 bottom-0 w-2 h-2 rounded-full bg-secondary border border-white animate-pulse"></span>
</div>
<div className="min-w-0">
<p className="text-xs font-bold text-on-surface truncate">Alex Rivera</p>
<p className="text-[9px] text-outline font-semibold uppercase tracking-wider">DevOps Lead</p>
</div>
</div>
<span className="p-1 text-primary cursor-help" title="Pro Account Status">
<Crown className="w-4 h-4 text-amber-500 fill-amber-500" />
</span>
</div>
</div>
</aside>
{/* Main workspace arena content panel */}
<main className="flex-1 flex flex-col min-w-0 relative overflow-y-auto h-screen no-scrollbar">
{/* Top workspace action app bar */}
<header className="px-4 sm:px-6 py-4 bg-white/60 backdrop-blur-sm border-b border-outline-variant flex items-center justify-between z-10 gap-3">
<div className="flex items-center gap-2 sm:gap-4 min-w-0">
<h2 className="text-xs sm:text-sm font-bold text-on-surface tracking-tight uppercase truncate">
{navItems.find(i => i.id === currentView)?.label}
</h2>
{connectedHost && currentView === 'terminal' && (
<span className="px-2 py-0.5 bg-primary-container/20 border border-primary/20 text-primary rounded-lg text-[9px] sm:text-[10px] font-mono font-bold animate-pulse truncate max-w-[120px] sm:max-w-none" title={`CONNECTED TO: ${connectedHost}`}>
<span className="hidden sm:inline">CONNECTED TO: </span>{connectedHost}
</span>
)}
</div>
<div className="flex items-center gap-4 shrink-0">
{/* Quick Brief activator */}
<button
onClick={() => setIsBriefOpen(true)}
className="flex items-center gap-1.5 px-2.5 sm:px-3 py-1.5 bg-primary text-white hover:bg-primary-container hover:text-on-primary-container text-xs font-bold rounded-xl shadow-md transition-all cursor-pointer"
>
<FileQuestion className="w-4 h-4 animate-bounce" />
<span className="hidden sm:inline">AI System Brief</span>
<span className="sm:hidden">Brief</span>
</button>
</div>
</header>
{/* View render hub */}
<div className="flex-1 bg-surface-container-low/10 overflow-y-auto pb-24 md:pb-12">
{renderView()}
</div>
</main>
{/* Mobile Responsive Navigation Footer Bar */}
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white/95 backdrop-blur-md border-t border-outline-variant/60 flex justify-around py-2 px-1 z-[100] shadow-lg">
{navItems.map((item) => {
const Icon = item.icon;
const isActive = currentView === item.id;
return (
<button
key={item.id}
onClick={() => setCurrentView(item.id as ViewType)}
className={`flex flex-col items-center gap-1 px-2.5 py-1 rounded-xl transition-all cursor-pointer ${isActive ? 'text-primary' : 'text-outline hover:text-on-surface-variant'}`}
>
<Icon className="w-5 h-5" />
<span className="text-[9px] font-bold leading-none">{item.label.split(' ')[0]}</span>
</button>
);
})}
</nav>
{/* Floating System Brief Drawer Overlay */}
<BriefOverlay isOpen={isBriefOpen} onClose={() => setIsBriefOpen(false)} />
</div>
);
}
+165
View File
@@ -0,0 +1,165 @@
import React from 'react';
import { X, Terminal, Cpu, Palette, Layout, Layers, Box, Code } from 'lucide-react';
interface BriefOverlayProps {
isOpen: boolean;
onClose: () => void;
}
export default function BriefOverlay({ isOpen, onClose }: BriefOverlayProps) {
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[200] flex justify-end transition-opacity duration-300">
<div className="w-full max-w-2xl bg-white h-screen shadow-2xl flex flex-col overflow-hidden animate-slide-in">
{/* Header */}
<div className="px-6 py-5 bg-primary text-white flex items-center justify-between">
<div className="flex items-center gap-3">
<Terminal className="w-6 h-6 animate-pulse" />
<div>
<h2 className="text-xl font-bold tracking-tight">AI & Developer Brief</h2>
<p className="text-xs text-primary-fixed opacity-90">Design System & Blueprint for HostKeeper</p>
</div>
</div>
<button
onClick={onClose}
className="p-1.5 hover:bg-white/10 rounded-lg transition-colors cursor-pointer text-white"
title="Close brief"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
{/* Intro Guidance */}
<div className="bg-primary-container/10 border border-primary/20 rounded-xl p-4 text-sm text-on-surface">
<span className="font-bold text-primary block mb-1">🤖 AI-to-AI / Developer System Instructions</span>
This documentation is built directly into the codebase to guide downstream development agents.
Maintain the <strong className="text-primary">Bright-Tech / Lumina Terminal</strong> aesthetic. Avoid dark-mode-by-default stereotypes unless requested; focus on high-clarity slate cards, interactive neon accents, and crisp typography.
</div>
{/* Section: Palette */}
<section className="space-y-3">
<h3 className="font-bold text-on-background flex items-center gap-2 text-base">
<Palette className="w-5 h-5 text-primary" />
Colors (Lumina System)
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-[#0050cb] mb-1.5"></div>
<p className="font-bold">Primary (Blue)</p>
<code className="text-[10px] text-outline">#0050cb</code>
</div>
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-[#006e2f] mb-1.5"></div>
<p className="font-bold">Secondary (Green)</p>
<code className="text-[10px] text-outline">#006e2f</code>
</div>
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-[#7e23cc] mb-1.5"></div>
<p className="font-bold">Tertiary (Purple)</p>
<code className="text-[10px] text-outline">#7e23cc</code>
</div>
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-[#f7f9fb] mb-1.5 border border-outline-variant/50"></div>
<p className="font-bold">Surface Base</p>
<code className="text-[10px] text-outline">#f7f9fb</code>
</div>
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-white mb-1.5 border border-outline-variant/50"></div>
<p className="font-bold">Container Lowest</p>
<code className="text-[10px] text-outline">#ffffff</code>
</div>
<div className="p-3 border border-outline-variant rounded-lg bg-surface-container-low text-xs">
<div className="w-full h-8 rounded bg-[#191c1e] mb-1.5"></div>
<p className="font-bold">On Surface</p>
<code className="text-[10px] text-outline">#191c1e</code>
</div>
</div>
</section>
{/* Section: Typography */}
<section className="space-y-3">
<h3 className="font-bold text-on-background flex items-center gap-2 text-base">
<Code className="w-5 h-5 text-primary" />
Typography Specifications
</h3>
<div className="border border-outline-variant rounded-xl divide-y divide-outline-variant/50 overflow-hidden text-sm">
<div className="p-4 bg-surface-container-lowest">
<p className="font-bold text-xs text-outline mb-1 uppercase tracking-wider">UI Font Pairing (Inter)</p>
<p className="font-sans text-lg font-bold">Inter Sans-Serif</p>
<p className="text-xs text-on-surface-variant">Used for all lists, forms, dashboard metrics, setting menus, and high-contrast labels.</p>
</div>
<div className="p-4 bg-surface-container-lowest font-mono">
<p className="font-bold text-xs text-outline mb-1 uppercase tracking-wider font-sans">Technical Font (JetBrains Mono)</p>
<p className="text-base font-medium">JetBrains Mono</p>
<p className="text-xs text-on-surface-variant font-sans mt-1">Used for terminal outputs, commands, snippets, directory paths, and code metadata.</p>
</div>
</div>
</section>
{/* Section: Layout & Spacing */}
<section className="space-y-3">
<h3 className="font-bold text-on-background flex items-center gap-2 text-base">
<Layout className="w-5 h-5 text-primary" />
Layout & Spacing Architecture
</h3>
<ul className="list-disc pl-5 space-y-2 text-sm text-on-surface-variant">
<li>
<strong className="text-on-background">The Dot Grid Texture:</strong> A 16px background repeating dot grid system (<code className="text-xs bg-surface-container px-1 rounded">radial-gradient(#E2E8F0 1.5px, transparent 1.5px)</code>) anchors the content, providing depth and mimicking physical engineering logs.
</li>
<li>
<strong className="text-on-background">Asymmetric Bento Sizing:</strong> Balance large focus modules (such as the active Terminal Session or SFTP local-remote streams) alongside secondary narrow utility sheets.
</li>
<li>
<strong className="text-on-background">Standardized Metrics:</strong> Sidebar width is exactly <code className="text-xs bg-surface-container px-1 rounded">260px</code>. Terminal containers use <code className="text-xs bg-surface-container px-1 rounded">20px</code> internal padding.
</li>
</ul>
</section>
{/* Section: Elevation & Glassmorphism */}
<section className="space-y-3">
<h3 className="font-bold text-on-background flex items-center gap-2 text-base">
<Layers className="w-5 h-5 text-primary" />
Depth & Glassmorphism Rules
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
<div className="p-4 border border-outline-variant/60 rounded-xl space-y-1">
<span className="font-bold text-primary text-xs uppercase tracking-wider block">Level 1: Translucency</span>
<p className="text-on-surface-variant">Sidebars, Top App Bars, and overlay cards use <code className="text-[11px] bg-surface-container px-1 rounded">backdrop-blur-md</code> and semi-transparent backgrounds to overlay smoothly on background grid lines.</p>
</div>
<div className="p-4 border border-outline-variant/60 rounded-xl space-y-1">
<span className="font-bold text-primary text-xs uppercase tracking-wider block">Level 2: Glowing States</span>
<p className="text-on-surface-variant">Active focus objects should never look heavy. Apply accent borders (<code className="text-[11px] bg-surface-container px-1 rounded">border-primary</code>) and soft drop shadow glows of the accent color.</p>
</div>
</div>
</section>
{/* Section: Future Enhancements Blueprint */}
<section className="space-y-3">
<h3 className="font-bold text-on-background flex items-center gap-2 text-base">
<Box className="w-5 h-5 text-primary" />
AI Implementation Guidelines
</h3>
<div className="p-4 bg-surface-container-low rounded-xl text-xs space-y-2">
<p>When modifying this code or adding database handlers:</p>
<ol className="list-decimal pl-4 space-y-1.5 text-on-surface-variant">
<li>Always maintain class structures using state routers instead of traditional pages for fluid interactions.</li>
<li>Import standard icons only from <code className="text-xs font-mono text-primary font-bold">lucide-react</code>. Do not write custom inline SVG structures.</li>
<li>Ensure interactive switches use the custom <code className="text-xs font-mono">checked</code> properties with transition toggles.</li>
<li>For terminal commands, enhance the custom parser inside <code className="text-xs font-mono">TerminalView.tsx</code> to handle rich multi-line logs.</li>
</ol>
</div>
</section>
</div>
{/* Footer */}
<div className="p-4 border-t border-outline-variant bg-surface-container-low flex justify-between items-center text-xs text-outline font-medium">
<span>Lumina Technical Blueprint v1.0</span>
<span>Google AI Studio Build</span>
</div>
</div>
</div>
);
}
+467
View File
@@ -0,0 +1,467 @@
import React, { useState } from 'react';
import { Host, BackgroundTransfer } from '../types';
import {
Server,
Database,
Cpu,
ArrowRight,
Search,
Plus,
LayoutGrid,
List,
AlertTriangle,
TrendingUp,
CheckCircle,
FileCheck,
Zap,
Globe,
Monitor,
Laptop
} from 'lucide-react';
interface DashboardViewProps {
onConnectHost: (hostName: string) => void;
hosts: Host[];
onAddHost: (host: Host) => void;
}
export default function DashboardView({ onConnectHost, hosts, onAddHost }: DashboardViewProps) {
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'offline'>('all');
// Quick manual host addition modal/state
const [showAddModal, setShowAddModal] = useState(false);
const [newHostName, setNewHostName] = useState('');
const [newHostIp, setNewHostIp] = useState('');
const [newHostOs, setNewHostOs] = useState<'Ubuntu 22.04' | 'Debian 11' | 'Windows Server' | 'macOS Ventura' | 'CentOS 7' | 'Alpine Linux'>('Ubuntu 22.04');
const [newHostProvider, setNewHostProvider] = useState<'AWS US-East' | 'GCP Cloud' | 'Hetzner' | 'Vercel Proxy' | 'Local Docker' | 'Bare Metal'>('AWS US-East');
const [newHostType, setNewHostType] = useState<'api' | 'db' | 'edge' | 'web' | 'server'>('api');
// Background transfer list
const [transfers, setTransfers] = useState<BackgroundTransfer[]>([
{
id: '1',
fileName: 'backup_v4.tar.gz',
source: 'production-api-01',
destination: 'Local Machine',
progress: 82,
speed: '12.4 MB/s',
type: 'download'
},
{
id: '2',
fileName: 'assets_deploy.zip',
source: 'Local Storage',
destination: 'web-frontend-02',
progress: 35,
speed: '4.8 MB/s',
type: 'upload'
}
]);
const handleCreateHost = (e: React.FormEvent) => {
e.preventDefault();
if (!newHostName || !newHostIp) return;
const newHost: Host = {
id: Date.now().toString(),
name: newHostName.toLowerCase().replace(/\s+/g, '-'),
ip: newHostIp,
os: newHostOs,
provider: newHostProvider,
status: 'active',
lastSeen: 'Just now',
type: newHostType
};
onAddHost(newHost);
setShowAddModal(false);
setNewHostName('');
setNewHostIp('');
};
const filteredHosts = hosts.filter(host => {
const matchesSearch = host.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
host.ip.includes(searchQuery) ||
host.provider.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || host.status === statusFilter;
return matchesSearch && matchesStatus;
});
const getOsIcon = (os: string) => {
switch (os) {
case 'macOS Ventura': return <Laptop className="w-5 h-5" />;
case 'Windows Server': return <Monitor className="w-5 h-5" />;
default: return <Database className="w-5 h-5" />;
}
};
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-fade-in">
{/* Top Banner Stats */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
{/* Left Stats Block */}
<div className="md:col-span-8 p-6 rounded-2xl bg-white border border-outline-variant shadow-sm flex items-center justify-between overflow-hidden relative group">
<div className="relative z-10 space-y-2">
<h3 className="text-on-surface-variant font-bold text-xs uppercase tracking-wider">Active Connections</h3>
<div className="flex items-end gap-3">
<span className="text-4xl font-black text-primary">12</span>
<span className="text-secondary font-bold text-sm mb-1 flex items-center bg-secondary-container/20 px-2 py-0.5 rounded-full">
<TrendingUp className="w-4 h-4 mr-1" />
+2 today
</span>
</div>
<p className="text-xs text-on-surface-variant">Global performance metrics sync integrity active.</p>
</div>
{/* Accent Grid Glow lines */}
<div className="absolute right-0 top-0 w-1/3 h-full bg-gradient-to-l from-primary/5 to-transparent pointer-events-none"></div>
<div className="flex gap-2 items-end h-16 shrink-0">
<div className="w-2.5 h-10 bg-primary/20 rounded-full animate-pulse"></div>
<div className="w-2.5 h-14 bg-primary/40 rounded-full animate-pulse delay-75"></div>
<div className="w-2.5 h-8 bg-primary/10 rounded-full animate-pulse delay-150"></div>
<div className="w-2.5 h-16 bg-primary/60 rounded-full animate-pulse delay-200"></div>
</div>
</div>
{/* Right Stats Block - Promotion Card */}
<div className="md:col-span-4 p-6 rounded-2xl bg-primary text-on-primary shadow-xl shadow-primary/15 flex flex-col justify-between relative overflow-hidden">
<div className="relative z-10">
<Zap className="w-8 h-8 opacity-90 text-white mb-2" />
<p className="font-bold text-lg leading-tight">Fastest Access Enabled</p>
</div>
<p className="text-xs opacity-85 mt-2 relative z-10">
Smart routing engine successfully reduces terminal latency by <span className="font-bold">24ms</span> across European and Asian clusters.
</p>
<div className="absolute -right-6 -bottom-6 opacity-10">
<Zap className="w-32 h-32 rotate-12" />
</div>
</div>
</div>
{/* Grid Filter and Controls Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-4">
<div className="flex items-center gap-3">
<h3 className="text-lg font-bold text-on-surface">Recent Hosts</h3>
<div className="flex bg-surface-container rounded-lg p-0.5 text-xs font-semibold text-outline-variant border border-outline-variant/20">
<button
onClick={() => setStatusFilter('all')}
className={`px-2.5 py-1 rounded-md transition-all ${statusFilter === 'all' ? 'bg-white text-primary shadow-sm' : 'text-on-surface-variant hover:text-on-surface'}`}
>
All
</button>
<button
onClick={() => setStatusFilter('active')}
className={`px-2.5 py-1 rounded-md transition-all ${statusFilter === 'active' ? 'bg-white text-primary shadow-sm' : 'text-on-surface-variant hover:text-on-surface'}`}
>
Active
</button>
<button
onClick={() => setStatusFilter('offline')}
className={`px-2.5 py-1 rounded-md transition-all ${statusFilter === 'offline' ? 'bg-white text-primary shadow-sm' : 'text-on-surface-variant hover:text-on-surface'}`}
>
Offline
</button>
</div>
</div>
<div className="flex items-center gap-3">
{/* Inline Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-outline w-4 h-4" />
<input
type="text"
placeholder="Search hosts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-1.5 rounded-full bg-surface-container-low border border-outline-variant/30 text-sm focus:border-primary focus:bg-white focus:outline-none focus:ring-1 focus:ring-primary w-48 sm:w-60 transition-all"
/>
</div>
<div className="flex border border-outline-variant/50 rounded-lg p-0.5 bg-white">
<button
onClick={() => setViewMode('grid')}
className={`p-1.5 rounded ${viewMode === 'grid' ? 'bg-surface-container text-primary' : 'text-on-surface-variant'}`}
title="Grid View"
>
<LayoutGrid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-1.5 rounded ${viewMode === 'list' ? 'bg-surface-container text-primary' : 'text-on-surface-variant'}`}
title="List View"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
{/* Grid vs List View hosts container */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredHosts.map((host) => (
<div
key={host.id}
className="group bg-white p-5 rounded-2xl border border-outline-variant hover:border-primary hover:shadow-lg transition-all cursor-pointer relative overflow-hidden"
>
<div className="flex justify-between items-start mb-4">
<div className="w-11 h-11 rounded-xl bg-surface-container-low flex items-center justify-center text-primary border border-outline-variant/20 group-hover:bg-primary-container/10 group-hover:border-primary/20 transition-colors">
{getOsIcon(host.os)}
</div>
<div className="flex items-center gap-1.5">
<span className={`w-2 h-2 rounded-full ${host.status === 'active' ? 'bg-secondary animate-pulse' : 'bg-outline'}`}></span>
<span className={`text-[10px] font-bold uppercase tracking-wider ${host.status === 'active' ? 'text-secondary' : 'text-outline'}`}>
{host.status}
</span>
</div>
</div>
<h4 className="font-bold text-on-surface group-hover:text-primary transition-colors text-base truncate">{host.name}</h4>
<p className="font-mono text-xs text-outline mb-4">{host.ip}</p>
<div className="flex flex-wrap gap-1.5 mb-4">
<span className="px-2 py-0.5 bg-surface-container-low rounded text-[10px] font-medium text-on-surface-variant">
{host.os}
</span>
<span className="px-2 py-0.5 bg-surface-container-low rounded text-[10px] font-medium text-on-surface-variant">
{host.provider}
</span>
</div>
<div className="pt-3 border-t border-outline-variant/30 flex justify-between items-center text-xs">
<span className="text-outline italic">Last seen {host.lastSeen}</span>
<button
onClick={() => onConnectHost(host.name)}
className="bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-1.5 px-3 rounded-lg transition-all"
>
Connect
</button>
</div>
</div>
))}
{/* Add Host Button Placeholder */}
<div
onClick={() => setShowAddModal(true)}
className="group border-2 border-dashed border-outline-variant hover:border-primary hover:bg-primary/5 rounded-2xl flex flex-col items-center justify-center p-8 transition-all cursor-pointer text-center text-outline hover:text-primary"
>
<Plus className="w-8 h-8 mb-2 group-active:scale-90 transition-transform" />
<span className="font-bold text-sm text-on-surface-variant group-hover:text-primary">Add New Host</span>
<p className="text-[10px] uppercase tracking-widest mt-1 opacity-70">Manual Config or Discovery</p>
</div>
</div>
) : (
/* List View Mode */
<div className="bg-white border border-outline-variant rounded-2xl overflow-hidden shadow-sm divide-y divide-outline-variant/30">
{filteredHosts.map((host) => (
<div
key={host.id}
className="flex items-center justify-between p-4 hover:bg-primary/5 transition-colors cursor-pointer group"
>
<div className="flex items-center gap-4 min-w-0 flex-1">
<div className="w-10 h-10 rounded-lg bg-surface-container-low flex items-center justify-center text-primary shrink-0 border border-outline-variant/20">
{getOsIcon(host.os)}
</div>
<div className="min-w-0 flex-1 sm:grid sm:grid-cols-3 sm:gap-4 items-center">
<div>
<h4 className="font-bold text-on-surface group-hover:text-primary transition-colors text-sm truncate">{host.name}</h4>
<span className="font-mono text-xs text-outline">{host.ip}</span>
</div>
<div className="hidden sm:block">
<p className="text-xs text-on-surface-variant font-medium">{host.os}</p>
<p className="text-[10px] text-outline">{host.provider}</p>
</div>
<div className="hidden md:block">
<span className="text-xs text-outline italic">Seen {host.lastSeen}</span>
</div>
</div>
</div>
<div className="flex items-center gap-4 ml-4 shrink-0">
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-1 ${host.status === 'active' ? 'bg-secondary-container/20 text-secondary' : 'bg-surface-container text-outline'}`}>
<span className={`w-1.5 h-1.5 rounded-full ${host.status === 'active' ? 'bg-secondary animate-pulse' : 'bg-outline'}`}></span>
{host.status}
</span>
<button
onClick={() => onConnectHost(host.name)}
className="bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-1.5 px-4 rounded-lg transition-all"
>
Connect
</button>
</div>
</div>
))}
<div
onClick={() => setShowAddModal(true)}
className="p-4 hover:bg-primary/5 transition-colors cursor-pointer flex items-center justify-center gap-2 text-outline hover:text-primary font-bold text-sm"
>
<Plus className="w-5 h-5" /> Add New Host
</div>
</div>
)}
{/* Transfer Activity - Asymmetric Bento footer panel */}
<div className="space-y-4">
<div className="flex items-center gap-4">
<h3 className="text-base font-bold text-on-surface tracking-tight shrink-0">Background Transfers</h3>
<div className="flex-1 h-px bg-outline-variant/30"></div>
</div>
<div className="bg-white/50 backdrop-blur-sm border border-outline-variant rounded-2xl overflow-hidden shadow-sm">
<div className="p-4 border-b border-outline-variant flex items-center justify-between bg-surface-container-low">
<div className="flex gap-12 text-xs font-bold text-on-surface-variant uppercase tracking-tight">
<span>Source & File</span>
<span>Destination</span>
</div>
<span className="text-xs font-bold text-on-surface-variant uppercase tracking-tight">Progress</span>
</div>
<div className="divide-y divide-outline-variant/30 bg-white">
{transfers.map((item) => (
<div key={item.id} className="p-4 flex flex-col sm:flex-row sm:items-center justify-between gap-4 hover:bg-primary/5 transition-colors">
<div className="flex items-center gap-4 min-w-0 flex-1">
<div className={`p-2 rounded-lg ${item.type === 'download' ? 'bg-tertiary/10 text-tertiary' : 'bg-primary/10 text-primary'}`}>
<FileCheck className="w-5 h-5" />
</div>
<div className="min-w-0 flex-1 grid grid-cols-1 sm:grid-cols-2 gap-2">
<div>
<p className="text-sm font-bold truncate text-on-surface">{item.fileName}</p>
<span className="text-[10px] font-mono text-outline block">{item.source}</span>
</div>
<div className="flex items-center gap-1 text-on-surface-variant text-xs">
<ArrowRight className="w-4 h-4 text-outline" />
<span className="truncate">{item.destination}</span>
</div>
</div>
</div>
<div className="w-full sm:w-48 shrink-0">
<div className="flex justify-between items-center mb-1">
<span className="text-[10px] font-bold text-secondary">{item.progress}%</span>
<span className="text-[10px] text-outline font-mono">{item.speed}</span>
</div>
<div className="h-1.5 w-full bg-surface-container-high rounded-full overflow-hidden">
<div
className="h-full bg-secondary rounded-full transition-all duration-500 shadow-[0_0_8px_rgba(0,110,47,0.3)]"
style={{ width: `${item.progress}%` }}
></div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Manual Add Host Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[150] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-md w-full border border-outline-variant shadow-2xl overflow-hidden p-6 space-y-4 animate-scale-up">
<div className="flex justify-between items-center border-b border-outline-variant/30 pb-3">
<h3 className="font-bold text-lg text-on-surface">Add Host Credentials</h3>
<button
onClick={() => setShowAddModal(false)}
className="text-outline hover:text-primary cursor-pointer p-1 rounded-lg"
>
Cancel
</button>
</div>
<form onSubmit={handleCreateHost} className="space-y-4 text-sm">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">HOST IDENTIFIER</label>
<input
type="text"
required
placeholder="e.g. prod-db-replica"
value={newHostName}
onChange={(e) => setNewHostName(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">IP ADDRESS / DOMAIN</label>
<input
type="text"
required
placeholder="e.g. 10.0.12.19"
value={newHostIp}
onChange={(e) => setNewHostIp(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-mono"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">OPERATING SYSTEM</label>
<select
value={newHostOs}
onChange={(e) => setNewHostOs(e.target.value as any)}
className="bg-surface-container-low border border-outline-variant/40 rounded-lg py-2 px-3 focus:outline-none focus:border-primary"
>
<option value="Ubuntu 22.04">Ubuntu 22.04</option>
<option value="Debian 11">Debian 11</option>
<option value="Alpine Linux">Alpine Linux</option>
<option value="Windows Server">Windows Server</option>
<option value="macOS Ventura">macOS Ventura</option>
<option value="CentOS 7">CentOS 7</option>
</select>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">PROVIDER</label>
<select
value={newHostProvider}
onChange={(e) => setNewHostProvider(e.target.value as any)}
className="bg-surface-container-low border border-outline-variant/40 rounded-lg py-2 px-3 focus:outline-none focus:border-primary"
>
<option value="AWS US-East">AWS Cloud</option>
<option value="GCP Cloud">GCP Cloud</option>
<option value="Hetzner">Hetzner</option>
<option value="Vercel Proxy">Vercel</option>
<option value="Local Docker">Docker</option>
</select>
</div>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">HOST PURPOSE / TYPE</label>
<div className="grid grid-cols-3 gap-2">
{['api', 'db', 'web'].map((t) => (
<button
key={t}
type="button"
onClick={() => setNewHostType(t as any)}
className={`py-2 px-3 border rounded-lg font-bold text-xs uppercase tracking-wider transition-all ${newHostType === t ? 'bg-primary text-white border-primary' : 'bg-surface-container-low border-outline-variant/40 text-on-surface-variant hover:bg-surface-container'}`}
>
{t} Node
</button>
))}
</div>
</div>
<div className="pt-4 flex justify-end gap-3">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="px-4 py-2 text-xs font-bold text-outline hover:text-on-surface"
>
Cancel
</button>
<button
type="submit"
className="bg-primary text-white py-2 px-5 rounded-lg font-bold text-xs shadow-md hover:bg-primary-container hover:text-on-primary-container transition-all"
>
Add Host
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
+359
View File
@@ -0,0 +1,359 @@
import React, { useState } from 'react';
import { KeychainItem } from '../types';
import { Shield, Key, Lock, Eye, EyeOff, Copy, Check, Plus, Search, Trash2, CheckCircle2, AlertTriangle, Info, HelpCircle } from 'lucide-react';
export default function KeychainView() {
const [searchQuery, setSearchQuery] = useState('');
const [copiedId, setCopiedId] = useState<string | null>(null);
const [revealPassId, setRevealPassId] = useState<string | null>(null);
// Keychain credentials collection
const [keychain, setKeychain] = useState<KeychainItem[]>([
{
id: '1',
name: 'id_ed25519_alex',
type: 'ED25519',
user: 'root',
lastUsed: '2 hours ago',
strength: 'secure',
fingerprint: 'SHA256:7mP9K9+fVj5bW0vQ8zD1y2u3t4m5n6p7q8r9s0v1w2x',
passphrase: 'supersecret_argon2_hashed',
connectedHosts: ['production-api-01', 'edge-node-berlin'],
keyFile: '— Private ED25519 Key file loaded —'
},
{
id: '2',
name: 'aws_prod_deploy_secret',
type: 'AWS Key',
user: 'deploy-agent',
lastUsed: '1 day ago',
strength: 'secure',
fingerprint: 'SHA256:8kU3A4+gYg2bS1vT8zP1q2e3r4t5y6u7i8o9p0a1s2d',
passphrase: 'aws_access_key_id_and_secret',
connectedHosts: ['web-frontend-02'],
keyFile: '— AWS Credentials payload —'
},
{
id: '3',
name: 'root_pg_prod',
type: 'PASSWORD',
user: 'postgres',
lastUsed: '5 hours ago',
strength: 'moderate',
passphrase: 'pg_db_master_password_2026',
connectedHosts: ['db-cluster-main'],
keyFile: 'Password token'
},
{
id: '4',
name: 'legacy_rsa_key',
type: 'SSH RSA',
user: 'admin',
lastUsed: '3 weeks ago',
strength: 'weak',
fingerprint: 'SHA256:legacy_md5_fingerprint_block',
passphrase: 'pass',
connectedHosts: [],
keyFile: '— 1024-bit Legacy SSH Keyfile —'
}
]);
const [showAddModal, setShowAddModal] = useState(false);
const [newName, setNewName] = useState('');
const [newType, setNewType] = useState<'SSH RSA' | 'PASSWORD' | 'ED25519' | 'Bearer Token' | 'AWS Key'>('ED25519');
const [newUser, setNewUser] = useState('');
const [newPass, setNewPass] = useState('');
const triggerCopy = (id: string, text: string) => {
navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => {
setCopiedId(null);
}, 2000);
};
const handleCreateCredential = (e: React.FormEvent) => {
e.preventDefault();
if (!newName || !newUser) return;
const strengthVal = newPass.length > 12 ? 'secure' : newPass.length > 7 ? 'moderate' : 'weak';
const item: KeychainItem = {
id: Date.now().toString(),
name: newName,
type: newType,
user: newUser,
lastUsed: 'Just now',
strength: strengthVal,
passphrase: newPass,
connectedHosts: [],
keyFile: newType.includes('Key') || newType.includes('ED25519') ? '— Custom key payload generated —' : 'Password credential'
};
setKeychain([item, ...keychain]);
setShowAddModal(false);
setNewName('');
setNewUser('');
setNewPass('');
};
const handleDelete = (id: string) => {
setKeychain(prev => prev.filter(k => k.id !== id));
};
const filteredKeychain = keychain.filter(k =>
k.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
k.user.toLowerCase().includes(searchQuery.toLowerCase()) ||
k.type.toLowerCase().includes(searchQuery.toLowerCase())
);
const getStrengthBadge = (strength: string) => {
switch (strength) {
case 'secure':
return (
<span className="shrink-0 px-2.5 py-0.5 bg-secondary-container/20 text-secondary border border-secondary/20 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-1">
<CheckCircle2 className="w-3.5 h-3.5 text-secondary" />
SECURE
</span>
);
case 'moderate':
return (
<span className="shrink-0 px-2.5 py-0.5 bg-[#f0dbff] text-tertiary border border-tertiary/20 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-1">
<Info className="w-3.5 h-3.5 text-tertiary" />
MODERATE
</span>
);
default:
return (
<span className="shrink-0 px-2.5 py-0.5 bg-error-container/20 text-error border border-error/20 rounded-full text-[10px] font-bold uppercase tracking-wider flex items-center gap-1">
<AlertTriangle className="w-3.5 h-3.5 text-error animate-bounce" />
WEAK
</span>
);
}
};
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-fade-in">
{/* Header Panel */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-4">
<div>
<h2 className="text-xl font-bold text-on-surface tracking-tight">Keychain & Credentials</h2>
<p className="text-xs text-on-surface-variant mt-0.5">Secure sandbox containing cluster credentials, private SSH keys, and tokens</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-outline w-4 h-4" />
<input
type="text"
placeholder="Search credentials..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-1.5 rounded-full bg-surface-container-low border border-outline-variant/30 text-sm focus:border-primary focus:bg-white focus:outline-none focus:ring-1 focus:ring-primary w-48 sm:w-60 transition-all"
/>
</div>
<button
onClick={() => setShowAddModal(true)}
className="flex items-center gap-1.5 bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-2 px-4 rounded-xl shadow-md transition-all cursor-pointer"
>
<Plus className="w-4 h-4" />
Add Key
</button>
</div>
</div>
{/* Grid List */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredKeychain.map((item) => (
<div
key={item.id}
className="bg-white p-5 rounded-2xl border border-outline-variant hover:border-primary hover:shadow-lg transition-all flex flex-col justify-between space-y-4"
>
<div className="flex justify-between items-start gap-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-surface-container-low border border-outline-variant/20 flex items-center justify-center text-primary">
<Shield className="w-5 h-5" />
</div>
<div>
<h3 className="font-bold text-on-surface text-base truncate max-w-[180px]">{item.name}</h3>
<span className="text-[10px] font-mono text-outline uppercase tracking-wider">{item.type}</span>
</div>
</div>
{getStrengthBadge(item.strength)}
</div>
<div className="space-y-2 text-xs">
<div className="flex justify-between items-center py-1 border-b border-outline-variant/10">
<span className="text-outline font-semibold uppercase tracking-wider text-[10px]">User Login</span>
<span className="font-mono text-on-surface font-semibold">{item.user}</span>
</div>
<div className="flex justify-between items-center py-1 border-b border-outline-variant/10">
<span className="text-outline font-semibold uppercase tracking-wider text-[10px]">Credential Secrets</span>
<div className="flex items-center gap-2">
<span className="font-mono text-on-surface-variant">
{revealPassId === item.id ? item.passphrase : '••••••••••••••••'}
</span>
<button
onClick={() => setRevealPassId(revealPassId === item.id ? null : item.id)}
className="p-1 hover:bg-surface-container rounded text-outline transition-colors"
title={revealPassId === item.id ? "Hide password" : "Show password"}
>
{revealPassId === item.id ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
<button
onClick={() => triggerCopy(item.id, item.passphrase || '')}
className="p-1 hover:bg-surface-container rounded text-outline transition-colors"
title="Copy secret"
>
{copiedId === item.id ? <Check className="w-3.5 h-3.5 text-secondary" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{item.fingerprint && (
<div className="py-1">
<span className="text-outline font-semibold uppercase tracking-wider text-[10px] block mb-0.5">SHA256 FINGERPRINT</span>
<code className="text-[10px] text-on-surface-variant font-mono block break-all bg-surface-container-low p-2 rounded border border-outline-variant/20 select-all">{item.fingerprint}</code>
</div>
)}
</div>
<div className="pt-3 border-t border-outline-variant/30 flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-[10px] text-outline">
<span className="shrink-0">Last used {item.lastUsed}</span>
<div className="flex items-center justify-between sm:justify-end gap-2 flex-1 min-w-0 w-full">
<div className="flex items-center gap-1.5 min-w-0 flex-1 justify-start sm:justify-end">
<span className="text-outline shrink-0 font-bold">Hosts:</span>
{item.connectedHosts && item.connectedHosts.length > 0 ? (
<div className="flex flex-wrap gap-1 max-h-[44px] overflow-y-auto no-scrollbar justify-start sm:justify-end">
{item.connectedHosts.map((host) => (
<span
key={host}
className="bg-primary-container/15 text-primary px-1.5 py-0.5 rounded font-mono text-[9px] hover:bg-primary/10 transition-colors shrink-0"
title={host}
>
{host}
</span>
))}
</div>
) : (
<span className="text-outline italic">None mapped</span>
)}
</div>
<button
onClick={() => handleDelete(item.id)}
className="p-1 hover:bg-error-container hover:text-error rounded text-outline transition-colors shrink-0 cursor-pointer ml-1"
title="Remove credential"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
))}
</div>
{/* Security Advisory Bento */}
<div className="p-5 border border-outline-variant/60 rounded-2xl bg-white/40 flex items-start gap-4 text-xs text-on-surface">
<Info className="w-5 h-5 text-primary shrink-0 mt-0.5" />
<div className="space-y-1">
<h4 className="font-bold text-on-surface">HostKeeper Security Policy & Local Sandbox</h4>
<p className="text-on-surface-variant leading-relaxed">
Keys are parsed locally inside your isolated browser environment and transmitted strictly within cryptographically verified SSH tunnels. Passphrases are hashed with Argon2id instantly and never touch intermediate telemetry systems.
</p>
</div>
</div>
{/* Add Credential Modal */}
{showAddModal && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[150] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-md w-full border border-outline-variant shadow-2xl p-6 space-y-4 animate-scale-up">
<div className="flex justify-between items-center border-b border-outline-variant/30 pb-3">
<h3 className="font-bold text-lg text-on-surface">Register Secure Credential</h3>
<button
onClick={() => setShowAddModal(false)}
className="text-outline hover:text-primary cursor-pointer p-1 rounded"
>
Cancel
</button>
</div>
<form onSubmit={handleCreateCredential} className="space-y-4 text-sm">
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">CREDENTIAL NAME / LABEL</label>
<input
type="text"
required
placeholder="e.g. key_aws_alex"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">CREDENTIAL TYPE</label>
<select
value={newType}
onChange={(e) => setNewType(e.target.value as any)}
className="bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-3 focus:outline-none focus:border-primary"
>
<option value="ED25519">ED25519 (Modern)</option>
<option value="SSH RSA">SSH RSA (Legacy)</option>
<option value="PASSWORD">Master Password</option>
<option value="Bearer Token">Bearer Token</option>
<option value="AWS Key">AWS Access Key</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">LOGIN USERNAME</label>
<input
type="text"
required
placeholder="e.g. root or ubuntu"
value={newUser}
onChange={(e) => setNewUser(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-3 focus:outline-none focus:border-primary font-medium"
/>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">PASSPHRASE / TOKEN KEY</label>
<input
type="password"
required
placeholder="Enter secure password secret..."
value={newPass}
onChange={(e) => setNewPass(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-mono"
/>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-outline-variant/20">
<button
type="button"
onClick={() => setShowAddModal(false)}
className="px-4 py-2 text-xs font-bold text-outline hover:text-on-surface"
>
Cancel
</button>
<button
type="submit"
className="bg-primary text-white py-2 px-5 rounded-lg font-bold text-xs shadow-md hover:bg-primary-container hover:text-on-primary-container transition-all"
>
Save Credential
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
+249
View File
@@ -0,0 +1,249 @@
import React, { useState } from 'react';
import { Device } from '../types';
import { User, Bell, Shield, Smartphone, Laptop, CheckCircle, Zap, Save, RefreshCw, Star, LogOut, Check } from 'lucide-react';
export default function SettingsView() {
const [name, setName] = useState('Alex Rivera');
const [email, setEmail] = useState('alex.rivera@hostkeeper.io');
const [role, setRole] = useState('Senior DevOps Lead');
// Toggle states
const [keyBackup, setKeyBackup] = useState(true);
const [bgSync, setBgSync] = useState(true);
const [notifyTransfer, setNotifyTransfer] = useState(false);
const [showSaveSuccess, setShowSaveSuccess] = useState(false);
// Active connected sync devices
const [devices, setDevices] = useState<Device[]>([
{ id: '1', name: 'Alex MacBook Pro 16"', type: 'desktop', status: 'online' },
{ id: '2', name: 'Rivera iPad Air 5', type: 'tablet', status: 'online' },
{ id: '3', name: 'iPhone 15 Pro Max', type: 'mobile', status: 'offline' }
]);
const handleSaveProfile = (e: React.FormEvent) => {
e.preventDefault();
setShowSaveSuccess(true);
setTimeout(() => {
setShowSaveSuccess(false);
}, 2500);
};
const getDeviceIcon = (type: string) => {
switch (type) {
case 'desktop': return <Laptop className="w-5 h-5 text-primary" />;
default: return <Smartphone className="w-5 h-5 text-outline" />;
}
};
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-fade-in text-sm">
{/* Toast Alert Simulation */}
{showSaveSuccess && (
<div className="fixed bottom-6 right-6 z-[180] bg-inverse-surface text-inverse-on-surface px-5 py-3.5 rounded-xl shadow-xl flex items-center gap-3 border border-outline/20 animate-slide-in text-xs font-semibold">
<CheckCircle className="w-4 h-4 text-secondary animate-bounce" />
<span>Profile parameters updated successfully!</span>
</div>
)}
{/* Header Panel */}
<div className="border-b border-outline-variant/30 pb-4">
<h2 className="text-xl font-bold text-on-surface tracking-tight">Workspace Preferences</h2>
<p className="text-xs text-on-surface-variant mt-0.5">Configure authentication sync protocols and cloud billing</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left Side: Profile Form and Toggle configs */}
<div className="lg:col-span-8 space-y-8">
{/* Section: Profile Form */}
<div className="bg-white p-6 rounded-2xl border border-outline-variant shadow-sm space-y-6">
<h3 className="font-bold text-on-surface text-base flex items-center gap-2">
<User className="w-5 h-5 text-primary" />
Developer Profile
</h3>
<form onSubmit={handleSaveProfile} className="grid grid-cols-1 sm:grid-cols-2 gap-5">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">FULL NAME</label>
<input
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-semibold text-on-surface"
/>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-on-surface-variant">EMAIL ADDRESS</label>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium text-on-surface"
/>
</div>
<div className="flex flex-col gap-1.5 sm:col-span-2">
<label className="text-xs font-bold text-on-surface-variant">WORKPLACE DESIGNATION / ROLE</label>
<input
type="text"
required
value={role}
onChange={(e) => setRole(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium text-on-surface"
/>
</div>
<div className="sm:col-span-2 pt-2 flex justify-end">
<button
type="submit"
className="flex items-center gap-2 bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-2.5 px-6 rounded-xl shadow-md transition-all cursor-pointer"
>
<Save className="w-4 h-4" />
Save Changes
</button>
</div>
</form>
</div>
{/* Section: Feature Toggles */}
<div className="bg-white p-6 rounded-2xl border border-outline-variant shadow-sm space-y-6">
<h3 className="font-bold text-on-surface text-base flex items-center gap-2">
<Shield className="w-5 h-5 text-primary" />
Security & Transmission Protocols
</h3>
<div className="space-y-4 divide-y divide-outline-variant/20">
{/* Toggle Row 1 */}
<div className="flex items-center justify-between py-3">
<div className="space-y-0.5 pr-4">
<h4 className="font-bold text-on-surface text-xs sm:text-sm">Cryptographic Keychain Backup</h4>
<p className="text-xs text-on-surface-variant">Synchronize securely encrypted keys automatically inside iCloud/Google drive sandboxes.</p>
</div>
<button
onClick={() => setKeyBackup(!keyBackup)}
className={`w-11 h-6 rounded-full transition-colors relative flex items-center shrink-0 cursor-pointer ${keyBackup ? 'bg-primary' : 'bg-outline-variant'}`}
>
<span className={`w-4 h-4 bg-white rounded-full shadow transition-transform absolute ${keyBackup ? 'translate-x-6' : 'translate-x-1'}`}></span>
</button>
</div>
{/* Toggle Row 2 */}
<div className="flex items-center justify-between pt-3">
<div className="space-y-0.5 pr-4">
<h4 className="font-bold text-on-surface text-xs sm:text-sm">Real-time SFTP Connection Polling</h4>
<p className="text-xs text-on-surface-variant">Enable background file watchers to automatically refresh the directory tree upon local file modifications.</p>
</div>
<button
onClick={() => setBgSync(!bgSync)}
className={`w-11 h-6 rounded-full transition-colors relative flex items-center shrink-0 cursor-pointer ${bgSync ? 'bg-primary' : 'bg-outline-variant'}`}
>
<span className={`w-4 h-4 bg-white rounded-full shadow transition-transform absolute ${bgSync ? 'translate-x-6' : 'translate-x-1'}`}></span>
</button>
</div>
{/* Toggle Row 3 */}
<div className="flex items-center justify-between pt-3">
<div className="space-y-0.5 pr-4">
<h4 className="font-bold text-on-surface text-xs sm:text-sm">Desktop Push Notifications</h4>
<p className="text-xs text-on-surface-variant">Trigger native system indicators upon complete transmissions of download/upload streams.</p>
</div>
<button
onClick={() => setNotifyTransfer(!notifyTransfer)}
className={`w-11 h-6 rounded-full transition-colors relative flex items-center shrink-0 cursor-pointer ${notifyTransfer ? 'bg-primary' : 'bg-outline-variant'}`}
>
<span className={`w-4 h-4 bg-white rounded-full shadow transition-transform absolute ${notifyTransfer ? 'translate-x-6' : 'translate-x-1'}`}></span>
</button>
</div>
</div>
</div>
</div>
{/* Right Side: Billing Status and Devices List */}
<div className="lg:col-span-4 space-y-8">
{/* Pro Billing Card */}
<div className="bg-primary text-on-primary p-6 rounded-2xl shadow-lg relative overflow-hidden flex flex-col justify-between h-48">
<div className="relative z-10">
<div className="flex justify-between items-start">
<span className="px-2.5 py-1 bg-white/20 rounded-full font-bold text-[9px] uppercase tracking-wider text-white">HostKeeper Pro</span>
<Star className="w-5 h-5 text-white fill-white" />
</div>
<h3 className="font-black text-2xl tracking-tight mt-3">Active Workspace</h3>
</div>
<div className="relative z-10 flex justify-between items-end">
<div>
<span className="text-xs opacity-80 uppercase font-semibold">RENEWAL CYCLE</span>
<p className="text-xs font-bold text-white">July 2026</p>
</div>
<span className="text-[10px] font-bold bg-white text-primary rounded-lg px-2.5 py-1 flex items-center gap-1">
<Check className="w-3.5 h-3.5" /> PAID ACCOUNT
</span>
</div>
<div className="absolute right-0 bottom-0 opacity-10 pointer-events-none">
<Zap className="w-48 h-48 rotate-45" />
</div>
</div>
{/* Connected Sync Devices list */}
<div className="bg-white p-5 rounded-2xl border border-outline-variant shadow-sm space-y-4">
<h3 className="font-bold text-on-surface text-sm flex items-center gap-2">
<Smartphone className="w-4.5 h-4.5 text-primary" />
Synchronized Devices
</h3>
<div className="space-y-3.5">
{devices.map((device) => (
<div key={device.id} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2.5 min-w-0">
<div className="p-1.5 bg-surface-container-low border border-outline-variant/20 rounded-lg text-primary shrink-0">
{getDeviceIcon(device.type)}
</div>
<span className="font-bold text-on-surface truncate pr-2">{device.name}</span>
</div>
<span className={`px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center gap-1 shrink-0 ${device.status === 'online' ? 'bg-secondary-container/20 text-secondary' : 'bg-surface-container text-outline'}`}>
<span className={`w-1 h-1 rounded-full ${device.status === 'online' ? 'bg-secondary animate-pulse' : 'bg-outline'}`}></span>
{device.status}
</span>
</div>
))}
</div>
</div>
{/* Danger Zone */}
<div className="p-4 border border-error-container/40 rounded-2xl bg-error-container/5 space-y-3 text-xs">
<h4 className="font-bold text-error flex items-center gap-2">
<LogOut className="w-4 h-4" /> Reset Local State
</h4>
<p className="text-on-surface-variant">Resetting will clear all your locally loaded keystores, snippets, and connection cache buffers from the browser storage.</p>
<button
onClick={() => {
if(confirm("Are you sure you want to flush all local storage configurations?")) {
localStorage.clear();
alert("Workspace storage cleared successfully.");
window.location.reload();
}
}}
className="w-full py-2 border border-error/30 rounded-xl font-bold text-error hover:bg-error/10 transition-colors"
>
Flush All App State
</button>
</div>
</div>
</div>
</div>
);
}
+389
View File
@@ -0,0 +1,389 @@
import React, { useState } from 'react';
import { FileItem } from '../types';
import {
Folder,
File,
FileCode,
FileImage,
Lock,
ArrowLeftRight,
Upload,
Download,
RefreshCw,
ChevronRight,
Search,
FolderPlus,
Trash2,
CheckCircle,
HardDrive,
Globe
} from 'lucide-react';
export default function SftpView() {
const [localPath, setLocalPath] = useState('/Users/alex/workspace/hostkeeper');
const [remotePath, setRemotePath] = useState('/var/www/prod-api-cluster/src');
const [localSearch, setLocalSearch] = useState('');
const [remoteSearch, setRemoteSearch] = useState('');
// Active Toast simulation state
const [toastMessage, setToastMessage] = useState<string | null>(null);
// Initial local mock file system
const [localFiles, setLocalFiles] = useState<FileItem[]>([
{ name: 'public', size: '—', modified: '2 hours ago', type: 'folder' },
{ name: 'src', size: '—', modified: '10 mins ago', type: 'folder' },
{ name: 'package.json', size: '1.4 KB', modified: 'Just now', type: 'code' },
{ name: 'vite.config.ts', size: '860 B', modified: '2 days ago', type: 'code' },
{ name: 'tailwind.config.js', size: '1.1 KB', modified: '3 days ago', type: 'code' },
{ name: 'App.tsx', size: '12.4 KB', modified: 'Just now', type: 'code' },
{ name: 'logo_hero.png', size: '240 KB', modified: '1 week ago', type: 'image' },
{ name: '.env', size: '280 B', modified: 'Yesterday', type: 'lock' }
]);
// Initial remote mock file system
const [remoteFiles, setRemoteFiles] = useState<FileItem[]>([
{ name: 'controllers', size: '—', modified: '1 day ago', type: 'folder' },
{ name: 'models', size: '—', modified: '1 day ago', type: 'folder' },
{ name: 'routes', size: '—', modified: '4 hours ago', type: 'folder' },
{ name: 'server.js', size: '8.4 KB', modified: '2 hours ago', type: 'code' },
{ name: 'config.production.json', size: '2.1 KB', modified: '1 week ago', type: 'lock' },
{ name: 'index.html', size: '3.2 KB', modified: '3 hours ago', type: 'code' },
{ name: 'avatar_default.jpg', size: '18 KB', modified: '2 weeks ago', type: 'image' },
{ name: 'node_modules.tar.gz', size: '42 MB', modified: '3 days ago', type: 'file' }
]);
const triggerToast = (msg: string) => {
setToastMessage(msg);
setTimeout(() => {
setToastMessage(null);
}, 3500);
};
const handleLocalClick = (item: FileItem) => {
if (item.type === 'folder') {
setLocalPath(`${localPath}/${item.name}`);
triggerToast(`Navigated into local folder: ${item.name}`);
} else {
triggerToast(`Uploading "${item.name}" to remote server...`);
// Simulate transfer additions
setTimeout(() => {
setRemoteFiles(prev => [
{ name: item.name, size: item.size, modified: 'Just now', type: item.type },
...prev
]);
triggerToast(`Successfully uploaded "${item.name}" to ${remotePath}`);
}, 1500);
}
};
const handleRemoteClick = (item: FileItem) => {
if (item.type === 'folder') {
setRemotePath(`${remotePath}/${item.name}`);
triggerToast(`Navigated remote server path to: ${item.name}`);
} else {
triggerToast(`Downloading "${item.name}" from remote server...`);
setTimeout(() => {
setLocalFiles(prev => [
{ name: item.name, size: item.size, modified: 'Just now', type: item.type },
...prev
]);
triggerToast(`Successfully downloaded "${item.name}" to local environment.`);
}, 1500);
}
};
const handleCreateFolder = (target: 'local' | 'remote') => {
const folderName = prompt(`Enter new folder name for ${target}:`);
if (!folderName) return;
const newFolder: FileItem = {
name: folderName,
size: '—',
modified: 'Just now',
type: 'folder'
};
if (target === 'local') {
setLocalFiles([newFolder, ...localFiles]);
triggerToast(`Created folder "${folderName}" locally.`);
} else {
setRemoteFiles([newFolder, ...remoteFiles]);
triggerToast(`Created folder "${folderName}" on remote host.`);
}
};
const handleDeleteFile = (name: string, target: 'local' | 'remote') => {
if (target === 'local') {
setLocalFiles(prev => prev.filter(f => f.name !== name));
triggerToast(`Deleted local file "${name}"`);
} else {
setRemoteFiles(prev => prev.filter(f => f.name !== name));
triggerToast(`Deleted remote file "${name}"`);
}
};
const getFileIcon = (type: string) => {
switch (type) {
case 'folder':
return <Folder className="w-4 h-4 text-primary" />;
case 'code':
return <FileCode className="w-4 h-4 text-tertiary" />;
case 'image':
return <FileImage className="w-4 h-4 text-secondary" />;
case 'lock':
return <Lock className="w-4 h-4 text-error" />;
default:
return <File className="w-4 h-4 text-outline" />;
}
};
const filteredLocal = localFiles.filter(f => f.name.toLowerCase().includes(localSearch.toLowerCase()));
const filteredRemote = remoteFiles.filter(f => f.name.toLowerCase().includes(remoteSearch.toLowerCase()));
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-6 animate-fade-in relative">
{/* Toast Alert simulation */}
{toastMessage && (
<div className="fixed bottom-6 right-6 z-[180] bg-inverse-surface text-inverse-on-surface px-5 py-3.5 rounded-xl shadow-xl flex items-center gap-3 border border-outline/20 animate-slide-in text-xs font-semibold">
<CheckCircle className="w-4 h-4 text-secondary animate-bounce" />
<span>{toastMessage}</span>
</div>
)}
{/* Header Panel */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-4">
<div>
<h2 className="text-xl font-bold text-on-surface tracking-tight">SFTP Secure File Sync</h2>
<p className="text-xs text-on-surface-variant mt-0.5">Dual streams for real-time asset transmission and file editing</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
setLocalFiles([
{ name: 'public', size: '—', modified: '2 hours ago', type: 'folder' },
{ name: 'src', size: '—', modified: '10 mins ago', type: 'folder' },
{ name: 'package.json', size: '1.4 KB', modified: 'Just now', type: 'code' },
{ name: 'vite.config.ts', size: '860 B', modified: '2 days ago', type: 'code' },
{ name: 'tailwind.config.js', size: '1.1 KB', modified: '3 days ago', type: 'code' }
]);
setRemoteFiles([
{ name: 'controllers', size: '—', modified: '1 day ago', type: 'folder' },
{ name: 'models', size: '—', modified: '1 day ago', type: 'folder' },
{ name: 'server.js', size: '8.4 KB', modified: '2 hours ago', type: 'code' }
]);
triggerToast('Refreshed workspace file tree arrays.');
}}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-outline-variant hover:border-primary text-xs font-semibold text-on-surface bg-white transition-all hover:bg-surface-container"
>
<RefreshCw className="w-3.5 h-3.5" />
Reset State
</button>
</div>
</div>
{/* Dual Panel Body */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Local Stream File Panel */}
<div className="bg-white rounded-2xl border border-outline-variant overflow-hidden shadow-sm flex flex-col h-[520px]">
{/* Stream Header */}
<div className="p-4 border-b border-outline-variant/50 bg-surface-container-low flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<HardDrive className="w-4 h-4 text-outline shrink-0" />
<span className="font-bold text-xs text-on-surface truncate uppercase tracking-wider">Local Workstation</span>
</div>
<button
onClick={() => handleCreateFolder('local')}
className="text-primary hover:text-primary-container p-1 rounded hover:bg-primary/5 transition-all"
title="New Local Folder"
>
<FolderPlus className="w-4 h-4" />
</button>
</div>
{/* Breadcrumb path navigation bar */}
<div className="px-4 py-2 border-b border-outline-variant/20 bg-surface-container-lowest flex items-center gap-1.5 text-xs text-outline font-mono overflow-x-auto no-scrollbar">
<span className="text-primary-fixed-dim hover:underline cursor-pointer" onClick={() => setLocalPath('/Users/alex')}>alex</span>
<ChevronRight className="w-3 h-3 text-outline/40 shrink-0" />
<span className="text-primary-fixed-dim hover:underline cursor-pointer" onClick={() => setLocalPath('/Users/alex/workspace')}>workspace</span>
<ChevronRight className="w-3 h-3 text-outline/40 shrink-0" />
<span className="text-on-surface truncate">{localPath.split('/').pop()}</span>
</div>
{/* File Stream Search Filter */}
<div className="p-3 border-b border-outline-variant/10 relative">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-outline w-3.5 h-3.5" />
<input
type="text"
placeholder="Filter local files..."
value={localSearch}
onChange={(e) => setLocalSearch(e.target.value)}
className="w-full bg-surface-container-low pl-10 pr-4 py-1.5 border border-outline-variant/30 rounded-lg text-xs font-medium focus:outline-none focus:border-primary focus:bg-white"
/>
</div>
{/* Local files list table */}
<div className="flex-1 overflow-y-auto min-h-0">
<table className="w-full text-left border-collapse text-xs">
<thead>
<tr className="border-b border-outline-variant/20 text-outline uppercase tracking-wider font-bold">
<th className="py-2.5 px-4 font-semibold text-[10px]">Name</th>
<th className="py-2.5 px-4 font-semibold text-[10px] text-right">Size</th>
<th className="py-2.5 px-4 font-semibold text-[10px] text-right">Modified</th>
<th className="py-2.5 px-4 font-semibold text-[10px]"></th>
</tr>
</thead>
<tbody>
{filteredLocal.map((file) => (
<tr
key={file.name}
className="border-b border-outline-variant/10 hover:bg-primary/5 transition-colors group cursor-pointer"
>
<td className="py-2.5 px-4">
<div
onClick={() => handleLocalClick(file)}
className="font-semibold text-on-surface flex items-center gap-2.5 max-w-[180px] cursor-pointer"
>
<span className="shrink-0">{getFileIcon(file.type)}</span>
<span className="truncate group-hover:text-primary transition-colors">{file.name}</span>
</div>
</td>
<td className="py-2.5 px-4 text-right text-on-surface-variant font-mono">{file.size}</td>
<td className="py-2.5 px-4 text-right text-outline">{file.modified}</td>
<td className="py-2.5 px-4 text-right">
<div className="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleLocalClick(file)}
className="p-1 hover:bg-primary/10 rounded text-primary transition-colors"
title="Upload to remote"
>
<Upload className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteFile(file.name, 'local')}
className="p-1 hover:bg-error-container hover:text-error rounded text-outline transition-colors"
title="Delete file"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="p-3 bg-surface-container-low border-t border-outline-variant text-[10px] text-outline text-center font-bold">
TIP: Click file or upload button to transmit to server
</div>
</div>
{/* Remote Stream File Panel */}
<div className="bg-white rounded-2xl border border-outline-variant overflow-hidden shadow-sm flex flex-col h-[520px]">
{/* Stream Header */}
<div className="p-4 border-b border-outline-variant/50 bg-surface-container-low flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<Globe className="w-4 h-4 text-primary shrink-0" />
<span className="font-bold text-xs text-on-surface truncate uppercase tracking-wider">Remote Cluster Node</span>
</div>
<button
onClick={() => handleCreateFolder('remote')}
className="text-primary hover:text-primary-container p-1 rounded hover:bg-primary/5 transition-all"
title="New Remote Folder"
>
<FolderPlus className="w-4 h-4" />
</button>
</div>
{/* Breadcrumb path navigation bar */}
<div className="px-4 py-2 border-b border-outline-variant/20 bg-surface-container-lowest flex items-center gap-1.5 text-xs text-outline font-mono overflow-x-auto no-scrollbar">
<span className="text-primary-fixed-dim hover:underline cursor-pointer" onClick={() => setRemotePath('/var')}>var</span>
<ChevronRight className="w-3 h-3 text-outline/40 shrink-0" />
<span className="text-primary-fixed-dim hover:underline cursor-pointer" onClick={() => setRemotePath('/var/www')}>www</span>
<ChevronRight className="w-3 h-3 text-outline/40 shrink-0" />
<span className="text-on-surface truncate">{remotePath.split('/').pop()}</span>
</div>
{/* File Stream Search Filter */}
<div className="p-3 border-b border-outline-variant/10 relative">
<Search className="absolute left-6 top-1/2 -translate-y-1/2 text-outline w-3.5 h-3.5" />
<input
type="text"
placeholder="Filter remote files..."
value={remoteSearch}
onChange={(e) => setRemoteSearch(e.target.value)}
className="w-full bg-surface-container-low pl-10 pr-4 py-1.5 border border-outline-variant/30 rounded-lg text-xs font-medium focus:outline-none focus:border-primary focus:bg-white"
/>
</div>
{/* Remote files list table */}
<div className="flex-1 overflow-y-auto min-h-0">
<table className="w-full text-left border-collapse text-xs">
<thead>
<tr className="border-b border-outline-variant/20 text-outline uppercase tracking-wider font-bold">
<th className="py-2.5 px-4 font-semibold text-[10px]">Name</th>
<th className="py-2.5 px-4 font-semibold text-[10px] text-right">Size</th>
<th className="py-2.5 px-4 font-semibold text-[10px] text-right">Modified</th>
<th className="py-2.5 px-4 font-semibold text-[10px]"></th>
</tr>
</thead>
<tbody>
{filteredRemote.map((file) => (
<tr
key={file.name}
className="border-b border-outline-variant/10 hover:bg-primary/5 transition-colors group cursor-pointer"
>
<td className="py-2.5 px-4">
<div
onClick={() => handleRemoteClick(file)}
className="font-semibold text-on-surface flex items-center gap-2.5 max-w-[180px] cursor-pointer"
>
<span className="shrink-0">{getFileIcon(file.type)}</span>
<span className="truncate group-hover:text-primary transition-colors">{file.name}</span>
</div>
</td>
<td className="py-2.5 px-4 text-right text-on-surface-variant font-mono">{file.size}</td>
<td className="py-2.5 px-4 text-right text-outline">{file.modified}</td>
<td className="py-2.5 px-4 text-right">
<div className="flex items-center justify-end gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => handleRemoteClick(file)}
className="p-1 hover:bg-primary/10 rounded text-primary transition-colors"
title="Download to local"
>
<Download className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDeleteFile(file.name, 'remote')}
className="p-1 hover:bg-error-container hover:text-error rounded text-outline transition-colors"
title="Delete file"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="p-3 bg-surface-container-low border-t border-outline-variant text-[10px] text-outline text-center font-bold">
TIP: Click file or download button to fetch files to local system
</div>
</div>
</div>
{/* Drag and Drop Zone Simulator */}
<div className="p-8 border-2 border-dashed border-outline-variant rounded-2xl text-center bg-white/40 flex flex-col items-center justify-center space-y-2 relative group hover:border-primary transition-colors">
<ArrowLeftRight className="w-10 h-10 text-outline group-hover:text-primary group-hover:scale-110 transition-transform" />
<h3 className="font-bold text-sm text-on-surface">Drag files directly here to start batch upload</h3>
<p className="text-xs text-on-surface-variant max-w-sm">
Files are automatically split into parallel streams of 5MB chunks and secured with AES-256 keychain keys.
</p>
</div>
</div>
);
}
+350
View File
@@ -0,0 +1,350 @@
import React, { useState } from 'react';
import { Snippet } from '../types';
import { Search, Copy, Check, Terminal, Folder, Star, Clock, FolderGit, Cpu, Trash2, Plus, Zap, FileText } from 'lucide-react';
export default function SnippetsView() {
const [searchQuery, setSearchQuery] = useState('');
const [activeCollection, setActiveCollection] = useState<'all' | 'favorites' | 'recent'>('all');
const [activeTag, setActiveTag] = useState<string>('all');
const [copiedId, setCopiedId] = useState<string | null>(null);
// Snippets Initial Database State
const [snippets, setSnippets] = useState<Snippet[]>([
{
id: '1',
title: 'Docker Resource Prune Suite',
description: 'Force clean-up unused container images, volumes, logs, networks, and untagged builds safely.',
code: 'docker system prune -a --volumes --force',
tags: ['Docker', 'DevOps'],
collection: 'favorites',
updatedAt: '2 hours ago',
icon: 'Cpu'
},
{
id: '2',
title: 'Nginx TLS Reverse Proxy Template',
description: 'Standard secure production proxy server configuration block forwarding upstream headers.',
code: `server {
listen 443 ssl http2;
server_name api.domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}`,
tags: ['Nginx', 'SSL'],
collection: 'favorites',
updatedAt: 'Yesterday',
icon: 'Folder'
},
{
id: '3',
title: 'Git Force Fetch and Hard Reset',
description: 'Discards local changes completely and resets HEAD to track remote production origin main.',
code: 'git fetch origin && git reset --hard origin/main',
tags: ['Git', 'VCS'],
collection: 'recent',
updatedAt: '3 days ago',
icon: 'FolderGit'
},
{
id: '4',
title: 'Inspect SSL Certification Expiry',
description: 'Checks expiry metrics and issuer of ssl keys on active remote addresses.',
code: 'openssl s_client -connect google.com:443 | openssl x509 -noout -dates',
tags: ['SSL', 'Security'],
collection: 'recent',
updatedAt: 'Last week',
icon: 'Cpu'
}
]);
const [newTitle, setNewTitle] = useState('');
const [newDesc, setNewDesc] = useState('');
const [newCode, setNewCode] = useState('');
const [newTag, setNewTag] = useState('DevOps');
const [showAddForm, setShowAddForm] = useState(false);
const handleCopyCode = (id: string, code: string) => {
navigator.clipboard.writeText(code);
setCopiedId(id);
setTimeout(() => {
setCopiedId(null);
}, 2000);
};
const handleDeleteSnippet = (id: string) => {
setSnippets(prev => prev.filter(s => s.id !== id));
};
const handleCreateSnippet = (e: React.FormEvent) => {
e.preventDefault();
if (!newTitle || !newCode) return;
const added: Snippet = {
id: Date.now().toString(),
title: newTitle,
description: newDesc,
code: newCode,
tags: [newTag],
collection: 'recent',
updatedAt: 'Just now',
icon: 'FileText'
};
setSnippets([added, ...snippets]);
setShowAddForm(false);
setNewTitle('');
setNewDesc('');
setNewCode('');
};
const allTags = ['all', ...Array.from(new Set(snippets.flatMap(s => s.tags)))];
const filteredSnippets = snippets.filter(s => {
const matchesSearch = s.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
s.code.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCollection = activeCollection === 'all' || s.collection === activeCollection;
const matchesTag = activeTag === 'all' || s.tags.includes(activeTag);
return matchesSearch && matchesCollection && matchesTag;
});
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-fade-in">
{/* Search Header Panel */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-4">
<div>
<h2 className="text-xl font-bold text-on-surface tracking-tight">Snippets & Scripts Library</h2>
<p className="text-xs text-on-surface-variant mt-0.5">Quickly reuse and catalog command line shortcuts across remote clusters</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-outline w-4 h-4" />
<input
type="text"
placeholder="Search code snippets..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-4 py-1.5 rounded-full bg-surface-container-low border border-outline-variant/30 text-sm focus:border-primary focus:bg-white focus:outline-none focus:ring-1 focus:ring-primary w-48 sm:w-60 transition-all"
/>
</div>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-1.5 bg-primary text-white hover:bg-primary-container hover:text-on-primary-container font-bold text-xs py-2 px-4 rounded-xl shadow-md transition-all cursor-pointer"
>
<Plus className="w-4 h-4" />
New Snippet
</button>
</div>
</div>
{/* Grid Architecture */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left category rails filter */}
<div className="lg:col-span-3 space-y-5">
<div className="bg-white rounded-2xl border border-outline-variant p-4 space-y-4">
<h4 className="text-xs font-bold text-outline-variant uppercase tracking-wider px-2">Collections</h4>
<div className="space-y-1 text-sm font-semibold">
<button
onClick={() => setActiveCollection('all')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'all' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Folder className="w-4 h-4" /> All Code Blocks</span>
<span className="text-xs text-outline">{snippets.length}</span>
</button>
<button
onClick={() => setActiveCollection('favorites')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'favorites' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Star className="w-4 h-4 text-amber-500 fill-amber-500" /> Starred Favorites</span>
<span className="text-xs text-outline">{snippets.filter(s => s.collection === 'favorites').length}</span>
</button>
<button
onClick={() => setActiveCollection('recent')}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${activeCollection === 'recent' ? 'bg-primary/10 text-primary' : 'text-on-surface-variant hover:bg-surface-container-low'}`}
>
<span className="flex items-center gap-2"><Clock className="w-4 h-4" /> Recent</span>
<span className="text-xs text-outline">{snippets.filter(s => s.collection === 'recent').length}</span>
</button>
</div>
</div>
<div className="bg-white rounded-2xl border border-outline-variant p-4 space-y-3">
<h4 className="text-xs font-bold text-outline-variant uppercase tracking-wider px-2">Tag Categories</h4>
<div className="flex flex-wrap gap-1.5 p-1">
{allTags.map(tag => (
<button
key={tag}
onClick={() => setActiveTag(tag)}
className={`px-3 py-1 rounded-full text-xs font-bold transition-all ${activeTag === tag ? 'bg-primary text-white' : 'bg-surface-container-low text-on-surface-variant hover:bg-surface-container'}`}
>
{tag === 'all' ? 'All Tags' : tag}
</button>
))}
</div>
</div>
</div>
{/* Right main snippets cards list */}
<div className="lg:col-span-9 space-y-6">
{filteredSnippets.length === 0 ? (
<div className="p-12 border border-dashed border-outline-variant rounded-2xl text-center text-outline">
No matching snippet found in {activeCollection} category.
</div>
) : (
filteredSnippets.map((snippet) => (
<div
key={snippet.id}
className="bg-white border border-outline-variant rounded-2xl overflow-hidden shadow-sm flex flex-col group"
>
<div className="p-5 flex justify-between items-start gap-4">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-bold text-on-surface text-base group-hover:text-primary transition-colors">{snippet.title}</h3>
<div className="flex flex-wrap gap-1">
{snippet.tags.map(t => (
<span key={t} className="px-2 py-0.5 bg-surface-container text-[10px] font-bold rounded text-on-surface-variant">
{t}
</span>
))}
</div>
</div>
<p className="text-xs text-on-surface-variant leading-relaxed">{snippet.description}</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => handleCopyCode(snippet.id, snippet.code)}
className="p-1.5 border border-outline-variant rounded-lg bg-surface-container-low hover:border-primary text-outline hover:text-primary transition-colors cursor-pointer"
title="Copy code to clipboard"
>
{copiedId === snippet.id ? <Check className="w-4 h-4 text-secondary" /> : <Copy className="w-4 h-4" />}
</button>
<button
onClick={() => handleDeleteSnippet(snippet.id)}
className="p-1.5 border border-outline-variant rounded-lg bg-surface-container-low hover:border-error text-outline hover:text-error transition-colors"
title="Delete code snippet"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{/* Code syntax terminal container */}
<div className="bg-surface-container-low border-t border-outline-variant/40 p-4 font-mono text-xs text-on-surface relative group/code overflow-x-auto">
<pre className="text-on-surface select-all leading-relaxed whitespace-pre font-medium">{snippet.code}</pre>
<button
onClick={() => handleCopyCode(snippet.id, snippet.code)}
className="absolute right-4 top-4 bg-white/90 backdrop-blur border border-outline-variant shadow px-2.5 py-1 rounded text-[10px] font-mono font-bold hover:border-primary hover:text-primary transition-colors opacity-0 group-hover/code:opacity-100"
>
{copiedId === snippet.id ? 'COPIED ✓' : 'COPY'}
</button>
</div>
<div className="px-5 py-2 border-t border-outline-variant/20 bg-surface-container-lowest flex justify-between items-center text-[10px] text-outline font-semibold">
<span>Last adjusted {snippet.updatedAt}</span>
<span className="flex items-center gap-1 text-primary"><Zap className="w-3 h-3 fill-primary/10" /> Ready to inject to shell</span>
</div>
</div>
))
)}
</div>
</div>
{/* Add Snippet Modal Dialog */}
{showAddForm && (
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-[150] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl max-w-lg w-full border border-outline-variant shadow-2xl p-6 space-y-4 animate-scale-up">
<div className="flex justify-between items-center border-b border-outline-variant/30 pb-3">
<h3 className="font-bold text-lg text-on-surface">Store Custom Snippet</h3>
<button
onClick={() => setShowAddForm(false)}
className="text-outline hover:text-primary cursor-pointer p-1 rounded"
>
Cancel
</button>
</div>
<form onSubmit={handleCreateSnippet} className="space-y-4 text-sm">
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">SNIPPET TITLE</label>
<input
type="text"
required
placeholder="e.g. Purge Docker Cache"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">SHORT DESCRIPTION / USAGE</label>
<input
type="text"
placeholder="e.g. Safe cleanup to recover workspace disk capacity."
value={newDesc}
onChange={(e) => setNewDesc(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-medium"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">CODE SYNTAX BLOCK</label>
<textarea
required
rows={4}
placeholder="e.g. systemctl restart nginx"
value={newCode}
onChange={(e) => setNewCode(e.target.value)}
className="w-full bg-surface-container-low border border-outline-variant/40 rounded-lg py-2.5 px-4 focus:ring-2 focus:ring-primary/20 focus:border-primary focus:outline-none transition-all font-mono text-xs"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-bold text-on-surface-variant">PRIMARY TAG CATEGORY</label>
<select
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
className="bg-surface-container-low border border-outline-variant/40 rounded-lg py-2 px-3 focus:outline-none focus:border-primary"
>
<option value="DevOps">DevOps</option>
<option value="Docker">Docker</option>
<option value="Nginx">Nginx</option>
<option value="Git">Git</option>
<option value="SSL">SSL</option>
<option value="Security">Security</option>
<option value="Database">Database</option>
</select>
</div>
<div className="pt-4 flex justify-end gap-3 border-t border-outline-variant/20">
<button
type="button"
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-xs font-bold text-outline hover:text-on-surface"
>
Cancel
</button>
<button
type="submit"
className="bg-primary text-white py-2 px-5 rounded-lg font-bold text-xs shadow-md hover:bg-primary-container hover:text-on-primary-container transition-all"
>
Save Code Snippet
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
+304
View File
@@ -0,0 +1,304 @@
import React, { useState, useRef, useEffect } from 'react';
import { Terminal as TermIcon, TerminalSquare, AlertCircle, Plus, X, ArrowRight, CornerDownLeft, Play, RefreshCw, Layers } from 'lucide-react';
interface TerminalLine {
text: string;
type: 'input' | 'output' | 'error' | 'success';
}
interface Tab {
id: string;
title: string;
host: string;
}
export default function TerminalView() {
const [tabs, setTabs] = useState<Tab[]>([
{ id: '1', title: 'production-api-01', host: '10.0.1.201' },
{ id: '2', title: 'db-cluster-main', host: '10.0.2.14' }
]);
const [activeTabId, setActiveTabId] = useState('1');
const [commandInput, setCommandInput] = useState('');
// Separate history logs per tab
const [histories, setHistories] = useState<Record<string, TerminalLine[]>>({
'1': [
{ text: 'Connecting to production-api-01 (10.0.1.201) via port 22...', type: 'output' },
{ text: 'Using identity keychain file: id_ed25519_alex (strength: SECURE)', type: 'output' },
{ text: 'Welcome to Ubuntu 22.04 LTS (GNU/Linux 5.15.0-101-generic x86_64)', type: 'success' },
{ text: 'System load: 0.12 | Processes: 104 | Memory: 32% used', type: 'output' },
{ text: 'Type "help" to view custom interactive HostKeeper mock commands.', type: 'success' },
],
'2': [
{ text: 'Connecting to db-cluster-main (10.0.2.14) via port 5432...', type: 'output' },
{ text: 'Using identity password lookup: root_pg_prod (strength: STRONG)', type: 'output' },
{ text: 'PostgreSQL 15.3 (Debian 15.3-1.pgdg110+1) on x86_64-pc-linux-gnu', type: 'success' },
{ text: 'Type "help" to view custom database commands.', type: 'success' },
]
});
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [histories, activeTabId]);
const activeTab = tabs.find(t => t.id === activeTabId) || tabs[0];
const activeHistory = histories[activeTabId] || [];
const handleCommandSubmit = (e: React.FormEvent) => {
e.preventDefault();
const cmd = commandInput.trim();
if (!cmd) return;
// Add user command input line to logs
const newUserLine: TerminalLine = { text: `$ ${cmd}`, type: 'input' };
const currentHist = histories[activeTabId] || [];
let updatedLines = [...currentHist, newUserLine];
// Process commands
const cleanCmd = cmd.toLowerCase();
if (cleanCmd === 'clear') {
updatedLines = [];
} else if (cleanCmd === 'help') {
updatedLines.push(
{ text: 'HostKeeper Mock SSH Interactive Command Parser:', type: 'success' },
{ text: ' help - Display this support manifest list', type: 'output' },
{ text: ' ls - List contents of the current working directory', type: 'output' },
{ text: ' docker ps - List simulated running Docker containers on cluster', type: 'output' },
{ text: ' uname -a - Show operating system and machine kernel data', type: 'output' },
{ text: ' ping 8.8.8.8 - Probe network gateway performance', type: 'output' },
{ text: ' cat server.js - Output snippet of remote index server configuration', type: 'output' },
{ text: ' keychain - Query keychain credentials loaded for target session', type: 'output' },
{ text: ' clear - Wipe the terminal display buffer clean', type: 'output' }
);
} else if (cleanCmd === 'ls') {
updatedLines.push(
{ text: 'drwxr-xr-x 3 root root 4096 Jul 6 12:00 controllers', type: 'output' },
{ text: 'drwxr-xr-x 2 root root 4096 Jul 6 12:00 models', type: 'output' },
{ text: 'drwxr-xr-x 2 root root 4096 Jul 6 12:00 routes', type: 'output' },
{ text: '-rw-r--r-- 1 root root 280 Jul 6 11:34 .env', type: 'error' },
{ text: '-rw-r--r-- 1 root root 1432 Jul 6 14:20 package.json', type: 'output' },
{ text: '-rwxr-xr-x 1 root root 8412 Jul 6 15:43 server.js', type: 'success' }
);
} else if (cleanCmd === 'docker ps') {
updatedLines.push(
{ text: 'CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS', type: 'success' },
{ text: 'f87a2d12e9b0 node:18-alpine "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:3000->3000/tcp', type: 'output' },
{ text: 'c23e84bf9211 postgres:15-alpine "docker-entrypoint.s…" 5 hours ago Up 5 hours 0.0.0.0:5432->5432/tcp', type: 'output' },
{ text: '78da12b84e0c redis:7-alpine "docker-entrypoint.s…" 10 hours ago Up 10 hours 0.0.0.0:6379->6379/tcp', type: 'output' }
);
} else if (cleanCmd === 'uname -a') {
updatedLines.push(
{ text: `Linux ${activeTab.title} 5.15.0-101-generic #111-Ubuntu SMP Wed Jul 6 21:27:00 UTC 2026 x86_64 GNU/Linux`, type: 'output' }
);
} else if (cleanCmd.startsWith('ping')) {
updatedLines.push(
{ text: '64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=12.4 ms', type: 'output' },
{ text: '64 bytes from 8.8.8.8: icmp_seq=2 ttl=116 time=14.1 ms', type: 'output' },
{ text: '64 bytes from 8.8.8.8: icmp_seq=3 ttl=116 time=11.8 ms', type: 'output' },
{ text: '--- 8.8.8.8 ping statistics ---', type: 'success' },
{ text: '3 packets transmitted, 3 received, 0% packet loss, rtt min/avg/max = 11.8/12.76/14.1 ms', type: 'success' }
);
} else if (cleanCmd === 'cat server.js') {
updatedLines.push(
{ text: 'const express = require("express");', type: 'output' },
{ text: 'const app = express();', type: 'output' },
{ text: 'const PORT = process.env.PORT || 3000;', type: 'output' },
{ text: 'app.get("/api/health", (req, res) => res.send({ status: "healthy" }));', type: 'output' },
{ text: 'app.listen(PORT, () => console.log("Server active on cluster ingress"));', type: 'success' }
);
} else if (cleanCmd === 'keychain') {
updatedLines.push(
{ text: 'Keychain Credential Mapping Selected:', type: 'success' },
{ text: ` Active Key: id_ed25519_alex (ED25519 standard)`, type: 'output' },
{ text: ' Encryption: AES-256 GCM cryptokey payload', type: 'output' },
{ text: ' Fingerprint: SHA256:7mP9K9+fVj5bW0vQ8zD1y2u3t4m5n6p7q8r9s0v1w2x', type: 'output' }
);
} else {
updatedLines.push(
{ text: `hostkeeper: command not found: "${cmd}". Type "help" to view custom commands list.`, type: 'error' }
);
}
setHistories(prev => ({ ...prev, [activeTabId]: updatedLines }));
setCommandInput('');
};
const handleCreateTab = () => {
const title = prompt('Enter moniker or IP for the new tab:');
if (!title) return;
const newId = Date.now().toString();
const newTab: Tab = {
id: newId,
title: title.toLowerCase(),
host: '192.168.1.' + Math.floor(Math.random() * 254 + 1)
};
setTabs([...tabs, newTab]);
setHistories(prev => ({
...prev,
[newId]: [
{ text: `Spawning shell on customized terminal tab "${newTab.title}"...`, type: 'output' },
{ text: `Target host identifier: ${newTab.host}`, type: 'output' },
{ text: 'Type "help" to review simulation options.', type: 'success' }
]
}));
setActiveTabId(newId);
};
const handleCloseTab = (id: string, e: React.MouseEvent) => {
e.stopPropagation();
if (tabs.length === 1) return; // Keep at least one tab
const remaining = tabs.filter(t => t.id !== id);
setTabs(remaining);
if (activeTabId === id) {
setActiveTabId(remaining[0].id);
}
};
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-6 animate-fade-in flex flex-col h-[calc(100vh-120px)]">
{/* Session Tab Toolbar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-outline-variant/30 pb-3">
<div className="flex items-center gap-2 overflow-x-auto no-scrollbar scroll-smooth pr-6">
{tabs.map(tab => (
<div
key={tab.id}
onClick={() => setActiveTabId(tab.id)}
className={`flex items-center gap-2 px-3.5 py-2 rounded-t-xl text-xs font-bold font-mono transition-all cursor-pointer border-t-2 ${activeTabId === tab.id ? 'bg-white text-primary border-primary shadow-[0_-4px_12px_rgba(0,80,203,0.06)]' : 'bg-surface-container-low text-on-surface-variant hover:bg-surface-container border-transparent'}`}
>
<TerminalSquare className={`w-3.5 h-3.5 ${activeTabId === tab.id ? 'text-primary' : 'text-outline'}`} />
<span className="truncate max-w-[120px]">{tab.title}</span>
{tabs.length > 1 && (
<button
onClick={(e) => handleCloseTab(tab.id, e)}
className="p-0.5 hover:bg-black/10 rounded transition-colors"
>
<X className="w-3 h-3" />
</button>
)}
</div>
))}
<button
onClick={handleCreateTab}
className="p-1.5 bg-surface-container hover:bg-primary-container hover:text-on-primary-container text-on-surface-variant rounded-lg transition-all"
title="Open new terminal connection"
>
<Plus className="w-4 h-4" />
</button>
</div>
<div className="flex items-center gap-4 text-xs font-mono shrink-0">
<div className="flex items-center gap-1.5 text-secondary">
<span className="w-2 h-2 rounded-full bg-secondary animate-pulse"></span>
<span>SECURE CRYPTO PATH</span>
</div>
<div className="text-outline">
HOST: <span className="text-on-surface font-semibold">{activeTab.host}</span>
</div>
</div>
</div>
{/* Terminal shell frame */}
<div className="flex-1 bg-white border border-outline-variant rounded-2xl shadow-sm flex flex-col overflow-hidden">
{/* Terminal Header Accessories */}
<div className="px-4 py-2 bg-surface-container-low border-b border-outline-variant/30 flex justify-between items-center">
<div className="flex items-center gap-1.5">
<span className="w-3 h-3 rounded-full bg-error/80"></span>
<span className="w-3 h-3 rounded-full bg-primary-container/80"></span>
<span className="w-3 h-3 rounded-full bg-secondary/80"></span>
<span className="text-[10px] font-mono font-bold text-outline ml-2">ssh -i id_ed25519 root@{activeTab.host}</span>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => {
setHistories(prev => ({
...prev,
[activeTabId]: [
{ text: 'Session log display buffer cleared manually.', type: 'output' },
{ text: 'Type "help" to view interactive choices.', type: 'success' }
]
}));
}}
className="text-[10px] uppercase font-bold text-outline hover:text-primary transition-colors flex items-center gap-1"
>
<RefreshCw className="w-3 h-3" /> Clear Buffer
</button>
</div>
</div>
{/* Console display logs area */}
<div
ref={scrollRef}
className="flex-1 overflow-y-auto p-5 font-mono text-sm leading-relaxed space-y-2.5 custom-scrollbar bg-white"
>
{activeHistory.map((line, idx) => (
<div
key={idx}
className={`whitespace-pre-wrap ${
line.type === 'input' ? 'text-on-surface font-bold' :
line.type === 'error' ? 'text-error bg-error-container/20 px-2.5 py-1 rounded-md' :
line.type === 'success' ? 'text-[#006e2f] bg-secondary-container/10 px-2.5 py-1 rounded-md font-semibold' :
'text-[#2244aa]'
}`}
>
{line.text}
</div>
))}
{/* Virtual caret cursor anchor */}
<div className="h-4"></div>
</div>
{/* Input prompt line form */}
<form
onSubmit={handleCommandSubmit}
className="p-3 border-t border-outline-variant/40 bg-surface-container-low flex items-center gap-2.5"
>
<span className="font-mono text-xs font-bold text-primary pl-2 shrink-0 flex items-center gap-1">
<CornerDownLeft className="w-3.5 h-3.5" />
root@{activeTab.title}:~$
</span>
<input
type="text"
required
autoFocus
value={commandInput}
onChange={(e) => setCommandInput(e.target.value)}
placeholder='Type a command (e.g. "help", "ls", "docker ps", "ping google.com")...'
className="flex-1 bg-transparent border-none font-mono text-xs font-medium focus:ring-0 focus:outline-none text-on-surface p-0"
/>
<button
type="submit"
className="p-1.5 bg-primary text-white rounded-lg hover:bg-primary-container hover:text-on-primary-container transition-all"
title="Execute Command"
>
<Play className="w-3.5 h-3.5" />
</button>
</form>
</div>
{/* Bottom shortcut keys rail */}
<div className="flex flex-wrap items-center gap-2 text-[11px] font-mono font-bold text-on-surface-variant">
<span className="text-outline">SHORTCUT SUGGESTIONS:</span>
{['help', 'ls', 'docker ps', 'uname -a', 'ping 8.8.8.8', 'cat server.js', 'keychain', 'clear'].map(cmd => (
<button
key={cmd}
type="button"
onClick={() => {
setCommandInput(cmd);
}}
className="px-2.5 py-1 rounded-md bg-surface-container border border-outline-variant/30 hover:border-primary hover:bg-primary-container/10 transition-colors"
>
{cmd}
</button>
))}
</div>
</div>
);
}
+136
View File
@@ -0,0 +1,136 @@
@import "tailwindcss";
@theme {
--color-primary: #0050cb;
--color-on-primary: #ffffff;
--color-primary-container: #dae1ff;
--color-on-primary-container: #001849;
--color-secondary: #006e2f;
--color-on-secondary: #ffffff;
--color-secondary-container: #6bff8f;
--color-on-secondary-container: #007432;
--color-tertiary: #7e23cc;
--color-on-tertiary: #ffffff;
--color-tertiary-container: #9944e7;
--color-on-tertiary-container: #fef5ff;
--color-surface-container-lowest: #ffffff;
--color-surface-container-low: #f2f4f6;
--color-surface-container: #eceef0;
--color-surface-container-high: #e6e8ea;
--color-surface-container-highest: #e0e3e5;
--color-surface: #f7f9fb;
--color-on-surface: #191c1e;
--color-on-surface-variant: #424656;
--color-background: #f7f9fb;
--color-on-background: #191c1e;
--color-outline: #727687;
--color-outline-variant: #c2c6d8;
--color-error: #ba1a1a;
--color-on-error: #ffffff;
--color-error-container: #ffdad6;
--color-on-error-container: #93000a;
--color-on-tertiary-fixed-variant: #6900b3;
--color-secondary-fixed-dim: #4ae176;
--color-surface-bright: #f7f9fb;
--color-on-primary-fixed: #001849;
--color-on-tertiary-fixed: #2c0051;
--color-primary-fixed: #dae1ff;
--color-on-primary-fixed-variant: #003fa4;
--color-secondary-fixed: #6bff8f;
--color-surface-tint: #0054d6;
--color-inverse-surface: #2d3133;
--color-on-secondary-fixed: #002109;
--color-on-secondary-fixed-variant: #005321;
--color-inverse-primary: #b3c5ff;
--color-tertiary-fixed: #f0dbff;
--color-primary-fixed-dim: #b3c5ff;
--color-surface-dim: #d8dadc;
--color-tertiary-fixed-dim: #ddb7ff;
--color-inverse-on-surface: #eff1f3;
/* Fonts */
--font-sans: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
/* Base custom styles */
body {
font-family: var(--font-sans);
background-color: var(--color-background);
color: var(--color-on-background);
}
.dot-grid {
background-color: #f7f9fb;
background-image: radial-gradient(#e2e8f0 1.5px, transparent 1.5px);
background-size: 16px 16px;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
vertical-align: middle;
line-height: 1;
}
/* Frosted glass styles */
.glass-panel {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.glass-sidebar {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
/* Custom scrollbar for modern developer feel */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
/* Micro-animations */
@keyframes cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.terminal-cursor {
display: inline-block;
width: 8px;
height: 18px;
background-color: var(--color-primary);
vertical-align: middle;
animation: cursor-blink 1s infinite;
}
/* Hide scrollbar utility class */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
+10
View File
@@ -0,0 +1,10 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
+61
View File
@@ -0,0 +1,61 @@
export type ViewType = 'hosts' | 'sftp' | 'snippets' | 'keychain' | 'terminal' | 'settings';
export interface Host {
id: string;
name: string;
ip: string;
os: 'Ubuntu 22.04' | 'Debian 11' | 'Windows Server' | 'macOS Ventura' | 'CentOS 7' | 'Alpine Linux';
provider: 'AWS US-East' | 'GCP Cloud' | 'Hetzner' | 'Vercel Proxy' | 'Local Docker' | 'Bare Metal';
status: 'active' | 'offline';
lastSeen: string;
type: 'api' | 'db' | 'edge' | 'web' | 'desktop' | 'server';
}
export interface FileItem {
name: string;
size: string;
modified: string;
type: 'folder' | 'file' | 'image' | 'code' | 'lock';
extension?: string;
}
export interface Snippet {
id: string;
title: string;
description: string;
code: string;
tags: string[];
collection: 'favorites' | 'recent' | 'trash';
updatedAt: string;
icon: string;
}
export interface KeychainItem {
id: string;
name: string;
type: 'SSH RSA' | 'PASSWORD' | 'ED25519' | 'Bearer Token' | 'AWS Key';
user: string;
lastUsed: string;
strength: 'secure' | 'weak' | 'moderate';
fingerprint?: string;
passphrase?: string;
connectedHosts?: string[];
keyFile?: string;
}
export interface Device {
id: string;
name: string;
type: 'desktop' | 'tablet' | 'mobile';
status: 'online' | 'offline';
}
export interface BackgroundTransfer {
id: string;
fileName: string;
source: string;
destination: string;
progress: number;
speed: string;
type: 'upload' | 'download';
}
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}
+22
View File
@@ -0,0 +1,22 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig} from 'vite';
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify—file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
watch: process.env.DISABLE_HMR === 'true' ? null : {},
},
};
});