argobox-portfolio/src/pages/homelab.astro

838 lines
48 KiB
Plaintext

---
// src/pages/homelab.astro - Converted from static homelab.html
import BaseLayout from '../layouts/BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
const title = "Home Lab | Argobox - LaForceIT Tech Hub";
const description = "ArgoBox - A production-grade Kubernetes homelab for DevOps experimentation, infrastructure automation, and containerized application deployment.";
// Data for services (can be fetched or defined here)
// Example structure - replace with actual data source if needed
const servicesData = {
development: [
{ name: "Gitea", description: "Self-hosted Git service", url: "https://git.argobox.com", status: "live", icon: "fas fa-code-branch", available: true },
{ name: "VS Code Server", description: "Remote development environment", url: "https://code.argobox.com", status: "live", icon: "fas fa-terminal", available: true },
{ name: "Drone CI", description: "Continuous Integration server", url: "https://drone.argobox.com", status: "live", icon: "fas fa-cogs", available: true },
],
media: [
{ name: "Plex", description: "Media streaming server", url: "https://plex.argobox.com", status: "live", icon: "fas fa-play-circle", available: true },
{ name: "Jellyfin", description: "Open source media system", url: "https://jellyfin.argobox.com", status: "live", icon: "fas fa-film", available: true },
{ name: "Sonarr", description: "TV show management", url: "#", status: "restricted", icon: "fas fa-tv", available: false },
{ name: "Radarr", description: "Movie management", url: "#", status: "restricted", icon: "fas fa-video", available: false },
{ name: "Prowlarr", description: "Indexer management", url: "#", status: "restricted", icon: "fas fa-search", available: false },
],
utilities: [
{ name: "File Browser", description: "Web file manager", url: "https://files.argobox.com", status: "live", icon: "fas fa-folder-open", available: true },
{ name: "Vaultwarden", description: "Password manager", url: "https://vault.argobox.com", status: "live", icon: "fas fa-key", available: true },
{ name: "Homepage", description: "Service dashboard", url: "https://dash.argobox.com", status: "live", icon: "fas fa-tachometer-alt", available: true },
{ name: "Uptime Kuma", description: "Status monitoring", url: "https://status.argobox.com", status: "live", icon: "fas fa-heartbeat", available: true },
],
infrastructure: [
{ name: "Proxmox VE", description: "Virtualization platform", url: "#", status: "restricted", icon: "fas fa-server", available: false },
{ name: "Kubernetes (K3s)", description: "Container orchestration", url: "#", status: "restricted", icon: "fas fa-dharmachakra", available: false },
{ name: "Traefik", description: "Ingress controller", url: "#", status: "restricted", icon: "fas fa-route", available: false },
{ name: "OPNsense", description: "Firewall/Router", url: "#", status: "restricted", icon: "fas fa-shield-alt", available: false },
]
};
// Data for projects (can be fetched or defined here)
const projectsData = [
{ title: "Ansible Playbooks", description: "Collection of playbooks for automating system configuration and application deployment.", icon: "fab fa-ansible", tech: ["Ansible", "YAML", "Jinja2"], url: "/resources/iac" }, // Link to relevant resource page
{ title: "Kubernetes Manifests", description: "YAML definitions for deploying various applications and services on Kubernetes.", icon: "fas fa-dharmachakra", tech: ["Kubernetes", "YAML", "Helm"], url: "/resources/kubernetes" },
{ title: "Monitoring Dashboards", description: "Grafana dashboards for visualizing infrastructure and application metrics.", icon: "fas fa-chart-line", tech: ["Grafana", "PromQL", "JSON"], url: "/resources/config-files" }, // Link to relevant resource page
{ title: "Cloudflare Tunnel Setup", description: "Securely exposing home lab services to the internet using Cloudflare Tunnels.", icon: "fas fa-cloud", tech: ["Cloudflare", "Networking", "Security"], url: "/posts/cloudflare-tunnel-setup" } // Link to blog post
];
// Data for dashboards (can be fetched or defined here)
const dashboardsData = [
{ title: "Infrastructure Overview", description: "Key metrics for Proxmox hosts, network devices, and storage.", previewClass: "infrastructure", url: "https://dash.argobox.com/goto/...", icon: "fas fa-server" },
{ title: "Kubernetes Cluster", description: "Detailed view of K3s cluster resources, node status, and pod health.", previewClass: "kubernetes", url: "https://dash.argobox.com/goto/...", icon: "fas fa-dharmachakra" },
{ title: "Network Traffic", description: "Real-time and historical network usage, firewall logs, and connection tracking.", previewClass: "network", url: "https://dash.argobox.com/goto/...", icon: "fas fa-network-wired" },
{ title: "Service Performance", description: "Application-specific metrics, request latency, and error rates.", previewClass: "services", url: "https://dash.argobox.com/goto/...", icon: "fas fa-cogs" }
];
// Data for contact form (if keeping it on this page)
// const contactInfo = { email: "daniel.laforce@argobox.com", /* other info */ };
---
<BaseLayout {title} {description}>
{/* Add Font Awesome if not loaded globally by BaseLayout */}
{/* <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> */}
<Header slot="header" />
<main class="homelab-page">
<!-- Hero Section -->
<section id="home" class="hero">
<div class="particles-container" id="particles-container"></div>
<div class="container">
<div class="hero-content">
<div class="hero-text">
<h1 class="hero-title">
Enterprise-Grade <span class="highlight">Home Lab</span> Environment
</h1>
<p class="hero-description">
A production-ready infrastructure platform for DevOps experimentation, distributed systems, and automating everything with code.
</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-microchip"></i></div>
<div class="stat-detail">
<div class="stat-value">32+</div>
<div class="stat-name">CPU Cores</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-memory"></i></div>
<div class="stat-detail">
<div class="stat-value">64GB</div>
<div class="stat-name">RAM</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-hdd"></i></div>
<div class="stat-detail">
<div class="stat-value">12TB</div>
<div class="stat-name">Storage</div>
</div>
</div>
<div class="stat-item">
<div class="stat-icon"><i class="fas fa-server"></i></div>
<div class="stat-detail">
<div class="stat-value">{Object.values(servicesData).flat().length}+</div>
<div class="stat-name">Services</div>
</div>
</div>
</div>
<div class="cta-buttons">
{/* Link to the Astro page for Ansible Sandbox */}
<a href="/ansible-sandbox" class="btn btn-danger" id="ansible-sandbox-btn">
<i class="fab fa-ansible btn-icon"></i>
<span class="btn-text">Try Ansible Sandbox</span>
<span class="offline-badge">Offline</span> {/* Status updated by JS */}
</a>
<a href="#architecture" class="btn btn-outline">
<i class="fas fa-network-wired btn-icon"></i>
<span class="btn-text">Explore Architecture</span>
</a>
</div>
</div>
<div class="hero-terminal">
<div class="terminal-header">
<div class="terminal-buttons">
<span class="terminal-btn close"></span>
<span class="terminal-btn minimize"></span>
<span class="terminal-btn maximize"></span>
</div>
<div class="terminal-title">argobox ~ k8s-status</div>
</div>
<div class="terminal-body">
<div class="terminal-line">$ kubectl get nodes</div>
<div class="terminal-line output">NAME STATUS ROLES AGE VERSION</div>
<div class="terminal-line output">argobox Ready control-plane,master 154d v1.25.16+k3s1</div>
<div class="terminal-line output">argobox-lite Ready worker 154d v1.25.16+k3s1</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ kubectl get pods -A | grep Running | wc -l</div>
<div class="terminal-line output">32</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ uptime</div>
<div class="terminal-line output">14:30:25 up 154 days, 23:12, 1 user, load average: 0.22, 0.18, 0.15</div>
<div class="terminal-line blank">&nbsp;</div>
<div class="terminal-line">$ ansible-playbook status.yml</div>
<div class="terminal-line output">PLAY [Check system status] *******************************************</div>
<div class="terminal-line output">TASK [Gathering Facts] **********************************************</div>
<div class="terminal-line output success">ok: [argobox]</div>
<div class="terminal-line output success">ok: [argobox-lite]</div>
<div class="terminal-line output">TASK [Check service status] *****************************************</div>
<div class="terminal-line output success">ok: [argobox]</div>
<div class="terminal-line output success">ok: [argobox-lite]</div>
<div class="terminal-line output">PLAY RECAP **********************************************************</div>
<div class="terminal-line output success">argobox : ok=2 changed=0 unreachable=0 failed=0 skipped=0</div>
<div class="terminal-line output success">argobox-lite: ok=2 changed=0 unreachable=0 failed=0 skipped=0</div>
<div class="terminal-line typing">$ <span class="cursor">|</span></div>
</div>
</div>
</div>
</div>
</section>
<!-- Architecture Section -->
<section id="architecture" class="architecture section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Infrastructure Architecture</h2>
<p class="section-description">
Enterprise-grade network topology with redundancy, virtualization, and secure segmentation.
</p>
</div>
{/* Consider replacing with an actual image or a more maintainable diagram component */}
<img src="/images/homelab/argobox-architecture.svg" alt="ArgoBox Architecture Diagram" class="architecture-diagram-image" />
{/* Fallback or simplified diagram if image isn't available */}
<div class="architecture-details">
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-shield-alt"></i></div>
<h3 class="detail-title">Network Security</h3>
<p class="detail-description">
Enterprise firewall with network segmentation using VLANs and strict access controls. Redundant routing with automatic failover between OPNsense and OpenWrt.
</p>
</div>
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-cloud"></i></div>
<h3 class="detail-title">Virtualization</h3>
<p class="detail-description">
Proxmox virtualization platform with ZFS storage pools in RAID10 configuration. Optimized storage pools for VMs and containers with proper resource allocation.
</p>
</div>
<div class="detail-card">
<div class="detail-icon"><i class="fas fa-route"></i></div>
<h3 class="detail-title">High Availability</h3>
<p class="detail-description">
Full redundancy with failover routing, replicated storage, and resilient services. Automatic service recovery and load balancing across nodes.
</p>
</div>
</div>
</div>
</section>
<!-- Technologies Section -->
<section id="technologies" class="technologies section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Core Technologies</h2>
<p class="section-description">
The ArgoBox lab leverages cutting-edge open source technologies to create a powerful, flexible infrastructure.
</p>
</div>
<div class="tech-grid">
{/* Data could be externalized */}
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-dharmachakra"></i></div>
<h3 class="tech-title">Kubernetes (K3s)</h3>
<p class="tech-description">Lightweight Kubernetes distribution running across multiple nodes for container orchestration. Powers all microservices and applications.</p>
<div class="tech-features"><span class="tech-feature">Multi-node cluster</span><span class="tech-feature">Persistent volumes</span><span class="tech-feature">Traefik ingress</span><span class="tech-feature">Auto-healing</span></div>
</div>
<div class="tech-card featured">
<div class="tech-icon"><i class="fab fa-ansible"></i></div>
<h3 class="tech-title">Ansible Automation</h3>
<p class="tech-description">Infrastructure as code platform for automated provisioning, configuration management, and application deployment across the entire environment.</p>
<div class="tech-features"><span class="tech-feature">Playbook library</span><span class="tech-feature">Role-based configs</span><span class="tech-feature">Interactive sandbox</span><span class="tech-feature">Idempotent workflows</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-server"></i></div>
<h3 class="tech-title">Proxmox</h3>
<p class="tech-description">Enterprise-class virtualization platform running virtual machines and containers with ZFS storage backend for data integrity.</p>
<div class="tech-features"><span class="tech-feature">ZFS storage</span><span class="tech-feature">Resource balancing</span><span class="tech-feature">Live migration</span><span class="tech-feature">Hardware passthrough</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-shield-alt"></i></div>
<h3 class="tech-title">Zero Trust Security</h3>
<p class="tech-description">Comprehensive security architecture with Cloudflare tunnels, network segmentation, and authentication at all service boundaries.</p>
<div class="tech-features"><span class="tech-feature">Cloudflare tunnels</span><span class="tech-feature">OPNsense firewall</span><span class="tech-feature">VLAN segmentation</span><span class="tech-feature">WireGuard VPN</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-database"></i></div>
<h3 class="tech-title">PostgreSQL</h3>
<p class="tech-description">Enterprise database cluster for application data storage with automated backups, replication, and performance optimization.</p>
<div class="tech-features"><span class="tech-feature">Automated backups</span><span class="tech-feature">Connection pooling</span><span class="tech-feature">Optimized for K8s</span><span class="tech-feature">Multi-app support</span></div>
</div>
<div class="tech-card">
<div class="tech-icon"><i class="fas fa-chart-line"></i></div>
<h3 class="tech-title">Monitoring Stack</h3>
<p class="tech-description">Comprehensive monitoring with Prometheus, Grafana, and AlertManager for real-time visibility into all infrastructure components.</p>
<div class="tech-features"><span class="tech-feature">Prometheus metrics</span><span class="tech-feature">Grafana dashboards</span><span class="tech-feature">Automated alerts</span><span class="tech-feature">Historical data</span></div>
</div>
</div>
</div>
</section>
<!-- Services Section -->
<section id="services" class="services section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Available Services</h2>
<p class="section-description">
Explore the various services and applications hosted in the ArgoBox environment.
</p>
</div>
<div class="services-info-banner">
<i class="fas fa-info-circle info-icon"></i>
<p>Some services require authentication and are restricted. Available public services are highlighted and clickable.</p>
</div>
<div class="services-grid">
{Object.entries(servicesData).map(([categoryKey, categoryServices]) => (
<div class="services-category">
<h3 class="category-title">
<i class={`fas ${
categoryKey === 'development' ? 'fa-code' :
categoryKey === 'media' ? 'fa-photo-video' :
categoryKey === 'utilities' ? 'fa-tools' :
'fa-cogs' // Default for infrastructure
} category-icon`}></i>
{categoryKey.charAt(0).toUpperCase() + categoryKey.slice(1)} Tools
</h3>
<div class="service-items">
{categoryServices.map(service => (
<a
href={service.available ? service.url : '#'}
class:list={["service-item", { available: service.available }, `service-${service.name.toLowerCase().replace(/\s+/g, '-')}`]}
target={service.available ? "_blank" : undefined}
rel={service.available ? "noopener noreferrer" : undefined}
aria-disabled={!service.available}
>
<div class="service-icon"><i class={service.icon}></i></div>
<div class="service-info">
<div class="service-name">{service.name}</div>
<p class="service-description">{service.description}</p>
</div>
<span class={`service-status ${service.status}`}>
<span class="status-dot"></span>
{service.status.charAt(0).toUpperCase() + service.status.slice(1)}
</span>
</a>
))}
</div>
</div>
))}
</div>
</div>
</section>
<!-- Projects Section -->
<section id="projects" class="projects section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Related Projects & Code</h2>
<p class="section-description">
Explore associated projects, configurations, and code repositories related to the ArgoBox lab.
</p>
</div>
<div class="projects-grid">
{projectsData.map(project => (
<a href={project.url} class="project-card" target={project.url.startsWith('http') ? '_blank' : undefined} rel={project.url.startsWith('http') ? 'noopener noreferrer' : undefined}>
<div class="project-header">
<div class="project-icon"><i class={project.icon}></i></div>
<h3 class="project-title">{project.title}</h3>
</div>
<p class="project-description">{project.description}</p>
<div class="project-tech">
{project.tech.map(tech => <span class="tech-badge">{tech}</span>)}
</div>
<div class="project-cta">
<span class="btn btn-sm btn-outline">
{project.url.startsWith('http') ? 'View Project' : 'View Details'} <i class="fas fa-arrow-right"></i>
</span>
</div>
</a>
))}
</div>
</div>
</section>
<!-- Dashboards Section -->
<section id="dashboards" class="dashboards section-padding">
<div class="container">
<div class="section-header">
<h2 class="section-title">Live Dashboards</h2>
<p class="section-description">
Real-time monitoring dashboards providing insights into the lab's performance and status. (Authentication Required)
</p>
</div>
<div class="services-info-banner"> {/* Reusing banner style */}
<i class="fas fa-lock info-icon"></i>
<p>Access to live dashboards requires authentication via Cloudflare Access.</p>
</div>
<div class="dashboard-grid">
{dashboardsData.map(dash => (
<a href={dash.url} class="dashboard-card" target="_blank" rel="noopener noreferrer">
<div class={`dashboard-preview ${dash.previewClass}`}>
<div class="dashboard-overlay">
<div class="overlay-content">
<div class="overlay-icon"><i class={dash.icon}></i></div>
<div class="overlay-text">View Dashboard</div>
</div>
</div>
</div>
<div class="dashboard-info">
<h3 class="dashboard-title">{dash.title}</h3>
<p class="dashboard-description">{dash.description}</p>
<div class="dashboard-cta">
<span class="btn btn-sm btn-primary">Access Dashboard</span>
</div>
</div>
</a>
))}
</div>
</div>
</section>
{/* Contact Section - Optional: Can be moved to a separate page */}
{/*
<section id="contact" class="contact section-padding alt-bg">
<div class="container">
<div class="section-header">
<h2 class="section-title">Get In Touch</h2>
<p class="section-description">Have questions or want to collaborate? Reach out!</p>
</div>
<div class="contact-grid">
<div class="contact-info">
<div class="contact-item">
<div class="contact-icon"><i class="fas fa-envelope"></i></div>
<h3 class="contact-title">Email</h3>
<a href={`mailto:${contactInfo.email}`} class="contact-link">{contactInfo.email}</a>
</div>
Add other contact items like LinkedIn, GitHub etc.
</div>
<div class="contact-form-container">
<form id="contact-form" class="contact-form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div class="form-group">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" required />
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
</div>
</div>
</div>
</section>
*/}
</main>
<Footer slot="footer" />
</BaseLayout>
<style is:global>
/* Import relevant styles from styles.css, adapted for Astro */
/* NOTE: General resets, body, typography, basic buttons are handled by global.css/theme.css */
/* Hero Section Specific Styles */
.hero {
min-height: 100vh; /* Use min-height instead of height */
display: flex;
align-items: center;
position: relative;
overflow: hidden;
padding-top: 6rem; /* Adjust as needed */
padding-bottom: 4rem; /* Add padding */
background: linear-gradient(180deg, var(--bg-secondary), var(--bg-primary)); /* Use theme vars */
}
.hero-content {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 3rem;
align-items: center;
position: relative; /* Ensure content is above particles */
z-index: 1;
}
.particles-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
background-color: var(--accent); /* Use theme var */
border-radius: 50%;
pointer-events: none;
/* Animation will be added by script */
}
@keyframes float-particle { /* Keep keyframes */
0% { transform: translateY(0) translateX(0); opacity: 0.3; }
50% { transform: translateY(-100px) translateX(50px); opacity: 0.1; }
100% { transform: translateY(0) translateX(0); opacity: 0.3; }
}
.hero-text { /* Styles seem okay */ }
.hero-title {
font-size: clamp(2.5rem, 5vw, 3.25rem); /* Adjust size */
margin-bottom: 1.5rem;
line-height: 1.2;
font-weight: 700; /* Use theme font weight */
color: var(--text-primary); /* Use theme var */
}
.hero-title .highlight {
background: var(--accent-gradient); /* Use theme var */
-webkit-background-clip: text;
background-clip: text;
color: transparent;
position: relative;
}
.hero-title .highlight::after { /* Optional underline effect */
content: '';
position: absolute;
width: 100%;
height: 4px; /* Adjust thickness */
bottom: 2px; /* Adjust position */
left: 0;
background: var(--accent-gradient);
border-radius: 2px;
opacity: 0.3;
z-index: -1;
}
.hero-description {
font-size: var(--font-size-lg); /* Use theme var */
color: var(--text-secondary); /* Use theme var */
margin-bottom: 2rem;
max-width: 550px;
}
.hero-stats {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin: 2rem 0;
}
.stat-item { display: flex; align-items: center; gap: 0.75rem; }
.stat-icon {
width: 3rem; height: 3rem; display: flex; align-items: center; justify-content: center;
background: var(--accent-gradient); border-radius: 0.5rem; color: white; font-size: 1.25rem;
}
.stat-detail { display: flex; flex-direction: column; }
.stat-value { font-weight: 700; font-size: 1.25rem; color: var(--text-primary); }
.stat-name { font-size: 0.875rem; color: var(--text-secondary); }
.cta-buttons { display: flex; flex-wrap: wrap; gap: 1rem; }
/* Use Astro theme button classes (.cta-button.primary/secondary) if possible */
/* Or adapt these styles */
.btn {
display: inline-flex; align-items: center; justify-content: center; padding: 0.75rem 1.5rem;
font-weight: 600; border-radius: 0.5rem; transition: all var(--transition-normal);
cursor: pointer; position: relative; overflow: hidden; border: none; outline: none; gap: 0.75rem;
text-decoration: none; /* Ensure links look like buttons */
}
.btn-primary { background: var(--accent-gradient); color: white; /* ... other styles */ }
.btn-primary:hover { /* ... hover styles */ }
.btn-outline { background-color: transparent; border: 2px solid var(--accent); color: var(--accent); }
.btn-outline:hover { background-color: var(--accent); color: white; }
.btn-danger { background-color: var(--error); border-color: var(--error); color: white; } /* Use theme error color */
.btn-icon { font-size: 0.9rem; }
.offline-badge, .online-badge { /* Styles from inline block */
display: inline-block; padding: 0.25rem 0.5rem; border-radius: 0.25rem;
font-size: 0.75rem; font-weight: 600; margin-left: 0.5rem;
}
.offline-badge { background-color: #991b1b; color: white; } /* Darker red */
.online-badge { background-color: var(--success); color: white; display: none; } /* Use theme success */
/* Hero Terminal Styles */
.hero-terminal {
width: 100%; border-radius: 0.75rem; overflow: hidden;
box-shadow: var(--card-shadow); /* Use theme shadow */
background-color: #0d1117; /* Keep specific terminal bg */
border: 1px solid var(--border); /* Use theme border */
/* transform: perspective(1000px) rotateY(5deg) rotateX(2deg); */ /* Remove or adjust transform */
transition: all 0.3s ease;
}
/* .hero-terminal:hover { transform: perspective(1000px) rotateY(0deg) rotateX(0deg); } */
.terminal-header { display: flex; align-items: center; background-color: #161b22; padding: 0.5rem 1rem; border-bottom: 1px solid var(--border); }
.terminal-buttons { display: flex; gap: 0.5rem; margin-right: 1rem; }
.terminal-btn { width: 12px; height: 12px; border-radius: 50%; }
.terminal-btn.close { background-color: #ff5f56; }
.terminal-btn.minimize { background-color: #ffbd2e; }
.terminal-btn.maximize { background-color: #27c93f; }
.terminal-title { color: var(--text-secondary); font-size: 0.875rem; font-family: var(--font-mono); }
.terminal-body { padding: 1rem; font-family: var(--font-mono); font-size: 0.875rem; color: var(--text-primary); line-height: 1.4; max-height: 400px; overflow-y: auto; }
.terminal-line { margin-bottom: 0.25rem; white-space: pre-wrap; }
.terminal-line.output { color: var(--text-secondary); }
.terminal-line.success { color: var(--success); } /* Use theme color */
.terminal-line.error { color: var(--error); } /* Use theme color */
.terminal-line.typing { color: var(--accent); } /* Use theme color */
.terminal-line .cursor { display: inline-block; width: 8px; height: 15px; background-color: var(--accent); animation: blink 1s step-end infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
/* Architecture Section */
.architecture { /* Use section-padding */ }
.section-header { text-align: center; margin-bottom: 3rem; } /* Adjust margin */
.section-title { font-size: var(--font-size-3xl); margin-bottom: 1rem; position: relative; display: inline-block; color: var(--text-primary); }
.section-title::after { content: ''; position: absolute; bottom: -0.5rem; left: 50%; transform: translateX(-50%); width: 80px; height: 4px; background: var(--accent-gradient); border-radius: 2px; }
.section-description { max-width: 700px; margin: 0 auto; color: var(--text-secondary); font-size: var(--font-size-lg); }
.architecture-diagram-image { display: block; max-width: 800px; margin: 3rem auto; border-radius: 8px; box-shadow: var(--card-shadow); }
.architecture-details { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.detail-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: all var(--transition-normal); }
.detail-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.detail-icon { font-size: 2rem; color: var(--accent); margin-bottom: 1rem; }
.detail-title { font-size: 1.25rem; margin-bottom: 0.75rem; color: var(--text-primary); }
.detail-description { color: var(--text-secondary); font-size: var(--font-size-sm); line-height: 1.6; }
/* Technologies Section */
.technologies { /* Use section-padding alt-bg */ }
.tech-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.tech-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 2rem; transition: all var(--transition-normal); display: flex; flex-direction: column; }
.tech-card:hover { transform: translateY(-10px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.tech-card.featured { background: linear-gradient(145deg, rgba(59, 130, 246, 0.1), rgba(37, 99, 235, 0.05)); border-color: var(--accent); position: relative; overflow: hidden; }
.tech-card.featured::before { content: ''; position: absolute; top: 0; right: 0; width: 100px; height: 100px; background: var(--accent-gradient); transform: rotate(45deg) translate(20px, -60px); opacity: 0.3; }
.tech-icon { font-size: 2.5rem; color: var(--accent); margin-bottom: 1.5rem; transition: all var(--transition-normal); }
.tech-card:hover .tech-icon { transform: scale(1.1); /* text-shadow: var(--accent-glow); */ } /* Glow might be too much */
.tech-title { font-size: 1.5rem; margin-bottom: 1rem; color: var(--text-primary); }
.tech-description { color: var(--text-secondary); margin-bottom: 1.5rem; flex-grow: 1; font-size: var(--font-size-sm); }
.tech-features { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: auto; } /* Push to bottom */
.tech-feature { background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); color: var(--accent); font-size: 0.8rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; }
/* Services Section */
.services { /* Use section-padding */ }
.services-info-banner { display: flex; align-items: center; gap: 1rem; background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 0.75rem; padding: 1rem 1.5rem; margin-bottom: 2rem; }
.info-icon { font-size: 1.5rem; color: var(--accent); }
.services-info-banner p { color: var(--text-secondary); margin: 0; font-size: var(--font-size-sm); }
.services-grid { display: flex; flex-direction: column; gap: 2.5rem; margin-top: 2rem; }
.services-category { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; overflow: hidden; transition: all var(--transition-normal); }
.services-category:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.category-title { display: flex; align-items: center; gap: 0.75rem; padding: 1.25rem; background-color: rgba(15, 23, 42, 0.5); border-bottom: 1px solid var(--border); font-size: 1.35rem; color: var(--text-primary); }
.category-icon { color: var(--accent); }
.service-items { display: flex; flex-direction: column; }
.service-item { display: flex; align-items: center; padding: 1.25rem; border-bottom: 1px solid var(--border); transition: all var(--transition-normal); color: var(--text-primary); text-decoration: none; }
.service-item:last-child { border-bottom: none; }
.service-item.available { cursor: pointer; }
.service-item.available:hover { background-color: rgba(15, 23, 42, 0.3); }
.service-item:not(.available) { cursor: default; opacity: 0.6; } /* Dim unavailable */
.service-icon { width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; margin-right: 1rem; background-color: rgba(59, 130, 246, 0.1); border-radius: 0.5rem; font-size: 1.25rem; color: var(--accent); transition: all var(--transition-normal); flex-shrink: 0; }
.service-item.available:hover .service-icon { background-color: rgba(59, 130, 246, 0.2); transform: scale(1.1); }
.service-info { flex: 1; }
.service-name { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; color: var(--text-primary); }
.service-description { font-size: 0.9rem; color: var(--text-secondary); }
.service-status { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; margin-left: 1rem; }
.service-status.live { background-color: rgba(16, 185, 129, 0.1); color: var(--success); }
.service-status.maintenance { background-color: rgba(245, 158, 11, 0.1); color: var(--warning); }
.service-status.restricted { background-color: rgba(107, 114, 128, 0.1); color: var(--text-secondary); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
.service-status.live .status-dot { background-color: var(--success); }
.service-status.maintenance .status-dot { background-color: var(--warning); }
.service-status.restricted .status-dot { background-color: var(--text-secondary); }
/* Offline service styling from inline */
.offline-service { position: relative; }
.offline-service::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: 0.5rem; pointer-events: none; z-index: 1; }
.offline-service .service-icon { color: var(--error); }
.offline-service .service-status.offline .status-dot { background-color: var(--error); box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); animation: pulse-red 2s infinite; }
@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } 70% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
/* Projects Section */
.projects { /* Use section-padding alt-bg */ }
.projects-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 2rem; margin-top: 3rem; }
.project-card { background-color: var(--card-bg); border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; transition: all var(--transition-normal); display: flex; flex-direction: column; text-decoration: none; } /* Added text-decoration */
.project-card:hover { transform: translateY(-10px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.project-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; }
.project-icon { font-size: 1.75rem; color: var(--accent); transition: all var(--transition-normal); }
.project-card:hover .project-icon { transform: scale(1.1); }
.project-title { font-size: 1.25rem; margin: 0; color: var(--text-primary); }
.project-description { color: var(--text-secondary); margin-bottom: 1.5rem; flex-grow: 1; font-size: var(--font-size-sm); }
.project-tech { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1.5rem; }
.tech-badge { background-color: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); color: var(--accent); font-size: 0.8rem; font-weight: 500; padding: 0.25rem 0.75rem; border-radius: 9999px; }
.project-cta { margin-top: auto; }
.btn-sm { padding: 0.5rem 1rem; font-size: 0.875rem; } /* Define btn-sm if not global */
/* Dashboards Section */
.dashboards { /* Use section-padding */ }
.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 3rem; } /* Use auto-fit */
.dashboard-card { background: var(--card-bg); border-radius: 8px; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; border: 1px solid var(--border); text-decoration: none; }
.dashboard-card:hover { transform: translateY(-5px); box-shadow: var(--card-shadow); border-color: var(--accent); }
.dashboard-preview { height: 200px; background-size: cover; background-position: center; position: relative; background-color: var(--secondary-bg); /* Fallback bg */ }
/* Add specific preview backgrounds if needed, or use actual images */
.dashboard-preview.infrastructure { /* background-image: ... */ }
.dashboard-preview.kubernetes { /* background-image: ... */ }
.dashboard-preview.network { /* background-image: ... */ }
.dashboard-preview.services { /* background-image: ... */ }
.dashboard-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; }
.dashboard-card:hover .dashboard-overlay { opacity: 1; }
.overlay-content { text-align: center; color: #ffffff; }
.overlay-icon { font-size: 2rem; margin-bottom: 0.5rem; }
.overlay-text { font-size: 1rem; opacity: 0.8; }
.dashboard-info { padding: 1.5rem; }
.dashboard-title { font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text-primary); }
.dashboard-description { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 1rem; }
.dashboard-cta .btn-sm { padding: 0.5rem 1rem; font-size: 0.9rem; }
/* General Responsive */
.section-padding { padding: 4rem 1rem; } /* Add horizontal padding */
.alt-bg {
/* Use padding instead of negative margins for full bleed */
padding-top: 4rem;
padding-bottom: 4rem;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-secondary);
border-bottom: 1px solid var(--border-secondary);
}
.alt-bg > .container { /* Ensure container inside alt-bg keeps padding */
padding-left: 1rem;
padding-right: 1rem;
}
@media (max-width: 1024px) {
.hero-content { grid-template-columns: 1fr; text-align: center; }
.hero-text { order: 2; }
.hero-terminal { order: 1; margin: 0 auto 2rem; max-width: 600px; }
.hero-stats { justify-content: center; }
.cta-buttons { justify-content: center; }
}
@media (max-width: 768px) {
.section-padding { padding: 3rem 1rem; }
.alt-bg { padding-top: 3rem; padding-bottom: 3rem; }
.hero { padding-top: 5rem; padding-bottom: 3rem; min-height: auto; }
.hero-title { font-size: 2.25rem; }
.section-title { font-size: var(--font-size-2xl); }
.section-header { flex-direction: column; align-items: center; gap: 0.5rem; margin-bottom: 2rem; }
.section-description { font-size: var(--font-size-base); }
.tech-grid, .projects-grid, .dashboard-grid, .architecture-details { grid-template-columns: 1fr; }
.services-grid { gap: 1.5rem; }
.service-item { flex-direction: column; align-items: flex-start; }
.service-status { margin-left: 0; margin-top: 0.5rem; }
}
</style>
<script>
// Adapted from script.js
// --- Particle Animation ---
function createBackgroundParticles(count = 30) { // Reduced default count
const particlesContainer = document.getElementById('particles-container');
if (!particlesContainer) return;
particlesContainer.innerHTML = ''; // Clear existing
for (let i = 0; i < count; i++) {
const particle = document.createElement('div');
particle.classList.add('particle');
const size = Math.random() * 3 + 1; // Smaller size
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
particle.style.opacity = (Math.random() * 0.2 + 0.05).toString(); // More subtle
particle.style.animation = `float-particle ${Math.random() * 25 + 15}s linear infinite`;
particle.style.animationDelay = `${Math.random() * 15}s`;
particlesContainer.appendChild(particle);
}
}
// --- Terminal Cursor ---
function initTerminalTyping() {
const cursor = document.querySelector('.hero-terminal .cursor');
if (cursor) {
setInterval(() => {
cursor.style.opacity = cursor.style.opacity === '0' ? '1' : '0';
}, 600);
}
}
// --- Status Checks (Placeholder - Requires actual API endpoints) ---
function checkAnsibleSandboxStatus() {
const btn = document.getElementById('ansible-sandbox-btn');
const badge = btn?.querySelector('.offline-badge');
if (!btn || !badge) return;
// Replace with actual API call
const isOnline = false; // Simulate offline for now
if (isOnline) {
btn.classList.remove('btn-danger');
btn.classList.add('btn-primary', 'pulse'); // Add pulse if desired
badge.textContent = 'Online';
badge.classList.remove('offline-badge');
badge.classList.add('online-badge');
badge.style.display = 'inline-block';
} else {
btn.classList.remove('btn-primary', 'pulse');
btn.classList.add('btn-danger');
badge.textContent = 'Offline';
badge.classList.remove('online-badge');
badge.classList.add('offline-badge');
badge.style.display = 'inline-block'; // Ensure it's shown
}
}
function checkLiveDashboardStatus() {
const indicator = document.querySelector('.dashboard-link .live-indicator');
if (!indicator) return;
// Replace with actual API call
const isOnline = false; // Simulate offline
if (isOnline) {
indicator.classList.remove('offline');
} else {
indicator.classList.add('offline');
}
}
// --- Initialization ---
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('particles-container')) {
createBackgroundParticles();
// Optional: Add resize listener if particle count should change
// window.addEventListener('resize', () => createBackgroundParticles(window.innerWidth <= 768 ? 15 : 30));
}
if (document.querySelector('.hero-terminal .cursor')) {
initTerminalTyping();
}
// Initial status checks
checkAnsibleSandboxStatus();
checkLiveDashboardStatus();
// Optional: Set interval to re-check status periodically
// setInterval(checkAnsibleSandboxStatus, 60000); // Check every minute
// setInterval(checkLiveDashboardStatus, 60000);
});
// --- Contact Form Submission (If form is included on this page) ---
/*
const contactForm = document.getElementById('contact-form');
if (contactForm) {
contactForm.addEventListener('submit', async (e) => {
e.preventDefault();
const submitButton = contactForm.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
const formMessages = contactForm.querySelector('.form-messages') || document.createElement('div'); // Add a div for messages if needed
formMessages.className = 'form-messages';
if (!contactForm.contains(formMessages)) {
contactForm.insertBefore(formMessages, submitButton);
}
formMessages.innerHTML = ''; // Clear previous messages
try {
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
const formData = new FormData(contactForm);
const data = Object.fromEntries(formData.entries());
const response = await fetch('/api/contact', { // Use the Astro API route
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
formMessages.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${result.message}</div>`;
contactForm.reset();
} else {
throw new Error(result.message || 'Failed to send message');
}
} catch (error) {
console.error('Error sending contact form:', error);
formMessages.innerHTML = `<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> ${error.message}</div>`;
} finally {
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
setTimeout(() => { formMessages.innerHTML = ''; }, 5000); // Clear message after 5s
}
});
}
*/
</script>