mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
small fix, don't worry about it
This commit is contained in:
1 parent
08bd0c6e5c
commit
08cb5754f3
638 files changed
+57330
-34704
No files matched your search
+1136
-34
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,385 @@
|
||||
1️⃣ What the Railway Dashboard Is
|
||||
|
||||
Railway is a PaaS (Platform as a Service).
|
||||
|
||||
The dashboard is where you:
|
||||
|
||||
Create services (apps, APIs, workers)
|
||||
|
||||
Add databases
|
||||
|
||||
Connect GitHub repos
|
||||
|
||||
Configure env variables
|
||||
|
||||
View logs & metrics
|
||||
|
||||
Define networking between services
|
||||
|
||||
Instead of showing raw Kubernetes or Docker Compose, Railway abstracts everything into a visual canvas.
|
||||
|
||||
2️⃣ What This “Canvas” Actually Is
|
||||
|
||||
The key part in your HTML:
|
||||
|
||||
<div class="react-flow ...">
|
||||
|
||||
|
||||
That tells us immediately:
|
||||
|
||||
👉 They’re using React Flow — a React library for building node-based editors.
|
||||
|
||||
React Flow powers:
|
||||
|
||||
Node graphs
|
||||
|
||||
Drag & drop canvases
|
||||
|
||||
Visual connections
|
||||
|
||||
Zoom/pan
|
||||
|
||||
Edges with arrows
|
||||
|
||||
So this canvas = a React Flow graph editor customized to represent infrastructure.
|
||||
|
||||
3️⃣ Architecture of the Canvas
|
||||
|
||||
Here’s what’s happening internally:
|
||||
|
||||
A) Root Layout
|
||||
stack-container
|
||||
nested-canvas
|
||||
canvas-container
|
||||
|
||||
|
||||
This is:
|
||||
|
||||
Grid layout
|
||||
|
||||
Responsive behavior
|
||||
|
||||
Sidebar + center canvas layout
|
||||
|
||||
Mobile floating button
|
||||
|
||||
They’re using:
|
||||
|
||||
Tailwind CSS
|
||||
|
||||
CSS Grid
|
||||
|
||||
Dark mode variants
|
||||
|
||||
Utility classes
|
||||
|
||||
Very modern stack.
|
||||
|
||||
B) The Graph Engine (React Flow)
|
||||
|
||||
This part:
|
||||
|
||||
<div class="react-flow__viewport"
|
||||
style="transform: translate(306px, 279px) scale(1);">
|
||||
|
||||
|
||||
This means:
|
||||
|
||||
The canvas supports panning (translate X/Y)
|
||||
|
||||
It supports zooming (scale)
|
||||
|
||||
Entire graph is rendered inside a transform container
|
||||
|
||||
So when you drag around → it updates the transform.
|
||||
When you zoom → scale changes.
|
||||
|
||||
Classic infinite canvas technique.
|
||||
|
||||
C) Nodes
|
||||
|
||||
Example:
|
||||
|
||||
<div class="react-flow__node react-flow__node-empty">
|
||||
|
||||
|
||||
Nodes represent:
|
||||
|
||||
GitHub repo
|
||||
|
||||
Database
|
||||
|
||||
Docker image
|
||||
|
||||
Function
|
||||
|
||||
Bucket
|
||||
|
||||
Empty service
|
||||
|
||||
Each node = React component.
|
||||
|
||||
Internally Railway likely stores something like:
|
||||
|
||||
{
|
||||
id: "service_123",
|
||||
type: "github",
|
||||
position: { x: 120, y: 400 },
|
||||
data: {
|
||||
repo: "...",
|
||||
env: {...}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
React Flow renders this declaratively.
|
||||
|
||||
D) Edges (Connections)
|
||||
|
||||
This part:
|
||||
|
||||
Edges are rendered in SVG.
|
||||
|
||||
You also saw:
|
||||
|
||||
<marker id="arrowhead">
|
||||
|
||||
|
||||
That defines arrowheads for:
|
||||
|
||||
Normal connection
|
||||
|
||||
Staged
|
||||
|
||||
Selected
|
||||
|
||||
Network
|
||||
|
||||
Alternative
|
||||
|
||||
So edges are dynamic SVG paths with markers attached.
|
||||
|
||||
4️⃣ What the Command Palette Is
|
||||
|
||||
This section:
|
||||
|
||||
<input placeholder="What would you like to create?" cmdk-input>
|
||||
|
||||
|
||||
This is a command palette UI.
|
||||
|
||||
Looks like they use:
|
||||
|
||||
👉 cmdk (a React command menu library)
|
||||
|
||||
So when you type:
|
||||
|
||||
“github”
|
||||
|
||||
“database”
|
||||
|
||||
“docker”
|
||||
|
||||
It filters options.
|
||||
|
||||
Selecting one → creates a new node in the graph.
|
||||
|
||||
This is very similar to:
|
||||
|
||||
Linear
|
||||
|
||||
Vercel
|
||||
|
||||
Raycast UI pattern
|
||||
|
||||
5️⃣ How the Dashboard Works Conceptually
|
||||
|
||||
The visual canvas is just a frontend representation of backend state.
|
||||
|
||||
Behind the scenes:
|
||||
|
||||
Step 1 — You Add a Service
|
||||
|
||||
→ Frontend sends API request:
|
||||
|
||||
POST /services
|
||||
|
||||
Step 2 — Railway Backend
|
||||
|
||||
Likely:
|
||||
|
||||
Creates container definition
|
||||
|
||||
Assigns internal network
|
||||
|
||||
Stores graph structure in DB
|
||||
|
||||
Triggers deployment pipeline
|
||||
|
||||
Step 3 — Infra Layer
|
||||
|
||||
Under the hood Railway probably uses:
|
||||
|
||||
Containers (Docker)
|
||||
|
||||
Orchestration (likely Kubernetes or custom scheduler)
|
||||
|
||||
Internal networking mesh
|
||||
|
||||
Volume storage
|
||||
|
||||
Load balancing
|
||||
|
||||
The canvas is just a visual abstraction over:
|
||||
|
||||
Containers + Networks + Volumes + Environment Variables
|
||||
|
||||
6️⃣ What “Stack Container” Means
|
||||
id="stack-container-root"
|
||||
|
||||
|
||||
A “stack” in Railway terms =
|
||||
|
||||
A collection of services that form one project.
|
||||
|
||||
Like:
|
||||
|
||||
API
|
||||
|
||||
Database
|
||||
|
||||
Redis
|
||||
|
||||
Worker
|
||||
|
||||
Cron
|
||||
|
||||
All connected together.
|
||||
|
||||
This is similar to:
|
||||
|
||||
Docker Compose stack
|
||||
|
||||
Kubernetes namespace
|
||||
|
||||
Vercel project
|
||||
|
||||
7️⃣ The Networking Animation
|
||||
|
||||
You saw:
|
||||
|
||||
@keyframes networkFlowEgress
|
||||
@keyframes networkFlowIngress
|
||||
|
||||
|
||||
That’s animated edge flow.
|
||||
|
||||
Meaning:
|
||||
|
||||
When traffic flows between services
|
||||
|
||||
They animate dots moving across edges
|
||||
|
||||
This gives visual feedback like:
|
||||
“API → Database”
|
||||
|
||||
Very clean UX detail.
|
||||
|
||||
8️⃣ What the Floating Button Is
|
||||
<button class="sm:hidden fixed right-4 bottom-4">
|
||||
|
||||
|
||||
That’s the mobile "Add Service" button.
|
||||
|
||||
On small screens:
|
||||
|
||||
Canvas hides
|
||||
|
||||
Floating action button appears
|
||||
|
||||
Material-inspired design pattern.
|
||||
|
||||
9️⃣ So What Is This Canvas Technically?
|
||||
|
||||
It is:
|
||||
|
||||
A React Flow–powered infrastructure graph editor that maps frontend node/edge state to backend service configuration.
|
||||
|
||||
Or simpler:
|
||||
|
||||
👉 A visual Docker/Kubernetes orchestrator UI.
|
||||
|
||||
🔟 If You Built This Yourself
|
||||
|
||||
Given your experience (Go backend + Railway + infra + React):
|
||||
|
||||
You’d need:
|
||||
|
||||
Frontend:
|
||||
|
||||
React
|
||||
|
||||
React Flow
|
||||
|
||||
Tailwind
|
||||
|
||||
Command palette (cmdk)
|
||||
|
||||
State manager (Zustand likely)
|
||||
|
||||
Backend:
|
||||
|
||||
Service registry
|
||||
|
||||
Deployment engine
|
||||
|
||||
Container runtime control
|
||||
|
||||
Network layer
|
||||
|
||||
Logs streaming
|
||||
|
||||
Optional:
|
||||
|
||||
WebSocket for real-time deployment status
|
||||
|
||||
Event-driven state updates
|
||||
|
||||
⚙️ Why This Design Is Smart
|
||||
|
||||
Non-dev friendly
|
||||
|
||||
Infrastructure becomes visual
|
||||
|
||||
Encourages service-based architecture
|
||||
|
||||
Makes networking intuitive
|
||||
|
||||
Looks “modern infra SaaS”
|
||||
|
||||
Railway basically turned:
|
||||
|
||||
docker-compose.yml
|
||||
|
||||
|
||||
into:
|
||||
|
||||
a draggable visual graph
|
||||
|
||||
🚀 Big Picture
|
||||
|
||||
Railway dashboard =
|
||||
|
||||
React app
|
||||
|
||||
React Flow canvas
|
||||
|
||||
Command palette-driven node creation
|
||||
|
||||
SVG-based edge system
|
||||
|
||||
Tailwind-based design
|
||||
|
||||
Backend infra orchestration API
|
||||
|
||||
It’s a visual abstraction over cloud container orchestration.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 623 KiB |
@@ -0,0 +1,63 @@
|
||||
# 🚆 What the Railway Dashboard Is
|
||||
|
||||
The Railway dashboard is the main web interface for managing your Railway cloud projects. It’s essentially your control panel where you can see, organize, configure, and monitor everything you’re hosting on Railway — services, deployments, databases, environments, logs, metrics, and more.
|
||||
|
||||
You typically land on the dashboard after signing into your Railway account. It’s the central hub that gives you a high-level view of all your projects and infrastructure.
|
||||
|
||||
## 🧠 What It Lets You Do
|
||||
|
||||
### 📦 1. See and Manage Projects
|
||||
|
||||
The dashboard shows all projects you own or belong to, sorted by recent activity.
|
||||
|
||||
Each project can contain multiple services (like a web app, API, worker, database).
|
||||
|
||||
From the dashboard you can click into each project to manage details.
|
||||
|
||||
### ⚙️ 2. Configure and Deploy Services
|
||||
|
||||
Within a project, you can link code repositories (GitHub) or container images.
|
||||
|
||||
Railway handles build and deploy processes automatically — including automatic configuration for many languages/frameworks.
|
||||
|
||||
Deployments are shown with statuses (building, deployed, errors).
|
||||
|
||||
### 📊 3. Monitoring & Observability
|
||||
|
||||
View logs and performance metrics (CPU, memory, network, disk usage) per service.
|
||||
|
||||
You can create dashboards with charts and custom widgets for things like request rates or error logs.
|
||||
|
||||
Set alerts for thresholds (like high CPU or errors) and receive notifications via Slack, Discord, email, and more.
|
||||
|
||||
### 🔐 4. Manage Secrets & Environment Variables
|
||||
|
||||
Set configuration variables (like API keys or database URLs) that your services use.
|
||||
|
||||
Variables can be scoped to services or shared across the entire project.
|
||||
|
||||
### 🔁 5. Staging & Environments
|
||||
|
||||
Railway supports multiple environments (dev, staging, prod).
|
||||
|
||||
You can preview changes in separate environments and promote or rollback deployments with a click.
|
||||
|
||||
### 📚 6. Templates & One-Click Deploy
|
||||
|
||||
Access a library of templates to quickly deploy common stacks (e.g., databases with backend, analytics tools).
|
||||
|
||||
Templates auto-provision services and databases as a project.
|
||||
|
||||
## 🧑💻 How It Works (Behind the Scenes)
|
||||
|
||||
Railway’s dashboard directly reflects the state of your projects by talking to its API backend (the same API used by their CLI). It pulls data like:
|
||||
|
||||
Project list and status
|
||||
|
||||
Deployment logs and history
|
||||
|
||||
Resources (CPU, memory, traffic)
|
||||
|
||||
Environment and configuration values
|
||||
|
||||
Changes you make in the dashboard (like redeploying or updating variables) are sent back to the Railway API and applied to your infrastructure.
|
||||
@@ -0,0 +1,254 @@
|
||||
# 🧩 What the Project Page Is
|
||||
|
||||
The Project page represents one isolated application environment on Railway.
|
||||
|
||||
Think of a project as:
|
||||
|
||||
> A self-contained box that holds all services, databases, configs, deployments, logs, metrics, and environments for one product or system.
|
||||
|
||||
### Example Structure
|
||||
|
||||
```
|
||||
One project = spark-screen
|
||||
├── Go backend
|
||||
├── Frontend (Vite/Solid)
|
||||
├── PostgreSQL
|
||||
├── Redis
|
||||
├── Worker / cron
|
||||
└── Preview + Production environments
|
||||
```
|
||||
|
||||
## 🗺️ Main Layout of the Project Page
|
||||
|
||||
Visually, the project page is built around a canvas + side panels model.
|
||||
|
||||
### 1️⃣ Project Canvas (Center)
|
||||
|
||||
This is the graph view you see first.
|
||||
|
||||
### What it shows:
|
||||
|
||||
- **Each service as a card** (app, db, worker, etc.)
|
||||
- **Lines showing relationships** (e.g. `backend → database`)
|
||||
- **Deployment status per service**
|
||||
|
||||
### Each card usually displays:
|
||||
|
||||
- **Service name**
|
||||
- **Source** (GitHub repo, Dockerfile, template)
|
||||
- **Current deployment status** (running, building, failed)
|
||||
- **Quick access to logs & settings**
|
||||
|
||||
> 👉 This canvas is not just visual — it reflects actual runtime dependencies.
|
||||
|
||||
### 2️⃣ Services (Core Units)
|
||||
|
||||
Each box on the canvas is a Service.
|
||||
|
||||
A service can be:
|
||||
|
||||
- **Web app** (Go, Node, Bun, Python, etc.)
|
||||
- **Background worker**
|
||||
- **Cron job**
|
||||
- **Database** (Postgres, Redis, MySQL)
|
||||
- **Custom Docker container**
|
||||
|
||||
Clicking a service opens its Service Detail Panel.
|
||||
|
||||
## 🔍 Inside a Service (Very Important)
|
||||
|
||||
When you click a service, you get several tabs:
|
||||
|
||||
### ⚙️ Settings
|
||||
|
||||
This is the brain of the service.
|
||||
|
||||
You configure:
|
||||
|
||||
- **Source**
|
||||
- GitHub repo (via GitHub App)
|
||||
- Branch
|
||||
- Root directory
|
||||
- **Build**
|
||||
- Build command
|
||||
- Start command
|
||||
- Dockerfile (optional)
|
||||
- **Resources**
|
||||
- CPU / RAM limits
|
||||
- **Networking**
|
||||
- Public domain
|
||||
- Internal service-to-service URLs
|
||||
|
||||
> **How it works:** Any change here triggers a new deployment. Railway stores this config and applies it at runtime.
|
||||
|
||||
### 🔐 Variables (Environment Variables)
|
||||
|
||||
This is where Railway shines.
|
||||
|
||||
Key–value environment variables
|
||||
|
||||
Can be:
|
||||
|
||||
- **Service-specific**
|
||||
- **Shared across the project**
|
||||
- **Environment-scoped** (prod / preview)
|
||||
|
||||
### Examples:
|
||||
|
||||
```
|
||||
DATABASE_URL
|
||||
REDIS_URL
|
||||
JWT_SECRET
|
||||
PORT
|
||||
```
|
||||
|
||||
|
||||
> **Behind the scenes:**
|
||||
>
|
||||
> - Injected into the container at runtime
|
||||
> - Never hardcoded into images
|
||||
> - Securely stored and masked in UI
|
||||
|
||||
### 📦 Deployments
|
||||
|
||||
This tab shows:
|
||||
|
||||
- **Full deployment history**
|
||||
- **Commit hash** (if GitHub)
|
||||
- **Build logs**
|
||||
- **Runtime logs**
|
||||
- **Rollback button**
|
||||
|
||||
> **How it works:**
|
||||
>
|
||||
> 1. Git push (or manual deploy)
|
||||
> 2. Railway pulls code
|
||||
> 3. Builds container (Railpack or Docker)
|
||||
> 4. Starts new instance
|
||||
> 5. Old one is replaced (zero-downtime when possible)
|
||||
|
||||
### 📜 Logs
|
||||
|
||||
### Real-time logs:
|
||||
|
||||
- **stdout / stderr**
|
||||
- **Build logs**
|
||||
- **Runtime errors**
|
||||
|
||||
Logs are:
|
||||
|
||||
- **Per-service**
|
||||
- **Per-deployment**
|
||||
- **Streamed live from containers**
|
||||
|
||||
> This is basically `docker logs` but cloud-native.
|
||||
|
||||
### 📊 Metrics
|
||||
|
||||
### Per-service metrics:
|
||||
|
||||
- **CPU usage**
|
||||
- **Memory usage**
|
||||
- **Network traffic**
|
||||
- **Disk usage** (for DBs)
|
||||
|
||||
This helps you:
|
||||
|
||||
- **Detect leaks**
|
||||
- **Decide when to optimize**
|
||||
- **Understand cost drivers**
|
||||
|
||||
## 🧱 Databases Inside a Project
|
||||
|
||||
Databases are first-class services, not addons.
|
||||
|
||||
When you add:
|
||||
|
||||
- **PostgreSQL**
|
||||
- **Redis**
|
||||
- **MySQL**
|
||||
|
||||
Railway:
|
||||
|
||||
- **Provisions it**
|
||||
- **Injects connection URLs**
|
||||
- **Handles backups** (plan-dependent)
|
||||
|
||||
Databases:
|
||||
|
||||
- **Appear on the same canvas**
|
||||
- **Can be linked to multiple services**
|
||||
- **Expose internal connection strings only**
|
||||
|
||||
### Database Benefits
|
||||
|
||||
- **Easy setup**
|
||||
- **Managed backups**
|
||||
- **Internal connection strings**
|
||||
|
||||
## 🌍 Environments (Prod, Preview, Dev)
|
||||
|
||||
Each project can have multiple environments.
|
||||
|
||||
### Common setup:
|
||||
|
||||
- **Production** – live app
|
||||
- **Preview** – per-PR deployments
|
||||
- **Development** – testing
|
||||
|
||||
### What changes per environment:
|
||||
|
||||
- **Variables**
|
||||
- **Domains**
|
||||
- **Deployments**
|
||||
- **Scaling**
|
||||
|
||||
> This is not separate projects — it's logical isolation inside one project.
|
||||
|
||||
## 👥 Project-Level Controls
|
||||
|
||||
At the project level (not service-level):
|
||||
|
||||
### 👤 Members & Permissions
|
||||
|
||||
- **Invite teammates**
|
||||
- **Control access** (admin / developer)
|
||||
|
||||
### 🔔 Notifications
|
||||
|
||||
- **Deploy failures**
|
||||
- **Usage alerts**
|
||||
- **Service crashes**
|
||||
|
||||
### 💳 Usage & Billing
|
||||
|
||||
- **CPU hours**
|
||||
- **RAM hours**
|
||||
- **Network**
|
||||
- **Cost per service**
|
||||
|
||||
## 🧠 How the Project Page Works Internally
|
||||
|
||||
Under the hood:
|
||||
|
||||
- **The UI talks to the Railway API**
|
||||
- **Every service = container + config**
|
||||
- **Every deploy = immutable container version**
|
||||
- **Variables = injected at runtime**
|
||||
- **Services communicate via private internal network**
|
||||
|
||||
> So the project page is essentially: a visual orchestrator for containers, deployments, configs, and environments
|
||||
|
||||
## 🧠 Mental Model (Best Way to Think About It)
|
||||
|
||||
If Docker Compose had:
|
||||
|
||||
- **A UI**
|
||||
- **GitHub auto-deploys**
|
||||
- **Built-in databases**
|
||||
- **Metrics**
|
||||
- **Logs**
|
||||
- **Secrets**
|
||||
- **Environments**
|
||||
|
||||
> 👉 That's the Railway project page.
|
||||
@@ -0,0 +1,263 @@
|
||||
# Self-Hosted PaaS on Proxmox (Railway-like Platform)
|
||||
|
||||
A fully self-hosted, open-source Platform-as-a-Service designed to run on top of **Proxmox infrastructure**, providing a Railway-like developer experience: Git-based deployments, container orchestration, automatic scaling, load balancing, and observability — all under your control.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Goals
|
||||
|
||||
- Railway-like UX, fully self-hosted
|
||||
- Built primarily on **Proxmox + Docker**
|
||||
- Horizontal scaling across multiple VMs/LXCs
|
||||
- Agent-based orchestration (no Kubernetes required)
|
||||
- Git-driven deploys
|
||||
- Production-grade networking, metrics, and reliability
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Core Architecture
|
||||
|
||||
- **Control Plane**: Central API + scheduler
|
||||
- **Node Agents**: Lightweight daemons running on each VM/LXC
|
||||
- **Execution Layer**: Docker containers
|
||||
- **Networking**: Traefik reverse proxy & load balancer
|
||||
- **State Layer**: Externalized databases and caches
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Technology Stack
|
||||
|
||||
### Backend / Control Plane
|
||||
- **Go (primary language)**
|
||||
- Scheduler
|
||||
- Orchestration logic
|
||||
- API server
|
||||
- Scaling engine
|
||||
- **Python (auxiliary tooling)**
|
||||
- Metrics processing
|
||||
- Background jobs
|
||||
- Scripts where Go is impractical
|
||||
- **Rust (performance-critical components)**
|
||||
- Node agent (optional)
|
||||
- Low-level system monitoring
|
||||
- High-throughput data collectors
|
||||
|
||||
---
|
||||
|
||||
### Frontend / Dashboard
|
||||
- **Bun**
|
||||
- Package manager
|
||||
- Runtime for tooling & scripts
|
||||
- [Documentation](https://bun.sh/)
|
||||
- **Vite**
|
||||
- Fast frontend build system
|
||||
- [Documentation](https://vitejs.dev/)
|
||||
- **React 18**
|
||||
- Modern UI framework with hooks and concurrent features
|
||||
- [React Documentation](https://react.dev/)
|
||||
- **TypeScript (TS / TSX)**
|
||||
- Strict typing
|
||||
- Shared contracts with backend
|
||||
- [Documentation](https://www.typescriptlang.org/)
|
||||
- **Tailwind CSS**
|
||||
- Utility-first CSS framework
|
||||
- Rapid UI development with design tokens
|
||||
- [Documentation](https://tailwindcss.com/)
|
||||
- **shadcn/ui**
|
||||
- Beautiful, accessible component library built on Radix UI
|
||||
- [Documentation](https://ui.shadcn.com/)
|
||||
- **Lucide React**
|
||||
- Beautiful icon library for React
|
||||
- [Documentation](https://lucide.dev/)
|
||||
|
||||
**Documentation:**
|
||||
- [React 18](https://react.dev/)
|
||||
- [Vite](https://vitejs.dev/)
|
||||
- [Tailwind CSS](https://tailwindcss.com/)
|
||||
- [shadcn/ui](https://ui.shadcn.com/)
|
||||
- [Lucide React](https://lucide.dev/)
|
||||
- [React Hook Form](https://react-hook-form.com/)
|
||||
- [TanStack Query](https://tanstack.com/query/latest)
|
||||
|
||||
---
|
||||
|
||||
### Infrastructure & Runtime
|
||||
- **Docker**
|
||||
- Container runtime
|
||||
- Image builds & execution
|
||||
- **Docker Compose / Custom Orchestrator**
|
||||
- Service definitions
|
||||
- Replica management
|
||||
- **Traefik**
|
||||
- Reverse proxy
|
||||
- Automatic service discovery
|
||||
- TLS & routing
|
||||
- **Proxmox**
|
||||
- VM & LXC orchestration
|
||||
- Compute isolation
|
||||
- Infrastructure backbone
|
||||
|
||||
---
|
||||
|
||||
### Data & State
|
||||
- **PostgreSQL**
|
||||
- Persistent platform state
|
||||
- Users, apps, deployments, scaling rules
|
||||
- **Redis**
|
||||
- Caching
|
||||
- Session storage
|
||||
- Queues & pub/sub
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Build System & Deployment
|
||||
|
||||
### Builder Types (Railway-inspired)
|
||||
|
||||
**1️⃣ Nixpacks (Primary, Default Builder) ⭐**
|
||||
|
||||
Auto-detection build system based on Nix for reproducible builds.
|
||||
|
||||
- **How it works**: Scans repo for `go.mod`, `package.json`, `requirements.txt`, etc.
|
||||
- **Supports**: Go, Node.js, Bun, Python, Ruby, PHP, Rust, Deno, Static sites
|
||||
- **Advantages**: No Dockerfile needed, fast builds, reproducible, easy overrides
|
||||
- **Default choice**: Used when no other builder is specified
|
||||
|
||||
**2️⃣ Dockerfile Builder**
|
||||
|
||||
Standard Docker build using your existing Dockerfile.
|
||||
|
||||
- **When to use**: Complex builds, custom system dependencies, non-standard runtimes
|
||||
- **Control**: Full control over base image, OS packages, multi-stage builds
|
||||
- **Tradeoff**: Slower than Nixpacks, more responsibility
|
||||
|
||||
**3️⃣ Prebuilt Image Deployment**
|
||||
|
||||
Deploy existing container images from Docker Hub, GHCR, or any OCI registry.
|
||||
|
||||
- **Use cases**: Workers, infrastructure tools, third-party services, optimized custom images
|
||||
- **Process**: No build step - Railway pulls and runs the image directly
|
||||
|
||||
**4️⃣ Platform Templates**
|
||||
|
||||
Predefined service configurations that wrap around the above builders.
|
||||
|
||||
- **Function**: Predefine services, choose builder type (usually Nixpacks), preconfigure variables & databases
|
||||
- **Underlying**: Still uses Nixpacks, Dockerfile, or prebuilt images
|
||||
|
||||
### Builder Selection Logic
|
||||
|
||||
```
|
||||
Dockerfile present? → Docker builder
|
||||
Else if image specified? → Image deploy
|
||||
Else → Nixpacks (default)
|
||||
```
|
||||
|
||||
### Build Output
|
||||
|
||||
Regardless of builder type, the result is always:
|
||||
- Container image
|
||||
- Versioned per deployment
|
||||
- Immutable
|
||||
- Rollback-able
|
||||
|
||||
### Stack Mapping
|
||||
|
||||
- **Go backend** → Nixpacks (perfect fit)
|
||||
- **Vite/React/Bun** → Nixpacks
|
||||
- **Rust helpers** → Nixpacks or Dockerfile
|
||||
- **Custom system deps** → Dockerfile
|
||||
- **Workers/cron** → Prebuilt image or Nixpacks
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Scaling Model
|
||||
|
||||
- Horizontal scaling via container replicas
|
||||
- Node-aware scheduling across Proxmox VMs/LXCs
|
||||
- Metrics-based auto-scaling:
|
||||
- CPU
|
||||
- Memory
|
||||
- Request rate
|
||||
- Stateless application containers
|
||||
- Externalized state (DB, cache, storage)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Deployment Flow
|
||||
|
||||
1. Git push / webhook trigger
|
||||
2. Backend schedules build
|
||||
3. Docker image is built & stored
|
||||
4. Scheduler selects optimal nodes
|
||||
5. Node agents start containers
|
||||
6. Traefik automatically routes traffic
|
||||
7. Metrics collected → scaling decisions applied
|
||||
|
||||
---
|
||||
|
||||
## � Analytics & Monitoring
|
||||
|
||||
### Umami Integration
|
||||
- **Privacy-focused web analytics** for user insights
|
||||
- **Self-hosted analytics dashboard** for application monitoring
|
||||
- **Real-time visitor tracking** with geographic and device data
|
||||
- **Performance metrics** including bounce rates and session duration
|
||||
- **Traffic source analysis** and top page tracking
|
||||
- **GDPR compliant** with no personal data collection
|
||||
- **Integration points**:
|
||||
- Dashboard analytics overview
|
||||
- Project-specific metrics
|
||||
- Service performance tracking
|
||||
- User behavior analysis
|
||||
|
||||
### Built-in Monitoring
|
||||
- **Real-time metrics** with custom visualizations
|
||||
- **Service health monitoring** with status indicators
|
||||
- **Resource usage tracking** (CPU, memory, storage, network)
|
||||
- **Geographic service distribution** with interactive globe
|
||||
- **Performance graphs** and data visualizations
|
||||
- **Build logs integration** with step-by-step progress tracking
|
||||
- **Deployment history** with rollback capabilities
|
||||
|
||||
---
|
||||
|
||||
## �🔐 Networking & Security
|
||||
|
||||
- TLS termination via Traefik
|
||||
- Internal service networking
|
||||
- Agent authentication via tokens / mTLS
|
||||
- No direct Docker socket exposure to UI
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Design Principles
|
||||
|
||||
- Infrastructure-first (Proxmox as the cloud)
|
||||
- Stateless by default
|
||||
- Explicit over magical
|
||||
- Horizontal scalability
|
||||
- Fail-fast & observable
|
||||
- Open-source friendly
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Long-Term Vision
|
||||
|
||||
- Multi-cluster support
|
||||
- Cloud bursting (home → cloud nodes)
|
||||
- Preview environments per Git branch
|
||||
- Resource-based billing (optional)
|
||||
- Plugin system for runtimes & buildpacks
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT / Apache-2.0 (TBD)
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Status
|
||||
|
||||
Early architecture & design phase.
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"assessments": {
|
||||
"naming_quality": 82,
|
||||
"error_consistency": 68,
|
||||
"abstraction_fitness": 75,
|
||||
"logic_clarity": 88,
|
||||
"ai_generated_debt": 72,
|
||||
"type_safety": 70,
|
||||
"contract_coherence": 74
|
||||
},
|
||||
"findings": [
|
||||
{
|
||||
"file": "src/lib/api.ts",
|
||||
"dimension": "type_safety",
|
||||
"line": 95,
|
||||
"confidence": "high",
|
||||
"message": "Return type `any` for user object — define a proper User type matching the auth response (e.g., `{ token: string; user: { id: string; email: string; name: string } }`)."
|
||||
},
|
||||
{
|
||||
"file": "src/lib/api.ts",
|
||||
"dimension": "abstraction_fitness",
|
||||
"line": 6,
|
||||
"confidence": "high",
|
||||
"message": "The `api` object (lines 5-67) duplicates token/auth header logic in all 4 methods, yet `apiCall()` (line 70) already implements this pattern — remove the `api` object or refactor it to use `apiCall` internally."
|
||||
},
|
||||
{
|
||||
"file": "src/lib/api.ts",
|
||||
"dimension": "type_safety",
|
||||
"line": 111,
|
||||
"confidence": "high",
|
||||
"message": "`getProfile` returns `apiCall<any>` — should return a typed `User` from @/types."
|
||||
},
|
||||
{
|
||||
"file": "src/lib/api.ts",
|
||||
"dimension": "type_safety",
|
||||
"line": 133,
|
||||
"confidence": "medium",
|
||||
"message": "`pagination: any` in getProjects response — define `PaginationMeta { page: number; limit: number; total: number; pages: number }`."
|
||||
},
|
||||
{
|
||||
"file": "src/pages/Settings.tsx",
|
||||
"dimension": "error_consistency",
|
||||
"line": 80,
|
||||
"confidence": "high",
|
||||
"message": "Catch block only logs error without propagating or setting error state — either rethrow, set an error state, or show user feedback via toast."
|
||||
},
|
||||
{
|
||||
"file": "src/pages/Settings.tsx",
|
||||
"dimension": "error_consistency",
|
||||
"line": 101,
|
||||
"confidence": "high",
|
||||
"message": "Catch block swallows error silently — add toast notification or error state to inform user of fetch failure."
|
||||
},
|
||||
{
|
||||
"file": "src/components/security/SecurityDashboard.tsx",
|
||||
"dimension": "error_consistency",
|
||||
"line": 108,
|
||||
"confidence": "high",
|
||||
"message": "Catch block only `console.error` — the UI shows stale/empty data with no indication of failure. Set an error state or show an error toast."
|
||||
},
|
||||
{
|
||||
"file": "src/components/security/SecurityDashboard.tsx",
|
||||
"dimension": "error_consistency",
|
||||
"line": 132,
|
||||
"confidence": "high",
|
||||
"message": "Catch block swallows scan start failure — user has no feedback that the scan didn't initiate. Add toast or error state."
|
||||
},
|
||||
{
|
||||
"file": "src/hooks/useAuth.tsx",
|
||||
"dimension": "type_safety",
|
||||
"line": 6,
|
||||
"confidence": "high",
|
||||
"message": "`user: any | null` should be typed as `User | null` using the User type from @/types."
|
||||
},
|
||||
{
|
||||
"file": "src/components/git/DeploymentTriggers.tsx",
|
||||
"dimension": "ai_generated_debt",
|
||||
"line": 64,
|
||||
"confidence": "high",
|
||||
"message": "Comment `// TODO: Replace with actual API call` indicates placeholder code — implement real API integration or extract to a proper mock service for development."
|
||||
},
|
||||
{
|
||||
"file": "src/components/git/DeploymentTriggers.tsx",
|
||||
"dimension": "ai_generated_debt",
|
||||
"line": 106,
|
||||
"confidence": "medium",
|
||||
"message": "Another `// TODO: Replace with actual API call` — these TODOs should be tracked in issue tracker, not committed as code comments."
|
||||
},
|
||||
{
|
||||
"file": "src/components/dashboard/ProjectCanvas.tsx",
|
||||
"dimension": "naming_quality",
|
||||
"line": 342,
|
||||
"confidence": "medium",
|
||||
"message": "Function `getActionButton` returns JSX, not just a button — rename to `renderActionButtons` or `getServiceActionButtons` for clarity."
|
||||
},
|
||||
{
|
||||
"file": "src/components/database/DatabaseDetailPanel.tsx",
|
||||
"dimension": "naming_quality",
|
||||
"line": 148,
|
||||
"confidence": "low",
|
||||
"message": "Parameter `_action` is prefixed with underscore to indicate unused, but the action is still destructured — either use it or remove from the parameter entirely if the mutation doesn't need it."
|
||||
},
|
||||
{
|
||||
"file": "src/pages/Scaling.tsx",
|
||||
"dimension": "ai_generated_debt",
|
||||
"line": 49,
|
||||
"confidence": "medium",
|
||||
"message": "Mock API object `scalingApi` with inline mock data (lines 49-167) should be moved to a separate mock/test utilities file for cleaner production code."
|
||||
},
|
||||
{
|
||||
"file": "src/pages/Projects.tsx",
|
||||
"dimension": "logic_clarity",
|
||||
"line": 128,
|
||||
"confidence": "medium",
|
||||
"message": "Type assertion `as ProjectWithStats[]` after `useMemo` — the API returns `Project[]` but stats are expected. Either add stats to the API response type or document the transformation."
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,437 @@
|
||||
# Containr Development Timeline
|
||||
|
||||
## Phase 1: Foundation & Core Infrastructure (Weeks 1-4)
|
||||
|
||||
### Week 1: Project Setup & Architecture
|
||||
- [x] **Project Documentation**
|
||||
- Complete project.md with React stack
|
||||
- Define architecture principles
|
||||
- Set up development roadmap
|
||||
- [x] **Repository Structure**
|
||||
- Initialize monorepo with backend and frontend
|
||||
- Set up Git workflows and CI/CD pipelines
|
||||
- Configure development environment standards
|
||||
|
||||
### Week 2: Backend Foundation
|
||||
- [x] **Go Backend Setup** ✅ COMPLETED
|
||||
- ✅ Initialize Go module structure with proper dependencies
|
||||
- ✅ Set up basic HTTP server with Gin framework
|
||||
- ✅ Implement middleware for auth, logging, CORS, and request tracking
|
||||
- [x] **Database Schema** ✅ COMPLETED
|
||||
- ✅ Design PostgreSQL schema for users, projects, deployments, services, environments
|
||||
- ✅ Implement database migrations with version tracking
|
||||
- ✅ Set up Redis for caching and sessions (structure ready)
|
||||
- [x] **Authentication System** ✅ COMPLETED
|
||||
- ✅ Implement JWT-based authentication with secure password hashing
|
||||
- ✅ Create user registration and login endpoints
|
||||
- ✅ Add profile management and token validation
|
||||
|
||||
### Week 3: Frontend Foundation
|
||||
- [x] **React + Vite Setup**
|
||||
- Initialize Vite + React + TypeScript project
|
||||
- Configure Tailwind CSS and shadcn/ui
|
||||
- Set up routing with React Router
|
||||
- [x] **Basic UI Framework**
|
||||
- Create layout components (Header, Sidebar, Main)
|
||||
- Implement authentication UI (login/register)
|
||||
- Set up state management with Zustand/Redux Toolkit
|
||||
- [x] **Dashboard Components Implementation**
|
||||
- Implement metric cards (Sales, Visitors, Conversion Rate) with interactive time period selectors
|
||||
- Build charts and visualizations (Visitor Channels, User Retention, Weekly Visitors) with real-time data
|
||||
- Create analytics and monitoring components with advanced styling
|
||||
- Add project canvas with service management UI
|
||||
- Implement responsive design with Tailwind CSS
|
||||
- [x] **Advanced Dashboard Features**
|
||||
- Support Analytics Card with ticket tracking and real-time activity
|
||||
- Campaign Data Card with performance metrics, budget utilization, and monthly trends
|
||||
- Product Categories Card with interactive charts and category performance
|
||||
- Enhanced Project Canvas with Railway-inspired service management
|
||||
- Interactive service detail panels with tabs (overview, deployments, variables, metrics)
|
||||
- Environment management (production, preview, development)
|
||||
- Database management as first-class services
|
||||
- Real-time status indicators and performance metrics
|
||||
- Git integration with commit tracking and deployment history
|
||||
- Advanced metric cards with device breakdown, funnel analysis, and sparkline visualizations
|
||||
- Interactive charts with period selection, hover states, and smooth transitions
|
||||
- Cohort retention heatmap with detailed analytics
|
||||
- Weekly visitor trends with dual-line visualizations
|
||||
|
||||
### Week 4: Infrastructure Setup
|
||||
- [x] **Backend API Foundation** ✅ COMPLETED EARLY
|
||||
- ✅ Complete Go backend with authentication and project management
|
||||
- ✅ RESTful API endpoints for users, projects, and authentication
|
||||
- ✅ Database migrations and seeding system
|
||||
- ✅ JWT-based authentication with secure password hashing
|
||||
- [x] **Frontend-Backend Integration** ✅ COMPLETED
|
||||
- ✅ Complete API client with TypeScript interfaces
|
||||
- ✅ React Query integration for data fetching
|
||||
- ✅ Authentication context and hooks
|
||||
- ✅ Login/register pages with form validation
|
||||
- ✅ Protected routes and authentication flow
|
||||
- [x] **Service Management API** ✅ COMPLETED
|
||||
- ✅ Complete CRUD operations for services
|
||||
- ✅ Project-based service isolation
|
||||
- ✅ Service types (web, worker, database, cron)
|
||||
- ✅ Environment management (production, preview, development)
|
||||
- [x] **Proxmox Integration** ✅ COMPLETED
|
||||
- ✅ Set up Proxmox API client with authentication
|
||||
- ✅ Implement VM/LXC creation and management
|
||||
- ✅ Add node monitoring and resource tracking
|
||||
- ✅ Create infrastructure management UI with real-time statistics
|
||||
- ✅ Implement container lifecycle management (start, stop, delete)
|
||||
- ✅ Add cluster overview and resource usage visualization
|
||||
- [x] **Docker Foundation** ✅ COMPLETED
|
||||
- ✅ Set up Docker client integration
|
||||
- ✅ Implement container management framework
|
||||
- ✅ Add service configuration types and deployment structures
|
||||
- ✅ Create Docker registry and networking support structure
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Platform Features (Weeks 5-8)
|
||||
|
||||
### Week 5: User Management & Auth
|
||||
- [x] **Authentication System** ✅ COMPLETED
|
||||
- ✅ Implement JWT-based authentication
|
||||
- ✅ Add OAuth providers (GitHub, GitLab) - structure ready
|
||||
- ✅ Create user profile management
|
||||
- [x] **Authorization** ✅ COMPLETED
|
||||
- ✅ Role-based access control (RBAC) - structure ready
|
||||
- ✅ Team/organization management - structure ready
|
||||
- ✅ Project permissions system - structure ready
|
||||
|
||||
### Week 6: Project Management
|
||||
- [x] **Project CRUD** ✅ COMPLETED
|
||||
- ✅ Complete project management interface with full CRUD operations
|
||||
- ✅ Project listing with search and filtering capabilities
|
||||
- ✅ Project creation and editing with modal interfaces
|
||||
- ✅ Project deletion with confirmation dialogs
|
||||
- ✅ Project statistics and activity tracking
|
||||
- [x] **Git Integration** ✅ COMPLETED
|
||||
- ✅ Git provider connections (GitHub, GitLab, Bitbucket)
|
||||
- ✅ Repository connection and management interface
|
||||
- ✅ Git provider authentication with access tokens
|
||||
- ✅ Repository browsing and connection workflow
|
||||
- [x] **Webhook Management** ✅ COMPLETED
|
||||
- ✅ Webhook creation and configuration interface
|
||||
- ✅ Event-based webhook triggers (push, pull_request, release)
|
||||
- ✅ Webhook secret management and security
|
||||
- ✅ Branch filtering for webhook triggers
|
||||
- [x] **Branch-Based Deployment Triggers** ✅ COMPLETED
|
||||
- ✅ Deployment trigger configuration per branch and service
|
||||
- ✅ Environment-based deployment routing (production, preview, development)
|
||||
- ✅ Auto-deployment vs manual deployment options
|
||||
- ✅ Custom build and start command configuration
|
||||
|
||||
### Week 7: Build System Integration
|
||||
- [x] **Nixpacks Integration** ✅ COMPLETED
|
||||
- ✅ Implement Nixpacks build detection for multiple runtimes (Go, Node.js, Python, Rust, Ruby, PHP, Deno, Bun, Static)
|
||||
- ✅ Create Nixpacks plan generation with automatic runtime detection
|
||||
- ✅ Build optimization and caching strategies
|
||||
- ✅ Environment variable injection and build customization
|
||||
- [x] **Dockerfile Support** ✅ COMPLETED
|
||||
- ✅ Custom Dockerfile builds with validation and optimization
|
||||
- ✅ Multi-stage build optimization suggestions
|
||||
- ✅ Dockerfile generation for common runtimes
|
||||
- ✅ Build context management and caching strategies
|
||||
|
||||
### Week 8: Deployment Engine
|
||||
- [x] **Basic Deployment** ✅ COMPLETED
|
||||
- ✅ Git push to deployment pipeline with build integration
|
||||
- ✅ Build and container creation with multiple builder support
|
||||
- ✅ Service registration and container orchestration
|
||||
- ✅ Deployment engine with scheduling and node management
|
||||
- ✅ Container lifecycle management (create, start, stop, delete)
|
||||
- [x] **Deployment History** ✅ COMPLETED
|
||||
- ✅ Track deployment versions with comprehensive metadata
|
||||
- ✅ Rollback capabilities with deployment history tracking
|
||||
- ✅ Deployment logs and status with real-time monitoring
|
||||
- ✅ Deployment statistics and analytics
|
||||
- ✅ Service and user deployment metrics
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Orchestration & Scaling (Weeks 9-12)
|
||||
|
||||
### Week 9: Node Agent System
|
||||
- [x] **Agent Architecture** ✅ COMPLETED
|
||||
- ✅ Design lightweight Go agent architecture with comprehensive type system
|
||||
- ✅ Agent registration and heartbeat system with secure authentication
|
||||
- ✅ Secure communication framework with token-based auth
|
||||
- [x] **Container Orchestration** ✅ COMPLETED
|
||||
- ✅ Agent-based container management with full lifecycle support
|
||||
- ✅ Health checks and auto-restart functionality with configurable policies
|
||||
- ✅ Resource monitoring and reporting with real-time metrics
|
||||
- [x] **Frontend Integration** ✅ COMPLETED
|
||||
- ✅ Complete Node Agents management page with real-time status
|
||||
- ✅ Agent resource monitoring with CPU, memory, and storage visualization
|
||||
- ✅ Container management interface with start/stop/restart actions
|
||||
- ✅ Agent capabilities and network interface visualization
|
||||
|
||||
### Week 10: Scheduling & Load Balancing
|
||||
- [x] **Scheduler Implementation** ✅ COMPLETED
|
||||
- ✅ Node selection algorithms (round-robin, least-loaded, best-fit, random)
|
||||
- ✅ Resource-aware scheduling with CPU and memory constraints
|
||||
- ✅ Affinity and anti-affinity rules framework
|
||||
- [x] **Advanced Networking** ✅ COMPLETED
|
||||
- ✅ Service discovery integration with internal DNS
|
||||
- ✅ Load balancing strategies (round-robin, least connections, IP hash, random)
|
||||
- ✅ Internal DNS setup with A and SRV records
|
||||
- ✅ Network utilities for IP management and port allocation
|
||||
|
||||
### Week 11: Auto-Scaling
|
||||
- [x] **Metrics Collection** ✅ COMPLETED
|
||||
- ✅ CPU, memory, network metrics with real-time collection
|
||||
- ✅ Custom application metrics framework
|
||||
- ✅ Performance data aggregation with PostgreSQL storage
|
||||
- ✅ Prometheus export format support
|
||||
- [x] **Scaling Engine** ✅ COMPLETED
|
||||
- ✅ Horizontal pod auto-scaling with configurable policies
|
||||
- ✅ Scaling policies and thresholds with cost optimization
|
||||
- ✅ Scale up/down steps and cooldown periods
|
||||
- ✅ Multi-metric scaling (CPU, memory, requests, error rate)
|
||||
- [x] **Cost Optimization** ✅ COMPLETED
|
||||
- ✅ Cost-aware scaling with maximum hourly budgets
|
||||
- ✅ Resource efficiency preferences
|
||||
- ✅ Idle timeout configurations
|
||||
- ✅ Scaling impact estimation
|
||||
|
||||
### Week 12: High Availability
|
||||
- [x] **Fault Tolerance** ✅ COMPLETED
|
||||
- ✅ Multi-node redundancy with automatic failover
|
||||
- ✅ Failover mechanisms with configurable policies
|
||||
- ✅ Disaster recovery procedures with backup nodes
|
||||
- ✅ Health checking system with multiple check types
|
||||
- [x] **Monitoring & Alerting** ✅ COMPLETED
|
||||
- ✅ Alert rule engine with configurable conditions
|
||||
- ✅ Multiple notification channels (email, Slack, webhook)
|
||||
- ✅ Alert lifecycle management (firing, resolved)
|
||||
- ✅ Health status monitoring and reporting
|
||||
- [x] **High Availability Manager** ✅ COMPLETED
|
||||
- ✅ Centralized HA management with comprehensive API
|
||||
- ✅ Failover policy management per service
|
||||
- ✅ Health check configuration and results
|
||||
- ✅ Alert rule and notifier management
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Advanced Features (Weeks 13-16)
|
||||
|
||||
### Week 13: Analytics & Observability
|
||||
- [x] **Umami Integration** ✅ COMPLETED
|
||||
- ✅ Self-hosted analytics setup with complete UI integration
|
||||
- ✅ Privacy-focused tracking with GDPR compliance
|
||||
- ✅ Real-time visitor analytics with geographic and device data
|
||||
- ✅ Performance metrics including bounce rates and session duration
|
||||
- ✅ Traffic source analysis and top page tracking features
|
||||
- [x] **Custom Metrics** ✅ COMPLETED
|
||||
- ✅ Application performance monitoring dashboard
|
||||
- ✅ Business metrics tracking and custom dashboard creation
|
||||
- ✅ Infrastructure metrics (CPU, memory, disk, network)
|
||||
- ✅ Real-time alerts and status monitoring
|
||||
- ✅ Custom KPI tracking with goal completion
|
||||
|
||||
### Week 14: Database Services
|
||||
- [x] **Managed Databases** ✅ COMPLETED
|
||||
- ✅ PostgreSQL as a service with automated provisioning
|
||||
- ✅ Redis as a service with real-time metrics
|
||||
- ✅ MySQL as a service with performance monitoring
|
||||
- ✅ Database backup and restore functionality
|
||||
- [x] **Database Integrations** ✅ COMPLETED
|
||||
- ✅ Connection pooling and optimization
|
||||
- ✅ Migration management with version tracking
|
||||
- ✅ Performance monitoring and metrics collection
|
||||
- ✅ Automated backup scheduling and retention policies
|
||||
|
||||
### Week 15: Advanced Build Features
|
||||
- [x] **Build Caching** ✅ COMPLETED
|
||||
- ✅ Layer caching optimization for faster builds with intelligent cache key generation
|
||||
- ✅ Remote build cache support with registry integration and authentication
|
||||
- ✅ Parallel build execution with worker pools and optimized scheduling
|
||||
- ✅ Cache statistics and management with cleanup capabilities
|
||||
- [x] **Custom Runtimes** ✅ COMPLETED
|
||||
- ✅ Support for custom buildpacks with Cloud Native Buildpacks integration
|
||||
- ✅ Runtime version management with automatic detection and configuration
|
||||
- ✅ Dependency optimization with caching and pre-built layers
|
||||
- ✅ Custom runtime detection for Elixir, Dart, Swift, Kotlin, and other languages
|
||||
- ✅ Template-based Dockerfile generation with health checks and optimization
|
||||
|
||||
### Week 16: Preview Environments
|
||||
- [x] **Branch Deployments** ✅ COMPLETED
|
||||
- ✅ Automatic preview environments with branch-based creation
|
||||
- ✅ Environment promotion workflows (preview → production/development)
|
||||
- ✅ Preview environment cleanup policies with TTL management
|
||||
- [x] **Collaboration Features** ✅ COMPLETED
|
||||
- ✅ Environment sharing and access management
|
||||
- ✅ Preview environment status tracking and monitoring
|
||||
- ✅ Integration with existing deployment triggers
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Production Readiness (Weeks 17-20)
|
||||
|
||||
### Week 17: Security Hardening ✅ COMPLETED
|
||||
- [x] **Security Audit System** ✅ COMPLETED
|
||||
- ✅ Comprehensive security scanning framework with vulnerability detection
|
||||
- ✅ Multiple scan types: dependency, configuration, and comprehensive scanning
|
||||
- ✅ Real-time vulnerability assessment with severity classification
|
||||
- ✅ Security metrics and scoring system with historical tracking
|
||||
- [x] **Compliance Management** ✅ COMPLETED
|
||||
- ✅ GDPR compliance framework initialization and assessment
|
||||
- ✅ Compliance control management with automated testing
|
||||
- ✅ Risk assessment and mitigation tracking
|
||||
- ✅ Compliance reporting with detailed findings and recommendations
|
||||
- [x] **Data Encryption & Privacy** ✅ COMPLETED
|
||||
- ✅ AES-256 encryption for sensitive data at rest and in transit
|
||||
- ✅ Data anonymization for GDPR compliance with retention policies
|
||||
- ✅ Secure audit logging with encrypted sensitive information
|
||||
- ✅ Data subject access request generation and management
|
||||
|
||||
### Week 18: Performance Optimization ✅ COMPLETED
|
||||
- [x] **Database Optimization** ✅ COMPLETED
|
||||
- [x] Enhanced database connection pooling with configurable parameters
|
||||
- [x] Added performance monitoring and statistics collection
|
||||
- [x] Implemented query optimization with prepared statements and timeouts
|
||||
- [x] Created comprehensive indexing strategy for common query patterns
|
||||
- [x] Added database views for optimized dashboard queries
|
||||
- [x] Implemented batch operations for reduced database round trips
|
||||
- [x] **Frontend Optimization** ✅ COMPLETED
|
||||
- [x] Implemented code splitting and lazy loading utilities
|
||||
- [x] Added React Query optimization with caching strategies
|
||||
- [x] Enhanced component memoization and callback optimization
|
||||
- [x] Configured Vite build optimization with chunk splitting
|
||||
- [x] Added asset optimization and compression settings
|
||||
- [x] Implemented pagination and search optimization
|
||||
- [x] Added performance monitoring and error boundaries
|
||||
|
||||
### Week 19: Documentation & Onboarding ✅ COMPLETED
|
||||
- [x] **Comprehensive Documentation** ✅ COMPLETED
|
||||
- [x] API documentation with OpenAPI specification
|
||||
- [x] User guides and tutorials (Getting Started, Advanced Configuration)
|
||||
- [x] Developer onboarding guides with complete setup instructions
|
||||
- [x] **CLI Tools** ✅ COMPLETED
|
||||
- [x] Command-line interface for management with auth and project commands
|
||||
- [x] Automation scripts for building and installation
|
||||
- [x] Integration with existing tools and Makefile targets
|
||||
|
||||
### Week 20: Beta Testing & Launch Preparation 📅 NEXT
|
||||
- [ ] **Beta Program**
|
||||
- [ ] Beta user onboarding
|
||||
- [ ] Feedback collection and analysis
|
||||
- [ ] Bug fixes and improvements
|
||||
- [ ] **Launch Preparation**
|
||||
- [ ] Production deployment checklist
|
||||
- [ ] Monitoring and alerting setup
|
||||
- [ ] Support documentation and processes
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Post-Launch (Ongoing)
|
||||
|
||||
### Continuous Improvement
|
||||
- [ ] **Feature Development**
|
||||
- User feedback-driven features
|
||||
- Performance improvements
|
||||
- New runtime support
|
||||
- [ ] **Community Building**
|
||||
- Open source community engagement
|
||||
- Contribution guidelines
|
||||
- Community support channels
|
||||
|
||||
### Advanced Features (Future)
|
||||
- [ ] **Multi-Cluster Support**
|
||||
- Cross-cluster deployments
|
||||
- Geographic distribution
|
||||
- Cloud bursting capabilities
|
||||
- [ ] **Plugin System**
|
||||
- Custom runtime plugins
|
||||
- Third-party integrations
|
||||
- Marketplace for extensions
|
||||
- [ ] **Advanced Analytics**
|
||||
- AI-powered optimization
|
||||
- Predictive scaling
|
||||
- Cost optimization recommendations
|
||||
|
||||
---
|
||||
|
||||
## Milestones & Deliverables
|
||||
|
||||
### MVP (Week 8) ✅ Frontend Dashboard Complete - ENHANCED + Week 6 Project Management Complete
|
||||
- [x] **Frontend Dashboard Implementation** ✅ COMPLETED WITH ADVANCED FEATURES
|
||||
- ✅ Complete dashboard UI with enhanced metric cards featuring interactive time period selectors
|
||||
- ✅ Analytics and monitoring components with real-time data visualization
|
||||
- ✅ Project canvas with comprehensive Railway-inspired service management
|
||||
- ✅ Responsive design with modern Tailwind CSS styling and smooth transitions
|
||||
- ✅ Advanced analytics features (Support, Campaigns, Product Categories) with detailed metrics
|
||||
- ✅ Interactive service management with real-time status indicators and performance tracking
|
||||
- ✅ Railway-inspired service detail panels with comprehensive tabs (overview, deployments, variables, metrics)
|
||||
- ✅ Environment management (production, preview, development) with proper isolation
|
||||
- ✅ Database management as first-class services with connection tracking
|
||||
- ✅ Git integration with deployment history, commit tracking, and rollback capabilities
|
||||
- ✅ Performance metrics and monitoring capabilities with visual charts
|
||||
- ✅ Advanced campaign and product category visualizations with interactive elements
|
||||
- ✅ Support analytics with ticket tracking, channel breakdown, and real-time activity feeds
|
||||
- ✅ Enhanced metric cards with device breakdown, conversion funnels, and sparkline charts
|
||||
- ✅ Interactive charts including visitor channels, user retention heatmaps, and weekly trends
|
||||
- ✅ Cohort analysis with detailed retention matrices and time-based comparisons
|
||||
- ✅ Modern UI components with hover states, loading animations, and micro-interactions
|
||||
- [x] **Project Management System** ✅ COMPLETED
|
||||
- ✅ Full CRUD operations for projects with modern UI
|
||||
- ✅ Project search, filtering, and pagination
|
||||
- ✅ Project statistics and activity tracking
|
||||
- ✅ Modal-based project creation and editing
|
||||
- ✅ Integration with service management
|
||||
- [x] **Git Integration Platform** ✅ COMPLETED
|
||||
- ✅ Multi-provider Git integration (GitHub, GitLab, Bitbucket)
|
||||
- ✅ Secure authentication with access token management
|
||||
- ✅ Repository connection and browsing interface
|
||||
- ✅ Webhook management with event-based triggers
|
||||
- ✅ Branch-based deployment triggers with environment routing
|
||||
- ✅ Auto-deployment configuration and manual approval options
|
||||
- ✅ Custom build and start command configuration per trigger
|
||||
- Basic user authentication
|
||||
- Simple project management
|
||||
- Git-based deployments
|
||||
- Basic container orchestration
|
||||
|
||||
### Beta (Week 16)
|
||||
- Full feature set
|
||||
- Advanced scaling capabilities
|
||||
- Analytics and monitoring
|
||||
- Preview environments
|
||||
|
||||
### Production (Week 20)
|
||||
- Security-hardened platform
|
||||
- Performance optimized
|
||||
- Comprehensive documentation
|
||||
- Production-ready monitoring
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment & Mitigation
|
||||
|
||||
### Technical Risks
|
||||
- **Proxmox API limitations**: Build fallback mechanisms and manual provisioning options
|
||||
- **Docker orchestration complexity**: Start with simple orchestration, evolve to more complex patterns
|
||||
- **Performance at scale**: Implement early performance testing and optimization
|
||||
|
||||
### Project Risks
|
||||
- **Scope creep**: Maintain strict MVP focus for initial release
|
||||
- **Technical debt**: Regular refactoring and code review practices
|
||||
- **Team coordination**: Clear documentation and communication protocols
|
||||
|
||||
---
|
||||
|
||||
## Success Metrics
|
||||
|
||||
### Technical Metrics
|
||||
- Deployment success rate > 99%
|
||||
- Platform uptime > 99.9%
|
||||
- Average deployment time < 5 minutes
|
||||
- Auto-scaling response time < 2 minutes
|
||||
|
||||
### Business Metrics
|
||||
- User adoption rate
|
||||
- Customer satisfaction score
|
||||
- Platform utilization metrics
|
||||
- Community engagement levels
|
||||
@@ -0,0 +1,149 @@
|
||||
# Usable UI Components
|
||||
|
||||
This document contains reusable UI components for the Containr project. All components are built with React, TypeScript, and styled using Tailwind CSS with a consistent design system.
|
||||
|
||||
## 📊 Dashboard & Analytics Components
|
||||
|
||||
### Metric Cards
|
||||
|
||||
#### Sales Metric Card
|
||||
Displays total sales with trend indicator and time period selector.
|
||||
|
||||
**Features:**
|
||||
- Large metric display ($128.32)
|
||||
- Trend indicator (+2% with green styling)
|
||||
- Time period buttons (1D, 1W, 1M, 3M, 6M, 1Y)
|
||||
- Report action button
|
||||
|
||||
**Usage:** Perfect for dashboard overviews, KPI displays, and analytics summaries.
|
||||
|
||||
---
|
||||
|
||||
#### Visitors Metric Card
|
||||
Shows visitor statistics with device breakdown and trends.
|
||||
|
||||
**Features:**
|
||||
- Visitor count display (237,456)
|
||||
- Trend indicator (-1.4% with red styling for decline)
|
||||
- Device type breakdown (Desktop: 27%, Mobile: 63%, Tablet: 10%)
|
||||
- Device-specific trend indicators
|
||||
- Report action button
|
||||
|
||||
**Usage:** Ideal for traffic analytics, user behavior dashboards, and marketing metrics.
|
||||
|
||||
---
|
||||
|
||||
#### Conversion Rate Card
|
||||
Displays conversion metrics with funnel breakdown.
|
||||
|
||||
**Features:**
|
||||
- Main conversion rate (16.9%)
|
||||
- Trend indicator (+2.1% growth)
|
||||
- Conversion funnel stages:
|
||||
- Added to Cart: 3,842 (+12%)
|
||||
- Checkout Started: 2,156 (+8%)
|
||||
- Payment Completed: 1,842 (+5%)
|
||||
- Mini sparkline chart
|
||||
- Details action button
|
||||
|
||||
**Usage:** E-commerce dashboards, sales funnel analysis, conversion optimization tracking.
|
||||
|
||||
---
|
||||
|
||||
## 📈 Charts & Visualizations
|
||||
|
||||
### Visitor Channels Chart
|
||||
Horizontal stacked bar chart showing traffic source distribution.
|
||||
|
||||
**Features:**
|
||||
- Overall percentage (78%)
|
||||
- Trend indicator (-0.4%)
|
||||
- Visual channel breakdown:
|
||||
- Organic Search: 45% (gray)
|
||||
- Direct Traffic: 40% (blue)
|
||||
- Social Media: 15% (green)
|
||||
- Channel labels with percentages
|
||||
- Details action button
|
||||
|
||||
**Usage:** Marketing analytics, traffic source analysis, channel performance tracking.</div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Report</button></div><div role="group" dir="ltr" class="-space-x-[1.5px] grid auto-cols-fr grid-flow-col" tabindex="0" style="outline: none;"><button type="button" data-state="off" role="radio" aria-checked="false" class="group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none border border-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:bg-bg-weak-50 focus:outline-none data-[state=on]:bg-bg-weak-50 data-[state=on]:text-text-strong-950 disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 h-6 gap-3 px-3 text-label-xs first:rounded-l-md last:rounded-r-md" tabindex="-1" data-radix-collection-item="">1D</button><button type="button" data-state="on" role="radio" aria-checked="true" class="group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none border border-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:bg-bg-weak-50 focus:outline-none data-[state=on]:bg-bg-weak-50 data-[state=on]:text-text-strong-950 disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 h-6 gap-3 px-3 text-label-xs first:rounded-l-md last:rounded-r-md" tabindex="-1" data-radix-collection-item="">1W</button><button type="button" data-state="off" role="radio" aria-checked="false" class="group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none border border-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:bg-bg-weak-50 focus:outline-none data-[state=on]:bg-bg-weak-50 data-[state=on]:text-text-strong-950 disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 h-6 gap-3 px-3 text-label-xs first:rounded-l-md last:rounded-r-md" tabindex="-1" data-radix-collection-item="">1M</button><button type="button" data-state="off" role="radio" aria-checked="false" class="group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none border border-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:bg-bg-weak-50 focus:outline-none data-[state=on]:bg-bg-weak-50 data-[state=on]:text-text-strong-950 disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 h-6 gap-3 px-3 text-label-xs first:rounded-l-md last:rounded-r-md" tabindex="-1" data-radix-collection-item="">3M</button><button type="button" data-state="off" role="radio" aria-checked="false" class="group relative flex items-center justify-center whitespace-nowrap bg-bg-white-0 text-center text-text-sub-600 outline-none border border-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:bg-bg-weak-50 focus:outline-none data-[state=on]:bg-bg-weak-50 data-[state=on]:text-text-strong-950 disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 h-6 gap-3 px-3 text-label-xs first:rounded-l-md last:rounded-r-md" tabindex="-1" data-radix-collection-item="">1Y</button></div><div class="recharts-responsive-container" style="width: 100%; height: 108px; min-width: 0px;"><div class="recharts-wrapper [&_.recharts-cartesian-grid-horizontal>line]:stroke-stroke-soft-200 [&_.recharts-cartesian-grid-horizontal>line]:[stroke-dasharray:0] [&_.recharts-cartesian-grid-vertical>line:last-child]:opacity-0 [&_.recharts-cartesian-grid-vertical>line:nth-last-child(2)]:opacity-0" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 108px; max-width: 481px;"><svg class="recharts-surface" width="481" height="108" viewBox="0 0 481 108" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts3-clip"><rect x="0" y="6" height="96" width="481"></rect></clipPath></defs><g class="recharts-cartesian-grid"><g class="recharts-cartesian-grid-horizontal"><line stroke-dasharray="4 4" class="stroke-stroke-soft-200" stroke="#ccc" fill="none" x="0" y="6" width="481" height="96" x1="0" y1="54" x2="481" y2="54"></line><line stroke-dasharray="4 4" class="stroke-stroke-soft-200" stroke="#ccc" fill="none" x="0" y="6" width="481" height="96" x1="0" y1="6" x2="481" y2="6Line truncated
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-5 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">Total Visitors</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">237,456</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-error-light text-error-dark">-1.4%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Report</button></div><div role="separator" class="relative flex w-full items-center h-1 before:absolute before:left-0 before:top-1/2 before:h-px before:w-full before:-translate-y-1/2 before:bg-stroke-soft-200"></div><div class="flex gap-4"><div class="flex h-60 flex-1 flex-col gap-4"><div class="w-full flex-1"><div class="text-label-sm text-text-soft-400">Desktop</div><div class="mt-1 text-title-h6 text-text-strong-950">27%</div></div><div class="flex items-center gap-0.5"><div class="text-label-sm text-text-sub-600">-3.2%</div><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 text-error-base"><path d="M9 13.589L17.6066 4.98242L19.0208 6.39664L10.4142 15.0032H18V17.0032H7V6.00324H9V13.589Z"></path></svg></div><div class="h-2 w-full rounded-sm bg-away-base"></div></div><div class="relative w-0"><div class="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 text-stroke-soft-200" style="background: linear-gradient(currentcolor 4px, transparent 4px) 50% 50% / 1px 10px repeat-y;"></div></div><div class="flex h-60 flex-1 flex-col gap-4"><div class="w-full flex-1"><div class="text-label-sm text-text-soft-400">Tablet</div><div class="mt-1 text-title-h6 text-text-strong-950">12%</div></div><div class="flex items-center gap-0.5"><div class="text-label-sm text-text-sub-600">-6.4%</div><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 text-error-base"><path d="M9 13.589L17.6066 4.98242L19.0208 6.39664L10.4142 15.0032H18V17.0032H7V6.00324H9V13.589Z"></path></svg></div><div class="h-2 w-full rounded-sm bg-verified-base"></div></div><div class="relative w-0"><div class="absolute left-1/2 top-0 h-full w-px -translate-x-1/2 text-stroke-soft-200" style="background: linear-gradient(currentcolor 4px, transparent 4px) 50% 50% / 1px 10px repeat-y;"></div></div><div class="flex h-60 flex-1 flex-col gap-4"><div class="w-full flex-1"><div class="text-label-sm text-text-soft-400">Mobile</div><div class="mt-1 text-title-h6 text-text-strong-950">61%</div></div><div class="flex items-center gap-0.5"><div class="text-label-sm text-text-sub-600">+0.8%</div><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 text-success-base"><path d="M16.0037 9.41421L7.39712 18.0208L5.98291 16.6066L14.5895 8H7.00373V6H18.0037V17H16.0037V9.41421Z"></path></svg></div><div class="h-2 w-full rounded-sm bg-feature-base"></div></div></div></div>
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-5 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">Conversion Rate</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">16.9%</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+2.1%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div role="separator" class="relative flex w-full items-center h-1 before:absolute before:left-0 before:top-1/2 before:h-px before:w-full before:-translate-y-1/2 before:bg-stroke-soft-200"></div><div class="flex w-full flex-col gap-3"><div class="flex items-center gap-1.5"><div class="flex-1 text-label-sm text-text-sub-600">Added to Cart</div><div class="flex items-center gap-1.5"><div class="min-w-16 text-label-sm tabular-nums text-text-sub-600">3,842</div><div class="flex min-w-16 items-center justify-end gap-0.5 pl-1 text-right tabular-nums"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 shrink-0 text-success-base"><path d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z"></path></svg><div class="text-paragraph-sm text-text-sub-600">+1.8%</div></div></div></div><div class="flex items-center gap-1.5"><div class="flex-1 text-label-sm text-text-sub-600">Reached Checkout</div><div class="flex items-center gap-1.5"><div class="min-w-16 text-label-sm tabular-nums text-text-sub-600">1,256</div><div class="flex min-w-16 items-center justify-end gap-0.5 pl-1 text-right tabular-nums"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 shrink-0 text-error-base"><path d="M13.0001 16.1716L18.3641 10.8076L19.7783 12.2218L12.0001 20L4.22192 12.2218L5.63614 10.8076L11.0001 16.1716V4H13.0001V16.1716Z"></path></svg><div class="text-paragraph-sm text-text-sub-600">-1.2%</div></div></div></div><div class="flex items-center gap-1.5"><div class="flex-1 text-label-sm text-text-sub-600">Purchased</div><div class="flex items-center gap-1.5"><div class="min-w-16 text-label-sm tabular-nums text-text-sub-600">649</div><div class="flex min-w-16 items-center justify-end gap-0.5 pl-1 text-right tabular-nums"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-5 text-success-base"><path d="M13.0001 7.82843V20H11.0001V7.82843L5.63614 13.1924L4.22192 11.7782L12.0001 4L19.7783 11.7782L18.3641 13.1924L13.0001 7.82843Z"></path></svg><div class="text-paragraph-sm text-text-sub-600">+2.4%</div></div></div></div></div><div class="relative w-full" style="height: 136px;"><div class="absolute inset-0 overflow-hidden rounded-lg ring-1 ring-inset ring-stroke-soft-200" style="height: 112px; background: linear-gradient(90deg, hsl(var(--stroke-soft-200)) 1px, #0000 1px 100%) 0 0 / calc(100% / 6) 112px repeat no-repeat,
|
||||
linear-gradient(180deg, hsl(var(--stroke-soft-200)) 1px, #0000 1px 100%) 0 0 / 100% calc(112px / 4) no-repeat repeat;"></div><div class="absolute bottom-6 left-0 z-10 size-4 overflow-hidden"><div class="size-4 rounded-bl-lg" style="box-shadow: -100px 100px 0 100px hsl(var(--bg-white-0));"></div></div><div class="absolute bottom-6 right-0 z-10 size-4 overflow-hidden"><div class="size-4 rounded-br-lg" style="box-shadow: 100px 100px 0 100px hsl(var(--bg-white-0));"></div></div><div class="recharts-responsive-container" style="width: 100%; height: 100%; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 136px; max-width: 481px;"><svg class="recharts-surface" width="481" height="136" viewBox="0 0 481 136" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts6-clip"><rect x="0" y="0" height="112" width="481"></rect></clipPath></defs><g class="recharts-layer recharts-cartesian-axis recharts-xAxis xAxis [&_.recharts-cartesian-axis-tick_text]:fill-text-soft-400 [&_.recharts-cartesian-axis-tick_text]:text-subheading-2xs"><g class="recharts-cartesian-axis-ticks"><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="28.294117647058822" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="28.294117647058822" dy="0.71em">JUL '23</tspan></text></g><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="113.17647058823529" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="113.17647058823529" dy="0.71em">OCT '23</tspan></text></g><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="198.05882352941177" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="198.05882352941177" dy="0.71em">JAN '24</tspan></text></g><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="282.94117647058823" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="282.94117647058823" dy="0.71em">APR '24</tspan></text></g><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="367.8235294117647" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="367.8235294117647" dy="0.71em">JUL '24</tspan></text></g><g class="recharts-layer recharts-cartesian-axis-tick"><text orientation="bottom" width="481" height="24" stroke="none" x="458.2562484741211" y="126" class="recharts-text recharts-cartesian-axis-tick-value" text-anchor="middle" fill="#666"><tspan x="458.2562484741211" dy="0.71em">NOV '24</tspan></text></g></g></g><g class="recharts-layer recharts-area fill-bg-white-0"><g class="recharts-layer"><path fill="hsl(var(--bg-white-0))" class="recharts-curve recharts-area-area" fill-opacity="1" width="481" height="112" stroke="none" d="M0,35L28.294,0L56.588,8.4L84.882,16.8L113.176,61.6L141.471,33.6L169.765,2.8L198.059,39.2L226.353,9.8L254.647,71.4L282.941,35L311.235,14L339.529,39.2L367.824,5.6L396.118,22.4L424.412,15.4L452.706,40.6L481,25.2L481,112L452.706,112L424.412,112L396.118,112L367.824,112L339.529,112L311.235,112L282.941,112L254.647,112L226.353,112L198.059,112L169.765,112L141.471,112L113.176,112L84.882,112L56.588,112L28.294,112L0,112Z"></path></g></g><g class="recharts-layer recharts-area fill-primary-alpha-16"><g class="recharts-layer"><path fill="hsl(var(--primary-alpha-16))" fill-opacity="1" class="recharts-curve recharts-area-area" width="481" height="112" stroke="none" d="M0,35L28.294,0L56.588,8.4L84.882,16.8L113.176,61.6L141.471,33.6L169.765,2.8L198.059,39.2L226.353,9.8L254.647,71.4L282.941,35L311.235,14L339.529,39.2L367.824,5.6L396.118,22.4L424.412,15.4L452.706,40.6L481,25.2L481,112L452.706,112L424.412,112L396.118,112L367.824,112L339.529,112L311.235,112L282.941,112L254.647,112L226.353,112L198.059,112L169.765,112L141.471,112L113.176,112L84.882,112L56.588,112L28.294,112L0,112Z"></path></g></g><g class="recharts-layer recharts-area fill-primary-base"><g class="recharts-layer"><path fill="hsl(var(--primary-base))" fill-opacity="1" class="recharts-curve recharts-area-area" width="481" height="112" stroke="none" d="M0,61.6L28.294,40.6L56.588,49L84.882,49L113.176,88.2L141.471,81.2L169.765,46.2L198.059,79.8L226.353,50.4L254.647,95.2L282.941,71.4L311.235,39.2L339.529,65.8L367.824,40.6L396.118,54.6L424.412,49L452.706,71.4L481,60.2L481,112L452.706,112L424.412,112L396.118,112L367.824,112L339.529,112L311.235,112L282.941,112L254.647,112L226.353,112L198.059,112L169.765,112L141.471,112L113.176,112L84.882,112L56.588,112L28.Line truncated
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-5 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="relative flex w-full flex-col gap-5"><div class="flex items-start gap-2"><div class="flex-1"><div class="flex items-center gap-1"><div class="text-label-sm text-text-sub-600">Visitors Channels</div></div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">78%</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-error-light text-error-dark">-0.4%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="flex flex-col gap-5"><div class="flex gap-[5px]"><div class="h-2 rounded-sm transition-all" style="width: 45%;"><div class="chart-category-cell-load h-full rounded-sm bg-away-base" style="--i: 0;"></div></div><div class="h-2 rounded-sm transition-all" style="width: 40%;"><div class="chart-category-cell-load h-full rounded-sm bg-sky-500" style="--i: 1;"></div></div><div class="h-2 rounded-sm transition-all" style="width: 15%;"><div class="chart-category-cell-load h-full rounded-sm bg-feature-base" style="--i: 2;"></div></div></div><div class="flex flex-wrap gap-4"><div class="flex items-center gap-1 text-left text-label-xs text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-away-base"></div>Organic Search</div><div class="flex items-center gap-1 text-left text-label-xs text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-sky-500"></div>Referral</div><div class="flex items-center gap-1 text-left text-label-xs text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-feature-base"></div>Direct</div></div></div><div role="separator" class="relative flex w-full items-center h-1 before:absolute before:left-0 before:top-1/2 before:h-px before:w-full before:-translate-y-1/2 before:bg-stroke-soft-200"></div><table class="w-full" cellpadding="0"><thead class="text-left"><tr><th class="text-label-xs text-text-soft-400">Channels</th><th class="text-label-xs text-text-soft-400">Percent</th><th class="w-12 text-label-xs text-text-soft-400">Total</th></tr></thead><tbody aria-hidden="true" class="h-4"></tbody><tbody><tr><td><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><img src="/images/social/facebook.svg" alt="" class="size-5 shrink-0">Facebook</div></td><td><div class="text-label-sm text-text-sub-600">28%</div></td><td><div class="text-label-sm text-text-sub-600">6,958</div></td></tr><tr aria-hidden="true"><td colspan="999" class="h-4"></td></tr><tr><td><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><img src="/images/social/instagram.svg" alt="" class="size-5 shrink-0">Instagram</div></td><td><div class="text-label-sm text-text-sub-600">23%</div></td><td><div class="text-label-sm text-text-sub-600">5,716</div></td></tr><tr aria-hidden="true"><td colspan="999" class="h-4"></td></tr><tr><td><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><img src="/images/social/google.svg" alt="" class="size-5 shrink-0">Google</div></td><td><div class="text-label-sm text-text-sub-600">32%</div></td><td><div class="text-label-sm text-text-sub-600">7,952</div></td></tr></tbody></table><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-8 gap-2.5 rounded-lg px-2.5 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">View reports</button></div></div>
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-5 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">User Retention</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">24%</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+2.0%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class=""><table class="-m-px h-[194px] w-full border-collapse" cellpadding="0"><tbody><tr><td class="p-px" data-value="86"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.86;"></div></td><td class="p-px" data-value="92"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.92;"></div></td><td class="p-px" data-value="87"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.87;"></div></td><td class="p-px" data-value="81"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.81;"></div></td><td class="p-px" data-value="96"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.96;"></div></td><td class="p-px" data-value="94"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.94;"></div></td><td class="p-px" data-value="85"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.85;"></div></td><td class="p-px" data-value="89"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.89;"></div></td><td class="p-px" data-value="81"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.81;"></div></td><td class="p-px" data-value="95"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.95;"></div></td><td class="p-px" data-value="91"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.91;"></div></td><td class="p-px" data-value="91"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.91;"></div></td></tr><tr><td class="p-px" data-value="77"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.77;"></div></td><td class="p-px" data-value="80"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.8;"></div></td><td class="p-px" data-value="76"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.76;"></div></td><td class="p-px" data-value="86"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.86;"></div></td><td class="p-px" data-value="87"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.87;"></div></td><td class="p-px" data-value="94"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.94;"></div></td><td class="p-px" data-value="92"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.92;"></div></td><td class="p-px" data-value="88"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.88;"></div></td><td class="p-px" data-value="83"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.83;"></div></td><td class="p-px" data-value="82"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.82;"></div></td><td class="p-px" data-value="89"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.89;"></div></td><td class="p-px"><div class="h-full w-full rounded-[1px]"></div></td></tr><tr><td class="p-px" data-value="87"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.87;"></div></td><td class="p-px" data-value="86"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.86;"></div></td><td class="p-px" data-value="72"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.72;"></div></td><td class="p-px" data-value="71"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.71;"></div></td><td class="p-px" data-value="74"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.74;"></div></td><td class="p-px" data-value="82"><div class="h-full w-full rounded-[1px] bg-primary-base" style="opacity: 0.82;"></div></td><td class="p-px" data-value="77"><div class="h-full w-fulLine truncated
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-4 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">Weekly Visitors</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">16,008</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+1.1%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="flex w-full gap-1.5 rounded-lg bg-bg-white-0 py-1.5 ring-1 ring-inset ring-stroke-soft-200"><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-warning-base"></div></div><span class="text-label-xs text-text-sub-600">New visitors</span></div><div class="relative w-0 before:absolute before:left-0 before:top-0 before:h-full before:w-px before:bg-stroke-soft-200"></div><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-success-base"></div></div><span class="text-label-xs text-text-sub-600">Returning visitors</span></div></div><div class="recharts-responsive-container recharts-fade-in-axis-tick [&_.recharts-surface]:overflow-visible" style="width: 100%; height: 224px; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 224px; max-width: 481px;"><svg cx="50%" cy="50%" class="recharts-surface" width="481" height="224" viewBox="0 0 481 224" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts13-clip"><rect x="5" y="5" height="214" width="471"></rect></clipPath></defs><g class="recharts-polar-grid"><g class="recharts-polar-grid-angle"><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="32.81999999999999"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="56.01128506564917"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="319.68" y2="112"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="191.18"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564916" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="161.32" y2="112.00000000000001"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564913" y2="56.01128506564917"></line></g><g class="recharts-polar-grid-concentric"><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="0" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="19.795" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,92.205L 254.4971787335877,98.0028212664123L 260.295,112L 254.4971787335877,125.9971787335877L 240.5,131.79500000000002L 226.5028212664123,125.9971787335877L 220.70499999999998,112L 226.5028212664123,98.0028212664123Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="39.59" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,72.41L 268.4943574671754,84.00564253282458L 280.09000000000003,112L 268.4943574671754,139.9943574671754L 240.5,151.59L 212.5056425328246,139.9943574671754L 200.91,112L 212.50564253282457,84.00564253282458Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="59.385000000000005" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,52.614999999999995L 282.4915362007631,70.00846379923688L 299.885,112L 282.4915362007631,153.9915362007631L 240.5,171.385L 198.50846Line truncated
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-4 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">Weekly Visitors</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">16,008</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+1.1%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="flex w-full gap-1.5 rounded-lg bg-bg-white-0 py-1.5 ring-1 ring-inset ring-stroke-soft-200"><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-warning-base"></div></div><span class="text-label-xs text-text-sub-600">New visitors</span></div><div class="relative w-0 before:absolute before:left-0 before:top-0 before:h-full before:w-px before:bg-stroke-soft-200"></div><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-success-base"></div></div><span class="text-label-xs text-text-sub-600">Returning visitors</span></div></div><div class="recharts-responsive-container recharts-fade-in-axis-tick [&_.recharts-surface]:overflow-visible" style="width: 100%; height: 224px; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 224px; max-width: 481px;"><svg cx="50%" cy="50%" class="recharts-surface" width="481" height="224" viewBox="0 0 481 224" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts13-clip"><rect x="5" y="5" height="214" width="471"></rect></clipPath></defs><g class="recharts-polar-grid"><g class="recharts-polar-grid-angle"><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="32.81999999999999"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="56.01128506564917"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="319.68" y2="112"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="191.18"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564916" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="161.32" y2="112.00000000000001"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564913" y2="56.01128506564917"></line></g><g class="recharts-polar-grid-concentric"><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="0" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="19.795" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,92.205L 254.4971787335877,98.0028212664123L 260.295,112L 254.4971787335877,125.9971787335877L 240.5,131.79500000000002L 226.5028212664123,125.9971787335877L 220.70499999999998,112L 226.5028212664123,98.0028212664123Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="39.59" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,72.41L 268.4943574671754,84.00564253282458L 280.09000000000003,112L 268.4943574671754,139.9943574671754L 240.5,151.59L 212.5056425328246,139.9943574671754L 200.91,112L 212.50564253282457,84.00564253282458Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="59.385000000000005" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,52.614999999999995L 282.4915362007631,70.00846379923688L 299.885,112L 282.4915362007631,153.9915362007631L 240.5,171.385L 198.50846Line truncated
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col gap-4 rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="text-label-sm text-text-sub-600">Weekly Visitors</div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">16,008</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+1.1%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="flex w-full gap-1.5 rounded-lg bg-bg-white-0 py-1.5 ring-1 ring-inset ring-stroke-soft-200"><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-warning-base"></div></div><span class="text-label-xs text-text-sub-600">New visitors</span></div><div class="relative w-0 before:absolute before:left-0 before:top-0 before:h-full before:w-px before:bg-stroke-soft-200"></div><div class="flex flex-1 items-center justify-center gap-1"><div class="flex size-4 shrink-0 items-center justify-center"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-success-base"></div></div><span class="text-label-xs text-text-sub-600">Returning visitors</span></div></div><div class="recharts-responsive-container recharts-fade-in-axis-tick [&_.recharts-surface]:overflow-visible" style="width: 100%; height: 224px; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 224px; max-width: 481px;"><svg cx="50%" cy="50%" class="recharts-surface" width="481" height="224" viewBox="0 0 481 224" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts13-clip"><rect x="5" y="5" height="214" width="471"></rect></clipPath></defs><g class="recharts-polar-grid"><g class="recharts-polar-grid-angle"><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="32.81999999999999"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="56.01128506564917"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="319.68" y2="112"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="296.4887149343508" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="240.5" y2="191.18"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564916" y2="167.98871493435084"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="161.32" y2="112.00000000000001"></line><line stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" x1="240.5" y1="112" x2="184.51128506564913" y2="56.01128506564917"></line></g><g class="recharts-polar-grid-concentric"><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="0" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112L 240.5,112Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="19.795" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,92.205L 254.4971787335877,98.0028212664123L 260.295,112L 254.4971787335877,125.9971787335877L 240.5,131.79500000000002L 226.5028212664123,125.9971787335877L 220.70499999999998,112L 226.5028212664123,98.0028212664123Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="39.59" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,72.41L 268.4943574671754,84.00564253282458L 280.09000000000003,112L 268.4943574671754,139.9943574671754L 240.5,151.59L 212.5056425328246,139.9943574671754L 200.91,112L 212.50564253282457,84.00564253282458Z"></path><path stroke="hsl(var(--stroke-soft-200))" cx="240.5" cy="112" radius="59.385000000000005" fill="none" class="recharts-polar-grid-concentric-polygon" d="M 240.5,52.614999999999995L 282.4915362007631,70.00846379923688L 299.885,112L 282.4915362007631,153.9915362007631L 240.5,171.385L 198.50846Line truncated
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col rounded-2xl bg-bg-white-0 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex flex-col gap-4 p-5"><div class="flex items-start gap-2"><div class="flex-1"><div class="flex items-center gap-1"><div class="text-label-sm text-text-sub-600">Shipping Tracking</div><button data-state="closed"><svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-5 text-text-disabled-300"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 16.25C13.4518 16.25 16.25 13.4518 16.25 10C16.25 6.54822 13.4518 3.75 10 3.75C6.54822 3.75 3.75 6.54822 3.75 10C3.75 13.4518 6.54822 16.25 10 16.25ZM11.1158 13.2086L11.2156 12.8006C11.164 12.8249 11.0807 12.8526 10.9665 12.8841C10.852 12.9157 10.7489 12.9318 10.6583 12.9318C10.4654 12.9318 10.3295 12.9001 10.2507 12.8366C10.1724 12.773 10.1333 12.6534 10.1333 12.4783C10.1333 12.4089 10.1451 12.3054 10.1697 12.17C10.1936 12.0337 10.2211 11.9126 10.2516 11.8067L10.6242 10.4876C10.6607 10.3665 10.6857 10.2334 10.6992 10.0882C10.7129 9.94325 10.7193 9.84185 10.7193 9.78429C10.7193 9.50614 10.6218 9.28041 10.4268 9.10629C10.2317 8.93229 9.95393 8.84529 9.59396 8.84529C9.39365 8.84529 9.18188 8.88088 8.95776 8.952C8.73363 9.02294 8.49933 9.1084 8.25421 9.2082L8.15415 9.6165C8.22719 9.58949 8.31419 9.56043 8.41598 9.53034C8.51732 9.50038 8.61674 9.48489 8.71347 9.48489C8.91096 9.48489 9.04399 9.51856 9.1137 9.58488C9.18342 9.65139 9.21844 9.7697 9.21844 9.93883C9.21844 10.0324 9.20736 10.1363 9.18438 10.2492C9.16172 10.3628 9.13342 10.483 9.10013 10.6098L8.72595 11.9342C8.69266 12.0734 8.66834 12.1979 8.65304 12.3084C8.63786 12.4189 8.63057 12.5272 8.63057 12.6326C8.63057 12.9048 8.73114 13.1292 8.93222 13.3063C9.13329 13.4826 9.41523 13.5714 9.77769 13.5714C10.0137 13.5714 10.2209 13.5406 10.3992 13.4785C10.5773 13.4167 10.8164 13.3268 11.1158 13.2086ZM11.0495 7.8502C11.2235 7.68882 11.3101 7.49254 11.3101 7.26272C11.3101 7.03341 11.2236 6.83675 11.0495 6.67331C10.8758 6.51032 10.6666 6.42857 10.4219 6.42857C10.1765 6.42857 9.96635 6.51013 9.79107 6.67331C9.61579 6.83675 9.52796 7.03334 9.52796 7.26272C9.52796 7.49254 9.61579 7.68875 9.79107 7.8502C9.96667 8.01217 10.1764 8.09321 10.4219 8.09321C10.6666 8.09321 10.8758 8.01217 11.0495 7.8502Z" fill="currentColor"></path></svg></button></div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">3,844</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">+1.8%</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 rounded-lg text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950 gap-2 px-2.5" type="button" role="combobox" aria-controls="radix-:r19:" aria-expanded="false" aria-autocomplete="none" dir="ltr" data-state="closed"><span style="pointer-events: none;"><span class="flex flex-1 items-center gap-2 group-disabled:text-text-disabled-300"><span class="line-clamp-1">Daily</span></span></span><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon flex size-5 shrink-0 items-center justify-center -mx-1"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg></button></div><div role="group" dir="ltr" class="flex flex-wrap gap-2.5" tabindex="0" style="outline: none;"><button type="button" data-state="on" role="radio" aria-checked="true" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Delivered</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">In-transit</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Returned</button></div></div>Line truncated
|
||||
linear-gradient(360deg, hsl(var(--stroke-soft-200)) 1px, #0000 1px 100%) 0 0 / 100% calc(152px / 4) no-repeat repeat;"><div class="recharts-responsive-container" style="width: 100%; height: 76px; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 76px; max-width: 445px;"><svg class="recharts-surface" width="445" height="76" viewBox="0 0 445 76" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts16-clip"><rect x="0" y="1" height="74" width="445"></rect></clipPath></defs><g class="recharts-layer recharts-line stroke-primary-base"><path class="recharts-curve recharts-line-curve" stroke="hsl(var(--primary-base))" stroke-width="2" fill="none" width="445" height="74" d="M0,52.93L37.083,52.93L37.083,26.965L111.25,26.965L111.25,75L185.417,75L185.417,1L259.583,1L259.583,39.947L333.75,39.947L333.75,65.912L407.917,65.912L407.917,10.088L445,10.088"></path><g class="recharts-layer"></g></g></svg></div></div></div><div class="grid auto-cols-fr grid-flow-col gap-0.5 px-4 py-3 text-center"><div class="text-label-xs text-text-soft-400">Mon</div><div class="text-label-xs text-text-soft-400">Tue</div><div class="text-label-xs text-text-soft-400">Wed</div><div class="text-label-xs text-text-soft-400">Thu</div><div class="text-label-xs text-text-soft-400">Fri</div><div class="text-label-xs text-text-soft-400">Sat</div><div class="text-label-xs text-text-soft-400">Sun</div></div></div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="-col-end-1 row-span-2 row-start-1"><div class="relative flex w-full flex-col rounded-2xl bg-bg-white-0 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex flex-col gap-3 p-5"><div class="flex items-start gap-2"><div class="flex-1"><div class="flex items-center gap-1"><div class="text-label-sm text-text-sub-600">Support Analytics</div><button data-state="closed"><svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-5 text-text-disabled-300"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 16.25C13.4518 16.25 16.25 13.4518 16.25 10C16.25 6.54822 13.4518 3.75 10 3.75C6.54822 3.75 3.75 6.54822 3.75 10C3.75 13.4518 6.54822 16.25 10 16.25ZM11.1158 13.2086L11.2156 12.8006C11.164 12.8249 11.0807 12.8526 10.9665 12.8841C10.852 12.9157 10.7489 12.9318 10.6583 12.9318C10.4654 12.9318 10.3295 12.9001 10.2507 12.8366C10.1724 12.773 10.1333 12.6534 10.1333 12.4783C10.1333 12.4089 10.1451 12.3054 10.1697 12.17C10.1936 12.0337 10.2211 11.9126 10.2516 11.8067L10.6242 10.4876C10.6607 10.3665 10.6857 10.2334 10.6992 10.0882C10.7129 9.94325 10.7193 9.84185 10.7193 9.78429C10.7193 9.50614 10.6218 9.28041 10.4268 9.10629C10.2317 8.93229 9.95393 8.84529 9.59396 8.84529C9.39365 8.84529 9.18188 8.88088 8.95776 8.952C8.73363 9.02294 8.49933 9.1084 8.25421 9.2082L8.15415 9.6165C8.22719 9.58949 8.31419 9.56043 8.41598 9.53034C8.51732 9.50038 8.61674 9.48489 8.71347 9.48489C8.91096 9.48489 9.04399 9.51856 9.1137 9.58488C9.18342 9.65139 9.21844 9.7697 9.21844 9.93883C9.21844 10.0324 9.20736 10.1363 9.18438 10.2492C9.16172 10.3628 9.13342 10.483 9.10013 10.6098L8.72595 11.9342C8.69266 12.0734 8.66834 12.1979 8.65304 12.3084C8.63786 12.4189 8.63057 12.5272 8.63057 12.6326C8.63057 12.9048 8.73114 13.1292 8.93222 13.3063C9.13329 13.4826 9.41523 13.5714 9.77769 13.5714C10.0137 13.5714 10.2209 13.5406 10.3992 13.4785C10.5773 13.4167 10.8164 13.3268 11.1158 13.2086ZM11.0495 7.8502C11.2235 7.68882 11.3101 7.49254 11.3101 7.26272C11.3101 7.03341 11.2236 6.83675 11.0495 6.67331C10.8758 6.51032 10.6666 6.42857 10.4219 6.42857C10.1765 6.42857 9.96635 6.51013 9.79107 6.67331C9.61579 6.83675 9.52796 7.03334 9.52796 7.26272C9.52796 7.49254 9.61579 7.68875 9.79107 7.8502C9.96667 8.01217 10.1764 8.09321 10.4219 8.09321C10.6666 8.09321 10.8758 8.01217 11.0495 7.8502Z" fill="currentColor"></path></svg></button></div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">2450</div><div class="text-label-sm text-text-sub-600"><span class="text-success-base">+5.4%</span> total tickets</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 rounded-lg text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950 gap-2 px-2.5" type="button" role="combobox" aria-controls="radix-:r1v:" aria-expanded="false" aria-autocomplete="none" dir="ltr" data-state="closed"><span style="pointer-events: none;"><span class="flex flex-1 items-center gap-2 group-disabled:text-text-disabled-300"><span class="line-clamp-1">Daily</span></span></span><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon flex size-5 shrink-0 items-center justify-center -mx-1"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg></button></div><div role="group" dir="ltr" class="flex flex-wrap gap-1.5" tabindex="0" style="outline: none;"><button type="button" data-state="on" role="radio" aria-checked="true" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Technical</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Billing</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Account</button><button type="button" data-state="offLine truncated
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="relative flex w-full flex-col overflow-hidden rounded-2xl bg-bg-white-0 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2 p-5 pb-4"><div class="flex-1"><div class="flex items-center gap-1"><div class="text-label-sm text-text-sub-600">Campaign Data</div><button data-state="closed"><svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-5 text-text-disabled-300"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 16.25C13.4518 16.25 16.25 13.4518 16.25 10C16.25 6.54822 13.4518 3.75 10 3.75C6.54822 3.75 3.75 6.54822 3.75 10C3.75 13.4518 6.54822 16.25 10 16.25ZM11.1158 13.2086L11.2156 12.8006C11.164 12.8249 11.0807 12.8526 10.9665 12.8841C10.852 12.9157 10.7489 12.9318 10.6583 12.9318C10.4654 12.9318 10.3295 12.9001 10.2507 12.8366C10.1724 12.773 10.1333 12.6534 10.1333 12.4783C10.1333 12.4089 10.1451 12.3054 10.1697 12.17C10.1936 12.0337 10.2211 11.9126 10.2516 11.8067L10.6242 10.4876C10.6607 10.3665 10.6857 10.2334 10.6992 10.0882C10.7129 9.94325 10.7193 9.84185 10.7193 9.78429C10.7193 9.50614 10.6218 9.28041 10.4268 9.10629C10.2317 8.93229 9.95393 8.84529 9.59396 8.84529C9.39365 8.84529 9.18188 8.88088 8.95776 8.952C8.73363 9.02294 8.49933 9.1084 8.25421 9.2082L8.15415 9.6165C8.22719 9.58949 8.31419 9.56043 8.41598 9.53034C8.51732 9.50038 8.61674 9.48489 8.71347 9.48489C8.91096 9.48489 9.04399 9.51856 9.1137 9.58488C9.18342 9.65139 9.21844 9.7697 9.21844 9.93883C9.21844 10.0324 9.20736 10.1363 9.18438 10.2492C9.16172 10.3628 9.13342 10.483 9.10013 10.6098L8.72595 11.9342C8.69266 12.0734 8.66834 12.1979 8.65304 12.3084C8.63786 12.4189 8.63057 12.5272 8.63057 12.6326C8.63057 12.9048 8.73114 13.1292 8.93222 13.3063C9.13329 13.4826 9.41523 13.5714 9.77769 13.5714C10.0137 13.5714 10.2209 13.5406 10.3992 13.4785C10.5773 13.4167 10.8164 13.3268 11.1158 13.2086ZM11.0495 7.8502C11.2235 7.68882 11.3101 7.49254 11.3101 7.26272C11.3101 7.03341 11.2236 6.83675 11.0495 6.67331C10.8758 6.51032 10.6666 6.42857 10.4219 6.42857C10.1765 6.42857 9.96635 6.51013 9.79107 6.67331C9.61579 6.83675 9.52796 7.03334 9.52796 7.26272C9.52796 7.49254 9.61579 7.68875 9.79107 7.8502C9.96667 8.01217 10.1764 8.09321 10.4219 8.09321C10.6666 8.09321 10.8758 8.01217 11.0495 7.8502Z" fill="currentColor"></path></svg></button></div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950">$1,750</div><div class="inline-flex items-center justify-center rounded-full transition duration-200 ease-out h-5 gap-1.5 px-2 text-label-xs bg-success-light text-success-dark">Last 15 days</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="grid h-[86px] grid-cols-2 border-t border-faded-lighter"><div class="recharts-responsive-container" style="width: 100%; height: 100%; min-width: 0px;"><div class="recharts-wrapper" style="position: relative; cursor: default; width: 100%; height: 100%; max-height: 85px; max-width: 261px;"><svg class="recharts-surface" width="261" height="85" viewBox="0 0 261 85" style="width: 100%; height: 100%;"><title></title><desc></desc><defs><clipPath id="recharts19-clip"><rect x="0" y="0" height="85" width="261"></rect></clipPath></defs><g class="recharts-layer recharts-area fill-bg-white-0 stroke-primary-base"><g class="recharts-layer"><path stroke-width="2" fill="hsl(var(--primary-alpha-10))" class="recharts-curve recharts-area-area" fill-opacity="1" width="261" height="85" stroke="none" d="M0,38.25L15.353,52.417L30.706,32.583L46.059,32.583L61.412,21.25L76.765,32.583L92.118,48.167L107.471,21.25L122.824,17L138.176,48.167L153.529,34L168.882,22.667L184.235,38.25L199.588,31.167L214.941,34L230.294,55.25L245.647,42.5L261,24.083L261,85L245.647,85L230.294,85L214.941,85L199.588,85L184.235,85L168.882,85L153.529,85L138.176,85L122.824,85L107.471,85L92.118,85L76.765,85L61.412,85L46.059,85L30.706,85L15.353,85L0,85Z"></path><path stroke="hsl(var(--primary-base))" stroke-width="2" fill="none" class="recharts-curve recharts-area-curve" fill-opacity="1" width="261" height="85" d="M0,38.25L15.353,52.417L30.706,32.583L46.059,32.583L61.412,21.25L76.765,32.583L92.118,48.167L107.471,21.25L122.824,17L138.176,48.167L153.529,34L168.882,22.667L184.235,38.25L199.588,31.167L214.941,34L230.294,55.25L245.647,42.5L261,24.083"></path></g></g></svg></div></div><div class="flex flex-col items-start justify-end border-l-2 border-primary-base px-4 pb-4"><div class="text-Line truncated
|
||||
|
||||
|
||||
|
||||
<div class="flex flex-col gap-6"><div class="relative flex w-full flex-col rounded-2xl bg-bg-white-0 p-5 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><div class="flex items-start gap-2"><div class="flex-1"><div class="flex items-center gap-1"><div class="text-label-sm text-text-sub-600">Product Categories</div><button data-state="closed"><svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="size-5 text-text-disabled-300"><path fill-rule="evenodd" clip-rule="evenodd" d="M10 16.25C13.4518 16.25 16.25 13.4518 16.25 10C16.25 6.54822 13.4518 3.75 10 3.75C6.54822 3.75 3.75 6.54822 3.75 10C3.75 13.4518 6.54822 16.25 10 16.25ZM11.1158 13.2086L11.2156 12.8006C11.164 12.8249 11.0807 12.8526 10.9665 12.8841C10.852 12.9157 10.7489 12.9318 10.6583 12.9318C10.4654 12.9318 10.3295 12.9001 10.2507 12.8366C10.1724 12.773 10.1333 12.6534 10.1333 12.4783C10.1333 12.4089 10.1451 12.3054 10.1697 12.17C10.1936 12.0337 10.2211 11.9126 10.2516 11.8067L10.6242 10.4876C10.6607 10.3665 10.6857 10.2334 10.6992 10.0882C10.7129 9.94325 10.7193 9.84185 10.7193 9.78429C10.7193 9.50614 10.6218 9.28041 10.4268 9.10629C10.2317 8.93229 9.95393 8.84529 9.59396 8.84529C9.39365 8.84529 9.18188 8.88088 8.95776 8.952C8.73363 9.02294 8.49933 9.1084 8.25421 9.2082L8.15415 9.6165C8.22719 9.58949 8.31419 9.56043 8.41598 9.53034C8.51732 9.50038 8.61674 9.48489 8.71347 9.48489C8.91096 9.48489 9.04399 9.51856 9.1137 9.58488C9.18342 9.65139 9.21844 9.7697 9.21844 9.93883C9.21844 10.0324 9.20736 10.1363 9.18438 10.2492C9.16172 10.3628 9.13342 10.483 9.10013 10.6098L8.72595 11.9342C8.69266 12.0734 8.66834 12.1979 8.65304 12.3084C8.63786 12.4189 8.63057 12.5272 8.63057 12.6326C8.63057 12.9048 8.73114 13.1292 8.93222 13.3063C9.13329 13.4826 9.41523 13.5714 9.77769 13.5714C10.0137 13.5714 10.2209 13.5406 10.3992 13.4785C10.5773 13.4167 10.8164 13.3268 11.1158 13.2086ZM11.0495 7.8502C11.2235 7.68882 11.3101 7.49254 11.3101 7.26272C11.3101 7.03341 11.2236 6.83675 11.0495 6.67331C10.8758 6.51032 10.6666 6.42857 10.4219 6.42857C10.1765 6.42857 9.96635 6.51013 9.79107 6.67331C9.61579 6.83675 9.52796 7.03334 9.52796 7.26272C9.52796 7.49254 9.61579 7.68875 9.79107 7.8502C9.96667 8.01217 10.1764 8.09321 10.4219 8.09321C10.6666 8.09321 10.8758 8.01217 11.0495 7.8502Z" fill="currentColor"></path></svg></button></div><div class="mt-1 flex items-center gap-2"><div class="text-title-h5 text-text-strong-950"><number-flow-react aria-label="58%" role="img" data="{"pre":[],"integer":[{"type":"integer","value":5,"key":"integer:1","pos":1},{"type":"integer","value":8,"key":"integer:0","pos":0}],"fraction":[],"post":[{"type":"suffix","value":"%","key":"suffix:0"}],"valueAsString":"58%","value":58}"></number-flow-react></div><div class="text-label-sm text-text-sub-600"><span class="text-success-base">+2.1%</span> vs last week</div></div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div class="mt-3.5"><div class="w-full"><div class="relative h-8 w-full bg-bg-soft-200" style="mask-image: linear-gradient(90deg, rgb(0, 0, 0) 6px, rgba(0, 0, 0, 0) 6px); mask-size: 9px 100%; mask-repeat: space; background-position: 0px 0px; width: 477px;"><div class="h-full [clip-path:inset(0)]" style="width: 279px;"><div class="absolute inset-0 bg-primary-base"></div></div></div></div></div><div class="mt-3 flex items-center justify-between"><div class="flex items-center gap-2"><div class="whitespace-nowrap text-label-sm text-text-sub-600">Accessories</div><div class="flex"><button type="button" class="flex size-5 shrink-0 items-center justify-center rounded-l-md bg-bg-white-0 ring-1 ring-inset ring-stroke-soft-200 transition duration-200 ease-out hover:bg-bg-weak-50 focus:outline-none focus-visible:bg-bg-weak-50"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-[18px] text-text-sub-600"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"></path></svg></button><button type="button" class="flex size-5 shrink-0 items-center justify-center rounded-r-md bg-bg-white-0 ring-1 ring-inset ring-stroke-soft-200 transitioLine truncated
|
||||
A2,2,0,0,0,46.90697674418604,4.048701312969584
|
||||
A45,45,0,0,0,11.675700356952994,74.13755469722625
|
||||
A2,2,0,0,0,14.527729829104658,74.62542856743667
|
||||
L21.760574790804693,69.24879537830967
|
||||
A2,2,0,0,0,22.239908781709396,66.5470088046016
|
||||
A32,32,0,0,1,47.11764705882353,17.055411296984207
|
||||
A2,2,0,0,0,49,15.058874503045722Z" role="img"></path></g><g class="recharts-layer recharts-pie-sector" tabindex="-1"><path cx="49" cy="49" name="Regular" stroke="#fff" fill="fill-away-base" width="98" height="98" id="regular" class="recharts-sector stroke-stroke-white-0 fill-away-base" tabindex="-1" d="M 15.44304396447263,75.81288312788618
|
||||
A2,2,0,0,0,15.188785304223302,78.69514709167298
|
||||
A45,45,0,0,0,83.67389837264565,77.68310951838104
|
||||
A2,2,0,0,0,83.33458412158471,74.80961706801988
|
||||
L76.13062794043542,69.39433812502051
|
||||
A2,2,0,0,0,73.40365180385973,69.69931831331586
|
||||
A32,32,0,0,1,25.218638186436458,70.4113715182466
|
||||
A2,2,0,0,0,22.483841080192434,70.18710258953567Z" role="img"></path></g><g class="recharts-layer recharts-pie-sector" tabindex="-1"><path cx="49" cy="49" name="New" stroke="#fff" fill="fill-success-base" width="98" height="98" id="new" class="recharts-sector stroke-stroke-white-0 fill-success-base" tabindex="-1" d="M 84.21441106638544,73.59563484132156
|
||||
A2,2,0,0,0,87.05077924506833,73.02370077325882
|
||||
A45,45,0,0,0,89.16840101844096,28.71454807943115
|
||||
A2,2,0,0,0,86.39950338723784,27.874727306186426
|
||||
L78.55247711645711,32.30715433842154
|
||||
A2,2,0,0,0,77.73987056957552,34.92804776713456
|
||||
A32,32,0,0,1,76.26689436756766,65.74862595999764
|
||||
A2,2,0,0,0,76.82585283108298,68.4350691849273Z" role="img"></path></g><g class="recharts-layer recharts-pie-sector" tabindex="-1"><g class="recharts-layer recharts-active-shape"><path cx="49" cy="49" name="Others" stroke="#fff" fill="fill-bg-weak-50" width="98" height="98" id="others" class="recharts-sector stroke-stroke-white-0 fill-bg-weak-50" tabindex="-1" d="M 86.53215891440124,26.03617960303355
|
||||
A 44,44,0,
|
||||
0,0,
|
||||
50.53557785491004,5.026803611159785
|
||||
L 50.15168339118253,16.02010270836984
|
||||
A 33,33,0,
|
||||
0,1,
|
||||
77.14911918580091,31.777134702275163 Z" role="img"></path></g></g></g></g></svg></div></div><div class="flex flex-1 flex-col gap-[13px]"><div class="flex items-center justify-between gap-1"><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-warning-base"></div>Premium</div><div class="flex items-center gap-1.5 tabular-nums"><div class="text-label-sm text-text-sub-600">$6,450</div><div class="text-paragraph-sm text-text-disabled-300">·</div><div class="text-paragraph-sm text-text-soft-400">36%</div></div></div><div class="flex items-center justify-between gap-1"><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-away-base"></div>Regular</div><div class="flex items-center gap-1.5 tabular-nums"><div class="text-label-sm text-text-sub-600">$5,320</div><div class="text-paragraph-sm text-text-disabled-300">·</div><div class="text-paragraph-sm text-text-soft-400">30%</div></div></div><div class="flex items-center justify-between gap-1"><div class="flex items-center gap-2 text-label-sm text-text-sub-600"><div class="size-3 shrink-0 rounded-full border-2 border-stroke-white-0 shadow-regular-sm bg-success-base"></div>New</div><div class="flex items-center gap-1.5 tabular-nums"><div class="text-label-sm text-text-sub-600">$3,280</div><div class="text-paragraph-sm text-text-disabled-300">·</div><div class="text-paragraph-sm text-text-soft-400">18%</div></div></div></div></div></div></div>
|
||||
|
||||
|
||||
|
||||
<div class="w-full rounded-2xl bg-bg-white-0 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200" data-protonpass-form=""><div class="p-5"><div class="flex items-start gap-3"><div class="flex-1"><div class="text-label-md text-text-strong-950">Recent Activities</div><div class="mt-1 text-paragraph-sm text-text-sub-600">7 new activities today</div></div><button class="group relative inline-flex items-center justify-center whitespace-nowrap outline-none transition duration-200 ease-out focus:outline-none disabled:pointer-events-none disabled:bg-bg-weak-50 disabled:text-text-disabled-300 disabled:ring-transparent ring-1 ring-inset h-7 gap-2.5 rounded-lg px-2 text-label-sm bg-bg-white-0 text-text-sub-600 shadow-regular-xs ring-stroke-soft-200 hover:bg-bg-weak-50 hover:text-text-strong-950 hover:shadow-none hover:ring-transparent focus-visible:text-text-strong-950 focus-visible:shadow-button-important-focus focus-visible:ring-stroke-strong-950">Details</button></div><div role="group" dir="ltr" class="mt-4 flex flex-wrap gap-2.5" tabindex="0" style="outline: none;"><button type="button" data-state="on" role="radio" aria-checked="true" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Today</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">Yesterday</button><button type="button" data-state="off" role="radio" aria-checked="false" class="flex h-7 items-center justify-center rounded-lg bg-bg-weak-50 px-2.5 text-label-sm text-text-sub-600 transition duration-200 ease-out data-[state=on]:bg-primary-alpha-10 data-[state=on]:text-primary-base" tabindex="-1" data-radix-collection-item="">This Week</button></div></div><div class="relative"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon pointer-events-none absolute left-5 top-1/2 size-5 -translate-y-1/2 text-text-soft-400"><path d="M11 2C15.968 2 20 6.032 20 11C20 15.968 15.968 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2ZM11 18C14.8675 18 18 14.8675 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18ZM19.4853 18.0711L22.3137 20.8995L20.8995 22.3137L18.0711 19.4853L19.4853 18.0711Z"></path></svg><input placeholder="Search for activities..." class="h-12 w-full border-y border-stroke-soft-200 bg-transparent bg-none pl-[50px] pr-5 text-paragraph-sm text-text-strong-950 caret-primary-base placeholder:text-text-soft-400 focus:outline-none" type="text"></div><div class="flex flex-col gap-6 p-5"><div class="relative flex items-start gap-4"><div class="absolute -bottom-4 left-3.5 top-9 w-px bg-stroke-soft-200"></div><div class="flex size-7 shrink-0 items-center justify-center rounded-full bg-bg-white-0 shadow-regular-xs ring-1 ring-inset ring-stroke-soft-200"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="remixicon size-4 text-information-base"><path d="M20.0833 15.1999L21.2854 15.9212C21.5221 16.0633 21.5989 16.3704 21.4569 16.6072C21.4146 16.6776 21.3557 16.7365 21.2854 16.7787L12.5144 22.0412C12.1977 22.2313 11.8021 22.2313 11.4854 22.0412L2.71451 16.7787C2.47772 16.6366 2.40093 16.3295 2.54301 16.0927C2.58523 16.0223 2.64413 15.9634 2.71451 15.9212L3.9166 15.1999L11.9999 20.0499L20.0833 15.1999ZM20.0833 10.4999L21.2854 11.2212C21.5221 11.3633 21.5989 11.6704 21.4569 11.9072C21.4146 11.9776 21.3557 12.0365 21.2854 12.0787L11.9999 17.6499L2.71451 12.0787C2.47772 11.9366 2.40093 11.6295 2.54301 11.3927C2.58523 11.3223 2.64413 11.2634 2.71451 11.2212L3.9166 10.4999L11.9999 15.3499L20.0833 10.4999ZM12.5144 1.30864L21.2854 6.5712C21.5221 6.71327 21.5989 7.0204 21.4569 7.25719C21.4146 7.32757 21.3557 7.38647 21.2854 7.42869L11.9999 12.9999L2.71451 7.42869C2.47772 7.28662 2.40093 6.97949 2.54301 6.7427C2.58523 6.67232 2.64413 6.61343 2.71451 6.5712L11.4854 1.30864C11.8021 1.11864 12.1977 1.11864 12.5144 1.30864ZM11.9999 3.33233L5.88723 6.99995L11.9999 10.6676L18.1126 6.99995L11.9999 3.33233Z"></path></svg></div><div class="flex-1"><div class="flex items-center justify-between gap-1.5"><div class="text-label-sm text-text-strong-950">Inventory Updated</div><div class="text-right text-subheading-2xs uppercase text-text-soft-400">11:30 AM</div></div><div class="mt-1 text-paragraph-xs text-text-sub-600">Women's Summer Dress - Blue</div><div class="mt-1 text-label-xs text-text-sub-600">Stock: +150 units added</div></div></div><div class="relative flex items-start gap-4"><div class="absolute -bottom-4 left-3.5 top-9 w-px bg-stroke-softLine truncated
|
||||
@@ -0,0 +1,631 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Railway Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
railway: {
|
||||
bg: '#0d0d12',
|
||||
sidebar: '#0a0a0f',
|
||||
card: '#15151d',
|
||||
cardHover: '#1a1a24',
|
||||
border: '#232330',
|
||||
accent: '#9d4edd',
|
||||
accentHover: '#7b2cbf',
|
||||
success: '#22c55e',
|
||||
muted: '#6b7280',
|
||||
text: '#f3f4f6',
|
||||
textMuted: '#9ca3af'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
position: absolute;
|
||||
border: 2px dashed #3d3d55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.connection-line.horizontal {
|
||||
height: 2px;
|
||||
border-top: 2px dashed #3d3d55;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.service-card {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.group-container {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
animation: dropdownIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-icon:hover {
|
||||
background-color: rgba(157, 78, 221, 0.1);
|
||||
color: #9d4edd;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 4px 15px rgba(157, 78, 221, 0.3);
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-backdrop {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0a0a0f;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #3d3d55;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4d4d66;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-railway-bg text-railway-text h-screen overflow-hidden">
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-16 bg-railway-sidebar border-r border-railway-border flex flex-col items-center py-4 flex-shrink-0">
|
||||
<!-- Logo -->
|
||||
<div class="mb-6">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-railway-accent to-railway-accentHover flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav Icons -->
|
||||
<nav class="flex flex-col gap-2 flex-1">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-accent bg-railway-card">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Bottom Icons -->
|
||||
<div class="flex flex-col gap-2 mt-auto">
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="sidebar-icon w-10 h-10 rounded-lg flex items-center justify-center text-railway-textMuted">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<!-- Header -->
|
||||
<header class="h-14 border-b border-railway-border flex items-center justify-between px-6 bg-railway-bg/50 backdrop-blur-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="flex items-center gap-2 text-railway-text hover:text-white transition-colors">
|
||||
<span class="font-semibold">dazzling-curiosity</span>
|
||||
<svg class="w-4 h-4 text-railway-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="text-railway-muted">/</span>
|
||||
<button class="flex items-center gap-2 text-railway-textMuted hover:text-white transition-colors text-sm">
|
||||
<span>production</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="add-btn flex items-center gap-2 bg-railway-card hover:bg-railway-cardHover border border-railway-border px-3 py-1.5 rounded-lg text-sm font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
Add
|
||||
</button>
|
||||
<div class="w-px h-6 bg-railway-border mx-1"></div>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="w-8 h-8 rounded-lg flex items-center justify-center text-railway-textMuted hover:text-white hover:bg-railway-card transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
<div class="flex-1 overflow-auto p-6">
|
||||
<div id="groups-container" class="flex flex-col gap-6 max-w-7xl mx-auto">
|
||||
<!-- Groups will be rendered here -->
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Add Service Modal -->
|
||||
<div id="add-service-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="modal-backdrop absolute inset-0 bg-black/60" onclick="closeModal()"></div>
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div class="bg-railway-card border border-railway-border rounded-xl w-full max-w-md p-6 shadow-2xl">
|
||||
<h3 class="text-lg font-semibold mb-4">Add New Service</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Service Name</label>
|
||||
<input type="text" id="service-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., MyApp">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Domain</label>
|
||||
<input type="text" id="service-domain" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="e.g., myapp.example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Group</label>
|
||||
<select id="service-group" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent">
|
||||
<option value="homelab">Homelab</option>
|
||||
<option value="competition">Competition</option>
|
||||
<option value="apis">API's</option>
|
||||
<option value="new">+ New Group</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="new-group-input" class="hidden">
|
||||
<label class="block text-sm text-railway-textMuted mb-1">New Group Name</label>
|
||||
<input type="text" id="new-group-name" class="w-full bg-railway-bg border border-railway-border rounded-lg px-3 py-2 text-sm focus:outline-none focus:border-railway-accent" placeholder="Group name">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-railway-textMuted mb-1">Icon</label>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button onclick="selectIcon('server')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="server">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('database')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="database">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('cloud')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="cloud">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('code')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="code">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('mail')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="mail">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
|
||||
</button>
|
||||
<button onclick="selectIcon('globe')" class="icon-select w-10 h-10 rounded-lg bg-railway-bg border border-railway-border flex items-center justify-center hover:border-railway-accent" data-icon="globe">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button onclick="closeModal()" class="px-4 py-2 text-sm text-railway-textMuted hover:text-white transition-colors">Cancel</button>
|
||||
<button onclick="addService()" class="px-4 py-2 bg-railway-accent hover:bg-railway-accentHover text-white rounded-lg text-sm font-medium transition-colors">Add Service</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initial data
|
||||
let groups = [
|
||||
{
|
||||
id: 'homelab',
|
||||
name: 'Homelab',
|
||||
icon: 'home',
|
||||
services: [
|
||||
{
|
||||
id: 'koffan',
|
||||
name: 'Koffan',
|
||||
domain: 'shopping.tdvorak.dev',
|
||||
status: 'online',
|
||||
volume: 'koffan-volume',
|
||||
icon: 'server'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'competition',
|
||||
name: 'Competition',
|
||||
icon: 'trophy',
|
||||
services: [
|
||||
{
|
||||
id: 'insightful-optimism',
|
||||
name: 'insightful-optimism',
|
||||
domain: 'co-back.tdvorak.dev',
|
||||
status: 'online',
|
||||
connections: ['postgres'],
|
||||
icon: 'github'
|
||||
},
|
||||
{
|
||||
id: 'postgres',
|
||||
name: 'Postgres',
|
||||
domain: '',
|
||||
status: 'online',
|
||||
volume: 'postgres-volume-89XR',
|
||||
icon: 'database'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'apis',
|
||||
name: "API's",
|
||||
icon: 'code',
|
||||
services: [
|
||||
{
|
||||
id: 'sendmail',
|
||||
name: 'SendMail',
|
||||
domain: 'sendmail.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'mail'
|
||||
},
|
||||
{
|
||||
id: 'youtubescraper',
|
||||
name: 'YoutubeScraper',
|
||||
domain: 'youtube.tdvorak.dev',
|
||||
status: 'online',
|
||||
icon: 'youtube'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let selectedIcon = 'server';
|
||||
let dropdownOpen = null;
|
||||
|
||||
// Icon SVGs
|
||||
const icons = {
|
||||
server: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 01-2 2v4a2 2 0 012 2h14a2 2 0 012-2v-4a2 2 0 01-2-2m-2-4h.01M17 16h.01"></path></svg>',
|
||||
database: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>',
|
||||
cloud: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>',
|
||||
mail: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>',
|
||||
globe: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>',
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>',
|
||||
github: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>',
|
||||
youtube: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>'
|
||||
};
|
||||
|
||||
function renderIcon(name) {
|
||||
return icons[name] || icons.server;
|
||||
}
|
||||
|
||||
function getGroupIcon(icon) {
|
||||
const groupIcons = {
|
||||
home: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>',
|
||||
trophy: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 21h8m-4-4v4m-5-16l2.5 9h7L19 5H4zM4 5v6a2 2 0 002 2h2V5H4zm14 0v8h2a2 2 0 002-2V5h-4z"></path></svg>',
|
||||
code: '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"></path></svg>'
|
||||
};
|
||||
return groupIcons[icon] || groupIcons.home;
|
||||
}
|
||||
|
||||
function toggleDropdown(groupId, event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (dropdownOpen === groupId) {
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown();
|
||||
dropdownOpen = groupId;
|
||||
|
||||
const dropdown = document.getElementById(`dropdown-${groupId}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
if (dropdownOpen) {
|
||||
const dropdown = document.getElementById(`dropdown-${dropdownOpen}`);
|
||||
if (dropdown) {
|
||||
dropdown.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
dropdownOpen = null;
|
||||
}
|
||||
|
||||
function deleteGroup(groupId) {
|
||||
groups = groups.filter(g => g.id !== groupId);
|
||||
renderGroups();
|
||||
closeDropdown();
|
||||
}
|
||||
|
||||
function deleteService(groupId, serviceId) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services = group.services.filter(s => s.id !== serviceId);
|
||||
renderGroups();
|
||||
}
|
||||
}
|
||||
|
||||
function renderConnection() {
|
||||
// Connection line between insightful-optimism and postgres
|
||||
return `
|
||||
<div class="hidden md:flex items-center justify-center w-16 relative">
|
||||
<div class="absolute h-0.5 border-t-2 border-dashed border-railway-border w-full"></div>
|
||||
<div class="absolute -left-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<div class="absolute -right-1 w-2 h-2 rounded-full bg-railway-border"></div>
|
||||
<svg class="w-4 h-4 text-railway-muted absolute right-0 transform translate-x-full" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderServiceCard(service, groupId) {
|
||||
const hasVolume = service.volume ? `
|
||||
<div class="flex items-center gap-2 text-xs text-railway-textMuted mt-3 pt-3 border-t border-railway-border/50">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||
</svg>
|
||||
${service.volume}
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="service-card bg-railway-card border border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] relative group">
|
||||
<button onclick="deleteService('${groupId}', '${service.id}')" class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity text-railway-textMuted hover:text-red-400 p-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex items-start gap-3 mb-2">
|
||||
<div class="w-10 h-10 rounded-lg bg-railway-bg flex items-center justify-center text-railway-text shrink-0">
|
||||
${renderIcon(service.icon)}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="font-semibold text-sm truncate">${service.name}</h4>
|
||||
<p class="text-xs text-railway-textMuted truncate">${service.domain || 'Internal Service'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full bg-railway-success status-dot"></span>
|
||||
<span class="text-xs text-railway-success font-medium">Online</span>
|
||||
</div>
|
||||
${hasVolume}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroup(group) {
|
||||
const servicesHtml = group.services.map((service, index) => {
|
||||
const card = renderServiceCard(service, group.id);
|
||||
// Add connection line if this is the first service and there are 2 services
|
||||
if (group.id === 'competition' && index === 0 && group.services.length > 1) {
|
||||
return card + renderConnection();
|
||||
}
|
||||
return card;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="group-container">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-railway-accent">
|
||||
${getGroupIcon(group.icon)}
|
||||
</div>
|
||||
<h3 class="font-semibold text-railway-text">${group.name}</h3>
|
||||
</div>
|
||||
<button onclick="toggleDropdown('${group.id}', event)" class="relative p-1 text-railway-textMuted hover:text-white transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
<div id="dropdown-${group.id}" class="dropdown-menu hidden absolute right-0 top-full mt-1 w-32 bg-railway-card border border-railway-border rounded-lg shadow-xl z-10">
|
||||
<button onclick="deleteGroup('${group.id}')" class="w-full text-left px-3 py-2 text-sm text-red-400 hover:bg-railway-bg rounded-lg">Delete Group</button>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
${servicesHtml}
|
||||
<button onclick="openModal('${group.id}')" class="border-2 border-dashed border-railway-border rounded-lg p-4 min-w-[220px] max-w-[280px] flex items-center justify-center text-railway-textMuted hover:text-railway-accent hover:border-railway-accent/50 transition-all group">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-6 h-6 group-hover:scale-110 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Add Service</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGroups() {
|
||||
const container = document.getElementById('groups-container');
|
||||
container.innerHTML = groups.map(renderGroup).join('');
|
||||
}
|
||||
|
||||
function openModal(preselectedGroup = null) {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
if (preselectedGroup) {
|
||||
document.getElementById('service-group').value = preselectedGroup;
|
||||
}
|
||||
|
||||
// Reset form
|
||||
document.getElementById('service-name').value = '';
|
||||
document.getElementById('service-domain').value = '';
|
||||
document.getElementById('new-group-name').value = '';
|
||||
document.getElementById('new-group-input').classList.add('hidden');
|
||||
selectIcon('server');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function selectIcon(icon) {
|
||||
selectedIcon = icon;
|
||||
document.querySelectorAll('.icon-select').forEach(btn => {
|
||||
if (btn.dataset.icon === icon) {
|
||||
btn.classList.add('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.remove('border-railway-border');
|
||||
} else {
|
||||
btn.classList.remove('border-railway-accent', 'text-railway-accent');
|
||||
btn.classList.add('border-railway-border');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addService() {
|
||||
const name = document.getElementById('service-name').value.trim();
|
||||
const domain = document.getElementById('service-domain').value.trim();
|
||||
const groupSelect = document.getElementById('service-group').value;
|
||||
const newGroupName = document.getElementById('new-group-name').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('Please enter a service name');
|
||||
return;
|
||||
}
|
||||
|
||||
let groupId = groupSelect;
|
||||
|
||||
if (groupSelect === 'new') {
|
||||
if (!newGroupName) {
|
||||
alert('Please enter a group name');
|
||||
return;
|
||||
}
|
||||
groupId = 'group-' + Date.now();
|
||||
groups.push({
|
||||
id: groupId,
|
||||
name: newGroupName,
|
||||
icon: 'home',
|
||||
services: []
|
||||
});
|
||||
}
|
||||
|
||||
const service = {
|
||||
id: 'service-' + Date.now(),
|
||||
name: name,
|
||||
domain: domain,
|
||||
status: 'online',
|
||||
icon: selectedIcon
|
||||
};
|
||||
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
group.services.push(service);
|
||||
}
|
||||
|
||||
renderGroups();
|
||||
closeModal();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('click', closeDropdown);
|
||||
|
||||
document.getElementById('service-group').addEventListener('change', function() {
|
||||
const newGroupInput = document.getElementById('new-group-input');
|
||||
if (this.value === 'new') {
|
||||
newGroupInput.classList.remove('hidden');
|
||||
} else {
|
||||
newGroupInput.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('.add-btn').addEventListener('click', () => openModal());
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
closeDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
renderGroups();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,612 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<title>[NuFest] – App Project · Clounest (1:1 Match)</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet"/>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: { sans: ['Inter','system-ui','sans-serif'] },
|
||||
colors: {
|
||||
base:'#16171c', sidebar:'#111217', card:'#1c1d24',
|
||||
pink:'#e8316a', green:'#3dd68c', orange:'#ff7043',
|
||||
purple:'#9c7ef0', muted:'#6b6e7d', dim:'#9295a4', primary:'#e8e9f0',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
html,body{height:100%;margin:0;background:#16171c;color:#e8e9f0;font-family:'Inter',sans-serif;overflow:hidden}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:99px}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes pulse-dot{0%{box-shadow:0 0 0 0 rgba(61, 214, 140, 0.7)}70%{box-shadow:0 0 0 6px rgba(61, 214, 140, 0)}100%{box-shadow:0 0 0 0 rgba(61, 214, 140, 0)}}
|
||||
@keyframes slideInRight{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
|
||||
@keyframes slideOutRight{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(20px)}}
|
||||
|
||||
/* Components */
|
||||
.nav-item{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#6b6e7d;transition:background .15s,color .15s}
|
||||
.nav-item:hover{background:rgba(255,255,255,.06);color:#9295a4}
|
||||
.nav-item.active{background:rgba(255,255,255,.09);color:#e8e9f0}
|
||||
.nav-item svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.tab{display:flex;align-items:center;gap:6px;padding:10px 14px;font-size:13.5px;font-weight:500;color:#6b6e7d;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:all .15s;white-space:nowrap;user-select:none}
|
||||
.tab:hover{color:#9295a4}
|
||||
.tab.active{color:#e8e9f0;border-bottom-color:#e8316a}
|
||||
.tab svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round}
|
||||
|
||||
.card{background:#1c1d24;border:1px solid rgba(255,255,255,.08);border-radius:20px;padding:20px;display:flex;flex-direction:column;transition:border-color .2s;animation:fadeUp .4s ease both}
|
||||
.card:hover{border-color:rgba(255,255,255,.14)}
|
||||
.card:nth-child(1){animation-delay:.04s}.card:nth-child(2){animation-delay:.09s}.card:nth-child(3){animation-delay:.14s}.card:nth-child(4){animation-delay:.19s}.card:nth-child(5){animation-delay:.24s}
|
||||
|
||||
.card-icon{width:34px;height:34px;border-radius:10px;background:rgba(255,255,255,.07);display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.card-icon svg{width:16px;height:16px;stroke:#9295a4;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.arrow-btn{width:30px;height:30px;border-radius:9px;background:#e8316a;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:box-shadow .15s,transform .15s}
|
||||
.arrow-btn:hover{transform:scale(1.07)}
|
||||
.arrow-btn svg{width:13px;height:13px;stroke:white;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
.search-box{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:0 12px;height:34px;width:220px}
|
||||
.search-box svg{width:14px;height:14px;stroke:#6b6e7d;fill:none;stroke-width:2;stroke-linecap:round;flex-shrink:0}
|
||||
.search-box input{background:none;border:none;outline:none;color:#9295a4;font-size:13px;width:100%;font-family:inherit}
|
||||
.search-box input::placeholder{color:#6b6e7d}
|
||||
|
||||
.pill-group{display:flex;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:10px;overflow:hidden}
|
||||
.pill{padding:5px 14px;font-size:12.5px;font-weight:500;color:#6b6e7d;cursor:pointer;transition:all .15s;user-select:none}
|
||||
.pill.active{background:rgba(255,255,255,.1);color:#e8e9f0}
|
||||
.pill:hover:not(.active){color:#9295a4}
|
||||
|
||||
.btn-stop{height:38px;padding:0 20px;border-radius:11px;border:1px solid rgba(255,255,255,.15);background:rgba(255,255,255,.06);color:#e8e9f0;font-size:13.5px;font-weight:700;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;transition:background .15s}
|
||||
.btn-stop:hover{background:rgba(255,255,255,.1)}
|
||||
.btn-stop.disabled{opacity:0.5;cursor:not-allowed;}
|
||||
|
||||
.btn-restart{height:38px;padding:0 20px;border-radius:11px;border:none;background:#e8316a;color:white;font-size:13.5px;font-weight:700;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s}
|
||||
.btn-restart:hover{background:#d12960}
|
||||
.btn-restart.disabled{background:#444;cursor:not-allowed;}
|
||||
|
||||
.btn-restart svg,.btn-stop svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round}
|
||||
|
||||
.chart-wrap{position:relative;width:100%}
|
||||
.chart-wrap canvas{width:100%!important;height:100%!important}
|
||||
|
||||
.flag{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;background:rgba(255,255,255,.05)}
|
||||
.stat-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
||||
.badge-active{padding:2px 8px;border-radius:6px;background:rgba(61, 214, 140, 0.15);font-size:10px;font-weight:700;letter-spacing:.7px;color:#3dd68c;text-transform:uppercase;transition: all 0.3s;}
|
||||
.badge-stopped{padding:2px 8px;border-radius:6px;background:rgba(255, 255, 255, 0.08);font-size:10px;font-weight:700;letter-spacing:.7px;color:#6b6e7d;text-transform:uppercase;}
|
||||
|
||||
.cache-seg{height:32px;transition:opacity .2s,filter .2s;cursor:pointer}
|
||||
.cache-seg:hover{filter:brightness(1.15)}
|
||||
.speed-row{display:flex;align-items:center;gap:5px;font-size:12.5px;font-weight:600}
|
||||
.speed-row svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round}
|
||||
|
||||
/* Live Indicator */
|
||||
.live-dot{width:8px;height:8px;background:#3dd68c;border-radius:50%;display:inline-block;margin-right:6px;animation:pulse-dot 2s infinite}
|
||||
.live-dot.stopped{background:#6b6e7d;animation:none}
|
||||
|
||||
/* Toast Notifications */
|
||||
.toast-container{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:10px;pointer-events:none}
|
||||
.toast{pointer-events:auto;background:#1c1d24;border:1px solid rgba(255,255,255,.1);color:#e8e9f0;padding:12px 16px;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.4);font-size:13px;font-weight:500;display:flex;align-items:center;gap:10px;animation:slideInRight .3s ease forwards}
|
||||
.toast.hiding{animation:slideOutRight .3s ease forwards}
|
||||
.toast svg{width:18px;height:18px;flex-shrink:0}
|
||||
.toast.success svg{stroke:#3dd68c}
|
||||
.toast.info svg{stroke:#6c8ef0}
|
||||
.toast.warning svg{stroke:#ff7043}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div style="display:flex;height:100vh;overflow:hidden">
|
||||
|
||||
<!-- ════ SIDEBAR ════ -->
|
||||
<aside style="width:64px;background:#111217;border-right:1px solid rgba(255,255,255,.07);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:5px;flex-shrink:0">
|
||||
<div style="width:38px;height:38px;border-radius:50%;background:#e8316a;display:flex;align-items:center;justify-content:center;margin-bottom:14px">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
||||
</div>
|
||||
<div class="nav-item active"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v6c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 11v6c0 1.66 4.03 3 9 3s9-1.34 9-3v-6"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="18" cy="8" r="3"/><circle cx="6" cy="15" r="3"/><path d="M18 11a9 9 0 0 1-9 9M6 12a9 9 0 0 1 9-9"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></div>
|
||||
<div style="flex:1"></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg></div>
|
||||
<div class="nav-item"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg></div>
|
||||
<div style="width:34px;height:34px;border-radius:50%;background:#22233a;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#9295a4;cursor:pointer;margin-top:4px;letter-spacing:-.3px">w.</div>
|
||||
</aside>
|
||||
|
||||
<!-- ════ MAIN ════ -->
|
||||
<div style="flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0">
|
||||
|
||||
<!-- ── TOPBAR ── -->
|
||||
<header style="height:52px;background:#111217;border-bottom:1px solid rgba(255,255,255,.07);display:flex;align-items:center;padding:0 22px;gap:14px;flex-shrink:0">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
<input type="text" placeholder="Search logs..."/>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;align-items:center;gap:8px">
|
||||
<button style="height:32px;padding:0 14px;border-radius:9px;border:1px solid rgba(255,255,255,.1);background:transparent;color:#9295a4;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit">Support</button>
|
||||
<button style="height:32px;padding:0 14px;border-radius:9px;border:none;background:rgba(255,255,255,.08);color:#e8e9f0;font-size:13px;font-weight:500;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:6px">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><polyline points="17 11 12 6 7 11"/><polyline points="17 18 12 13 7 18"/></svg>
|
||||
Upgrade
|
||||
</button>
|
||||
<button style="width:32px;height:32px;border-radius:9px;border:1px solid rgba(255,255,255,.1);background:transparent;display:flex;align-items:center;justify-content:center;cursor:pointer">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#9295a4" stroke-width="2" stroke-linecap="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── SCROLL CONTENT ── -->
|
||||
<div style="flex:1;overflow-y:auto;padding:0 24px 28px">
|
||||
|
||||
<!-- breadcrumb -->
|
||||
<div style="display:flex;align-items:center;gap:6px;padding:14px 0 10px;color:#6b6e7d;font-size:13px">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b6e7d" stroke-width="2" stroke-linecap:round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
|
||||
<a href="#" style="color:#6b6e7d;text-decoration:none">Servers</a>
|
||||
<span style="opacity:.4">/</span>
|
||||
<span style="color:#9295a4">[NuFest] - App Project</span>
|
||||
</div>
|
||||
|
||||
<!-- project header -->
|
||||
<div style="display:flex;align-items:center;padding-bottom:18px">
|
||||
<div style="width:46px;height:46px;border-radius:13px;background:#e8316a;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-right:14px">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="white"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 14H9V8h2v8zm4 0h-2V8h2v8z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<span style="font-size:20px;font-weight:800;letter-spacing:-.5px">[NuFest] - App Project</span>
|
||||
<span class="badge-active" id="statusBadge"><span class="live-dot" id="liveDot"></span>Active</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-top:4px">
|
||||
<a href="#" style="display:flex;align-items:center;gap:4px;color:#6b6e7d;font-size:12.5px;text-decoration:none">https://nufest-dth.app <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>
|
||||
<a href="#" style="display:flex;align-items:center;gap:4px;color:#6b6e7d;font-size:12.5px;text-decoration:none">Project Information <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="9 18 15 12 9 6"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;gap:10px">
|
||||
<button class="btn-stop" id="btnStop">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/></svg>
|
||||
STOP
|
||||
</button>
|
||||
<button class="btn-restart disabled" id="btnRestart" disabled>
|
||||
<svg viewBox="0 0 24 24"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.5"/></svg>
|
||||
RESTART
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tabs -->
|
||||
<div style="display:flex;border-bottom:1px solid rgba(255,255,255,.07);margin-bottom:18px">
|
||||
<div class="tab active" onclick="setTab(this)"><svg viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>Metrics</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>Requests</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>APIs</div>
|
||||
<div class="tab" onclick="setTab(this)"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>Config</div>
|
||||
</div>
|
||||
|
||||
<!-- metrics header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||
<span style="font-size:15px;font-weight:700">Metrics</span>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<button style="height:32px;padding:0 12px;border-radius:9px;border:1px solid rgba(255,255,255,.09);background:rgba(255,255,255,.04);color:#9295a4;font-size:12.5px;font-weight:500;font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:6px">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
|
||||
Filter
|
||||
</button>
|
||||
<div class="pill-group" id="timePills">
|
||||
<div class="pill active" onclick="setPill(this)">Day</div>
|
||||
<div class="pill" onclick="setPill(this)">Month</div>
|
||||
<div class="pill" onclick="setPill(this)">Year</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ████ ROW 1 ████ -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1.95fr;gap:13px;margin-bottom:13px">
|
||||
|
||||
<!-- CPU -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">CPU Usage</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="cpuText">12%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#3dd68c;font-weight:700" id="cpuStatus">Good</span> Daily usage</div>
|
||||
<div class="chart-wrap" style="height:76px;margin:12px 0 6px"><canvas id="cpuChart"></canvas></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:4px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAM -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="8" rx="2"/><path d="M6 8V6M10 8V6M14 8V6M18 8V6M6 16v2M18 16v2"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">RAM Usage</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="ramText">65%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#f0a040;font-weight:700">Average</span> Daily usage</div>
|
||||
<div style="display:flex;justify-content:center;align-items:center;margin:10px 0 4px;position:relative">
|
||||
<canvas id="ramCanvas" width="160" height="94"></canvas>
|
||||
<div style="position:absolute;bottom:14px;text-align:center">
|
||||
<div style="font-size:10.5px;color:#6b6e7d;margin-bottom:1px">Used</div>
|
||||
<div style="font-size:12.5px;font-weight:700" id="ramDetail">5.4 GB / 8GB</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:4px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CACHE -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Cache</span>
|
||||
</div>
|
||||
<div style="font-size:38px;font-weight:900;letter-spacing:-1.5px;line-height:1">352 MB</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#f0a040;font-weight:700">220MB Average</span> cached images and files</div>
|
||||
|
||||
<!-- SEGMENTED BAR -->
|
||||
<div style="display:flex;align-items:center;gap:5px;margin:16px 0 15px;height:32px">
|
||||
<div class="cache-seg" style="width:43%;background:#ff6b5b;border-radius:10px 4px 4px 10px"></div>
|
||||
<div class="cache-seg" style="width:13%;background:#8c6ef0;border-radius:5px"></div>
|
||||
<div class="cache-seg" style="flex:1;background:rgba(255,255,255,.07);border-radius:4px 10px 10px 4px"></div>
|
||||
</div>
|
||||
|
||||
<!-- stats row -->
|
||||
<div style="display:grid;grid-template-columns:1fr auto 1fr auto 1fr;gap:0;align-items:stretch">
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#6b6e7d;margin-bottom:5px">
|
||||
<div class="stat-dot" style="background:#ff6b5b"></div> Cache
|
||||
</div>
|
||||
<div style="display:flex;align-items:baseline;gap:4px">
|
||||
<span style="font-size:15px;font-weight:800">212 MB</span>
|
||||
<span style="font-size:11px;color:#6b6e7d">12%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:1px;background:rgba(255,255,255,.08);margin:0 16px"></div>
|
||||
<div>
|
||||
<div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#6b6e7d;margin-bottom:5px">
|
||||
<div class="stat-dot" style="background:#8c6ef0"></div> Non-Cache
|
||||
</div>
|
||||
<div style="display:flex;align-items:baseline;gap:4px">
|
||||
<span style="font-size:15px;font-weight:800">85.5 MB</span>
|
||||
<span style="font-size:11px;color:#6b6e7d">4%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width:1px;background:rgba(255,255,255,.08);margin:0 16px"></div>
|
||||
<div>
|
||||
<div style="font-size:11px;color:#6b6e7d;margin-bottom:5px">Total</div>
|
||||
<div style="font-size:15px;font-weight:800">1.75 GB</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:16px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /row 1 -->
|
||||
|
||||
<!-- ████ ROW 2 ████ -->
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:13px">
|
||||
|
||||
<!-- ACTIVE USER (Changed to Line Area Chart) -->
|
||||
<div class="card" style="flex-direction:row;padding:0;overflow:hidden">
|
||||
<div style="flex:1;padding:20px 18px 18px 20px;display:flex;flex-direction:column">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Active User</span>
|
||||
</div>
|
||||
<div style="font-size:36px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="userText">475 K</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px">User active right now</div>
|
||||
<div style="display:flex;align-items:center;gap:5px;margin-top:12px;flex-wrap:wrap">
|
||||
<span class="flag">🇨🇳</span><span class="flag">🇮🇩</span><span class="flag">🇲🇲</span><span class="flag">🇲🇾</span><span class="flag">🇯🇵</span><span class="flag">🇮🇳</span><span class="flag">🇰🇷</span><span class="flag">🇵🇭</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:auto;padding-top:14px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Details</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Line Area Chart -->
|
||||
<div style="width:50%;padding:16px 14px 46px 0;display:flex;align-items:flex-end">
|
||||
<div class="chart-wrap" style="height:130px"><canvas id="userChart"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PERFORMANCE -->
|
||||
<div class="card">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<div class="card-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></div>
|
||||
<span style="font-size:14px;font-weight:600">Performance</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:flex-start;gap:16px;flex:1">
|
||||
<div style="flex:1">
|
||||
<div style="font-size:36px;font-weight:900;letter-spacing:-1.5px;line-height:1" id="perfText">89%</div>
|
||||
<div style="font-size:12px;color:#6b6e7d;margin-top:4px"><span style="color:#3dd68c;font-weight:700">Good</span> Last scan on Jun 12, 2024</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px">
|
||||
<div class="chart-wrap" style="width:134px;height:58px"><canvas id="perfChart"></canvas></div>
|
||||
<div style="display:flex;flex-direction:column;gap:4px;align-items:flex-end">
|
||||
<div class="speed-row" style="color:#6c8ef0">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 19 19 12"/></svg>
|
||||
<span id="upSpeed">10.4</span> Mbps
|
||||
</div>
|
||||
<div class="speed-row" style="color:#e8316a">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 5 5 12"/></svg>
|
||||
<span id="downSpeed">5.2</span> Mbps
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding-top:14px">
|
||||
<span style="font-size:13px;color:#6b6e7d;font-weight:500;cursor:pointer">Check Speed</span>
|
||||
<div class="arrow-btn"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /row 2 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toastContainer"></div>
|
||||
|
||||
<!-- ══════════════════════ SCRIPTS ══════════════════════ -->
|
||||
<script>
|
||||
Chart.defaults.font.family = "'Inter',sans-serif";
|
||||
Chart.defaults.color = '#6b6e7d';
|
||||
Chart.defaults.animation.duration = 800;
|
||||
Chart.defaults.animation.easing = 'easeInOutQuart';
|
||||
|
||||
// System State
|
||||
let isSystemRunning = true;
|
||||
let simulationInterval;
|
||||
const MAX_DATA_POINTS = 24;
|
||||
|
||||
/* ── TOAST SYSTEM ── */
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
let icon = '';
|
||||
if(type === 'success') icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
|
||||
else if(type === 'warning') icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||||
else icon = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';
|
||||
|
||||
toast.innerHTML = `${icon}<span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/* ── DATA & CHART SETUP ── */
|
||||
const initialData = {
|
||||
cpu: Array.from({length: MAX_DATA_POINTS}, () => Math.floor(Math.random() * 30) + 10),
|
||||
user: Array.from({length: 15}, () => Math.floor(Math.random() * 50) + 40),
|
||||
perf: Array.from({length: 12}, () => Math.floor(Math.random() * 20) + 70),
|
||||
perf2: Array.from({length: 12}, () => Math.floor(Math.random() * 20) + 60),
|
||||
};
|
||||
|
||||
/* ── CPU CHART (Line with Dots) ── */
|
||||
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
|
||||
const cpuChart = new Chart(cpuCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length: MAX_DATA_POINTS}, (_,i) => i),
|
||||
datasets:[{
|
||||
data: initialData.cpu,
|
||||
borderColor:'#ff7043',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'transparent', // No fill
|
||||
tension: 0.3, // Slight curve, not too wavy
|
||||
pointRadius: 2, // Visible small dots
|
||||
pointBackgroundColor: '#ff7043',
|
||||
pointHoverRadius: 5,
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{
|
||||
x:{display:false},
|
||||
y:{display:false, min:0, max:100}
|
||||
},
|
||||
interaction:{mode:'index',intersect:false},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── RAM DONUT (Flat Solid Color) ── */
|
||||
let currentRamPercent = 65;
|
||||
function drawRam(percent){
|
||||
const c=document.getElementById('ramCanvas'), ctx=c.getContext('2d');
|
||||
const W=c.width, H=c.height, cx=W/2, cy=H-8, R=68, r=52;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const segs=28, gap=.048, filled=Math.round(segs * (percent/100));
|
||||
for(let i=0;i<segs;i++){
|
||||
const a0=Math.PI+(Math.PI/segs)*i+gap/2;
|
||||
const a1=Math.PI+(Math.PI/segs)*(i+1)-gap/2;
|
||||
const f=i<filled;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx,cy,R,a0,a1);
|
||||
ctx.arc(cx,cy,r,a1,a0,true);
|
||||
ctx.closePath();
|
||||
if(f){
|
||||
ctx.fillStyle = '#9c7ef0';
|
||||
} else {
|
||||
ctx.fillStyle='rgba(255,255,255,.07)';
|
||||
}
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
drawRam(currentRamPercent);
|
||||
|
||||
/* ── USER CHART (Line Area Chart - 1:1 Match) ── */
|
||||
const userCtx=document.getElementById('userChart').getContext('2d');
|
||||
const userChart=new Chart(userCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length:15},(_,i)=>i),
|
||||
datasets:[{
|
||||
data:initialData.user,
|
||||
borderColor:'#e8316a', // Pink Line
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(232, 49, 106, 0.15)', // Subtle Pink Fill
|
||||
tension: 0.4, // Smooth curve
|
||||
pointRadius: 0, // No dots, just the wave
|
||||
fill: true, // This creates the area chart
|
||||
borderCapStyle: 'round'
|
||||
}]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{
|
||||
x:{display:false},
|
||||
y:{display:false, min:0, max:120}
|
||||
},
|
||||
interaction:{mode:'index',intersect:false},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── PERF CHART (Solid Area Fill) ── */
|
||||
const perfCtx=document.getElementById('perfChart').getContext('2d');
|
||||
const perfChart=new Chart(perfCtx,{
|
||||
type:'line',
|
||||
data:{
|
||||
labels:Array.from({length:12},(_,i)=>i),
|
||||
datasets:[
|
||||
{
|
||||
data:initialData.perf,
|
||||
borderColor:'#6c8ef0',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(108, 142, 240, 0.15)',
|
||||
tension:.4,
|
||||
pointRadius: 0,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
data:initialData.perf2,
|
||||
borderColor:'#9c7ef0',
|
||||
borderWidth: 2,
|
||||
backgroundColor: 'rgba(156, 126, 240, 0.15)',
|
||||
tension:.4,
|
||||
pointRadius: 0,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options:{
|
||||
responsive:true,maintainAspectRatio:false,
|
||||
plugins:{legend:{display:false},tooltip:{enabled:false}},
|
||||
scales:{x:{display:false},y:{display:false}},
|
||||
}
|
||||
});
|
||||
|
||||
/* ── SIMULATION LOGIC ── */
|
||||
function getRandom(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function updateDashboard() {
|
||||
if(!isSystemRunning) return;
|
||||
|
||||
// 1. Update CPU
|
||||
const newCpu = getRandom(10, 45);
|
||||
document.getElementById('cpuText').innerText = `${newCpu}%`;
|
||||
|
||||
cpuChart.data.datasets[0].data.shift();
|
||||
cpuChart.data.datasets[0].data.push(newCpu);
|
||||
cpuChart.update('none');
|
||||
|
||||
// 2. Update RAM
|
||||
currentRamPercent = getRandom(50, 85);
|
||||
document.getElementById('ramText').innerText = `${currentRamPercent}%`;
|
||||
const gbUsed = (8 * (currentRamPercent/100)).toFixed(1);
|
||||
document.getElementById('ramDetail').innerText = `${gbUsed} GB / 8GB`;
|
||||
drawRam(currentRamPercent);
|
||||
|
||||
// 3. Update Users
|
||||
const newUserVal = getRandom(60, 110);
|
||||
document.getElementById('userText').innerText = `${newUserVal} K`;
|
||||
userChart.data.datasets[0].data.shift();
|
||||
userChart.data.datasets[0].data.push(newUserVal);
|
||||
userChart.update('none');
|
||||
|
||||
// 4. Update Perf & Speed
|
||||
const perfVal = getRandom(82, 99);
|
||||
document.getElementById('perfText').innerText = `${perfVal}%`;
|
||||
|
||||
const up = (Math.random() * 5 + 8).toFixed(1);
|
||||
const down = (Math.random() * 3 + 4).toFixed(1);
|
||||
document.getElementById('upSpeed').innerText = up;
|
||||
document.getElementById('downSpeed').innerText = down;
|
||||
|
||||
perfChart.data.datasets[0].data.shift();
|
||||
perfChart.data.datasets[0].data.push(perfVal);
|
||||
perfChart.data.datasets[1].data.shift();
|
||||
perfChart.data.datasets[1].data.push(perfVal - 5);
|
||||
perfChart.update('none');
|
||||
}
|
||||
|
||||
simulationInterval = setInterval(updateDashboard, 2000);
|
||||
|
||||
/* ── CONTROLS ── */
|
||||
const btnStop = document.getElementById('btnStop');
|
||||
const btnRestart = document.getElementById('btnRestart');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
const liveDot = document.getElementById('liveDot');
|
||||
|
||||
btnStop.addEventListener('click', () => {
|
||||
if(!isSystemRunning) return;
|
||||
isSystemRunning = false;
|
||||
statusBadge.className = 'badge-stopped';
|
||||
statusBadge.innerHTML = 'Stopped';
|
||||
liveDot.classList.add('stopped');
|
||||
btnStop.classList.add('disabled');
|
||||
btnStop.disabled = true;
|
||||
btnRestart.classList.remove('disabled');
|
||||
btnRestart.disabled = false;
|
||||
showToast('System monitoring paused', 'warning');
|
||||
});
|
||||
|
||||
btnRestart.addEventListener('click', () => {
|
||||
if(isSystemRunning) return;
|
||||
isSystemRunning = true;
|
||||
statusBadge.className = 'badge-active';
|
||||
statusBadge.innerHTML = '<span class="live-dot" id="liveDot"></span>Active';
|
||||
btnRestart.classList.add('disabled');
|
||||
btnRestart.disabled = true;
|
||||
btnStop.classList.remove('disabled');
|
||||
btnStop.disabled = false;
|
||||
showToast('System restarted successfully', 'success');
|
||||
});
|
||||
|
||||
/* ── TAB/PILL SWITCH ── */
|
||||
function setPill(el){
|
||||
document.querySelectorAll('#timePills .pill').forEach(p=>p.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
function setTab(el){
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user