Compare commits
2 Commits
fa64c4a254
...
4019d4cb10
| Author | SHA1 | Date | |
|---|---|---|---|
| 4019d4cb10 | |||
| dee891ef68 |
BIN
public/homelabber.png
Normal file
BIN
public/homelabber.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -5,6 +5,60 @@ interface RichTextRendererProps {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseInline = (text: string) => {
|
||||||
|
const parts = text.split(/(\*\*.*?\*\*)/g);
|
||||||
|
return parts.map((part, index) => {
|
||||||
|
if (part.startsWith('**') && part.endsWith('**')) {
|
||||||
|
return <strong key={index} style={{ color: 'white' }}>{part.slice(2, -2)}</strong>;
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMarkdown = (text: string) => {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const elements: React.ReactNode[] = [];
|
||||||
|
let currentListItems: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) {
|
||||||
|
const content = trimmed.substring(2);
|
||||||
|
currentListItems.push(
|
||||||
|
<li key={`li-${i}`} style={{ marginBottom: '4px' }}>
|
||||||
|
{parseInline(content)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (currentListItems.length > 0) {
|
||||||
|
elements.push(
|
||||||
|
<ul key={`ul-${i}`} style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
|
||||||
|
{currentListItems}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
currentListItems = [];
|
||||||
|
}
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
elements.push(
|
||||||
|
<p key={`p-${i}`} style={{ marginBottom: '16px' }}>
|
||||||
|
{parseInline(line)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentListItems.length > 0) {
|
||||||
|
elements.push(
|
||||||
|
<ul key="ul-end" style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
|
||||||
|
{currentListItems}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
|
const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
|
||||||
if (!content) return null;
|
if (!content) return null;
|
||||||
|
|
||||||
@@ -22,11 +76,11 @@ const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
|
|||||||
return <CodeBlock key={index} language={language} code={code} />;
|
return <CodeBlock key={index} language={language} code={code} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular text (split by newlines for paragraph handling)
|
// Regular text with markdown parsing
|
||||||
return (
|
return (
|
||||||
<span key={index} style={{ whiteSpace: 'pre-wrap' }}>
|
<div key={index}>
|
||||||
{part}
|
{parseMarkdown(part)}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,6 +103,92 @@ jobs:
|
|||||||
content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull"
|
content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
title: "Home Lab Architecture",
|
||||||
|
description: "A comprehensive self-hosted homelab running 20+ services. Orchestrated with Docker Compose for media automation, personal cloud storage, and secure remote access.",
|
||||||
|
techStack: ["Docker", "Nginx", "Tailscale", "Authentik", "PostgreSQL"],
|
||||||
|
image: "/homelabber.png", // Placeholder
|
||||||
|
links: {},
|
||||||
|
hasDetails: true,
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
title: 'Overview',
|
||||||
|
content: "My homelab acts as my personal cloud, giving me control over my data and providing a playground for learning system administration. It runs on a dedicated machine using Docker Compose to manage a suite of interconnected microservices. Access is secured via Tailscale VPN and Authentik SSO."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
title: 'Media & Automation',
|
||||||
|
content: "The core of the lab is the media stack. **Jellyfin** serves as the streaming frontend. Behind the scenes, automation tools handle content acquisition and organization. **Jellyseerr** provides a clean interface for requesting new additions to the library."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
title: 'Personal Cloud & Productivity',
|
||||||
|
content: "I've replaced several commercial services with self-hosted alternatives:\n\n* **Immich**: A powerful photo and video backup solution, replacing Google Photos.\n* **Gitea**: My self-hosted Git server for code management (including CI/CD runners).\n* **Mealie**: A recipe manager to organize and plan meals."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
title: 'Infrastructure & Security',
|
||||||
|
content: "Reliability and security are key. **Nginx** handles reverse proxying with **Certbot** managing SSL certificates. **Fail2ban** prevents intrusion attempts. **Authentik** provides a unified identity layer. **Portainer** offers a GUI for Docker management, and **Tailscale** creates a secure mesh network for remote access without exposing ports."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
title: 'Monitoring',
|
||||||
|
content: "**Homepage** serves as the main dashboard. **Beszel** and **Uptime Kuma** monitor system resources and service availability, while **Speedtest Tracker** keeps a historic log of network performance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'code',
|
||||||
|
language: 'yaml',
|
||||||
|
title: 'Docker Compose Configuration',
|
||||||
|
code: `services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:stable
|
||||||
|
restart: unless-stopped
|
||||||
|
ports: ["80:80", "443:443"]
|
||||||
|
# ... configurations
|
||||||
|
|
||||||
|
immich-server:
|
||||||
|
image: ghcr.io/immich-app/immich-server:release
|
||||||
|
environment:
|
||||||
|
- DB_HOSTNAME=immich-postgres
|
||||||
|
- REDIS_HOSTNAME=immich-redis
|
||||||
|
depends_on: [immich-redis, immich-postgres]
|
||||||
|
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:latest
|
||||||
|
ports: ["3000:3000", "2222:22"]
|
||||||
|
environment:
|
||||||
|
- GITEA__actions__ENABLED=true
|
||||||
|
|
||||||
|
jellyfin:
|
||||||
|
image: jellyfin/jellyfin:latest
|
||||||
|
ports: ["8096:8096"]
|
||||||
|
volumes:
|
||||||
|
- ./media:/media
|
||||||
|
|
||||||
|
# Monitoring
|
||||||
|
beszel:
|
||||||
|
image: henrygd/beszel:latest
|
||||||
|
ports: ["8090:8090"]
|
||||||
|
|
||||||
|
uptime-kuma:
|
||||||
|
image: louislam/uptime-kuma:latest
|
||||||
|
ports: ["3002:3001"]
|
||||||
|
|
||||||
|
# Security & Networking
|
||||||
|
tailscale:
|
||||||
|
image: tailscale/tailscale:latest
|
||||||
|
network_mode: host
|
||||||
|
|
||||||
|
authentik-server:
|
||||||
|
image: ghcr.io/goauthentik/server:latest
|
||||||
|
ports: ["9001:9000", "9444:9443"]
|
||||||
|
|
||||||
|
# ... full configuration includes 20+ services`
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user