update
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
1
.kiro/specs/archnest-dashboard/.config.kiro
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"specId": "7d81eaca-5010-4445-bc59-f3a49742d6c3", "workflowType": "requirements-first", "specType": "feature"}
|
||||
194
.kiro/specs/archnest-dashboard/requirements.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
ArchNest Dashboard is a 6-page custom dashboard designed as a portfolio showcase piece. It provides system health monitoring, infrastructure management, network analytics, bookmark management, an embedded SSH terminal, and user settings — all unified under a dark/gold aesthetic. The application targets deployment on AWS infrastructure as a free, open-source tool with a potential paid tier for advanced features.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Dashboard**: The ArchNest single-page application providing all 6 pages of functionality
|
||||
- **Sidebar**: The fixed left navigation component (80px wide) containing the logo, nav items, and system status indicator
|
||||
- **Top_Bar**: The shared header component (56px height) displaying page title, search bar, notifications, and user info
|
||||
- **Hero_Banner**: A full-width banner image component shown on certain pages (Glance, Infrastructure, Network, BookNest)
|
||||
- **Card**: A styled container element with 12px border radius, dark background (#141518), and 1px border (#1E2025)
|
||||
- **Glance_Page**: The main operations dashboard showing system health overview at route `/`
|
||||
- **Infrastructure_Page**: The cloud infrastructure monitoring page at route `/infrastructure`
|
||||
- **Network_Page**: The network performance and security monitoring page at route `/network`
|
||||
- **BookNest_Page**: The digital library and bookmark management page at route `/booknest`
|
||||
- **Terminal_Page**: The embedded SSH terminal page at route `/terminal`
|
||||
- **Settings_Page**: The user preferences and system configuration page at route `/settings`
|
||||
- **Sparkline**: A small inline chart showing data trends without axes or labels
|
||||
- **Progress_Ring**: A circular progress indicator displaying percentage values
|
||||
- **Status_Indicator**: A colored dot (green/yellow/red) representing operational state
|
||||
- **Gold_Accent**: The primary accent color (#C8A434) used for active states, links, and highlights
|
||||
- **CDN**: The GitHub-hosted asset repository at `https://raw.githubusercontent.com/SamuelSJames/assets-public/master/`
|
||||
- **Terminal_Bridge**: The Node.js WebSocket-to-SSH bridge service enabling browser-based terminal connections
|
||||
- **Bookmark**: A stored link entry with name, URL, icon, category, and favorite status
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Application Shell and Navigation
|
||||
|
||||
**User Story:** As a user, I want a consistent navigation sidebar and top bar across all pages, so that I can move between dashboard sections efficiently.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL render a fixed left Sidebar of 80px width containing the ArchNest logo, 6 navigation items (Glance, Infrastructure, Network, BookNest, Terminal, Settings) each displaying an icon at 18px and a label at 14px, a collapse/expand toggle button, and a system status indicator at the bottom showing a colored dot (green for operational, orange for degraded, red for critical) with accompanying status text.
|
||||
2. WHEN a user clicks a navigation item in the Sidebar, THE Dashboard SHALL navigate to the corresponding route, highlight the active item with Gold_Accent color and a 3px left border, and remove the highlight from the previously active item.
|
||||
3. WHEN a user clicks the Sidebar collapse toggle, THE Sidebar SHALL collapse to icon-only mode at 50px width (hiding labels and showing only icons), and clicking the toggle again SHALL expand the Sidebar back to 80px with labels visible.
|
||||
4. THE Top_Bar SHALL display at a fixed height of 56px the current page title (bold, uppercase, 18px), a search bar (300px width, rounded), a notification bell icon, and a user avatar (36px with gold ring) with the user name and role label.
|
||||
5. IF the notification count is greater than zero, THEN THE Top_Bar SHALL display a badge on the notification bell icon showing the numeric count up to 99, and "99+" for counts exceeding 99.
|
||||
6. WHILE the viewport width is between 768px and 1200px, THE Sidebar SHALL auto-collapse to icon-only mode at 50px width and display a tooltip with the navigation item label when the user hovers over a navigation icon.
|
||||
7. WHILE the viewport width is below 768px, THE Sidebar SHALL transform into a bottom navigation bar displaying all 6 navigation items as icons with labels below each icon, and the system status indicator SHALL be hidden.
|
||||
8. IF a user navigates to an undefined route, THEN THE Dashboard SHALL redirect the user to the Glance page at the root route.
|
||||
|
||||
### Requirement 2: Global Design System
|
||||
|
||||
**User Story:** As a user, I want a cohesive dark theme with gold accents throughout the application, so that the interface is visually consistent and professional.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL use background color #0D0E10 for pages, #141518 for Cards, and #0A0B0D for the Sidebar.
|
||||
2. THE Dashboard SHALL use #C8A434 as the primary Gold_Accent color for links, active states, progress bars, and interactive highlights.
|
||||
3. THE Dashboard SHALL use #2ECC71 for success/operational states, #E67E22 for warnings, and #E74C3C for critical/error states.
|
||||
4. THE Dashboard SHALL render text in #E8E6E0 (primary, 14px body), #7A7D85 (secondary/labels, 12-13px), and #C8A434 (accent/active elements).
|
||||
5. THE Dashboard SHALL render Card titles at 12-13px, uppercase, with 1px letter-spacing in secondary color (#7A7D85), and large numbers at 28-36px bold in primary color (#E8E6E0).
|
||||
6. WHEN a user hovers over a Card, THE Dashboard SHALL transition the Card border color to Gold_Accent (#C8A434) over 0.2 seconds using ease timing.
|
||||
7. THE Dashboard SHALL apply 12px border radius, 1px solid #1E2025 border, 20-24px padding, and no box-shadow (flat design) to all Cards.
|
||||
8. WHEN a Card or interactive element receives keyboard focus, THE Dashboard SHALL display a visible Gold_Accent (#C8A434) outline of at least 2px to meet accessibility focus indicator requirements.
|
||||
|
||||
### Requirement 3: Hero Banner Display
|
||||
|
||||
**User Story:** As a user, I want a branded hero banner on main pages, so that the dashboard has visual identity and branding.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Hero_Banner SHALL display the archnest-brand.png image from the CDN at full width (100%) of the main content area with 12px border radius, overflow hidden, and the image scaled to cover the entire banner area while maintaining its aspect ratio.
|
||||
2. WHILE the Glance_Page is active, THE Hero_Banner SHALL render at 200px height.
|
||||
3. WHILE the Infrastructure_Page, Network_Page, or BookNest_Page is active, THE Hero_Banner SHALL render at 120px height.
|
||||
4. WHILE the Terminal_Page or Settings_Page is active, THE Dashboard SHALL hide the Hero_Banner.
|
||||
5. IF the hero banner image fails to load, THEN THE Hero_Banner SHALL display the banner container at the specified height with the card background color (#141518) and no broken-image icon visible.
|
||||
|
||||
### Requirement 4: Glance Page — System Overview
|
||||
|
||||
**User Story:** As a user, I want a high-level overview of system health, resources, alerts, and network traffic on the main page, so that I can assess operational status at a glance.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Glance_Page SHALL display a top row of 4 status Cards showing: System Status (percentage 0–100% with Progress_Ring, "Last checked: Xm ago" timestamp, and a gold Sparkline at the bottom showing recent health check trend), Infrastructure (server icon + resource count + "Total Resources" subtitle + colored dot breakdown: Running/Warning/Critical), Security (shield icon + alert count + "Active Alerts" subtitle + severity breakdown: Low/Medium/High), and Network (network icon + uptime percentage + "Network Uptime" subtitle + gold area Sparkline showing 24h uptime trend).
|
||||
2. THE Glance_Page SHALL display a middle row with 3 columns (30%/40%/30%): Resource Overview (card title + X close button, 5 progress bars each with an icon, label, gold fill bar, and "current / max" value), Recent Activity (card title + X close button, 5 timestamped events each showing an icon, event title, source/service subtitle, and relative timestamp), and Top Alerts (card title + "View all" link, up to 4 alerts each with severity-colored dot, alert name, source subtitle, and relative timestamp).
|
||||
3. THE Glance_Page SHALL display a bottom row with 2 columns (65%/35%): Network Traffic (card title, dark area chart with gold/amber gradient fill, stats showing Incoming Gbps with percentage change and Outgoing Gbps with percentage change) and Shortcuts (card title, 4 icon buttons in a 2x2 or 1x4 grid, each with an outlined icon in a bordered container and label below: Add Server, Create Backup, Deploy App, View Logs).
|
||||
4. THE Glance_Page footer SHALL display contextual stats: System Status percentage, Resource count, Alert count, and Uptime percentage, along with a "Last updated" timestamp on the right indicating time since last data refresh.
|
||||
5. IF data for any status Card or section fails to load, THEN THE Glance_Page SHALL display a loading skeleton placeholder within that Card or section and not block rendering of other sections.
|
||||
6. WHEN the Glance_Page loads, THE Progress_Ring and Sparkline SHALL animate their fill from 0 to the current value within 1 second.
|
||||
7. WHEN a user clicks the X close button on a Card (Resource Overview or Recent Activity), THE Glance_Page SHALL hide that Card from view for the current session.
|
||||
|
||||
### Requirement 5: Infrastructure Page — Resource Management
|
||||
|
||||
**User Story:** As a user, I want to monitor cloud infrastructure resources, costs, and health across regions, so that I can manage my cloud environment effectively.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Infrastructure_Page SHALL display sub-tabs with a maximum of 4 tabs visible at once (Overview, Compute, Storage, Database) and remaining tabs (Network, Containers, Load Balancers, DNS, Backups, Tags) accessible via a scrollable overflow menu, defaulting to the Overview tab as active.
|
||||
2. THE Infrastructure_Page SHALL display a top row of 5 status Cards: Total Resources (integer count with percentage trend and up/down arrow), Total Cost MTD (dollar amount formatted as $XX,XXX.XX with percentage trend), Resource Health (percentage 0–100% with trend), Active Alerts (integer count with percentage trend), and Uptime (percentage 0–100% with trend).
|
||||
3. THE Infrastructure_Page SHALL display a middle row with 3 columns (30%/40%/30%): Resource Distribution (donut chart showing category name and percentage share), Infrastructure Map (dark world map with gold glowing dots at region locations and connection lines, footer showing region count, AZ count, and resource count with a "Live" indicator), and Top Resources by Utilization (ranked list of up to 5 items showing instance name, type, and percentage utilization).
|
||||
4. THE Infrastructure_Page SHALL display a bottom row with 3 columns (35%/35%/30%): Resource Trend (multi-line area chart showing Compute, Storage, Database, and Network lines over a 7-day X-axis), Cost Breakdown MTD (donut chart showing service name, dollar amount, and percentage share), and Recent Activity (timestamped list of up to 5 events ordered newest-first).
|
||||
5. THE Infrastructure_Page SHALL provide an "+ Add Resource" action button positioned adjacent to the sub-tabs.
|
||||
6. THE Infrastructure_Page footer SHALL display contextual stats: provider name, region count, AZ count, resource count, health percentage, MTD cost, and alert count.
|
||||
|
||||
### Requirement 6: Network Page — Performance Monitoring
|
||||
|
||||
**User Story:** As a user, I want to monitor network performance, traffic patterns, and security threats, so that I can ensure network reliability and safety.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Network_Page SHALL display sub-tabs with a maximum of 4 tabs visible at once (Overview, Traffic, Topology, Devices) and remaining tabs (Interfaces, VPN, DNS, Firewalls, Load Balancers, Alerts, Reports) accessible via a scrollable overflow menu, defaulting to the Overview tab as active.
|
||||
2. THE Network_Page SHALL display a top row of 6 status Cards: Network Health (percentage, 0–100%), Total Traffic (throughput in Gbps or Tbps), Packet Loss (percentage, 0–100%), Active Connections (integer count), Threats Blocked (integer count with an orange warning icon displayed when count exceeds 0), and Average Latency (value in milliseconds).
|
||||
3. THE Network_Page SHALL display a middle row with 3 columns (25%/50%/25%): Top Talkers (ranked list of up to 5 items showing host name and throughput in Gbps), Network Topology (interactive map supporting click-and-drag pan, zoom in/out buttons, and a fullscreen toggle, with gold nodes and connection lines, and a legend indicating Live in green, Warning in orange, and Critical in red), and Interface Utilization (up to 5 interfaces with percentage bars) stacked with Alert Summary (counts grouped by Critical, Warning, and Info severity).
|
||||
4. THE Network_Page SHALL display a bottom row with 3 columns (35%/35%/30%): Traffic Over Time (area chart with inbound, outbound, and total lines over a 24-hour X-axis from 00:00 to 24:00, Y-axis ranging from 250 Gbps to 1.5 Tbps), Protocol Distribution (donut chart showing protocol name and percentage share), and Recent Events (timestamped list of up to 5 entries, ordered newest first).
|
||||
5. THE Network_Page SHALL provide a "Last 24 Hours" time-range dropdown and an "Export Report" action button positioned in the page action area adjacent to the sub-tabs.
|
||||
6. THE Network_Page footer SHALL display contextual stats: region count, site count, device count, interface count, connection count, and network health percentage.
|
||||
|
||||
### Requirement 7: BookNest Page — Bookmark Management
|
||||
|
||||
**User Story:** As a user, I want to organize, access, and monitor my bookmarks by category with health status tracking, so that I can efficiently manage my digital resources.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE BookNest_Page SHALL display page stats below the title showing total link count, category count, and favorites count in the format: "🔗 {count} Links | 📁 {count} Categories | ⭐ {count} Favorites".
|
||||
2. THE BookNest_Page SHALL display a Quick Access row with 5 category Cards (Infrastructure, Development, AI Tools, AWS, Networking), each showing up to 5 service icons from the CDN and the total link count for that category.
|
||||
3. THE BookNest_Page SHALL display bookmark groups in 2 rows of 4 columns each: Row 1 (Infrastructure & Self Hosted, Development & Code, Lab & Networking, AWS) and Row 2 (AI Tools, Learning, Finance, Life), each group displaying up to 20 Bookmark items.
|
||||
4. THE BookNest_Page SHALL display a right sidebar (25% width) with 4 widgets: Favorites list (maximum 10 items), Recently Used list showing the 5 most recently accessed Bookmarks with relative timestamps (e.g., "5m", "1h", "2h"), Link Health donut chart (Online/Warning/Offline), and Category Breakdown donut chart.
|
||||
5. WHEN a user clicks the star toggle on a Bookmark item, THE BookNest_Page SHALL add or remove the Bookmark from the Favorites list and render the star icon in Gold_Accent color when favorited or in secondary text color (#7A7D85) when not favorited.
|
||||
6. THE BookNest_Page SHALL render each Bookmark item with a colored icon (16-20px), name (14px), and star toggle aligned to the right.
|
||||
7. THE BookNest_Page SHALL classify each Bookmark's link health as "Online" when the link responds successfully, "Warning" when the link responds with degraded performance or non-success status, or "Offline" when the link is unreachable, and display the counts in the Link Health donut chart.
|
||||
8. IF a Bookmark's service icon fails to load from the CDN, THEN THE BookNest_Page SHALL display a generic fallback icon at the same dimensions (16-20px) without breaking the layout of the Bookmark item.
|
||||
|
||||
### Requirement 8: Terminal Page — Embedded SSH
|
||||
|
||||
**User Story:** As a user, I want an embedded SSH terminal in the browser with multiple session tabs, so that I can manage remote servers without leaving the dashboard.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Terminal_Page SHALL render a full-height terminal viewport using xterm.js with JetBrains Mono or Fira Code font at 14px, 5000-line scrollback, and gold block cursor with 500ms blink interval.
|
||||
2. THE Terminal_Page SHALL display a tab bar (40px height) at the top showing open sessions (maximum 10 simultaneous tabs) with host name and connection Status_Indicator (green for connected, yellow for connecting, gray for disconnected), with the active tab having a gold bottom border.
|
||||
3. WHEN a user clicks the "+" button in the tab bar, THE Terminal_Page SHALL display a quick-connect dropdown listing saved hosts (Pre, Proxmox, Linode, RackNerd 1, RackNerd 3, Studio).
|
||||
4. WHEN a user selects a saved host from the dropdown, THE Terminal_Page SHALL initiate an SSH connection through the Terminal_Bridge with a 10-second connection timeout and open a new terminal tab displaying the host name.
|
||||
5. THE Terminal_Page SHALL display a status bar (28px height) at the bottom showing connection state (Connecting, Connected, or Disconnected), host name, session duration in HH:MM:SS format, and shell type, with split-pane, fullscreen, and disconnect controls on the right.
|
||||
6. THE Terminal_Page SHALL apply syntax coloring: Gold_Accent for prompts, #E8E6E0 for commands, #7A7D85 for output, #E74C3C for errors, and #1ABC9C for directories.
|
||||
7. IF an SSH connection attempt fails or times out, THEN THE Terminal_Page SHALL display an error message indicating the failure reason in the terminal viewport and set the tab Status_Indicator to disconnected (gray).
|
||||
8. WHEN a user clicks the close button on a terminal tab, THE Terminal_Page SHALL terminate the SSH session for that tab and remove the tab from the tab bar, switching focus to the nearest remaining tab.
|
||||
9. IF the Terminal_Bridge service is unreachable, THEN THE Terminal_Page SHALL display an error message indicating the bridge is unavailable and disable the quick-connect dropdown until connectivity is restored.
|
||||
|
||||
### Requirement 9: Settings Page — Configuration
|
||||
|
||||
**User Story:** As a user, I want to configure my profile, appearance, integrations, and notification preferences, so that I can personalize the dashboard experience.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Settings_Page SHALL display a left navigation panel (200px) with sections: Profile, Appearance, Integrations, Notifications, Data & Backup, and About, with the active section highlighted using Gold_Accent color.
|
||||
2. THE Settings_Page Profile section SHALL display editable fields for avatar (64px with gold ring), display name (maximum 50 characters), role (maximum 30 characters), email (validated for standard email format), and timezone (selectable from standard timezone list), with a "Save Changes" button.
|
||||
3. THE Settings_Page Appearance section SHALL provide: a Dark/Light theme toggle, accent color swatches (Gold, Teal, Purple, Blue, Green, Red), a font size slider (12-16px in 1px increments), a sidebar expanded/collapsed toggle, an animations on/off toggle, and a card border radius slider (4-16px in 1px increments).
|
||||
4. THE Settings_Page Integrations section SHALL display configuration Cards for Proxmox, Docker, NetBird, Cloudflare, AWS, Uptime Kuma, and Weather API, each with a Status_Indicator (green for connected, red for disconnected), configuration fields, masked secrets with eye toggle for visibility, and a "Test Connection" button.
|
||||
5. WHEN a user clicks "Test Connection" on an integration Card, THE Settings_Page SHALL display a loading state during the connection attempt and then show a success indicator if the connection succeeds within 10 seconds.
|
||||
6. IF a "Test Connection" attempt fails or times out after 10 seconds, THEN THE Settings_Page SHALL display an error indicator with a message describing the failure reason.
|
||||
7. THE Settings_Page Notifications section SHALL provide toggles for enable/disable, threshold selection (All/Critical/Warning+), email notifications, browser push notifications, and sound with volume slider (0-100%).
|
||||
8. THE Settings_Page Data & Backup section SHALL provide actions for Export Bookmarks (JSON), Import Bookmarks (accepting JSON files up to 5MB), Export Settings, Reset to Defaults, and Clear Cache.
|
||||
9. WHEN a user clicks "Reset to Defaults", THE Settings_Page SHALL display a confirmation dialog requiring explicit user confirmation before executing the reset, and SHALL not proceed if the user dismisses or cancels the dialog.
|
||||
10. THE Settings_Page About section SHALL display application version, author name, repository link, technology stack, and license information.
|
||||
|
||||
### Requirement 10: Responsive Layout
|
||||
|
||||
**User Story:** As a user, I want the dashboard to adapt to different screen sizes, so that I can use the application on desktop, tablet, and mobile devices.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHILE the viewport width exceeds 1200px, THE Dashboard SHALL render the full layout with expanded Sidebar (80px), multi-column Card grids (up to 6 columns per row as defined per page), and all widgets visible.
|
||||
2. WHILE the viewport width is between 768px and 1200px, THE Dashboard SHALL collapse the Sidebar to icon-only mode (50px), arrange status Card rows in 2-column grids, and stack middle/bottom row columns vertically where they exceed 2 columns.
|
||||
3. WHILE the viewport width is below 768px, THE Dashboard SHALL replace the Sidebar with a bottom navigation bar (56px height), arrange all Cards in single-column layout, and hide the BookNest right sidebar widgets in favor of a toggleable overlay.
|
||||
4. THE Dashboard SHALL support a minimum viewport width of 320px without horizontal scrolling or content overflow.
|
||||
|
||||
### Requirement 11: Asset Loading from CDN
|
||||
|
||||
**User Story:** As a user, I want dashboard assets (logos, banners, service icons) to load reliably from the CDN, so that the interface renders correctly with all visual elements.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL load the ArchNest logo from `logos/brands/archnest-logo.png` on the CDN for the Sidebar.
|
||||
2. THE Dashboard SHALL load the hero banner image from `backgrounds/archnest-brand.png` on the CDN.
|
||||
3. THE BookNest_Page SHALL load service icons from the CDN repository for the following services: Proxmox, GitHub, GitLab, Gitea, AWS, Docker, Cloudflare, NetBird, CasaOS, Portainer, Cockpit, ChatGPT, Claude, GNS3, EVE-NG, and Wireshark.
|
||||
4. IF a CDN asset does not produce a successful image load event within 10 seconds, THEN THE Dashboard SHALL treat the asset as failed and render a fallback placeholder element that occupies the same dimensions as the intended asset, preserving the surrounding layout without visible reflow.
|
||||
5. IF a CDN asset fails to load, THEN THE Dashboard SHALL display a generic icon placeholder for service icons (matching the 16-20px icon size) or a solid background fill matching the Card background color for banner and logo assets.
|
||||
|
||||
### Requirement 12: State Management
|
||||
|
||||
**User Story:** As a user, I want my preferences, session data, and UI state to persist across navigation, so that the dashboard remembers my context.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL use Zustand for global state management of user preferences, active sessions, navigation state, and bookmark data.
|
||||
2. WHEN a user changes a setting in the Settings_Page, THE Dashboard SHALL persist the change to local storage and apply the setting to the UI within 100 milliseconds without triggering a full page reload.
|
||||
3. WHEN a user navigates between pages, THE Dashboard SHALL preserve and restore terminal session scrollback buffers, scroll positions (to the nearest pixel), and active sub-tab selections so that returning to a previously visited page displays the same state the user left.
|
||||
4. WHEN the Dashboard loads and local storage contains previously persisted state, THE Dashboard SHALL restore user preferences, theme settings, sidebar expansion state, bookmark favorites, notification preferences, and active sub-tab selections from the stored data within 200 milliseconds of app initialization.
|
||||
5. IF local storage is unavailable or the storage quota is exceeded, THEN THE Dashboard SHALL continue operating with in-memory state for the current session and display an indicator in the Settings_Page that persistence is degraded.
|
||||
6. IF stored state fails validation or is corrupt, THEN THE Dashboard SHALL discard the invalid data, apply default values for all preferences, and operate normally without displaying an error to the user.
|
||||
97
.kiro/steering/design-rules.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# ArchNest Design Rules — Must Follow on Every Page
|
||||
|
||||
These are hard-learned rules from the Glance page build. Apply to ALL pages.
|
||||
|
||||
---
|
||||
|
||||
## Sidebar
|
||||
|
||||
- **Expanded width**: 100px (NOT 80px)
|
||||
- **Collapsed width**: 60px (NOT 50px)
|
||||
- **Logo size**: 40px expanded, 28px collapsed — must have gold glow drop-shadow
|
||||
- **Logo text "ARCHNEST"**: 9px, bold, tracking 2.5px, gold, with subtle glow
|
||||
- **Nav icons**: 20px, strokeWidth 2 when active, 1.5 when inactive
|
||||
- **Nav labels**: 10px, below icon, font-medium
|
||||
- **Active indicator**: 3px gold left bar with glow shadow
|
||||
- **System status**: at bottom, green dot with glow, text 8-9px
|
||||
- **Manually collapsible** via toggle button (not just responsive)
|
||||
|
||||
## Top Bar
|
||||
|
||||
- **Page title**: 18px, bold, uppercase, tracking-wide, **TEXT COLOR IS GOLD (#C8A434)** — NOT white
|
||||
- **Height**: 56px, sticky, z-40
|
||||
- **Search bar**: 260px, 32px height, rounded-full, 12px text
|
||||
- **Avatar**: 36px, gold border + gold glow shadow, initials inside
|
||||
- **Dropdown menu**: Profile, Appearance, Security, Help & Support, Sign Out (red)
|
||||
|
||||
## Hero Banner (pages that have it: Glance, Infrastructure, Network, BookNest)
|
||||
|
||||
- **Image positioning**: `object-cover` with `object-position: center 30%` — shows upper portion (skyline), NOT centered
|
||||
- **KPI status cards OVERLAP the bottom** of the banner (negative margin -mt-12)
|
||||
- **Cards have backdrop-blur** and 95% opacity background for glass effect over banner
|
||||
- **Banner height**: 200px on Glance, 120px on sub-pages (Infra, Network, BookNest)
|
||||
- **No banner** on Terminal or Settings
|
||||
|
||||
## KPI Status Cards
|
||||
|
||||
- **Asymmetric widths**: Cards 1 and 4 are wider (1.3fr), cards 2 and 3 are narrower (1fr)
|
||||
- **Grid**: `grid-cols-[1.3fr_1fr_1fr_1.3fr]` with 12px gap
|
||||
- **Padding**: 16px (compact, not 20-24px)
|
||||
- **Font sizes**: Title 10px uppercase tracking 1.5px, numbers 24px bold, subtitles 10px, breakdowns 9px
|
||||
- **Icons in KPIs**: 16px, gold color
|
||||
- **Dots in breakdowns**: 6px (1.5 tailwind) diameter
|
||||
|
||||
## Content Rows
|
||||
|
||||
- **Must align width-wise** with the KPI cards above — use consistent horizontal padding (px-8 for middle/bottom rows matches px-6 + px-2 on the banner area)
|
||||
- **Middle row**: 3 columns 30%/40%/30%
|
||||
- **Bottom row**: 2 columns 65%/35%
|
||||
|
||||
## No Footer
|
||||
|
||||
- The Glance page has **NO footer/status bar** at the bottom
|
||||
- Other pages: check their specific .md spec
|
||||
|
||||
## Typography (exact sizes used)
|
||||
|
||||
| Element | Size | Weight | Color |
|
||||
|---------|------|--------|-------|
|
||||
| Page title | 18px | bold | gold |
|
||||
| Card titles | 10-11px | medium | secondary (#7A7D85) |
|
||||
| Large KPI numbers | 24px | bold | primary (#E8E6E0) |
|
||||
| KPI subtitles | 10px | normal | secondary |
|
||||
| Breakdown dots text | 9px | normal | secondary |
|
||||
| Activity titles | 13px | medium | primary |
|
||||
| Activity subtitles | 11px | normal | secondary |
|
||||
| Timestamps | 11px | normal | secondary |
|
||||
| Progress bar labels | 13px | normal | primary |
|
||||
| Progress bar values | 12px | normal | secondary |
|
||||
|
||||
## Card Styling
|
||||
|
||||
- Background: #141518 at 95% opacity (with backdrop-blur when over banner)
|
||||
- Border: 1px solid #1E2025
|
||||
- Radius: 12px
|
||||
- Padding: 16px for KPI cards, 20px for content cards
|
||||
- Hover: border → gold, 0.2s ease transition
|
||||
- **NO box-shadow** (flat design)
|
||||
|
||||
## Colors — Final
|
||||
|
||||
- Page bg: #0D0E10
|
||||
- Card bg: #141518
|
||||
- Sidebar bg: #0A0B0D
|
||||
- Border: #1E2025
|
||||
- Gold accent: #C8A434
|
||||
- Success: #2ECC71
|
||||
- Warning: #E67E22
|
||||
- Danger: #E74C3C
|
||||
- Text primary: #E8E6E0
|
||||
- Text secondary: #7A7D85
|
||||
- Teal: #1ABC9C
|
||||
|
||||
## Target Screen
|
||||
|
||||
- Primary design target: **16-inch display** (1920px+ viewport)
|
||||
- Should feel spacious, not cramped
|
||||
- Use the full width — don't constrain to a narrow container
|
||||
75
README.md
|
|
@ -1,2 +1,73 @@
|
|||
# archnest
|
||||
dashboard for the wise
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
|
|
|||
BIN
assets/archnest-hero-banner.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
assets/archnest-logo.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
assets/archnest-network-traffic-bg.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
82
design-decisions.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# ArchNest — Design Decisions & Lessons Learned
|
||||
|
||||
> This file captures all visual/UX decisions made during Glance page development.
|
||||
> Apply these consistently to ALL future pages to avoid repeated iteration.
|
||||
|
||||
---
|
||||
|
||||
## Global Rules (Apply to Every Page)
|
||||
|
||||
### Sidebar
|
||||
- **Expanded width**: 100px (not 80px — needs room for labels)
|
||||
- **Collapsed width**: 60px (icon only)
|
||||
- **User can manually collapse/expand** via toggle button (not just responsive)
|
||||
- **Main content margin-left** must match sidebar width exactly
|
||||
|
||||
### Page Title (Top Bar)
|
||||
- **Color**: Gold (#C8A434) — NOT white. Use inline `style={{ color: '#C8A434' }}` if Tailwind class doesn't apply
|
||||
- **Font**: 18px, bold, uppercase, tracking-wide
|
||||
- **No border on top bar** — blends into the page background
|
||||
|
||||
### Colors — Use Inline Styles When Tailwind Fails
|
||||
- Tailwind v4 `@theme` custom colors (text-gold, bg-card, etc.) may not always apply
|
||||
- If a color isn't rendering correctly, fall back to inline `style={{ color: '#C8A434' }}`
|
||||
- Always verify visually after changes
|
||||
|
||||
### Content Alignment
|
||||
- **All rows must share the same horizontal padding** (`px-6` applied once at the parent container level)
|
||||
- **Do NOT** use different padding for different rows — this causes misalignment
|
||||
- The hero banner, status cards, middle row, and bottom row must all line up left and right edges
|
||||
|
||||
### Hero Banner + KPI Overlap
|
||||
- Banner height: 200px
|
||||
- Status cards overlap via negative margin: `-mt-12`
|
||||
- Banner image: `object-cover` with `object-position: center 25%` (show the top/skyline, not center)
|
||||
- Cards use `backdrop-blur-sm` and `bg-card/95` for glass effect over the banner
|
||||
|
||||
### KPI Card Sizing
|
||||
- KPI 1 (System Status) and KPI 4 (Network): **wider** — `1.3fr`
|
||||
- KPI 2 (Infrastructure) and KPI 3 (Security): **standard** — `1fr`
|
||||
- Grid: `grid-cols-[1.3fr_1fr_1fr_1.3fr]`
|
||||
- Cards have compact padding: `p-4` (not p-5 or p-6)
|
||||
|
||||
### No Footer
|
||||
- The mockup does NOT have a footer/status bar
|
||||
- Do not add one unless explicitly requested
|
||||
|
||||
### Target Display
|
||||
- Primary design target: **16-inch screen / 1920px width**
|
||||
- Lots of horizontal space available — don't constrain content width unnecessarily
|
||||
- Design should feel spacious, not cramped
|
||||
|
||||
### Typography Sizes (smaller than default)
|
||||
- Card titles: 10-11px, uppercase, tracking-[1.5px], secondary color, font-medium
|
||||
- Large numbers: 24-28px, bold, primary color
|
||||
- Subtitles/labels: 10-11px, secondary color
|
||||
- Body text in lists: 13px, primary color
|
||||
- Timestamps: 11px, secondary color
|
||||
- Breakdowns: 9-10px, secondary color
|
||||
|
||||
### Animations
|
||||
- Card hover: border → gold, 0.2s ease
|
||||
- Progress ring: animates from 0 to value in 1s
|
||||
- Sparklines: draw animation 1s
|
||||
- Progress bars: fill animation 0.8s
|
||||
|
||||
### Icons
|
||||
- Source: Lucide React (imported per component, tree-shaken)
|
||||
- Size: 14-18px depending on context
|
||||
- Color: gold for active/accent, text-secondary for inactive
|
||||
- Gold glow on active sidebar items: `shadow-[0_0_6px_rgba(200,164,52,0.5)]`
|
||||
|
||||
---
|
||||
|
||||
## Page-Specific Notes
|
||||
|
||||
### Glance Page
|
||||
- No footer
|
||||
- Status cards overlap hero banner
|
||||
- Middle row: 3 equal-ish columns (30/40/30)
|
||||
- Bottom row: 2 columns (65/35)
|
||||
- Network Traffic card has its own background image at low opacity
|
||||
- User avatar dropdown has: Profile, Appearance, Security, Help & Support, Sign Out
|
||||
22
eslint.config.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
356
glance.md
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
# Glance Page — Detailed Specification
|
||||
|
||||
> Purpose: The Glance page is the operational heartbeat of ArchNest. It provides an at-a-glance view of system health, resource utilization, security posture, and network connectivity across your homelab infrastructure. Every element answers a simple question: "Is everything okay right now?"
|
||||
|
||||
---
|
||||
|
||||
## Layout Structure
|
||||
|
||||
The Glance page uses this vertical stack (top to bottom):
|
||||
1. **Top Bar** (sticky, 56px)
|
||||
2. **Hero Banner** (200px, rounded, with KPI cards overlapping the bottom ~25%)
|
||||
3. **Middle Row** (3 columns: 30% | 40% | 30%)
|
||||
4. **Bottom Row** (2 columns: 65% | 35%)
|
||||
|
||||
**No footer.** The page scrolls naturally without a fixed status bar at the bottom.
|
||||
|
||||
---
|
||||
|
||||
## Top Bar
|
||||
|
||||
**Display:**
|
||||
- Height: 56px, sticky at top, z-index above all content
|
||||
- Background: Page color (#0D0E10), no border
|
||||
- Left: Page title "GLANCE" (18px, bold, uppercase, primary text with subtle gold drop-shadow glow)
|
||||
- Center-right: Search bar (260px, rounded-full, placeholder "Search resources...", card background)
|
||||
- Right: Notification bell (with red badge count) + User avatar + dropdown trigger
|
||||
|
||||
### User Avatar & Dropdown Menu
|
||||
|
||||
**Avatar Display:**
|
||||
- 32px circle with 2px gold border and subtle gold glow
|
||||
- Shows initials "AO" in gold
|
||||
- Adjacent: "ArchNest Ops" (12px, primary) + "Administrator" (9px, secondary)
|
||||
- Chevron icon (rotates on open)
|
||||
|
||||
**Dropdown Menu (on click):**
|
||||
- Position: Below avatar, aligned right
|
||||
- Background: Card color with border, rounded-xl, shadow
|
||||
- Header section: User name + email
|
||||
- Menu items:
|
||||
- **Profile** — navigates to Settings > Profile section
|
||||
- **Appearance** — navigates to Settings > Appearance section
|
||||
- **Security** — navigates to Settings > Security/Integrations section
|
||||
- **Help & Support** — opens documentation/wiki link
|
||||
- Divider
|
||||
- **Sign Out** — logs out of session (red text, danger action)
|
||||
|
||||
---
|
||||
|
||||
## Hero Banner
|
||||
|
||||
**Display:**
|
||||
- Full width of content area, 200px height, 12px border radius, overflow hidden
|
||||
- Image: `archnest-hero-banner.png` from `/public` (local) or CDN
|
||||
- Image positioning: `object-cover` with `object-position: center 30%` — prioritizes showing the upper portion (skyline/arch) rather than center-cropping
|
||||
- The bottom ~25% of the banner is overlapped by the KPI status cards (negative margin)
|
||||
- If image fails to load: shows card background color (#141518), no broken icon
|
||||
|
||||
---
|
||||
|
||||
## Top Row — Status KPI Cards (4 cards)
|
||||
|
||||
### Card Grid Layout
|
||||
|
||||
**IMPORTANT — Asymmetric widths:**
|
||||
- KPI 1 (System Status): **1.3fr** — wider, has progress ring + sparkline
|
||||
- KPI 2 (Infrastructure): **1fr** — standard width
|
||||
- KPI 3 (Security): **1fr** — standard width
|
||||
- KPI 4 (Network): **1.3fr** — wider, has sparkline chart
|
||||
|
||||
Grid: `grid-cols-[1.3fr_1fr_1fr_1.3fr]` with 12px gap.
|
||||
|
||||
**Card styling (all 4):**
|
||||
- Background: `#141518` at 95% opacity with backdrop-blur (glass effect over banner)
|
||||
- Border: 1px solid #1E2025, 12px radius
|
||||
- Padding: 16px
|
||||
- Hover: border transitions to gold (0.2s)
|
||||
|
||||
---
|
||||
|
||||
### 1. System Status
|
||||
|
||||
**What it represents:** Overall system health — a composite "green light" that confirms all critical services are reachable and all local packages/security updates are current.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Ping sweep**: Every 5 minutes, the backend pings all endpoints defined in a `systems.config` file (IP addresses of LXC containers, VMs, physical hosts, and key services). If all respond, health = 100%.
|
||||
- **Package currency check**: Queries `apt` (or equivalent) on monitored hosts for pending security updates. Any pending security update reduces the percentage.
|
||||
- **Calculation**: `(reachable_hosts / total_hosts) * weight_A + (up_to_date_hosts / total_hosts) * weight_B`. Default weights: 70% reachability, 30% package currency.
|
||||
|
||||
**Display:**
|
||||
- Card title: "SYSTEM STATUS" (10px, uppercase, tracking 1.5px, secondary color, font-medium)
|
||||
- Layout: Title top, then flex row with text left and ring right
|
||||
- Left content:
|
||||
- "All Systems" (13px, bold, primary)
|
||||
- "Operational" (13px, bold, gold, italic)
|
||||
- Right content: Progress Ring (44px diameter, 3px stroke, gold on dark track)
|
||||
- Divider: 1px border-top, border/60 opacity
|
||||
- Below divider: Sparkline (gold line chart, 20px height, no axes, showing last 12 check results)
|
||||
- Footer text: "Last checked: 2m ago" (9px, secondary)
|
||||
|
||||
**Thresholds:**
|
||||
- 100%: "All Systems Operational" (gold text)
|
||||
- 80–99%: "Degraded" (orange text)
|
||||
- Below 80%: "Critical" (red text)
|
||||
|
||||
---
|
||||
|
||||
### 2. Infrastructure
|
||||
|
||||
**What it represents:** Total count of managed resources (LXC containers, VMs, Docker containers, bare-metal hosts) and their operational state.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Config-driven**: Reads from `infra.config` which contains API endpoints, SSH keys, and connection details for each resource (Proxmox API for LXC/VMs, Docker socket/API for containers, SSH for bare-metal).
|
||||
- **Status polling**: Every 5 minutes, queries each resource's API or SSH to determine if it's running, stopped, or unreachable.
|
||||
- **Disk usage check**: Pulls primary disk utilization from each resource. Any resource exceeding 70% disk usage triggers a warning signal.
|
||||
|
||||
**Display:**
|
||||
- Card title: "INFRASTRUCTURE" (10px, uppercase, tracking 1.5px, secondary color)
|
||||
- Icon + number row: Server icon (16px, gold) + "24" (24px, bold, primary)
|
||||
- Subtitle: "Total Resources" (10px, secondary)
|
||||
- Divider
|
||||
- Breakdown row (9px): green dot + "24 Running" | yellow dot + "0 Warning" | red dot + "0 Critical"
|
||||
|
||||
**Signal logic:**
|
||||
- 🟢 Running: Resource is responsive and disk < 70%
|
||||
- 🟡 Warning: Resource is responsive but disk ≥ 70%, or resource response is slow (>2s)
|
||||
- 🔴 Critical: Resource is unreachable or disk ≥ 90%
|
||||
|
||||
---
|
||||
|
||||
### 3. Security
|
||||
|
||||
**What it represents:** Active security alerts — failed intrusion attempts, brute-force attacks detected by fail2ban, and outdated security packages.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Fail2ban logs**: Queries fail2ban on each monitored host for currently banned IPs and recent ban events (last 24h).
|
||||
- **Auth log monitoring**: Parses `/var/log/auth.log` (or equivalent) for failed login attempts exceeding a threshold (e.g., 5+ failures from same IP in 10 minutes).
|
||||
- **Security package check**: Counts hosts with pending security-specific updates (`apt list --upgradable` filtered to security repo).
|
||||
- **Alert count**: Sum of active fail2ban bans + hosts with outdated security packages.
|
||||
|
||||
**Display:**
|
||||
- Card title: "SECURITY" (10px, uppercase, tracking 1.5px, secondary color)
|
||||
- Icon + number row: Shield icon (16px, gold) + "2" (24px, bold, primary)
|
||||
- Subtitle: "Active Alerts" (10px, secondary)
|
||||
- Divider
|
||||
- Breakdown row (9px): green dot + "2 Low" | yellow dot + "0 Medium" | red dot + "0 High"
|
||||
|
||||
**Severity logic:**
|
||||
- Low (green): Informational — single failed login attempt, package update available
|
||||
- Medium (yellow): Multiple failed attempts from same IP, security package >7 days overdue
|
||||
- High (red): Active brute-force attack (10+ attempts in 5 min), critical vulnerability unpatched >14 days
|
||||
|
||||
---
|
||||
|
||||
### 4. Network
|
||||
|
||||
**What it represents:** Local network uptime — confirms internet connectivity and DNS resolution are functioning.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Ping probes**: Every 60 seconds, pings multiple resolvers (e.g., 1.1.1.1, 8.8.8.8, and one internal DNS) from the ArchNest host.
|
||||
- **Uptime calculation**: Tracks successful/failed pings over a rolling 24-hour window. Uptime % = (successful_pings / total_pings) * 100.
|
||||
- **Sparkline data**: Stores the last 24 data points (one per hour) for the mini trend chart.
|
||||
|
||||
**Display:**
|
||||
- Card title: "NETWORK" (10px, uppercase, tracking 1.5px, secondary color)
|
||||
- Icon + number row: Network icon (16px, gold) + "98.7%" (24px, bold, primary)
|
||||
- Subtitle: "Network Uptime" (10px, secondary)
|
||||
- Divider
|
||||
- Below divider: Area sparkline chart (gold gradient fill, 24px height, no axes, 24 data points representing hourly uptime)
|
||||
|
||||
**Thresholds:**
|
||||
- ≥99%: Healthy (no indicator needed)
|
||||
- 95–99%: Minor instability (orange sparkline highlight)
|
||||
- <95%: Degraded (red sparkline highlight, triggers alert)
|
||||
|
||||
---
|
||||
|
||||
## Middle Row — Detail Panels (3 columns: 30% | 40% | 30%)
|
||||
|
||||
### 5. Resource Overview (left, 30%)
|
||||
|
||||
**What it represents:** Top 5 resource utilization metrics across your infrastructure — a quick view of where capacity is being consumed.
|
||||
|
||||
**How it gets its data:**
|
||||
- Pulls from the same infrastructure polling that feeds the Infrastructure KPI
|
||||
- **Compute**: Running LXC/VM count vs total allocated slots (from infra.config)
|
||||
- **Storage**: Sum of used disk across all storage pools vs total capacity (e.g., Proxmox storage API, `df` on hosts)
|
||||
- **Database**: Active database instances vs provisioned (PostgreSQL/MariaDB connection count or instance count)
|
||||
- **Network**: Same uptime % from the Network KPI (displayed as a bar for consistency)
|
||||
- **Containers**: Running Docker containers vs total defined in docker-compose or container configs
|
||||
|
||||
**Display:**
|
||||
- Card title: "RESOURCE OVERVIEW" (uppercase, secondary color)
|
||||
- Close button (X) in top-right corner
|
||||
- 5 rows, each containing:
|
||||
- Icon (category-specific, 16px, secondary color)
|
||||
- Label (e.g., "Compute", "Storage") — 13px, primary color
|
||||
- Progress bar (gold fill on dark track, rounded ends)
|
||||
- Value text (e.g., "18 / 24" or "12.4 / 20 TB") — 12px, secondary color
|
||||
|
||||
**Bar color logic:**
|
||||
- 0–69%: Gold (#C8A434)
|
||||
- 70–89%: Warning orange (#E67E22)
|
||||
- 90–100%: Danger red (#E74C3C)
|
||||
|
||||
---
|
||||
|
||||
### 6. Recent Activity (center, 40%)
|
||||
|
||||
**What it represents:** A chronological feed of the most recent system events — gives context on what just happened across the infrastructure.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Event aggregation**: Collects events from multiple sources:
|
||||
- Backup completion notifications (from cron jobs or backup tools like restic/borgbackup)
|
||||
- Security scan results (from scheduled ClamAV/rkhunter scans)
|
||||
- Instance state changes (container start/stop/create from Proxmox/Docker APIs)
|
||||
- Configuration changes (git commits to infra-as-code repos, or config file modification timestamps)
|
||||
- Authentication events (successful logins from auth.log)
|
||||
- **Storage**: Events stored in a lightweight local database (SQLite or JSON file) with timestamp, type, title, source, and severity.
|
||||
|
||||
**Display:**
|
||||
- Card title: "RECENT ACTIVITY" (uppercase, secondary color)
|
||||
- Close button (X) in top-right corner
|
||||
- 5 items, each containing:
|
||||
- Icon (event-type-specific: checkmark for completion, shield for security, play for launch, gear for config, user for login) — 14px, in a small rounded container (28px, page background)
|
||||
- Event title (13px, bold, primary color) — e.g., "Backup completed"
|
||||
- Source subtitle (11px, secondary color) — e.g., "Database Cluster 01"
|
||||
- Relative timestamp (11px, secondary color, right-aligned) — e.g., "2m ago"
|
||||
|
||||
**Event ordering:** Newest first, max 5 displayed.
|
||||
|
||||
---
|
||||
|
||||
### 7. Top Alerts (right, 30%)
|
||||
|
||||
**What it represents:** The most urgent issues requiring attention — prioritized by severity then recency.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Alert aggregation**: Combines alerts from:
|
||||
- High CPU/RAM usage events (from resource polling — any resource >85% CPU or >90% RAM for 5+ minutes)
|
||||
- Disk space warnings (from Infrastructure polling — any disk >70%)
|
||||
- Security events (from fail2ban/auth.log — active attacks)
|
||||
- SSL certificate expiry (checks cert expiry dates for tracked domains, alerts at 30/14/7 days)
|
||||
- Service down events (from System Status polling — any unreachable service)
|
||||
|
||||
**Display:**
|
||||
- Card title: "TOP ALERTS" (uppercase, secondary color)
|
||||
- "View all" link (gold, top-right) — navigates to a full alerts view
|
||||
- Up to 4 items, each containing:
|
||||
- Severity dot (🔴 red/large for high, 🟡 yellow for medium) — 6px diameter
|
||||
- Alert title (13px, primary color, font-medium) — e.g., "High CPU Usage"
|
||||
- Source subtitle (11px, secondary color) — e.g., "App Server 02"
|
||||
- Relative timestamp (11px, secondary color, right-aligned) — e.g., "2m ago"
|
||||
|
||||
**Sort order:** High severity first, then by recency within same severity level.
|
||||
|
||||
---
|
||||
|
||||
## Bottom Row — Charts & Actions (2 columns: 65% | 35%)
|
||||
|
||||
### 8. Network Traffic (left, 65%)
|
||||
|
||||
**What it represents:** Visual representation of network throughput over the last 24 hours — helps spot unusual spikes or drops in traffic.
|
||||
|
||||
**How it gets its data:**
|
||||
- **Interface monitoring**: Reads network interface stats (bytes in/out) from the primary gateway or router. Options:
|
||||
- SNMP polling from router/switch
|
||||
- `vnstat` on the ArchNest host or gateway
|
||||
- Proxmox node network stats API
|
||||
- **Sampling**: Records bytes in/out every 5 minutes, calculates Mbps/Gbps rate
|
||||
- **Current values**: Latest sample provides the "Incoming X.XX Gbps" and "Outgoing X.XX Gbps" figures
|
||||
- **Trend calculation**: Compares current hour's average to same hour yesterday for the percentage change (↑/↓)
|
||||
|
||||
**Display:**
|
||||
- Card title: "NETWORK TRAFFIC" (uppercase, secondary color)
|
||||
- Background: Custom background image (`archnest-network-traffic-bg.png`) rendered at ~20% opacity behind the chart, giving the card a unique visual identity
|
||||
- Area chart (overlaid on background):
|
||||
- Fill: Gold/amber gradient (top: #C8A434 at ~30% opacity, fading to transparent)
|
||||
- Line: Gold (#C8A434) for inbound, amber (#E67E22) for outbound
|
||||
- X-axis: 24-hour span (no visible labels)
|
||||
- Y-axis: Auto-scaled (no visible labels)
|
||||
- Stats (right side of chart, vertically stacked):
|
||||
- "Incoming" label (11px, secondary) + "1.23 Gbps" (18px, bold, primary) + "↓ 12.4%" (11px, red)
|
||||
- "Outgoing" label (11px, secondary) + "1.08 Gbps" (18px, bold, primary) + "↑ 8.7%" (11px, green)
|
||||
|
||||
---
|
||||
|
||||
### 9. Shortcuts (right, 35%)
|
||||
|
||||
**What it represents:** Quick-action buttons for common administrative tasks — one-click access to frequent operations.
|
||||
|
||||
**How it works:**
|
||||
- Each shortcut triggers a predefined action or navigates to a specific workflow:
|
||||
- **Add Server**: Opens a form/modal to add a new resource to `infra.config` (host IP, type, credentials)
|
||||
- **Create Backup**: Triggers an on-demand backup job for a selected resource or all resources
|
||||
- **Deploy App**: Opens a deployment workflow (e.g., pull latest docker-compose, restart containers)
|
||||
- **View Logs**: Navigates to a log viewer or opens a terminal session with log tailing
|
||||
|
||||
**Display:**
|
||||
- Card title: "SHORTCUTS" (uppercase, secondary color)
|
||||
- 4 buttons in a horizontal row:
|
||||
- Each button: Outlined/stroked icon inside a bordered rounded container (40px)
|
||||
- Icon style: Lucide outlined icons (18px), secondary color, gold on hover
|
||||
- Label below icon: 10px, secondary color, centered
|
||||
- Container: 1px border (#1E2025), 8px radius, hover → gold border + gold icon
|
||||
|
||||
---
|
||||
|
||||
## Data Refresh & Polling Summary
|
||||
|
||||
| Data Source | Poll Interval | Used By |
|
||||
|-------------|---------------|---------|
|
||||
| System ping sweep | 5 minutes | System Status KPI |
|
||||
| Package update check | 1 hour | System Status KPI |
|
||||
| Infrastructure resource status | 5 minutes | Infrastructure KPI, Resource Overview |
|
||||
| Disk usage per resource | 5 minutes | Infrastructure KPI, Resource Overview, Top Alerts |
|
||||
| Fail2ban / auth.log | 2 minutes | Security KPI, Top Alerts |
|
||||
| Security package check | 1 hour | Security KPI |
|
||||
| Network ping probes | 60 seconds | Network KPI |
|
||||
| Network interface throughput | 5 minutes | Network Traffic chart |
|
||||
| Event log (activity) | Real-time (push) or 1 minute | Recent Activity |
|
||||
| SSL cert expiry | 24 hours | Top Alerts |
|
||||
| CPU/RAM per resource | 5 minutes | Top Alerts |
|
||||
|
||||
---
|
||||
|
||||
## Configuration Dependencies
|
||||
|
||||
| Config File | Purpose |
|
||||
|-------------|---------|
|
||||
| `systems.config` | List of IPs/hosts to ping for System Status |
|
||||
| `infra.config` | Resource definitions with API endpoints, SSH keys, connection types |
|
||||
| `alerts.config` | Threshold definitions (disk %, CPU %, failed login count, cert days) |
|
||||
| `network.config` | Resolver IPs to ping, interface to monitor for traffic |
|
||||
|
||||
---
|
||||
|
||||
## Interaction Behaviors
|
||||
|
||||
| Element | Action | Result |
|
||||
|---------|--------|--------|
|
||||
| X button (Resource Overview) | Click | Hides the card for current session |
|
||||
| X button (Recent Activity) | Click | Hides the card for current session |
|
||||
| "View all" (Top Alerts) | Click | Navigates to full alerts list view |
|
||||
| Shortcut button | Click | Triggers associated action/workflow |
|
||||
| Status card | Hover | Gold border transition (0.2s) |
|
||||
| Progress ring | Page load | Animates from 0 to value (1s) |
|
||||
| Sparkline | Page load | Draws line animation (1s) |
|
||||
| Progress bars | Page load | Fill animation (0.8s staggered) |
|
||||
| User avatar/chevron | Click | Opens/closes user dropdown menu |
|
||||
| Dropdown: Profile | Click | Navigates to Settings > Profile |
|
||||
| Dropdown: Appearance | Click | Navigates to Settings > Appearance |
|
||||
| Dropdown: Security | Click | Navigates to Settings > Security |
|
||||
| Dropdown: Help & Support | Click | Opens docs/wiki link |
|
||||
| Dropdown: Sign Out | Click | Ends session, returns to login |
|
||||
| Click outside dropdown | Click | Closes the dropdown menu |
|
||||
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>archnest</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3413
package-lock.json
generated
Normal file
34
package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "archnest",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwindcss": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.12.3",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.12"
|
||||
}
|
||||
}
|
||||
BIN
public/archnest-hero-banner.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
public/archnest-logo.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
public/archnest-network-traffic-bg.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
1
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
24
public/icons.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
70
src/App.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useState } from 'react'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import TopBar from './components/TopBar'
|
||||
import StatusCards from './components/StatusCards'
|
||||
import MiddleRow from './components/MiddleRow'
|
||||
import BottomRow from './components/BottomRow'
|
||||
|
||||
function App() {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
const sidebarWidth = sidebarCollapsed ? 60 : 140
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-screen overflow-hidden bg-page">
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
/>
|
||||
|
||||
<main
|
||||
className="h-screen overflow-hidden"
|
||||
style={{ marginLeft: `${sidebarWidth}px`, width: `calc(100vw - ${sidebarWidth}px)` }}
|
||||
>
|
||||
<TopBar />
|
||||
|
||||
<section
|
||||
className="w-full overflow-y-auto"
|
||||
style={{ height: 'calc(100vh - 56px)', scrollbarWidth: 'none', padding: '16px 24px 32px 24px' }}
|
||||
>
|
||||
<div className="flex w-full max-w-none flex-col gap-0">
|
||||
{/* Hero + KPI overlap — KPI bottom aligns with banner bottom */}
|
||||
<div className="relative">
|
||||
<div className="w-full overflow-hidden" style={{ borderRadius: '12px 12px 0 0' }}>
|
||||
<img
|
||||
src="/archnest-hero-banner.png"
|
||||
alt="ArchNest Banner"
|
||||
className="w-full"
|
||||
style={{ display: 'block' }}
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget
|
||||
target.style.display = 'none'
|
||||
target.parentElement!.classList.add('bg-card')
|
||||
target.parentElement!.style.height = '260px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* KPI cards positioned so their bottom edge aligns with banner bottom */}
|
||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-4">
|
||||
<StatusCards />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 24px breathing room between KPI row and middle row */}
|
||||
<div style={{ height: '24px' }} />
|
||||
|
||||
{/* Middle Row */}
|
||||
<MiddleRow />
|
||||
|
||||
{/* Gap */}
|
||||
<div style={{ height: '24px' }} />
|
||||
|
||||
{/* Bottom Row */}
|
||||
<BottomRow />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
BIN
src/assets/hero.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
1
src/assets/vite.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
106
src/components/BottomRow.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { AreaChart, Area, ResponsiveContainer } from 'recharts'
|
||||
import { ServerCog, DatabaseBackup, Rocket, FileText } from 'lucide-react'
|
||||
|
||||
const trafficData = Array.from({ length: 48 }, (_, i) => ({
|
||||
time: i,
|
||||
incoming: 800 + Math.sin(i / 6) * 300 + Math.random() * 150,
|
||||
outgoing: 700 + Math.cos(i / 8) * 250 + Math.random() * 100,
|
||||
}))
|
||||
|
||||
const shortcuts = [
|
||||
{ icon: ServerCog, label: 'Add Server' },
|
||||
{ icon: DatabaseBackup, label: 'Create Backup' },
|
||||
{ icon: Rocket, label: 'Deploy App' },
|
||||
{ icon: FileText, label: 'View Logs' },
|
||||
]
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)',
|
||||
transition: 'border-color 0.2s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
export default function BottomRow() {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-[1.8fr_1fr] gap-6">
|
||||
{/* Network Traffic */}
|
||||
<div style={cardBase} className="hover:!border-gold/15">
|
||||
{/* Background image at very low opacity */}
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.12, backgroundImage: 'url(/archnest-network-traffic-bg.png)', backgroundSize: 'cover', backgroundPosition: 'center', pointerEvents: 'none' }} />
|
||||
{/* Gold top edge */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||
Network Traffic
|
||||
</h3>
|
||||
<div className="flex items-end gap-6">
|
||||
<div style={{ flex: 1, height: '100px' }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={trafficData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="trafficGold" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#C8A434" stopOpacity={0.25} />
|
||||
<stop offset="100%" stopColor="#C8A434" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
<linearGradient id="trafficAmber" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#E67E22" stopOpacity={0.15} />
|
||||
<stop offset="100%" stopColor="#E67E22" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area type="monotone" dataKey="incoming" stroke="#C8A434" strokeWidth={1.5} fill="url(#trafficGold)" dot={false} isAnimationActive={true} animationDuration={1200} />
|
||||
<Area type="monotone" dataKey="outgoing" stroke="rgba(230,126,34,0.6)" strokeWidth={1} fill="url(#trafficAmber)" dot={false} isAnimationActive={true} animationDuration={1200} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 flex-shrink-0">
|
||||
<div>
|
||||
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Incoming</p>
|
||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.23 Gbps</p>
|
||||
<p style={{ fontSize: '11px', color: '#E74C3C' }}>↓ 12.4%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p style={{ fontSize: '11px', color: '#7A7D85' }}>Outgoing</p>
|
||||
<p style={{ fontSize: '18px', fontWeight: 700, color: '#E8E6E0' }}>1.08 Gbps</p>
|
||||
<p style={{ fontSize: '11px', color: '#2ECC71' }}>↑ 8.7%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shortcuts — miniature control panels */}
|
||||
<div style={cardBase} className="hover:!border-gold/15">
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '24px 24px', pointerEvents: 'none' }} />
|
||||
|
||||
<div className="relative z-10">
|
||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px' }}>
|
||||
Shortcuts
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{shortcuts.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<button
|
||||
key={item.label}
|
||||
className="flex flex-col items-center gap-2 cursor-pointer bg-transparent transition-all duration-200 group/btn"
|
||||
style={{ padding: '16px 12px', borderRadius: '10px', border: '1px solid rgba(200,164,52,0.08)', boxShadow: '0 0 12px rgba(200,164,52,0.02)' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'rgba(200,164,52,0.2)'; e.currentTarget.style.boxShadow = '0 0 16px rgba(200,164,52,0.06)' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'rgba(200,164,52,0.08)'; e.currentTarget.style.boxShadow = '0 0 12px rgba(200,164,52,0.02)' }}
|
||||
>
|
||||
<Icon size={20} style={{ color: '#7A7D85', transition: 'color 0.2s' }} />
|
||||
<span style={{ fontSize: '10px', color: '#7A7D85' }}>{item.label}</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
144
src/components/MiddleRow.tsx
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import { X, CircleCheck, Shield, Play, Settings, User } from 'lucide-react'
|
||||
|
||||
const resources = [
|
||||
{ label: 'Compute', current: 18, max: 24, unit: '' },
|
||||
{ label: 'Storage', current: 12.4, max: 20, unit: ' TB' },
|
||||
{ label: 'Database', current: 8, max: 12, unit: '' },
|
||||
{ label: 'Network', current: 98.7, max: 100, unit: '%' },
|
||||
{ label: 'Containers', current: 32, max: 40, unit: '' },
|
||||
]
|
||||
|
||||
const activities = [
|
||||
{ icon: CircleCheck, title: 'Backup completed', source: 'Database Cluster 01', time: '2m ago' },
|
||||
{ icon: Shield, title: 'Security scan completed', source: 'Web Frontend', time: '8m ago' },
|
||||
{ icon: Play, title: 'Instance launched', source: 'App Server 03', time: '15m ago' },
|
||||
{ icon: Settings, title: 'Configuration updated', source: 'Load Balancer', time: '22m ago' },
|
||||
{ icon: User, title: 'User login detected', source: 'admin@archnest.io', time: '35m ago' },
|
||||
]
|
||||
|
||||
const alerts = [
|
||||
{ severity: 'high', title: 'High CPU Usage', source: 'App Server 02', time: '2m ago' },
|
||||
{ severity: 'medium', title: 'Disk Space Low', source: 'Database Cluster 01', time: '15m ago' },
|
||||
{ severity: 'medium', title: 'Unauthorized Login Attempt', source: 'Web Frontend', time: '32m ago' },
|
||||
{ severity: 'medium', title: 'SSL Certificate Expiring', source: 'api.archnest.io', time: '1h ago' },
|
||||
]
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)',
|
||||
transition: 'border-color 0.2s ease',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
function getBarColor(percentage: number) {
|
||||
if (percentage >= 90) return '#E74C3C'
|
||||
if (percentage >= 70) return '#E67E22'
|
||||
return '#C8A434'
|
||||
}
|
||||
|
||||
export default function MiddleRow() {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-[1fr_1.4fr_1fr] gap-6">
|
||||
{/* Resource Overview */}
|
||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||
{/* Subtle hex pattern overlay */}
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.03, backgroundImage: 'radial-gradient(circle, #C8A434 1px, transparent 1px)', backgroundSize: '20px 20px', pointerEvents: 'none' }} />
|
||||
{/* Gold top edge lighting */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.15), transparent)', pointerEvents: 'none' }} />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
||||
Resource Overview
|
||||
</h3>
|
||||
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{resources.map((res) => {
|
||||
const percentage = res.unit === '%' ? res.current : (res.current / res.max) * 100
|
||||
const displayValue = res.unit === '%' ? `${res.current}%` : `${res.current} / ${res.max}${res.unit}`
|
||||
return (
|
||||
<div key={res.label} className="flex items-center gap-3">
|
||||
<span style={{ fontSize: '13px', color: '#E8E6E0', width: '90px' }}>{res.label}</span>
|
||||
<div style={{ flex: 1, height: '6px', backgroundColor: 'rgba(30,32,37,0.8)', borderRadius: '3px', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: `${percentage}%`, backgroundColor: getBarColor(percentage), borderRadius: '3px', transition: 'width 0.8s ease' }} />
|
||||
</div>
|
||||
<span style={{ fontSize: '12px', color: '#7A7D85', width: '80px', textAlign: 'right' }}>{displayValue}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity — visually dominant */}
|
||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||
{/* City grid texture */}
|
||||
<div style={{ position: 'absolute', inset: 0, opacity: 0.02, backgroundImage: 'linear-gradient(rgba(200,164,52,0.3) 1px, transparent 1px), linear-gradient(90deg, rgba(200,164,52,0.3) 1px, transparent 1px)', backgroundSize: '40px 40px', pointerEvents: 'none' }} />
|
||||
{/* Gold top edge */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '5%', right: '5%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(200,164,52,0.2), transparent)', pointerEvents: 'none' }} />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
||||
Recent Activity
|
||||
</h3>
|
||||
<button className="bg-transparent border-none cursor-pointer p-1" style={{ color: '#7A7D85' }}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{activities.map((item, i) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div style={{ width: '28px', height: '28px', borderRadius: '6px', backgroundColor: 'rgba(200,164,52,0.06)', border: '1px solid rgba(200,164,52,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxShadow: '0 0 8px rgba(200,164,52,0.04)' }}>
|
||||
<Icon size={13} style={{ color: '#C8A434' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.title}</p>
|
||||
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.source}</p>
|
||||
</div>
|
||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{item.time}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Alerts */}
|
||||
<div style={cardBase} className="hover:!border-gold/15 group">
|
||||
{/* Amber edge lighting */}
|
||||
<div style={{ position: 'absolute', top: 0, left: '10%', right: '10%', height: '1px', background: 'linear-gradient(90deg, transparent, rgba(231,126,34,0.15), transparent)', pointerEvents: 'none' }} />
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 style={{ fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500 }}>
|
||||
Top Alerts
|
||||
</h3>
|
||||
<a href="#" style={{ fontSize: '11px', color: '#C8A434', textDecoration: 'none' }}>View all</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{alerts.map((alert, i) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<div style={{ width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0, marginTop: '5px', backgroundColor: alert.severity === 'high' ? '#E74C3C' : '#E67E22', boxShadow: alert.severity === 'high' ? '0 0 6px rgba(231,76,60,0.3)' : '0 0 6px rgba(230,126,34,0.2)' }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<p style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.title}</p>
|
||||
<p style={{ fontSize: '11px', color: '#7A7D85', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{alert.source}</p>
|
||||
</div>
|
||||
<span style={{ fontSize: '11px', color: '#7A7D85', flexShrink: 0 }}>{alert.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
src/components/ProgressRing.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ProgressRingProps {
|
||||
percentage: number
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
export default function ProgressRing({ percentage, size = 56, strokeWidth = 4 }: ProgressRingProps) {
|
||||
const [animatedPercentage, setAnimatedPercentage] = useState(0)
|
||||
const radius = (size - strokeWidth) / 2
|
||||
const circumference = 2 * Math.PI * radius
|
||||
const offset = circumference - (animatedPercentage / 100) * circumference
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setAnimatedPercentage(percentage), 100)
|
||||
return () => clearTimeout(timer)
|
||||
}, [percentage])
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center justify-center" style={{ width: size, height: size }}>
|
||||
<svg width={size} height={size} className="-rotate-90">
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="#1E2025"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="#C8A434"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1000 ease-out"
|
||||
/>
|
||||
</svg>
|
||||
<span className="absolute text-xs font-bold text-text-primary">
|
||||
{percentage}%
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
116
src/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import {
|
||||
LayoutGrid,
|
||||
Server,
|
||||
Globe,
|
||||
Bookmark,
|
||||
Terminal,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface SidebarProps {
|
||||
collapsed: boolean
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ icon: LayoutGrid, label: 'Glance', route: '/', active: true },
|
||||
{ icon: Server, label: 'Infrastructure', route: '/infrastructure', active: false },
|
||||
{ icon: Globe, label: 'Network', route: '/network', active: false },
|
||||
{ icon: Bookmark, label: 'BookNest', route: '/booknest', active: false },
|
||||
{ icon: Terminal, label: 'Terminal', route: '/terminal', active: false },
|
||||
{ icon: Settings, label: 'Settings', route: '/settings', active: false },
|
||||
]
|
||||
|
||||
export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
||||
const width = collapsed ? 60 : 140
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="fixed left-0 top-0 z-50 h-screen overflow-hidden flex flex-col items-center py-5"
|
||||
style={{ width: `${width}px`, backgroundColor: '#0A0B0D' }}
|
||||
>
|
||||
{/* Logo — larger, aligned with top bar */}
|
||||
<div className="flex flex-col items-center mb-6" style={{ paddingTop: '8px' }}>
|
||||
<img
|
||||
src="/archnest-logo.png"
|
||||
alt="ArchNest"
|
||||
className="mb-1.5"
|
||||
style={{
|
||||
width: collapsed ? '28px' : '44px',
|
||||
height: collapsed ? '28px' : '44px',
|
||||
filter: 'drop-shadow(0 0 8px rgba(200,164,52,0.5))',
|
||||
}}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span style={{ fontSize: '9px', fontWeight: 700, letterSpacing: '2.5px', color: '#C8A434', textTransform: 'uppercase' }}>
|
||||
ArchNest
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav Items */}
|
||||
<nav className="flex-1 flex flex-col justify-center gap-6 w-full">
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<a
|
||||
key={item.label}
|
||||
href={item.route}
|
||||
className="relative flex w-full flex-col items-center justify-center gap-1.5 px-2 py-2 text-center no-underline transition-all duration-200"
|
||||
style={{ color: item.active ? '#C8A434' : '#7A7D85' }}
|
||||
title={collapsed ? item.label : undefined}
|
||||
onMouseEnter={(e) => { if (!item.active) e.currentTarget.style.backgroundColor = 'rgba(200,164,52,0.05)'; e.currentTarget.style.color = '#C8A434' }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; if (!item.active) e.currentTarget.style.color = '#7A7D85' }}
|
||||
>
|
||||
{item.active && (
|
||||
<div
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 rounded-r"
|
||||
style={{ width: '3px', height: '26px', backgroundColor: '#C8A434', boxShadow: '0 0 6px rgba(200,164,52,0.5)' }}
|
||||
/>
|
||||
)}
|
||||
<Icon className="h-5 w-5 shrink-0" strokeWidth={item.active ? 2 : 1.5} />
|
||||
{!collapsed && (
|
||||
<span className="max-w-[90px] truncate leading-tight font-medium" style={{ fontSize: '10px' }}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Collapse Toggle */}
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="p-1.5 rounded cursor-pointer bg-transparent transition-colors"
|
||||
style={{ border: '1px solid #1E2025', color: '#7A7D85', marginBottom: '12px' }}
|
||||
>
|
||||
{collapsed ? <ChevronRight size={12} /> : <ChevronLeft size={12} />}
|
||||
</button>
|
||||
|
||||
{/* System Status — rounded block, fits inside nav with breathing room */}
|
||||
<div style={{ width: '100%', padding: '0 12px 16px 12px' }}>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid rgba(122, 125, 133, 0.2)',
|
||||
borderRadius: '10px',
|
||||
padding: '10px 12px',
|
||||
backgroundColor: 'rgba(20, 21, 24, 0.5)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div style={{ width: '12px', height: '12px', borderRadius: '50%', border: '2px solid #2ECC71', flexShrink: 0 }} />
|
||||
{!collapsed && (
|
||||
<div>
|
||||
<span style={{ fontSize: '9px', color: '#E8E6E0', display: 'block', lineHeight: 1.3, fontWeight: 500 }}>System Status</span>
|
||||
<span style={{ fontSize: '8px', color: '#2ECC71', display: 'block', lineHeight: 1.3 }}>All Systems Operational</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
53
src/components/SparklineChart.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { AreaChart, Area, ResponsiveContainer, LineChart, Line } from 'recharts'
|
||||
|
||||
interface SparklineChartProps {
|
||||
data: number[]
|
||||
color?: string
|
||||
height?: number
|
||||
filled?: boolean
|
||||
}
|
||||
|
||||
export default function SparklineChart({ data, color = '#C8A434', height = 30, filled = false }: SparklineChartProps) {
|
||||
const chartData = data.map((value, index) => ({ index, value }))
|
||||
|
||||
if (filled) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<AreaChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="sparkGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
||||
<stop offset="100%" stopColor={color} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
fill="url(#sparkGradient)"
|
||||
dot={false}
|
||||
isAnimationActive={true}
|
||||
animationDuration={1000}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<LineChart data={chartData} margin={{ top: 0, right: 0, left: 0, bottom: 0 }}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
isAnimationActive={true}
|
||||
animationDuration={1000}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
104
src/components/StatusCards.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Server, Shield, Network } from 'lucide-react'
|
||||
import SparklineChart from './SparklineChart'
|
||||
import ProgressRing from './ProgressRing'
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
backgroundColor: 'rgba(10, 10, 12, 0.92)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
border: '1px solid rgba(200, 164, 52, 0.08)',
|
||||
borderRadius: '12px',
|
||||
padding: '16px',
|
||||
boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)',
|
||||
transition: 'border-color 0.2s ease',
|
||||
}
|
||||
|
||||
export default function StatusCards() {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-4 gap-5">
|
||||
{/* System Status */}
|
||||
<div style={cardStyle} className="hover:!border-gold/20">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
||||
System Status
|
||||
</h3>
|
||||
<p style={{ fontSize: '13px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1.2 }}>All Systems</p>
|
||||
<p style={{ fontSize: '13px', fontWeight: 700, color: '#C8A434', fontStyle: 'italic', lineHeight: 1.2 }}>Operational</p>
|
||||
</div>
|
||||
<ProgressRing percentage={100} size={44} strokeWidth={3} />
|
||||
</div>
|
||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
|
||||
<SparklineChart data={[100, 100, 99, 100, 100, 100, 98, 100, 100, 100, 100, 100]} color="#C8A434" height={20} />
|
||||
<p style={{ fontSize: '9px', color: '#7A7D85', marginTop: '4px' }}>Last checked: 2m ago</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Infrastructure */}
|
||||
<div style={cardStyle} className="hover:!border-gold/20">
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
||||
Infrastructure
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Server size={16} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>24</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Total Resources</p>
|
||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
|
||||
<span style={{ color: '#7A7D85' }}>24 Running</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
|
||||
<span style={{ color: '#7A7D85' }}>0 Warning</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
|
||||
<span style={{ color: '#7A7D85' }}>0 Critical</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security */}
|
||||
<div style={cardStyle} className="hover:!border-gold/20">
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
||||
Security
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={16} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>2</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Active Alerts</p>
|
||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)', fontSize: '9px' }} className="flex items-center gap-2 flex-wrap">
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#2ECC71' }} />
|
||||
<span style={{ color: '#7A7D85' }}>2 Low</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E67E22' }} />
|
||||
<span style={{ color: '#7A7D85' }}>0 Medium</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: '#E74C3C' }} />
|
||||
<span style={{ color: '#7A7D85' }}>0 High</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network */}
|
||||
<div style={cardStyle} className="hover:!border-gold/20">
|
||||
<h3 style={{ fontSize: '10px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '8px' }}>
|
||||
Network
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Network size={16} style={{ color: '#C8A434' }} />
|
||||
<span style={{ fontSize: '24px', fontWeight: 700, color: '#E8E6E0', lineHeight: 1 }}>98.7%</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '10px', color: '#7A7D85', marginTop: '4px' }}>Network Uptime</p>
|
||||
<div style={{ marginTop: '8px', paddingTop: '8px', borderTop: '1px solid rgba(200,164,52,0.06)' }}>
|
||||
<SparklineChart data={[99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98, 99, 100, 99, 99, 98, 99, 100, 99, 98]} color="#C8A434" height={24} filled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
101
src/components/TopBar.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Search, Bell, ChevronDown, User, Palette, LogOut, Shield, HelpCircle } from 'lucide-react'
|
||||
|
||||
export default function TopBar() {
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||
setUserMenuOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClick)
|
||||
return () => document.removeEventListener('mousedown', handleClick)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<header className="h-14 flex items-center px-6 bg-page sticky top-0 z-40">
|
||||
{/* Page Title — pushed away from sidebar edge */}
|
||||
<h1 className="text-[18px] font-bold uppercase tracking-wide" style={{ color: '#C8A434', marginLeft: '20px' }}>
|
||||
Glance
|
||||
</h1>
|
||||
|
||||
{/* Center section — Search bar */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2" style={{ color: '#7A7D85' }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search resources..."
|
||||
className="w-[300px] h-8 rounded-full bg-card border border-border text-[12px] text-text-primary placeholder:text-text-secondary focus:outline-none focus:border-gold transition-colors"
|
||||
style={{ paddingLeft: '36px', paddingRight: '16px' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right section — Bell + Avatar, moved toward center (away from window edge) */}
|
||||
<div className="flex items-center gap-4" style={{ marginRight: '40px' }}>
|
||||
|
||||
{/* Notifications */}
|
||||
<button className="relative p-1.5 text-text-secondary hover:text-gold transition-colors bg-transparent border-none cursor-pointer">
|
||||
<Bell size={17} />
|
||||
<span className="absolute -top-0.5 -right-1 w-3.5 h-3.5 bg-danger rounded-full text-[8px] text-white flex items-center justify-center font-bold">
|
||||
3
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* User Avatar + Dropdown */}
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
||||
className="flex items-center gap-2 bg-transparent border-none cursor-pointer p-0"
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full border-2 border-gold bg-card flex items-center justify-center text-gold font-bold text-[12px] shadow-[0_0_8px_rgba(200,164,52,0.4)]">
|
||||
AO
|
||||
</div>
|
||||
<div className="flex flex-col text-left">
|
||||
<span className="text-[12px] text-text-primary font-medium leading-tight">ArchNest Ops</span>
|
||||
<span className="text-[9px] text-text-secondary leading-tight">Administrator</span>
|
||||
</div>
|
||||
<ChevronDown size={12} className={`text-text-secondary transition-transform duration-200 ${userMenuOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{userMenuOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-48 bg-card border border-border rounded-xl overflow-hidden shadow-lg z-50">
|
||||
<div className="p-3 border-b border-border">
|
||||
<p className="text-[12px] text-text-primary font-medium">ArchNest Ops</p>
|
||||
<p className="text-[10px] text-text-secondary">admin@archnest.io</p>
|
||||
</div>
|
||||
<div className="py-1">
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors no-underline">
|
||||
<User size={14} />
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors no-underline">
|
||||
<Palette size={14} />
|
||||
<span>Appearance</span>
|
||||
</a>
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors no-underline">
|
||||
<Shield size={14} />
|
||||
<span>Security</span>
|
||||
</a>
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-text-secondary hover:text-text-primary hover:bg-page transition-colors no-underline">
|
||||
<HelpCircle size={14} />
|
||||
<span>Help & Support</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-border py-1">
|
||||
<a href="#" className="flex items-center gap-2.5 px-3 py-2 text-[12px] text-danger hover:bg-page transition-colors no-underline">
|
||||
<LogOut size={14} />
|
||||
<span>Sign Out</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
40
src/index.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-page: #0D0E10;
|
||||
--color-card: #141518;
|
||||
--color-sidebar: #0A0B0D;
|
||||
--color-border: #1E2025;
|
||||
--color-gold: #C8A434;
|
||||
--color-success: #2ECC71;
|
||||
--color-warning: #E67E22;
|
||||
--color-danger: #E74C3C;
|
||||
--color-text-primary: #E8E6E0;
|
||||
--color-text-secondary: #7A7D85;
|
||||
--color-teal: #1ABC9C;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #0D0E10;
|
||||
color: #E8E6E0;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
10
src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
25
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
7
vite.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
||||