setup full stack of microservices for refinement

This commit is contained in:
kert
2026-02-05 21:29:28 -05:00
parent 255e563c19
commit 25b008eb87
25 changed files with 4133 additions and 132 deletions

484
README.md
View File

@@ -1,45 +1,123 @@
# Homelab Stack # Homelab Stack
Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, and research tools. Self-hosted infrastructure stack with GPU support, S3-compatible storage, Git server with container registry, CI/CD, data lakehouse, observability, and research tools.
## Services ## Services
All services are accessible via Traefik reverse proxy on port 80.
| Service | URL | Description | | Service | URL | Description |
|---------|-----|-------------| |---------|-----|-------------|
| Gitea | http://gitea.home.fhirworx.io:3000 | Git server with LFS + container registry | | Dashboard | http://home.fhirworx.io | Homelab command center |
| Woodpecker CI | http://ci.home.fhirworx.io:8000 | CI/CD pipelines | | Traefik | http://traefik.home.fhirworx.io | Reverse proxy dashboard |
| RustFS | http://s3.home.fhirworx.io:9000 | S3-compatible object storage | | Gitea | http://gitea.home.fhirworx.io | Git server with LFS + container registry |
| RustFS Console | http://s3.home.fhirworx.io:9001 | Storage management UI | | Woodpecker CI | http://ci.home.fhirworx.io | CI/CD pipelines |
| Notebooks | http://notebooks.home.fhirworx.io:2718 | GPU-accelerated Jupyter | | RustFS Console | http://minio.home.fhirworx.io | S3 storage management UI |
| Zotero | http://zotero.home.fhirworx.io:8080 | Reference manager with VNC | | RustFS S3 API | http://s3.home.fhirworx.io | S3-compatible object storage |
| Notebooks | http://notebooks.home.fhirworx.io | GPU-accelerated Marimo notebooks |
| Zotero | http://zotero.home.fhirworx.io | Reference manager with VNC |
| Nessie | http://nessie.home.fhirworx.io | Git-like data catalog for Iceberg |
| Trino | http://trino.home.fhirworx.io | Distributed SQL query engine |
| Grafana | http://grafana.home.fhirworx.io | Observability dashboards |
| Prometheus | http://prometheus.home.fhirworx.io | Metrics collection |
| Jaeger | http://jaeger.home.fhirworx.io | Distributed tracing |
| Loki | http://loki.home.fhirworx.io | Log aggregation |
| PostgreSQL | localhost:5432 | Database (Bitnami hardened) | | PostgreSQL | localhost:5432 | Database (Bitnami hardened) |
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ intrastack network │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Traefik :80/:443 │ │
│ │ Reverse Proxy + Load Balancer + OTEL Tracing │ │
│ └───────────────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┼─────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Gitea │ │Woodpecker│ │ Notebooks│ │ Zotero │ │ Dashboard│ │
│ │ :3000 │ │ :8000 │ │ :2718 │ │ :8080 │ │ :80 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL :5432 │ │
│ │ (gitea, woodpecker, nessie) │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Data Lakehouse │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Trino │────▶│ Nessie │────▶│ RustFS │ │ │
│ │ │ :8080 │ │ :19120 │ │ S3 :9000 | Console :9001 │ │ │
│ │ └──────────┘ └──────────┘ │ Buckets: gitea, lakehouse │ │ │
│ │ Iceberg REST Catalog └──────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Observability Stack │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Grafana │────▶│Prometheus│ │ Jaeger │◀────│ Loki │ │ │
│ │ │ :3000 │ │ :9090 │ │ :16686 │ │ :3100 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ ▲ ▲ │ │
│ │ └──────── Dashboards ───────────────┴───────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Prerequisites ## Prerequisites
- Docker with rootless mode - Docker with rootless mode
- NVIDIA GPU with container toolkit - NVIDIA GPU with container toolkit
- Fix for rootless NVIDIA containers: - iptables for port 80 redirect (rootless Docker can't bind privileged ports)
```bash
sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-runtime/config.toml Fix for rootless NVIDIA containers:
``` ```bash
sudo sed -i 's/#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-runtime/config.toml
```
## DNS Setup ## DNS Setup
Add to `/etc/hosts` (replace with your server's LAN IP): Add to `/etc/hosts` (replace IP with your server's LAN IP):
```bash ```bash
cat << 'EOF' | sudo tee -a /etc/hosts # Homelab Stack
192.168.1.192 home.fhirworx.io
# homelab stack 192.168.1.192 dashboard.home.fhirworx.io
192.168.1.192 traefik.home.fhirworx.io
192.168.1.192 gitea.home.fhirworx.io 192.168.1.192 gitea.home.fhirworx.io
192.168.1.192 ci.home.fhirworx.io 192.168.1.192 ci.home.fhirworx.io
192.168.1.192 s3.home.fhirworx.io
192.168.1.192 notebooks.home.fhirworx.io 192.168.1.192 notebooks.home.fhirworx.io
192.168.1.192 zotero.home.fhirworx.io 192.168.1.192 zotero.home.fhirworx.io
EOF 192.168.1.192 nessie.home.fhirworx.io
192.168.1.192 trino.home.fhirworx.io
192.168.1.192 minio.home.fhirworx.io
192.168.1.192 s3.home.fhirworx.io
192.168.1.192 grafana.home.fhirworx.io
192.168.1.192 prometheus.home.fhirworx.io
192.168.1.192 jaeger.home.fhirworx.io
192.168.1.192 loki.home.fhirworx.io
``` ```
Or configure these records on your router/DNS server. ## Port 80 Redirect
Rootless Docker cannot bind to privileged ports. Use iptables to redirect port 80 to Traefik's port 8880:
```bash
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8880
sudo iptables -t nat -A OUTPUT -p tcp --dport 80 -o lo -j REDIRECT --to-port 8880
# Persist across reboots
sudo sh -c 'iptables-save > /etc/iptables/rules.v4'
```
## Setup ## Setup
@@ -59,6 +137,11 @@ WOODPECKER_DB_PASSWORD=<woodpecker_db_password>
WOODPECKER_AGENT_SECRET=<generated_secret> WOODPECKER_AGENT_SECRET=<generated_secret>
WOODPECKER_GITEA_CLIENT=<oauth_client_id> WOODPECKER_GITEA_CLIENT=<oauth_client_id>
WOODPECKER_GITEA_SECRET=<oauth_client_secret> WOODPECKER_GITEA_SECRET=<oauth_client_secret>
# Nessie Data Lake
NESSIE_DB_PASSWORD=<nessie_db_password>
NESSIE_S3_ACCESS_KEY=<nessie_s3_user>
NESSIE_S3_SECRET_KEY=<nessie_s3_password>
``` ```
Generate agent secret: Generate agent secret:
@@ -69,15 +152,19 @@ openssl rand -hex 32
### 2. Start Core Services ### 2. Start Core Services
```bash ```bash
docker compose up -d postgres rustfs docker compose up -d postgres rustfs traefik
``` ```
### 3. Configure RustFS ### 3. Configure RustFS
1. Access console at http://s3.home.fhirworx.io:9001 1. Access console at http://minio.home.fhirworx.io
2. Create user `git` with S3 credentials matching `GITEA_S3_ACCESS_KEY` and `GITEA_S3_SECRET_KEY` 2. Create user `git` with S3 credentials matching `GITEA_S3_ACCESS_KEY` and `GITEA_S3_SECRET_KEY`
3. Create buckets: `gitea`, `gitea-lfs`, `gitea-packages` 3. Create buckets: `gitea`, `gitea-lfs`, `gitea-packages`
4. Apply IAM policy to `git` user: 4. Create user `nessie` with S3 credentials matching `NESSIE_S3_ACCESS_KEY` and `NESSIE_S3_SECRET_KEY`
5. Create bucket: `lakehouse`
6. Apply IAM policies to users (see below)
**Gitea IAM Policy:**
```json ```json
{ {
@@ -115,6 +202,36 @@ docker compose up -d postgres rustfs
} }
``` ```
**Nessie IAM Policy:**
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": ["arn:aws:s3:::lakehouse"]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
"Resource": ["arn:aws:s3:::lakehouse/*"]
}
]
}
```
### 4. Configure PostgreSQL ### 4. Configure PostgreSQL
Create databases and users: Create databases and users:
@@ -127,68 +244,242 @@ docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREA
# Woodpecker database # Woodpecker database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE woodpecker;" docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE woodpecker;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER woodpecker WITH PASSWORD '<woodpecker_db_password>'; ALTER DATABASE woodpecker OWNER TO woodpecker;" docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER woodpecker WITH PASSWORD '<woodpecker_db_password>'; ALTER DATABASE woodpecker OWNER TO woodpecker;"
# Nessie database
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE USER nessie WITH PASSWORD '<nessie_db_password>';"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "CREATE DATABASE nessie OWNER nessie;"
docker exec -e PGPASSWORD=<postgres_password> postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE nessie TO nessie;"
``` ```
### 5. Start Gitea ### 5. Start All Services
```bash ```bash
docker compose up -d gitea docker compose up -d
``` ```
### 6. Configure Gitea ### 6. Configure Gitea
1. Complete initial setup at http://gitea.home.fhirworx.io:3000 1. Complete initial setup at http://gitea.home.fhirworx.io
2. Create organization `homelab` 2. Create organization `homelab`
3. Create repository `stack` 3. Create repository `stack`
4. Generate API token for container registry access 4. Generate API token for container registry access
5. Create OAuth2 application for Woodpecker: 5. Create OAuth2 application for Woodpecker:
- Name: `Woodpecker CI` - Name: `Woodpecker CI`
- Redirect URI: `http://ci.home.fhirworx.io:8000/authorize` - Redirect URI: `http://ci.home.fhirworx.io/authorize`
- Copy Client ID and Secret to `.env` - Copy Client ID and Secret to `.env`
### 7. Start Woodpecker CI ### 7. Configure Woodpecker Secrets
```bash Add secrets in Woodpecker UI (http://ci.home.fhirworx.io):
docker compose up -d woodpecker-server woodpecker-agent
```
Add secrets in Woodpecker UI (http://ci.home.fhirworx.io:8000):
- `registry_user` - Gitea username - `registry_user` - Gitea username
- `registry_pass` - Gitea token - `registry_pass` - Gitea token
### 8. SSH and GPG Keys ### 8. Verify Services
Generate SSH key:
```bash ```bash
ssh-keygen -t ed25519 -C "user@homelab" -f ~/.ssh/gitea_ed25519 -N "" # Check Traefik routes
curl -s http://localhost:8081/api/http/routers | jq -r '.[].name'
# Check Nessie
curl http://nessie.home.fhirworx.io/api/v2/config
# Check Trino
docker exec -it trino trino --execute "SHOW CATALOGS"
# Check Prometheus targets
curl -s http://prometheus.home.fhirworx.io/api/v1/targets | jq '.data.activeTargets[].labels.job'
``` ```
Generate GPG key: ## Observability
```bash
gpg --batch --gen-key <<EOF The stack includes a complete observability solution:
Key-Type: eddsa
Key-Curve: ed25519 ### Components
Key-Usage: sign
Subkey-Type: ecdh | Component | Purpose | Metrics |
Subkey-Curve: cv25519 |-----------|---------|---------|
Subkey-Usage: encrypt | **Prometheus** | Metrics collection and storage | Scrapes all services |
Name-Real: <your_name> | **Grafana** | Visualization and dashboards | Homelab Overview, Traefik |
Name-Email: <your_email> | **Jaeger** | Distributed tracing | OTLP collector |
Expire-Date: 2y | **Loki** | Log aggregation | Container logs via Promtail |
%no-protection | **Promtail** | Log shipper | Scrapes Docker container logs |
%commit
EOF ### Tracing
Services instrumented with OpenTelemetry:
| Service | Tracing |
|---------|---------|
| Traefik | Native OTLP export |
| Nessie | Quarkus OTEL |
| Trino | Native OTEL |
All traces flow to Jaeger via OTLP gRPC (port 4317).
### Dashboards
Pre-provisioned Grafana dashboards:
- **Homelab Overview** - Service health, logs, traces
- **Traefik** - Request rates, latencies, errors by router/service
### Prometheus Targets
| Job | Endpoint |
|-----|----------|
| prometheus | localhost:9090 |
| traefik | traefik:8080/metrics |
| jaeger | jaeger:14269 |
| loki | loki:3100 |
| nessie | nessie:19120/q/metrics |
| trino | trino:8080/v1/status |
## Data Lakehouse
The stack includes a complete data lakehouse built on Apache Iceberg:
### Components
| Component | Purpose |
|-----------|---------|
| **Nessie** | Git-like version control for data (branches, merges, time travel) |
| **Apache Iceberg** | Open table format with ACID transactions, schema evolution |
| **Trino** | Distributed SQL query engine with JDBC/ODBC support |
| **RustFS** | S3 storage backend (lakehouse bucket) |
| **PyIceberg** | Python client for Iceberg tables in notebooks |
### Query Access
| Method | Use Case | Connection |
|--------|----------|------------|
| **Trino CLI** | Ad-hoc SQL queries | `docker exec -it trino trino` |
| **Trino JDBC** | External tools (DBeaver, etc.) | `jdbc:trino://trino.home.fhirworx.io/iceberg` |
| **Trino Web UI** | Query monitoring | http://trino.home.fhirworx.io |
| **PyIceberg** | Python notebooks | Direct to Nessie REST API |
| **DuckDB** | Fast local queries | Read Iceberg via PyArrow |
### Example: Create a Table
```sql
-- Connect to Trino
docker exec -it trino trino
-- Create a schema
CREATE SCHEMA iceberg.analytics;
-- Create a partitioned table
CREATE TABLE iceberg.analytics.events (
event_id VARCHAR,
event_time TIMESTAMP(6) WITH TIME ZONE,
user_id VARCHAR,
event_type VARCHAR,
properties MAP(VARCHAR, VARCHAR)
)
WITH (
format = 'PARQUET',
partitioning = ARRAY['day(event_time)']
);
-- Insert data
INSERT INTO iceberg.analytics.events VALUES
('evt-001', CURRENT_TIMESTAMP, 'user-1', 'page_view', MAP(ARRAY['page'], ARRAY['/home']));
-- Query data
SELECT * FROM iceberg.analytics.events;
``` ```
Configure Git: ## Traefik Reverse Proxy
```bash
git config --global user.name "<your_name>" All HTTP services are routed through Traefik using file-based configuration (not Docker labels).
git config --global user.email "<your_email>"
git config --global user.signingkey <GPG_KEY_ID> ### Configuration
git config --global commit.gpgsign true
Routes are defined in `traefik/dynamic/services.yml`. To add a new service:
```yaml
http:
routers:
myservice:
rule: "Host(`myservice.home.fhirworx.io`)"
service: myservice
entryPoints:
- web
services:
myservice:
loadBalancer:
servers:
- url: "http://myservice:8080"
``` ```
Add to `~/.ssh/config`: ### Features
- **OTEL Tracing** - All requests traced to Jaeger
- **Prometheus Metrics** - Request rates, latencies by router/service
- **File Provider** - Routes defined in YAML, hot-reloaded
- **Dashboard** - http://traefik.home.fhirworx.io
### TCP/UDP Routing
Traefik can also route TCP and UDP traffic. See `traefik/dynamic/` for examples.
## Directory Structure
```
stack/
├── compose.yml # Docker Compose configuration
├── .env # Environment variables (not in git)
├── .woodpecker.yml # CI/CD pipeline
├── README.md # This file
├── data/ # Shared data directory
├── gitea/
│ └── custom/
│ └── public/assets/css/
│ └── theme-nes-sports.css
├── grafana/
│ ├── provisioning/
│ │ ├── dashboards/
│ │ │ └── dashboards.yml
│ │ └── datasources/
│ │ └── datasources.yml
│ └── dashboards/
│ ├── homelab-overview.json
│ └── traefik.json
├── loki/
│ ├── loki-config.yml
│ └── promtail-config.yml
├── nginx/
│ ├── nginx.conf # Dashboard nginx config
│ └── index.html # Dashboard HTML
├── notebooks/
│ ├── Dockerfile
│ ├── pyproject.toml
│ ├── .marimo.toml
│ └── retro-arcade.css
├── prometheus/
│ └── prometheus.yml
├── traefik/
│ ├── traefik.yml # Static configuration
│ └── dynamic/
│ ├── middlewares.yml # Rate limiting, security headers
│ └── services.yml # All HTTP routes
├── trino/
│ └── etc/
│ ├── config.properties
│ ├── jvm.config
│ ├── node.properties
│ └── catalog/
│ └── iceberg.properties
├── woodpecker/
│ └── custom.css
└── zotero/
├── Dockerfile
└── data/
```
## SSH Access
Configure Git SSH access in `~/.ssh/config`:
``` ```
Host gitea Host gitea
HostName gitea.home.fhirworx.io HostName gitea.home.fhirworx.io
@@ -198,79 +489,36 @@ Host gitea
IdentitiesOnly yes IdentitiesOnly yes
``` ```
Add host key: Generate and add SSH key:
```bash ```bash
ssh-keygen -t ed25519 -C "user@homelab" -f ~/.ssh/gitea_ed25519 -N ""
ssh-keyscan -p 2222 gitea.home.fhirworx.io >> ~/.ssh/known_hosts ssh-keyscan -p 2222 gitea.home.fhirworx.io >> ~/.ssh/known_hosts
``` ```
Add SSH and GPG public keys to Gitea: http://gitea.home.fhirworx.io:3000/user/settings/keys Add public key to Gitea: http://gitea.home.fhirworx.io/user/settings/keys
### 9. Deploy Key (for CI/CD) ## Container Registry
```bash
ssh-keygen -t ed25519 -C "deploy@homelab/stack" -f .deploy_key -N ""
```
Add `.deploy_key.pub` to repository deploy keys: http://gitea.home.fhirworx.io:3000/homelab/stack/settings/keys
### 10. Push to Gitea
```bash
git remote add gitea gitea:homelab/stack.git
git push -u gitea main
```
### 11. Container Registry
Login: Login:
```bash ```bash
echo "<GITEA_TOKEN>" | docker login localhost:3000 -u <username> --password-stdin echo "<GITEA_TOKEN>" | docker login gitea.home.fhirworx.io -u <username> --password-stdin
``` ```
Tag and push images: Tag and push:
```bash ```bash
docker tag <image>:latest localhost:3000/homelab/<image>:latest docker tag <image>:latest gitea.home.fhirworx.io/homelab/<image>:latest
docker push localhost:3000/homelab/<image>:latest docker push gitea.home.fhirworx.io/homelab/<image>:latest
``` ```
### 12. Start All Services ## Ports Reference
```bash | Port | Service | Notes |
docker compose up -d |------|---------|-------|
``` | 80 | Traefik HTTP | iptables redirect to 8880 |
| 443 | Traefik HTTPS | iptables redirect to 8443 |
## Container Registry Images | 2222 | Gitea SSH | Direct access |
| 4317 | Jaeger OTLP gRPC | Direct access for external traces |
All images are hosted in the Gitea container registry: | 4318 | Jaeger OTLP HTTP | Direct access for external traces |
| 5432 | PostgreSQL | Direct access |
- `localhost:3000/homelab/postgresql:latest` | 9000 | RustFS S3 API | Also via s3.home.fhirworx.io |
- `localhost:3000/homelab/rustfs:latest` | 9001 | RustFS Console | Also via minio.home.fhirworx.io |
- `localhost:3000/homelab/gitea:latest-rootless`
- `localhost:3000/homelab/woodpecker-server:latest`
- `localhost:3000/homelab/woodpecker-agent:latest`
- `localhost:3000/homelab/notebooks:latest`
- `localhost:3000/homelab/zotero:latest`
## CI/CD Pipeline
The `.woodpecker.yml` pipeline:
1. **validate** - Validates docker compose config
2. **build-notebooks** - Builds notebooks image (on changes to `notebooks/`)
3. **build-zotero** - Builds zotero image (on changes to `zotero/`)
4. **push-images** - Pushes to container registry (on main branch)
5. **deploy** - Pulls and restarts services (on main branch)
## Endpoints
| Service | URL |
|---------|-----|
| Gitea Web | http://gitea.home.fhirworx.io:3000 |
| Gitea SSH | ssh://git@gitea.home.fhirworx.io:2222 |
| Container Registry | http://gitea.home.fhirworx.io:3000/v2/ |
| Woodpecker CI | http://ci.home.fhirworx.io:8000 |
| RustFS S3 API | http://s3.home.fhirworx.io:9000 |
| RustFS Console | http://s3.home.fhirworx.io:9001 |
| PostgreSQL | localhost:5432 |
| Notebooks | http://notebooks.home.fhirworx.io:2718 |
| Zotero | http://zotero.home.fhirworx.io:8080 |

View File

@@ -4,6 +4,24 @@ networks:
internal: false internal: false
services: services:
traefik:
image: traefik:v3.3
container_name: traefik
networks:
- intrastack
ports:
- "8880:80"
- "8443:443"
- "8081:8080"
volumes:
- ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
- ./traefik/dynamic:/etc/traefik/dynamic:ro
environment:
- OTEL_SERVICE_NAME=traefik
security_opt:
- no-new-privileges:true
restart: unless-stopped
rustfs: rustfs:
image: localhost:3000/homelab/rustfs:latest image: localhost:3000/homelab/rustfs:latest
container_name: rustfs container_name: rustfs
@@ -40,7 +58,7 @@ services:
restart: unless-stopped restart: unless-stopped
gitea: gitea:
image: localhost:3000/homelab/gitea:latest-rootless image: gitea/gitea:1.25.4-rootless
container_name: gitea container_name: gitea
networks: networks:
- intrastack - intrastack
@@ -70,53 +88,53 @@ services:
- GITEA__packages__MINIO_BUCKET=gitea-packages - GITEA__packages__MINIO_BUCKET=gitea-packages
- GITEA__packages__MINIO_USE_SSL=false - GITEA__packages__MINIO_USE_SSL=false
- GITEA__server__DOMAIN=gitea.home.fhirworx.io - GITEA__server__DOMAIN=gitea.home.fhirworx.io
- GITEA__server__ROOT_URL=http://gitea.home.fhirworx.io:3000/ - GITEA__server__ROOT_URL=http://gitea.home.fhirworx.io/
- GITEA__server__SSH_DOMAIN=gitea.home.fhirworx.io - GITEA__server__SSH_DOMAIN=gitea.home.fhirworx.io
- GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.home.fhirworx.io,192.168.1.192,172.19.0.0/16 - GITEA__webhook__ALLOWED_HOST_LIST=woodpecker-server,ci.home.fhirworx.io,192.168.1.192,172.19.0.0/16
- GITEA__ui__THEMES=gitea-auto,gitea-light,gitea-dark,nes-sports
- GITEA__ui__DEFAULT_THEME=nes-sports
volumes: volumes:
- gitea_data:/var/lib/gitea - gitea_data:/var/lib/gitea
- gitea_config:/etc/gitea - gitea_config:/etc/gitea
- ./gitea/custom:/var/lib/gitea/custom:ro
ports: ports:
- "3000:3000"
- "2222:2222" - "2222:2222"
depends_on: depends_on:
- postgres - postgres
- rustfs - rustfs
security_opt:
- no-new-privileges:true
restart: unless-stopped restart: unless-stopped
woodpecker-server: woodpecker-server:
image: woodpeckerci/woodpecker-server:v3.3.0 image: woodpeckerci/woodpecker-server:v3.13.0
container_name: woodpecker-server container_name: woodpecker-server
networks: networks:
- intrastack - intrastack
extra_hosts: extra_hosts:
- "gitea.home.fhirworx.io:192.168.1.192" - "gitea.home.fhirworx.io:192.168.1.192"
environment: environment:
- WOODPECKER_HOST=http://ci.home.fhirworx.io:8000 - WOODPECKER_HOST=http://ci.home.fhirworx.io
- WOODPECKER_GRPC_ADDR=:9000 - WOODPECKER_GRPC_ADDR=:9000
- WOODPECKER_LOG_LEVEL=debug - WOODPECKER_LOG_LEVEL=debug
- WOODPECKER_ADMIN=kert - WOODPECKER_ADMIN=kert
- WOODPECKER_OPEN=true - WOODPECKER_OPEN=true
- WOODPECKER_GITEA=true - WOODPECKER_GITEA=true
- WOODPECKER_GITEA_URL=http://gitea.home.fhirworx.io:3000 - WOODPECKER_GITEA_URL=http://gitea.home.fhirworx.io
- WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT} - WOODPECKER_GITEA_CLIENT=${WOODPECKER_GITEA_CLIENT}
- WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET} - WOODPECKER_GITEA_SECRET=${WOODPECKER_GITEA_SECRET}
- WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET} - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
- WOODPECKER_DATABASE_DRIVER=postgres - WOODPECKER_DATABASE_DRIVER=postgres
- WOODPECKER_DATABASE_DATASOURCE=postgres://woodpecker:${WOODPECKER_DB_PASSWORD}@postgres:5432/woodpecker?sslmode=disable - WOODPECKER_DATABASE_DATASOURCE=postgres://woodpecker:${WOODPECKER_DB_PASSWORD}@postgres:5432/woodpecker?sslmode=disable
- WOODPECKER_CUSTOM_CSS_FILE=/etc/woodpecker/custom.css
volumes: volumes:
- woodpecker_data:/var/lib/woodpecker - woodpecker_data:/var/lib/woodpecker
ports: - ./woodpecker/custom.css:/etc/woodpecker/custom.css:ro
- "8000:8000"
depends_on: depends_on:
- gitea - gitea
- postgres - postgres
restart: unless-stopped restart: unless-stopped
woodpecker-agent: woodpecker-agent:
image: woodpeckerci/woodpecker-agent:v3.3.0 image: woodpeckerci/woodpecker-agent:v3.13.0
container_name: woodpecker-agent container_name: woodpecker-agent
networks: networks:
- intrastack - intrastack
@@ -140,8 +158,6 @@ services:
container_name: notebooks container_name: notebooks
networks: networks:
- intrastack - intrastack
ports:
- "2718:2718"
volumes: volumes:
- ./notebooks:/home/kert/notebooks - ./notebooks:/home/kert/notebooks
- ./data:/home/kert/data - ./data:/home/kert/data
@@ -164,7 +180,6 @@ services:
stdin_open: true stdin_open: true
tty: true tty: true
ports: ports:
- "8080:8080"
- "3478:3478" - "3478:3478"
- "3478:3478/udp" - "3478:3478/udp"
volumes: volumes:
@@ -191,6 +206,172 @@ services:
capabilities: [gpu] capabilities: [gpu]
restart: unless-stopped restart: unless-stopped
nessie:
image: ghcr.io/projectnessie/nessie:latest
container_name: nessie
networks:
- intrastack
environment:
- NESSIE_VERSION_STORE_TYPE=JDBC
- QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgres:5432/nessie
- QUARKUS_DATASOURCE_USERNAME=nessie
- QUARKUS_DATASOURCE_PASSWORD=${NESSIE_DB_PASSWORD}
# Iceberg REST Catalog settings
- NESSIE_CATALOG_DEFAULT_WAREHOUSE=s3://lakehouse/
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ENDPOINT=http://rustfs:9000
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_ACCESS_KEY_ID=${NESSIE_S3_ACCESS_KEY}
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_SECRET_ACCESS_KEY=${NESSIE_S3_SECRET_KEY}
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_PATH_STYLE_ACCESS=true
- NESSIE_CATALOG_SERVICE_S3_DEFAULT_OPTIONS_REGION=us-east-1
# OpenTelemetry tracing
- QUARKUS_OTEL_ENABLED=true
- QUARKUS_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://jaeger:4317
- QUARKUS_OTEL_SERVICE_NAME=nessie
depends_on:
- postgres
- rustfs
security_opt:
- no-new-privileges:true
restart: unless-stopped
trino:
image: trinodb/trino:latest
container_name: trino
networks:
- intrastack
environment:
- NESSIE_S3_ACCESS_KEY=${NESSIE_S3_ACCESS_KEY}
- NESSIE_S3_SECRET_KEY=${NESSIE_S3_SECRET_KEY}
volumes:
- ./trino/etc:/etc/trino:ro
depends_on:
- nessie
- rustfs
security_opt:
- no-new-privileges:true
restart: unless-stopped
dashboard:
image: nginx:alpine
container_name: dashboard
networks:
- intrastack
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/index.html:/usr/share/nginx/html/index.html:ro
security_opt:
- no-new-privileges:true
restart: unless-stopped
# Observability Stack
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
networks:
- intrastack
environment:
- COLLECTOR_OTLP_ENABLED=true
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
ports:
- "14268:14268" # Jaeger collector HTTP
- "14250:14250" # Jaeger collector gRPC
- "6831:6831/udp" # Jaeger agent UDP
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:16686 || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 15s
security_opt:
- no-new-privileges:true
restart: unless-stopped
loki:
image: grafana/loki:latest
container_name: loki
networks:
- intrastack
volumes:
- ./loki/loki-config.yml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
security_opt:
- no-new-privileges:true
restart: unless-stopped
promtail:
image: grafana/promtail:latest
container_name: promtail
networks:
- intrastack
volumes:
- ./loki/promtail-config.yml:/etc/promtail/config.yml:ro
- /home/kert/.local/share/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
depends_on:
- loki
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
container_name: prometheus
networks:
- intrastack
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9090/-/healthy || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 15s
security_opt:
- no-new-privileges:true
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
networks:
- intrastack
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
- GF_SERVER_ROOT_URL=http://grafana.home.fhirworx.io
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana_data:/var/lib/grafana
depends_on:
- loki
- jaeger
- prometheus
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
security_opt:
- no-new-privileges:true
restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:
rustfs_data: rustfs_data:
@@ -199,3 +380,6 @@ volumes:
gitea_config: gitea_config:
woodpecker_data: woodpecker_data:
woodpecker_agent_config: woodpecker_agent_config:
loki_data:
prometheus_data:
grafana_data:

View File

@@ -0,0 +1,349 @@
/* NES SPORTS THEME FOR GITEA - RBI Baseball / Tecmo Super Bowl */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
/* Theme metadata */
gitea-theme-meta-info {
--theme-display-name: "NES Sports";
}
:root {
--is-dark-theme: true;
/* NES Color Palette */
--nes-black: #0f0f0f;
--nes-dark-gray: #1a1a2e;
--nes-gray: #4a4a68;
--nes-light-gray: #9090a8;
--nes-white: #f0f0f0;
--nes-dark-blue: #1a1a40;
--nes-blue: #2840a0;
--nes-mid-blue: #4060c0;
--nes-light-blue: #70a0e0;
--nes-sky: #90c0f0;
--nes-red: #d03030;
--nes-orange: #e07020;
--nes-yellow: #e0b040;
--nes-green: #40a040;
--nes-light-green: #80d080;
--nes-cyan: #40b0c0;
--nes-cream: #e8d8c0;
/* Gitea color mappings */
--color-primary: var(--nes-mid-blue);
--color-primary-light: var(--nes-light-blue);
--color-primary-dark: var(--nes-blue);
--color-primary-alpha-10: rgba(64, 96, 192, 0.1);
--color-primary-alpha-20: rgba(64, 96, 192, 0.2);
--color-primary-alpha-40: rgba(64, 96, 192, 0.4);
--color-secondary: var(--nes-gray);
--color-secondary-light: var(--nes-light-gray);
--color-secondary-dark: var(--nes-dark-gray);
/* Background colors */
--color-body: var(--nes-dark-blue);
--color-box-body: var(--nes-black);
--color-box-header: var(--nes-dark-gray);
/* Text colors */
--color-text: var(--nes-white);
--color-text-light: var(--nes-light-gray);
--color-text-dark: var(--nes-cream);
/* Input colors */
--color-input-text: var(--nes-white);
--color-input-background: var(--nes-dark-gray);
--color-input-border: var(--nes-gray);
/* Status colors */
--color-success: var(--nes-green);
--color-success-text: var(--nes-white);
--color-warning: var(--nes-yellow);
--color-warning-text: var(--nes-black);
--color-error: var(--nes-red);
--color-error-text: var(--nes-white);
--color-info: var(--nes-light-blue);
--color-info-text: var(--nes-black);
/* Border */
--color-border: var(--nes-gray);
/* Link colors */
--color-link: var(--nes-sky);
--color-link-hover: var(--nes-white);
/* Code */
--color-code-bg: var(--nes-dark-gray);
--color-code-border: var(--nes-gray);
/* Shadows */
--color-shadow: rgba(0, 0, 0, 0.4);
}
/* Global styles */
body {
font-family: 'IBM Plex Mono', monospace !important;
background: linear-gradient(180deg, #0a0a28 0%, #1a1a40 30%, #1a1a40 70%, #0a0a28 100%) !important;
background-attachment: fixed !important;
}
/* Headings */
h1, h2, h3, h4, h5, h6,
.header-wrapper .header {
font-family: 'Press Start 2P', monospace !important;
letter-spacing: 0.5px !important;
}
h1 { font-size: 18px !important; color: var(--nes-light-green) !important; }
h2 { font-size: 14px !important; color: var(--nes-sky) !important; }
h3 { font-size: 12px !important; color: var(--nes-yellow) !important; }
/* Navbar */
.ui.secondary.menu.navbar,
.navbar {
background: var(--nes-black) !important;
border-bottom: 3px solid var(--nes-gray) !important;
}
.navbar .item {
font-family: 'IBM Plex Mono', monospace !important;
color: var(--nes-white) !important;
}
.navbar .item:hover {
background: var(--nes-dark-gray) !important;
color: var(--nes-sky) !important;
}
.navbar .item.active {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
}
/* Buttons */
.ui.button,
.button {
font-family: 'IBM Plex Mono', monospace !important;
font-weight: 500 !important;
border-radius: 4px !important;
transition: all 0.15s ease !important;
}
.ui.primary.button,
.ui.blue.button {
background: var(--nes-mid-blue) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-light-blue) !important;
}
.ui.primary.button:hover,
.ui.blue.button:hover {
background: var(--nes-light-blue) !important;
color: var(--nes-black) !important;
}
.ui.green.button {
background: var(--nes-green) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-light-green) !important;
}
.ui.green.button:hover {
background: var(--nes-light-green) !important;
color: var(--nes-black) !important;
}
.ui.red.button {
background: var(--nes-red) !important;
color: var(--nes-white) !important;
}
/* Segments and boxes */
.ui.segment,
.ui.segments,
.repository,
.box {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
.ui.attached.header,
.box-header {
background: var(--nes-dark-gray) !important;
border-bottom: 2px solid var(--nes-gray) !important;
font-family: 'Press Start 2P', monospace !important;
font-size: 11px !important;
color: var(--nes-cream) !important;
}
/* Tables */
.ui.table {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
}
.ui.table th {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
font-family: 'Press Start 2P', monospace !important;
font-size: 10px !important;
text-transform: uppercase !important;
border: 1px solid var(--nes-mid-blue) !important;
}
.ui.table td {
background: var(--nes-black) !important;
color: var(--nes-cream) !important;
border: 1px solid var(--nes-dark-gray) !important;
}
.ui.table tr:hover td {
background: var(--nes-dark-blue) !important;
}
/* Forms */
.ui.form input,
.ui.form textarea,
.ui.form select,
.ui.input input {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--nes-dark-gray) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
.ui.form input:focus,
.ui.form textarea:focus,
.ui.input input:focus {
border-color: var(--nes-light-blue) !important;
background: var(--nes-dark-gray) !important;
}
/* Labels */
.ui.label {
font-family: 'IBM Plex Mono', monospace !important;
border-radius: 4px !important;
}
/* Dropdowns */
.ui.dropdown .menu {
background: var(--nes-dark-gray) !important;
border: 2px solid var(--nes-gray) !important;
}
.ui.dropdown .menu .item {
color: var(--nes-white) !important;
}
.ui.dropdown .menu .item:hover {
background: var(--nes-blue) !important;
}
/* Code and diffs */
.code-view,
.file-view,
.CodeMirror,
.highlight {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--nes-black) !important;
}
.diff-file-box .diff-file-header {
background: var(--nes-dark-gray) !important;
}
/* Commit graph */
.repository .commit-list .commit {
border-bottom: 1px solid var(--nes-dark-gray) !important;
}
/* Issues and PRs */
.issue-list .item,
.pull-list .item {
border-bottom: 1px solid var(--nes-dark-gray) !important;
}
/* Footer */
.footer {
background: var(--nes-black) !important;
border-top: 3px solid var(--nes-gray) !important;
}
/* Cards */
.ui.card,
.ui.cards .card {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Messages */
.ui.message {
font-family: 'IBM Plex Mono', monospace !important;
border-radius: 4px !important;
}
.ui.positive.message,
.ui.success.message {
background: rgba(64, 160, 64, 0.2) !important;
border: 2px solid var(--nes-green) !important;
color: var(--nes-light-green) !important;
}
.ui.negative.message,
.ui.error.message {
background: rgba(208, 48, 48, 0.2) !important;
border: 2px solid var(--nes-red) !important;
color: var(--nes-red) !important;
}
.ui.warning.message {
background: rgba(224, 176, 64, 0.2) !important;
border: 2px solid var(--nes-yellow) !important;
color: var(--nes-yellow) !important;
}
.ui.info.message {
background: rgba(112, 160, 224, 0.2) !important;
border: 2px solid var(--nes-light-blue) !important;
color: var(--nes-light-blue) !important;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--nes-dark-gray);
}
::-webkit-scrollbar-thumb {
background: var(--nes-gray);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--nes-light-gray);
}
/* Selection */
::selection {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
}
/* Links */
a {
color: var(--nes-sky) !important;
}
a:hover {
color: var(--nes-white) !important;
}

22
grafana/custom.ini Normal file
View File

@@ -0,0 +1,22 @@
[server]
root_url = http://localhost:3001
[security]
admin_user = admin
admin_password = admin
[users]
allow_sign_up = false
[auth.anonymous]
enabled = true
org_role = Viewer
[dashboards]
default_home_dashboard_path = /var/lib/grafana/dashboards/homelab-overview.json
[unified_alerting]
enabled = true
[feature_toggles]
enable = traceqlEditor tempoServiceGraph traceQLStreaming

View File

@@ -0,0 +1,356 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": false,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "builder",
"expr": "{job=\"containerlogs\"} |= ``",
"queryType": "range",
"refId": "A"
}
],
"title": "All Container Logs",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 6,
"w": 12,
"x": 0,
"y": 4
},
"id": 2,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{container_name=~\"gitea|woodpecker.*\"} |= ``",
"queryType": "range",
"refId": "A"
}
],
"title": "Git & CI Logs",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 6,
"w": 12,
"x": 12,
"y": 4
},
"id": 3,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{container_name=~\"nessie|trino\"} |= ``",
"queryType": "range",
"refId": "A"
}
],
"title": "Data Lake Logs",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 6,
"w": 12,
"x": 0,
"y": 10
},
"id": 4,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{container_name=~\"postgres|rustfs\"} |= ``",
"queryType": "range",
"refId": "A"
}
],
"title": "Storage Logs",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"gridPos": {
"h": 6,
"w": 12,
"x": 12,
"y": 10
},
"id": 5,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": true,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "{job=\"containerlogs\"} |~ \"error|Error|ERROR|exception|Exception|EXCEPTION|failed|Failed|FAILED\"",
"queryType": "range",
"refId": "A"
}
],
"title": "Errors Across All Services",
"type": "logs"
},
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 50,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 16
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "loki",
"uid": "loki"
},
"editorMode": "code",
"expr": "sum by (container_name) (count_over_time({job=\"containerlogs\"} [1m]))",
"queryType": "range",
"refId": "A"
}
],
"title": "Log Volume by Container",
"type": "timeseries"
},
{
"datasource": {
"type": "jaeger",
"uid": "jaeger"
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 22
},
"id": 7,
"options": {},
"targets": [
{
"datasource": {
"type": "jaeger",
"uid": "jaeger"
},
"queryType": "search",
"refId": "A",
"service": "$service",
"limit": 20
}
],
"title": "Recent Traces",
"type": "traces"
}
],
"refresh": "5s",
"schemaVersion": 39,
"tags": ["homelab", "observability"],
"templating": {
"list": [
{
"current": {
"selected": true,
"text": "nessie",
"value": "nessie"
},
"datasource": {
"type": "jaeger",
"uid": "jaeger"
},
"definition": "services",
"hide": 0,
"includeAll": false,
"label": "Service",
"multi": false,
"name": "service",
"options": [],
"query": "services",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Homelab Overview",
"uid": "homelab-overview",
"version": 1,
"weekStart": ""
}

View File

@@ -0,0 +1,652 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 },
"id": 100,
"title": "Overview",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 },
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_requests_total[5m]))",
"refId": "A"
}
],
"title": "Requests / sec",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(traefik_open_connections)",
"refId": "A"
}
],
"title": "Open Connections",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 0.01 },
{ "color": "red", "value": 0.05 }
]
},
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_requests_total{code=~\"5..\"}[5m])) / sum(rate(traefik_entrypoint_requests_total[5m])) or vector(0)",
"refId": "A"
}
],
"title": "Error Rate (5xx)",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "histogram_quantile(0.95, sum(rate(traefik_entrypoint_request_duration_seconds_bucket[5m])) by (le))",
"refId": "A"
}
],
"title": "P95 Latency",
"type": "stat"
},
{
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 101,
"title": "Requests",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
"id": 5,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_requests_total[5m])) by (entrypoint)",
"legendFormat": "{{entrypoint}}",
"refId": "A"
}
],
"title": "Requests by Entrypoint",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "normal" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "reqps"
},
"overrides": [
{ "matcher": { "id": "byRegexp", "options": "2.." }, "properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }] },
{ "matcher": { "id": "byRegexp", "options": "3.." }, "properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }] },
{ "matcher": { "id": "byRegexp", "options": "4.." }, "properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }] },
{ "matcher": { "id": "byRegexp", "options": "5.." }, "properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }] }
]
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"id": 6,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_requests_total[5m])) by (code)",
"legendFormat": "{{code}}",
"refId": "A"
}
],
"title": "Requests by Status Code",
"type": "timeseries"
},
{
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
"id": 102,
"title": "Services",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 15 },
"id": 7,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_service_requests_total[5m])) by (service)",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "Requests by Service",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 15 },
"id": 8,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "histogram_quantile(0.95, sum(rate(traefik_service_request_duration_seconds_bucket[5m])) by (le, service))",
"legendFormat": "{{service}}",
"refId": "A"
}
],
"title": "P95 Latency by Service",
"type": "timeseries"
},
{
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
"id": 103,
"title": "Routers",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
"id": 9,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_router_requests_total[5m])) by (router)",
"legendFormat": "{{router}}",
"refId": "A"
}
],
"title": "Requests by Router",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
"id": 10,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "histogram_quantile(0.95, sum(rate(traefik_router_request_duration_seconds_bucket[5m])) by (le, router))",
"legendFormat": "{{router}}",
"refId": "A"
}
],
"title": "P95 Latency by Router",
"type": "timeseries"
},
{
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 },
"id": 104,
"title": "Bandwidth",
"type": "row"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "Bps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 },
"id": 11,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_requests_bytes_total[5m])) by (entrypoint)",
"legendFormat": "{{entrypoint}} in",
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(rate(traefik_entrypoint_responses_bytes_total[5m])) by (entrypoint)",
"legendFormat": "{{entrypoint}} out",
"refId": "B"
}
],
"title": "Bandwidth by Entrypoint",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 },
"id": 12,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "10.0.0",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"expr": "sum(traefik_open_connections) by (entrypoint, protocol)",
"legendFormat": "{{entrypoint}} ({{protocol}})",
"refId": "A"
}
],
"title": "Open Connections",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["traefik", "proxy"],
"templating": { "list": [] },
"time": { "from": "now-1h", "to": "now" },
"timepicker": {},
"timezone": "browser",
"title": "Traefik",
"uid": "traefik",
"version": 1
}

View File

@@ -0,0 +1,13 @@
apiVersion: 1
providers:
- name: 'Homelab Dashboards'
orgId: 1
folder: 'Homelab'
folderUid: 'homelab'
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards

View File

@@ -0,0 +1,32 @@
apiVersion: 1
deleteDatasources:
- name: Loki
orgId: 1
- name: Prometheus
orgId: 1
- name: Jaeger
orgId: 1
datasources:
- name: Loki
type: loki
uid: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: true
- name: Prometheus
type: prometheus
uid: prometheus
access: proxy
url: http://prometheus:9090
editable: true
- name: Jaeger
type: jaeger
uid: jaeger
access: proxy
url: http://jaeger:16686
editable: true

46
loki/loki-config.yml Normal file
View File

@@ -0,0 +1,46 @@
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
limits_config:
reject_old_samples: true
reject_old_samples_max_age: 168h
ingestion_rate_mb: 16
ingestion_burst_size_mb: 24
analytics:
reporting_enabled: false

38
loki/promtail-config.yml Normal file
View File

@@ -0,0 +1,38 @@
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# Scrape Docker container logs
- job_name: containers
static_configs:
- targets:
- localhost
labels:
job: containerlogs
__path__: /var/lib/docker/containers/*/*log
pipeline_stages:
- json:
expressions:
output: log
stream: stream
attrs:
- json:
expressions:
tag:
source: attrs
- regex:
expression: (?P<container_name>(?:[a-zA-Z0-9][a-zA-Z0-9_.-]+))
source: tag
- labels:
stream:
container_name:
- output:
source: output

471
nginx/index.html Normal file
View File

@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HOMELAB COMMAND CENTER</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Press Start 2P', monospace;
background: #0a0a0a;
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
/* CRT scanline effect */
body::before {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1000;
}
/* Animated starfield background */
.stars {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
}
.star {
position: absolute;
background: #fff;
border-radius: 50%;
animation: twinkle 2s infinite;
}
@keyframes twinkle {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 40px 20px;
position: relative;
}
h1 {
font-size: 2rem;
color: #00ff00;
text-shadow:
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 40px #00ff00,
4px 4px 0 #003300;
animation: glow 1.5s ease-in-out infinite alternate;
letter-spacing: 4px;
}
@keyframes glow {
from { text-shadow: 0 0 10px #00ff00, 0 0 20px #00ff00, 0 0 40px #00ff00, 4px 4px 0 #003300; }
to { text-shadow: 0 0 20px #00ff00, 0 0 40px #00ff00, 0 0 60px #00ff00, 4px 4px 0 #003300; }
}
.subtitle {
color: #ff00ff;
font-size: 0.6rem;
margin-top: 15px;
text-shadow: 0 0 10px #ff00ff;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0.7; }
}
.score-bar {
display: flex;
justify-content: center;
gap: 40px;
margin-top: 20px;
font-size: 0.5rem;
}
.score-item {
color: #ffff00;
text-shadow: 0 0 5px #ffff00;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
padding: 20px;
}
.tile {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border: 4px solid;
border-radius: 8px;
padding: 25px;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
text-decoration: none;
display: block;
}
.tile::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.tile:hover::before {
left: 100%;
}
.tile:hover {
transform: translateY(-5px) scale(1.02);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
/* Service-specific colors */
.tile.storage { border-color: #ff6b6b; }
.tile.storage .icon { color: #ff6b6b; text-shadow: 0 0 20px #ff6b6b; }
.tile.storage:hover { background: linear-gradient(135deg, #2a1a1a 0%, #1e1616 100%); }
.tile.database { border-color: #4ecdc4; }
.tile.database .icon { color: #4ecdc4; text-shadow: 0 0 20px #4ecdc4; }
.tile.database:hover { background: linear-gradient(135deg, #1a2a2a 0%, #162121 100%); }
.tile.git { border-color: #f7b731; }
.tile.git .icon { color: #f7b731; text-shadow: 0 0 20px #f7b731; }
.tile.git:hover { background: linear-gradient(135deg, #2a2a1a 0%, #212116 100%); }
.tile.ci { border-color: #a55eea; }
.tile.ci .icon { color: #a55eea; text-shadow: 0 0 20px #a55eea; }
.tile.ci:hover { background: linear-gradient(135deg, #2a1a2a 0%, #211621 100%); }
.tile.notebook { border-color: #26de81; }
.tile.notebook .icon { color: #26de81; text-shadow: 0 0 20px #26de81; }
.tile.notebook:hover { background: linear-gradient(135deg, #1a2a1a 0%, #162116 100%); }
.tile.research { border-color: #fd79a8; }
.tile.research .icon { color: #fd79a8; text-shadow: 0 0 20px #fd79a8; }
.tile.research:hover { background: linear-gradient(135deg, #2a1a22 0%, #21161a 100%); }
.tile.catalog { border-color: #00cec9; }
.tile.catalog .icon { color: #00cec9; text-shadow: 0 0 20px #00cec9; }
.tile.catalog:hover { background: linear-gradient(135deg, #1a2a2a 0%, #162121 100%); }
.tile.query { border-color: #6c5ce7; }
.tile.query .icon { color: #6c5ce7; text-shadow: 0 0 20px #6c5ce7; }
.tile.query:hover { background: linear-gradient(135deg, #1a1a2a 0%, #161621 100%); }
.tile.observability { border-color: #e17055; }
.tile.observability .icon { color: #e17055; text-shadow: 0 0 20px #e17055; }
.tile.observability:hover { background: linear-gradient(135deg, #2a1a1a 0%, #211616 100%); }
.tile.metrics { border-color: #fdcb6e; }
.tile.metrics .icon { color: #fdcb6e; text-shadow: 0 0 20px #fdcb6e; }
.tile.metrics:hover { background: linear-gradient(135deg, #2a2a1a 0%, #212116 100%); }
.tile.logs { border-color: #74b9ff; }
.tile.logs .icon { color: #74b9ff; text-shadow: 0 0 20px #74b9ff; }
.tile.logs:hover { background: linear-gradient(135deg, #1a1a2a 0%, #161621 100%); }
.tile.proxy { border-color: #55efc4; }
.tile.proxy .icon { color: #55efc4; text-shadow: 0 0 20px #55efc4; }
.tile.proxy:hover { background: linear-gradient(135deg, #1a2a2a 0%, #162121 100%); }
.icon {
font-size: 2.5rem;
margin-bottom: 15px;
display: block;
}
.tile-title {
font-size: 0.8rem;
color: #fff;
margin-bottom: 10px;
text-shadow: 2px 2px 0 #000;
}
.tile-desc {
font-size: 0.5rem;
color: #888;
line-height: 1.8;
margin-bottom: 15px;
}
.tile-port {
font-size: 0.45rem;
color: #00ff00;
background: rgba(0, 255, 0, 0.1);
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
border: 1px solid #00ff00;
}
.status {
position: absolute;
top: 15px;
right: 15px;
width: 12px;
height: 12px;
background: #00ff00;
border-radius: 50%;
animation: pulse 2s infinite;
box-shadow: 0 0 10px #00ff00;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.7; }
}
.pixel-border {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 4px;
background: repeating-linear-gradient(
90deg,
currentColor 0px,
currentColor 4px,
transparent 4px,
transparent 8px
);
opacity: 0.5;
}
footer {
text-align: center;
padding: 40px;
color: #444;
font-size: 0.4rem;
}
footer span {
color: #ff0000;
animation: heart 1s infinite;
display: inline-block;
}
@keyframes heart {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.3); }
}
/* Power-up badges */
.badge {
position: absolute;
top: -8px;
left: 20px;
background: #ff0000;
color: #fff;
font-size: 0.4rem;
padding: 4px 8px;
border-radius: 4px;
text-transform: uppercase;
animation: bounce 0.5s ease infinite alternate;
}
@keyframes bounce {
from { transform: translateY(0); }
to { transform: translateY(-3px); }
}
.badge.new { background: #ff0000; }
.badge.gpu { background: #ff6b00; }
.badge.data { background: #0066ff; }
</style>
</head>
<body>
<div class="stars" id="stars"></div>
<div class="container">
<header>
<h1>HOMELAB</h1>
<p class="subtitle">[ PRESS START TO CONTINUE ]</p>
<div class="score-bar">
<span class="score-item">SERVICES: 12</span>
<span class="score-item">STATUS: ONLINE</span>
<span class="score-item">LEVEL: MAX</span>
</div>
</header>
<div class="grid">
<a href="http://traefik.home.fhirworx.io" target="_blank" class="tile proxy">
<span class="badge new">NEW</span>
<span class="status"></span>
<span class="icon">&#128279;</span>
<h2 class="tile-title">TRAEFIK</h2>
<p class="tile-desc">Cloud-native reverse proxy and load balancer</p>
<span class="tile-port">traefik.home.fhirworx.io</span>
<div class="pixel-border" style="color: #55efc4;"></div>
</a>
<a href="http://minio.home.fhirworx.io" target="_blank" class="tile storage">
<span class="status"></span>
<span class="icon">&#128190;</span>
<h2 class="tile-title">RUSTFS</h2>
<p class="tile-desc">S3-compatible object storage for all your data needs</p>
<span class="tile-port">minio.home.fhirworx.io</span>
<div class="pixel-border" style="color: #ff6b6b;"></div>
</a>
<a href="http://gitea.home.fhirworx.io" target="_blank" class="tile git">
<span class="status"></span>
<span class="icon">&#128025;</span>
<h2 class="tile-title">GITEA</h2>
<p class="tile-desc">Self-hosted Git service with packages & CI</p>
<span class="tile-port">gitea.home.fhirworx.io</span>
<div class="pixel-border" style="color: #f7b731;"></div>
</a>
<a href="http://ci.home.fhirworx.io" target="_blank" class="tile ci">
<span class="status"></span>
<span class="icon">&#128296;</span>
<h2 class="tile-title">WOODPECKER</h2>
<p class="tile-desc">Simple yet powerful CI/CD engine</p>
<span class="tile-port">ci.home.fhirworx.io</span>
<div class="pixel-border" style="color: #a55eea;"></div>
</a>
<a href="http://notebooks.home.fhirworx.io" target="_blank" class="tile notebook">
<span class="badge gpu">GPU</span>
<span class="status"></span>
<span class="icon">&#128209;</span>
<h2 class="tile-title">NOTEBOOKS</h2>
<p class="tile-desc">Marimo reactive notebooks with CUDA</p>
<span class="tile-port">notebooks.home.fhirworx.io</span>
<div class="pixel-border" style="color: #26de81;"></div>
</a>
<a href="http://zotero.home.fhirworx.io" target="_blank" class="tile research">
<span class="badge gpu">GPU</span>
<span class="status"></span>
<span class="icon">&#128218;</span>
<h2 class="tile-title">ZOTERO</h2>
<p class="tile-desc">Research reference manager in the cloud</p>
<span class="tile-port">zotero.home.fhirworx.io</span>
<div class="pixel-border" style="color: #fd79a8;"></div>
</a>
<a href="http://nessie.home.fhirworx.io/api/v2/config" target="_blank" class="tile catalog">
<span class="badge data">DATA</span>
<span class="status"></span>
<span class="icon">&#127795;</span>
<h2 class="tile-title">NESSIE</h2>
<p class="tile-desc">Git-like versioned data catalog for Iceberg</p>
<span class="tile-port">nessie.home.fhirworx.io</span>
<div class="pixel-border" style="color: #00cec9;"></div>
</a>
<a href="http://trino.home.fhirworx.io" target="_blank" class="tile query">
<span class="badge data">DATA</span>
<span class="status"></span>
<span class="icon">&#128269;</span>
<h2 class="tile-title">TRINO</h2>
<p class="tile-desc">Distributed SQL query engine for the lakehouse</p>
<span class="tile-port">trino.home.fhirworx.io</span>
<div class="pixel-border" style="color: #6c5ce7;"></div>
</a>
<a href="http://grafana.home.fhirworx.io" target="_blank" class="tile observability">
<span class="status"></span>
<span class="icon">&#128202;</span>
<h2 class="tile-title">GRAFANA</h2>
<p class="tile-desc">Observability dashboards for logs and traces</p>
<span class="tile-port">grafana.home.fhirworx.io</span>
<div class="pixel-border" style="color: #e17055;"></div>
</a>
<a href="http://jaeger.home.fhirworx.io" target="_blank" class="tile metrics">
<span class="status"></span>
<span class="icon">&#128065;</span>
<h2 class="tile-title">JAEGER</h2>
<p class="tile-desc">Distributed tracing for microservices</p>
<span class="tile-port">jaeger.home.fhirworx.io</span>
<div class="pixel-border" style="color: #fdcb6e;"></div>
</a>
<a href="http://prometheus.home.fhirworx.io" target="_blank" class="tile logs">
<span class="status"></span>
<span class="icon">&#128200;</span>
<h2 class="tile-title">PROMETHEUS</h2>
<p class="tile-desc">Metrics collection and alerting</p>
<span class="tile-port">prometheus.home.fhirworx.io</span>
<div class="pixel-border" style="color: #74b9ff;"></div>
</a>
<a href="http://loki.home.fhirworx.io/ready" target="_blank" class="tile database">
<span class="status"></span>
<span class="icon">&#128203;</span>
<h2 class="tile-title">LOKI</h2>
<p class="tile-desc">Log aggregation system by Grafana</p>
<span class="tile-port">loki.home.fhirworx.io</span>
<div class="pixel-border" style="color: #4ecdc4;"></div>
</a>
</div>
<footer>
MADE WITH <span>&#9829;</span> FOR THE HOMELAB
</footer>
</div>
<script>
// Generate starfield
const starsContainer = document.getElementById('stars');
for (let i = 0; i < 100; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.left = Math.random() * 100 + '%';
star.style.top = Math.random() * 100 + '%';
star.style.width = Math.random() * 3 + 1 + 'px';
star.style.height = star.style.width;
star.style.animationDelay = Math.random() * 2 + 's';
starsContainer.appendChild(star);
}
// Add click sound effect (visual feedback)
document.querySelectorAll('.tile').forEach(tile => {
tile.addEventListener('click', function(e) {
this.style.transform = 'scale(0.95)';
setTimeout(() => {
this.style.transform = '';
}, 100);
});
});
</script>
</body>
</html>

20
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,20 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
}

3
notebooks/.marimo.toml Normal file
View File

@@ -0,0 +1,3 @@
[display]
theme = "dark"
custom_css = ["/home/kert/.config/marimo/retro-arcade.css"]

View File

@@ -37,8 +37,15 @@ RUN uv python install ${PYTHON_VERSION} \
&& uv init workspace --python ${PYTHON_VERSION} \ && uv init workspace --python ${PYTHON_VERSION} \
&& cd workspace \ && cd workspace \
&& uv add "marimo[recommended]" polars cudf-polars-cu12 pandas numpy pyarrow \ && uv add "marimo[recommended]" polars cudf-polars-cu12 pandas numpy pyarrow \
"pyiceberg[s3,pyarrow]>=0.7.0" "duckdb>=1.0.0" "narwhals>=1.0.0" "trino>=0.328.0" \
vega_datasets pyzotero \
--extra-index-url https://pypi.nvidia.com --extra-index-url https://pypi.nvidia.com
# Create marimo config directory and config
RUN mkdir -p /home/${USERNAME}/.config/marimo
COPY --chown=${USER_UID}:${USER_GID} .marimo.toml /home/${USERNAME}/.config/marimo/marimo.toml
COPY --chown=${USER_UID}:${USER_GID} retro-arcade.css /home/${USERNAME}/.config/marimo/retro-arcade.css
EXPOSE 2718 EXPOSE 2718
CMD ["uv", "run", "--project", "/home/kert/workspace", \ CMD ["uv", "run", "--project", "/home/kert/workspace", \

444
notebooks/retro-arcade.css Normal file
View File

@@ -0,0 +1,444 @@
/* NES SPORTS THEME - RBI Baseball / Tecmo Super Bowl */
/* Uses marimo's native CSS variable system for proper theming */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
/* ============================================
FONT CONFIGURATION
============================================ */
:root {
--marimo-monospace-font: 'IBM Plex Mono', 'Courier New', monospace;
--marimo-text-font: 'IBM Plex Mono', 'Courier New', monospace;
--marimo-heading-font: 'Press Start 2P', 'IBM Plex Mono', monospace;
}
/* ============================================
NES COLOR PALETTE
============================================ */
:root {
/* Core NES colors */
--nes-black: #0f0f0f;
--nes-dark-gray: #1a1a2e;
--nes-gray: #4a4a68;
--nes-light-gray: #9090a8;
--nes-white: #f0f0f0;
/* NES blues (the classic Tecmo/RBI look) */
--nes-dark-blue: #1a1a40;
--nes-blue: #2840a0;
--nes-mid-blue: #4060c0;
--nes-light-blue: #70a0e0;
--nes-sky: #90c0f0;
/* Accent colors */
--nes-red: #d03030;
--nes-orange: #e07020;
--nes-yellow: #e0b040;
--nes-green: #40a040;
--nes-light-green: #80d080;
--nes-cyan: #40b0c0;
--nes-cream: #e8d8c0;
}
/* ============================================
MARIMO THEME VARIABLES (Dark Mode)
============================================ */
:root {
/* Base colors */
--background: var(--nes-dark-blue);
--foreground: var(--nes-white);
/* Muted/secondary text */
--muted: var(--nes-dark-gray);
--muted-foreground: var(--nes-light-gray);
/* Popover/dropdown backgrounds */
--popover: var(--nes-dark-gray);
--popover-foreground: var(--nes-white);
/* Cards (cells) */
--card: var(--nes-black);
--card-foreground: var(--nes-white);
/* Borders and inputs */
--border: var(--nes-gray);
--input: var(--nes-dark-gray);
/* Primary action color */
--primary: var(--nes-mid-blue);
--primary-foreground: var(--nes-white);
/* Secondary elements */
--secondary: var(--nes-gray);
--secondary-foreground: var(--nes-white);
/* Accent/highlight */
--accent: var(--nes-light-blue);
--accent-foreground: var(--nes-black);
/* Focus ring */
--ring: var(--nes-light-blue);
}
/* ============================================
SEMANTIC COLORS
============================================ */
:root {
/* Error states */
--destructive: var(--nes-red);
--destructive-foreground: var(--nes-white);
--error: var(--nes-red);
--error-foreground: var(--nes-white);
/* Success states */
--success: var(--nes-green);
--success-foreground: var(--nes-white);
/* Action buttons (run, etc) */
--action: var(--nes-green);
--action-hover: var(--nes-light-green);
--action-foreground: var(--nes-black);
/* Links */
--link: var(--nes-sky);
--link-visited: var(--nes-light-blue);
/* Stale indicator */
--stale: rgba(224, 176, 64, 0.25);
}
/* ============================================
SHADOWS (subtle, NES-style)
============================================ */
:root {
--base-shadow: rgba(0, 0, 0, 0.4);
--base-shadow-darker: rgba(0, 0, 0, 0.6);
--base-shadow-opacity: 20%;
}
/* ============================================
BACKGROUND GRADIENT (Tecmo field style)
============================================ */
:root {
--background-image: linear-gradient(
180deg,
#0a0a28 0%,
#1a1a40 30%,
#1a1a40 70%,
#0a0a28 100%
);
--background-repeat: no-repeat;
--background-attachment: fixed;
--background-size: cover;
}
body {
background-image: var(--background-image) !important;
background-repeat: var(--background-repeat) !important;
background-attachment: var(--background-attachment) !important;
background-size: var(--background-size) !important;
}
/* ============================================
CODE EDITOR - CODEMIRROR STYLING
============================================ */
.cm-editor {
font-family: var(--marimo-monospace-font) !important;
font-size: 14px !important;
background: var(--nes-black) !important;
}
.cm-content {
font-family: var(--marimo-monospace-font) !important;
}
.cm-line {
font-family: var(--marimo-monospace-font) !important;
color: var(--nes-white) !important;
}
.cm-gutters {
background: var(--nes-dark-gray) !important;
border-right: 2px solid var(--nes-gray) !important;
}
.cm-lineNumbers .cm-gutterElement {
color: var(--nes-light-gray) !important;
font-family: var(--marimo-monospace-font) !important;
font-size: 12px !important;
}
/* Syntax highlighting */
.cm-keyword, .tok-keyword { color: var(--nes-sky) !important; }
.cm-string, .tok-string { color: var(--nes-light-green) !important; }
.cm-number, .tok-number { color: var(--nes-yellow) !important; }
.cm-comment, .tok-comment { color: var(--nes-light-gray) !important; font-style: italic !important; }
.cm-function, .tok-function { color: var(--nes-cream) !important; }
.cm-variableName, .tok-variableName { color: var(--nes-white) !important; }
.cm-operator, .tok-operator { color: var(--nes-cyan) !important; }
.cm-propertyName, .tok-propertyName { color: var(--nes-light-blue) !important; }
.tok-standard, .cm-builtin { color: var(--nes-orange) !important; }
.tok-definition, .cm-def { color: var(--nes-sky) !important; }
.tok-typeName { color: var(--nes-yellow) !important; }
.tok-bool { color: var(--nes-orange) !important; }
/* Cursor */
.cm-cursor {
border-left: 2px solid var(--nes-light-green) !important;
}
/* Selection */
.cm-selectionBackground,
::selection {
background: var(--nes-blue) !important;
}
/* Active line */
.cm-activeLine {
background: rgba(64, 96, 192, 0.15) !important;
}
/* ============================================
HEADINGS (Press Start 2P for that NES feel)
============================================ */
h1, h2, h3, h4, h5, h6 {
font-family: var(--marimo-heading-font) !important;
letter-spacing: 0.5px !important;
line-height: 1.8 !important;
}
h1 {
font-size: 18px !important;
color: var(--nes-light-green) !important;
}
h2 {
font-size: 14px !important;
color: var(--nes-sky) !important;
}
h3 {
font-size: 12px !important;
color: var(--nes-yellow) !important;
}
h4, h5, h6 {
font-size: 11px !important;
color: var(--nes-cream) !important;
}
/* ============================================
TABLES (Scoreboard style)
============================================ */
table {
border-collapse: collapse !important;
font-family: var(--marimo-monospace-font) !important;
}
th {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
font-family: var(--marimo-heading-font) !important;
font-size: 10px !important;
padding: 10px 12px !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
border: 1px solid var(--nes-mid-blue) !important;
}
td {
background: var(--nes-black) !important;
color: var(--nes-cream) !important;
padding: 8px 12px !important;
border: 1px solid var(--nes-dark-gray) !important;
font-size: 13px !important;
}
tr:nth-child(even) td {
background: var(--nes-dark-gray) !important;
}
tr:hover td {
background: var(--nes-dark-blue) !important;
}
/* ============================================
BUTTONS (NES menu style)
============================================ */
button,
[role="button"],
.btn {
font-family: var(--marimo-monospace-font) !important;
font-weight: 500 !important;
border-radius: 4px !important;
transition: all 0.15s ease !important;
}
/* Primary/action buttons should be visible */
button[data-variant="primary"],
button.primary,
[data-action="run"] {
background: var(--nes-green) !important;
color: var(--nes-black) !important;
border: 2px solid var(--nes-light-green) !important;
}
button[data-variant="primary"]:hover,
button.primary:hover,
[data-action="run"]:hover {
background: var(--nes-light-green) !important;
}
/* ============================================
INPUTS AND FORMS
============================================ */
input,
textarea,
select {
font-family: var(--marimo-monospace-font) !important;
background: var(--nes-dark-gray) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
padding: 8px 12px !important;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--nes-light-blue) !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(112, 160, 224, 0.3) !important;
}
/* ============================================
CODE BLOCKS (in markdown)
============================================ */
pre, code {
font-family: var(--marimo-monospace-font) !important;
}
code {
background: var(--nes-dark-gray) !important;
color: var(--nes-light-green) !important;
padding: 2px 6px !important;
border-radius: 3px !important;
font-size: 0.9em !important;
}
pre {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
padding: 12px !important;
}
pre code {
background: transparent !important;
padding: 0 !important;
}
/* ============================================
LINKS
============================================ */
a {
color: var(--nes-sky) !important;
text-decoration: none !important;
transition: color 0.15s ease !important;
}
a:hover {
color: var(--nes-white) !important;
text-decoration: underline !important;
}
a:visited {
color: var(--nes-light-blue) !important;
}
/* ============================================
SCROLLBARS
============================================ */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--nes-dark-gray);
}
::-webkit-scrollbar-thumb {
background: var(--nes-gray);
border-radius: 6px;
border: 2px solid var(--nes-dark-gray);
}
::-webkit-scrollbar-thumb:hover {
background: var(--nes-light-gray);
}
/* ============================================
MISC UI REFINEMENTS
============================================ */
/* Tooltips */
[role="tooltip"],
[data-state="open"][data-side] {
font-family: var(--marimo-monospace-font) !important;
font-size: 12px !important;
background: var(--nes-black) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Dropdown menus */
[role="menu"],
[role="listbox"] {
background: var(--nes-dark-gray) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
[role="menuitem"]:hover,
[role="option"]:hover,
[role="menuitem"][data-highlighted],
[role="option"][data-highlighted] {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
}
/* Badges and pills */
[class*="badge"],
[class*="pill"],
[class*="tag"] {
font-family: var(--marimo-monospace-font) !important;
font-size: 11px !important;
font-weight: 500 !important;
}
/* Focus visible for accessibility */
:focus-visible {
outline: 2px solid var(--nes-light-blue) !important;
outline-offset: 2px !important;
}
/* ============================================
CELL OUTPUT AREA
============================================ */
[data-testid="outputs"],
.output-area {
background: rgba(26, 26, 64, 0.5) !important;
border-top: 2px solid var(--nes-gray) !important;
padding: 12px !important;
}
/* DataFrame outputs */
[class*="dataframe"] {
font-family: var(--marimo-monospace-font) !important;
}
/* Plot/chart containers */
[class*="plot"],
[class*="chart"],
svg {
background: transparent !important;
}

449
notebooks/sample.py Normal file
View File

@@ -0,0 +1,449 @@
# ruff: noqa
import marimo
__generated_with = "0.9.14"
app = marimo.App(
layout_file="layouts/sample.slides.json")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
# Sample Markdown Block
---
# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5
###### Header 6
---
**Bold** | *Italic:* | ~~Strikethrough~~ | `Inline Code`
---
> "The only way to do great work is to love what you do."
> — *Steve Jobs*
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
---
1. Item One
2. Item Two
3. Item Three
---
- Bullet One
- Bullet Two
- Bullet Three
---
1. Main Item
- Sub Item
- Sub-sub Item
---
[Marimo](https://marimo.io/)
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
"""
| Syntax | Description | Example |
|-------------|-------------|-------------------|
| **Header** | Row Title | Content Here |
| **Row 1** | First Cell | `Some code` |
| **Row 2** | Second Cell | *Italicized text* |
---
```python
def greet(name: str) -> str:
\"""Return a greeting message.\"""
return f"Hello, {name}!"
```
---
Here is a sentence with a footnote reference[^1].
[^1]: This is the footnote content.
---
- Inline: $E = mc^2$
- Block:
$$
f(x) = \\int_{-\\infty}^{\\infty} e^{-x^2} \\, dx
$$
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(r"""# Incomplete Reference of Different Args' Use""")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `bg-muted` & `text-muted-foreground`
- hover on `fullscreen` and `cell action` buttons at the right of each cell
- The banner of collapsed cells
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `bg-popover` & `text-popover-foreground`
- `Snippets` side panel
- `Menu` items: `Share`, `Download`, `Helper panel`, `Present as`
- The popover snippets when you enter `:` in markdown cell
- The popover snippets when you enter code in python cell
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `bg-card` & `text-card-foreground`
- even rows of markdown table
| Syntax | Description | Example |
|-------------|-------------|-------------------|
| **Header** | Row Title | Content Here |
| **Row 1** | First Cell | `Some code` |
| **Row 2** | Second Cell | *Italicized text* |
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `bg-primary & text-primary-foreground`
- `machine stats` on the right bottom of notebook
- the header of `Documentation` panel
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md("""## `bg-secondary & text-secondary-foreground`""")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `bg-accent` & `text-accent-foreground`
- Floating Outline on the right center of the notebook
- Side panels on the left of the notebook
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## `destructive` & `destructive-foreground`
- delete button in each cell
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
r"""
## other args
- `border`: borders of cells, buttons, etc.
- `input`: markdown table separator line between header and content
- ...
"""
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(r"""# Marimo UI Components""")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.hstack(
[
mo.ui.button(
label="Danger",
kind="danger",
),
mo.ui.button(label="Info", kind="info"),
mo.ui.button(
label="Neutral",
kind="neutral",
),
mo.ui.button(
label="Success",
kind="success",
),
mo.ui.button(
label="Warn",
kind="warn",
),
mo.ui.button(
label="Alert",
kind="alert",
),
],
justify="space-around",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.chat(model="")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.checkbox()
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.code_editor()
@app.cell(hide_code=True)
def __(data, mo) -> None:
mo.ui.dataframe(data.cars())
@app.cell(hide_code=True)
def __(data, mo) -> None:
mo.ui.data_explorer(data.cars())
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.date(
value="2022-06-01",
start="2022-01-01",
stop="2022-12-31",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.dropdown(
options={"one": 1, "two": 2, "three": 3},
value="one",
label="pick a number",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.file(kind="area")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.file_browser()
@app.cell(hide_code=True)
def __(mo) -> None:
mo.md(
"""
**Your form.**
{name}
{date}
"""
).batch(
name=mo.ui.text(label="name"),
date=mo.ui.date(label="date"),
).form(show_clear_button=True, bordered=True)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.microphone()
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.multiselect(options=["a", "b", "c"], label="choose some options")
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.number(start=1, stop=10, step=2)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.radio(
options={"one": 1, "two": 2, "three": 3},
value="one",
label="pick a number",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.range_slider(start=1, stop=10, step=2, value=[2, 6], show_value=True)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.refresh(
options=["1m", "5m 30s", "10m"],
default_interval="10m",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.switch()
@app.cell(hide_code=True)
def __(data, mo) -> None:
mo.ui.table(
data.cars(),
show_column_summaries=True,
selection="multi",
)
@app.cell(hide_code=True)
def __(mo):
tab1 = mo.vstack(
[
mo.ui.slider(1, 10),
mo.ui.text(),
mo.ui.date(),
]
)
tab2 = mo.md("You can show arbitrary content in a tab.")
tabs = mo.ui.tabs({"Heading 1": tab1, "Heading 2": tab2})
tabs
return tab1, tab2, tabs
@app.cell(hide_code=True)
def __(mo) -> None:
mo.hstack(
[
mo.ui.text(value="Hello, Text", kind="text"),
mo.ui.text(value="Hello, Password", kind="password"),
mo.ui.text(value="Hello, Email", kind="email"),
mo.ui.text(value="Hello, URL", kind="url"),
],
justify="space-around",
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.ui.text_area()
@app.cell(hide_code=True)
def __(mo) -> None:
mo.accordion(
{
"Door 1": mo.md("Nothing!"),
"Door 2": mo.md("Nothing!"),
"Door 3": mo.md(
"![goat](https://images.unsplash.com/photo-1524024973431-2ad916746881)"
),
}
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.carousel(
[
mo.md("# Introduction"),
"By the marimo team",
mo.md("## What is marimo?"),
mo.md("![marimo moss ball](https://marimo.io/logo.png)"),
mo.md("## Questions?"),
]
)
@app.cell(hide_code=True)
def __(mo) -> None:
mo.vstack(
[
mo.callout("This is a info callout", kind="info"),
mo.callout("This is a neutral callout", kind="neutral"),
mo.callout("This is a warn callout", kind="warn"),
mo.callout("This is a danger callout", kind="danger"),
mo.callout("This is a success callout", kind="success"),
]
)
@app.cell(hide_code=True)
def __():
from vega_datasets import data
return (data,)
@app.cell(hide_code=True)
def __():
import marimo as mo
return (mo,)
if __name__ == "__main__":
app.run()

37
prometheus/prometheus.yml Normal file
View File

@@ -0,0 +1,37 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
# Prometheus self-monitoring
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# Jaeger metrics
- job_name: 'jaeger'
static_configs:
- targets: ['jaeger:14269']
# Loki metrics
- job_name: 'loki'
static_configs:
- targets: ['loki:3100']
# Trino metrics (if enabled)
- job_name: 'trino'
static_configs:
- targets: ['trino:8080']
metrics_path: /v1/status
# Nessie metrics
- job_name: 'nessie'
static_configs:
- targets: ['nessie:19120']
metrics_path: /q/metrics
# Traefik metrics
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8080']
metrics_path: /metrics

View File

@@ -0,0 +1,15 @@
# Dynamic Traefik Configuration - Middlewares
http:
middlewares:
# Security headers
secure-headers:
headers:
frameDeny: true
browserXssFilter: true
contentTypeNosniff: true
# Rate limiting
rate-limit:
rateLimit:
average: 100
burst: 50

View File

@@ -0,0 +1,168 @@
# Dynamic Traefik Configuration - Services
# All services defined manually instead of Docker provider
http:
routers:
# Dashboard
dashboard:
rule: "Host(`dashboard.home.fhirworx.io`) || Host(`home.fhirworx.io`)"
service: dashboard
entryPoints:
- web
# Gitea
gitea:
rule: "Host(`gitea.home.fhirworx.io`)"
service: gitea
entryPoints:
- web
# Woodpecker CI
woodpecker:
rule: "Host(`ci.home.fhirworx.io`)"
service: woodpecker
entryPoints:
- web
# Notebooks
notebooks:
rule: "Host(`notebooks.home.fhirworx.io`)"
service: notebooks
entryPoints:
- web
# Zotero
zotero:
rule: "Host(`zotero.home.fhirworx.io`)"
service: zotero
entryPoints:
- web
# Nessie
nessie:
rule: "Host(`nessie.home.fhirworx.io`)"
service: nessie
entryPoints:
- web
# Trino
trino:
rule: "Host(`trino.home.fhirworx.io`)"
service: trino
entryPoints:
- web
# RustFS S3 API
rustfs-api:
rule: "Host(`s3.home.fhirworx.io`)"
service: rustfs-api
entryPoints:
- web
# RustFS Console
rustfs-console:
rule: "Host(`minio.home.fhirworx.io`)"
service: rustfs-console
entryPoints:
- web
# Grafana
grafana:
rule: "Host(`grafana.home.fhirworx.io`)"
service: grafana
entryPoints:
- web
# Prometheus
prometheus:
rule: "Host(`prometheus.home.fhirworx.io`)"
service: prometheus
entryPoints:
- web
# Jaeger
jaeger:
rule: "Host(`jaeger.home.fhirworx.io`)"
service: jaeger
entryPoints:
- web
# Loki
loki:
rule: "Host(`loki.home.fhirworx.io`)"
service: loki
entryPoints:
- web
# Traefik Dashboard
traefik:
rule: "Host(`traefik.home.fhirworx.io`)"
service: api@internal
entryPoints:
- web
services:
dashboard:
loadBalancer:
servers:
- url: "http://dashboard:80"
gitea:
loadBalancer:
servers:
- url: "http://gitea:3000"
woodpecker:
loadBalancer:
servers:
- url: "http://woodpecker-server:8000"
notebooks:
loadBalancer:
servers:
- url: "http://notebooks:2718"
zotero:
loadBalancer:
servers:
- url: "http://zotero:8080"
nessie:
loadBalancer:
servers:
- url: "http://nessie:19120"
trino:
loadBalancer:
servers:
- url: "http://trino:8080"
rustfs-api:
loadBalancer:
servers:
- url: "http://rustfs:9000"
rustfs-console:
loadBalancer:
servers:
- url: "http://rustfs:9001"
grafana:
loadBalancer:
servers:
- url: "http://grafana:3000"
prometheus:
loadBalancer:
servers:
- url: "http://prometheus:9090"
jaeger:
loadBalancer:
servers:
- url: "http://jaeger:16686"
loki:
loadBalancer:
servers:
- url: "http://loki:3100"

34
traefik/traefik.yml Normal file
View File

@@ -0,0 +1,34 @@
# Traefik Static Configuration
api:
dashboard: true
insecure: true
# Enable Prometheus metrics
metrics:
prometheus:
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
providers:
file:
directory: /etc/traefik/dynamic
watch: true
log:
level: INFO
accessLog: {}
# Enable OpenTelemetry tracing
tracing:
otlp:
grpc:
endpoint: jaeger:4317
insecure: true

View File

@@ -0,0 +1,11 @@
connector.name=iceberg
iceberg.catalog.type=nessie
iceberg.nessie-catalog.uri=http://nessie:19120/api/v2
iceberg.nessie-catalog.ref=main
iceberg.nessie-catalog.default-warehouse-dir=s3://lakehouse/
fs.native-s3.enabled=true
s3.endpoint=http://rustfs:9000
s3.path-style-access=true
s3.region=us-east-1
s3.aws-access-key=${ENV:NESSIE_S3_ACCESS_KEY}
s3.aws-secret-key=${ENV:NESSIE_S3_SECRET_KEY}

View File

@@ -0,0 +1,8 @@
coordinator=true
node-scheduler.include-coordinator=true
http-server.http.port=8080
discovery.uri=http://localhost:8080
# OpenTelemetry tracing
tracing.enabled=true
tracing.exporter.endpoint=http://jaeger:4317

10
trino/etc/jvm.config Normal file
View File

@@ -0,0 +1,10 @@
-server
-Xmx4G
-XX:InitialRAMPercentage=80
-XX:MaxRAMPercentage=80
-XX:+UseG1GC
-XX:G1HeapRegionSize=32M
-XX:+ExplicitGCInvokesConcurrent
-XX:+HeapDumpOnOutOfMemoryError
-XX:+ExitOnOutOfMemoryError
-Djdk.attach.allowAttachSelf=true

View File

@@ -0,0 +1,3 @@
node.environment=production
node.id=trino-coordinator
node.data-dir=/data/trino

381
woodpecker/custom.css Normal file
View File

@@ -0,0 +1,381 @@
/* NES SPORTS THEME FOR WOODPECKER CI - RBI Baseball / Tecmo Super Bowl */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root {
/* NES Color Palette */
--nes-black: #0f0f0f;
--nes-dark-gray: #1a1a2e;
--nes-gray: #4a4a68;
--nes-light-gray: #9090a8;
--nes-white: #f0f0f0;
--nes-dark-blue: #1a1a40;
--nes-blue: #2840a0;
--nes-mid-blue: #4060c0;
--nes-light-blue: #70a0e0;
--nes-sky: #90c0f0;
--nes-red: #d03030;
--nes-orange: #e07020;
--nes-yellow: #e0b040;
--nes-green: #40a040;
--nes-light-green: #80d080;
--nes-cyan: #40b0c0;
--nes-cream: #e8d8c0;
/* Woodpecker color overrides */
--color-wp-primary-100: var(--nes-sky);
--color-wp-primary-200: var(--nes-light-blue);
--color-wp-primary-300: var(--nes-mid-blue);
--color-wp-primary-400: var(--nes-blue);
--color-wp-primary-500: var(--nes-dark-blue);
--color-wp-success-100: var(--nes-light-green);
--color-wp-success-200: var(--nes-green);
--color-wp-error-100: #f08080;
--color-wp-error-200: var(--nes-red);
--color-wp-warning-100: var(--nes-yellow);
--color-wp-warning-200: var(--nes-orange);
--color-wp-gray-100: var(--nes-white);
--color-wp-gray-200: var(--nes-light-gray);
--color-wp-gray-300: var(--nes-gray);
--color-wp-gray-400: var(--nes-dark-gray);
--color-wp-gray-500: var(--nes-dark-blue);
--color-wp-gray-600: var(--nes-black);
}
/* Global styles */
body,
html {
font-family: 'IBM Plex Mono', monospace !important;
background: linear-gradient(180deg, #0a0a28 0%, #1a1a40 30%, #1a1a40 70%, #0a0a28 100%) !important;
background-attachment: fixed !important;
color: var(--nes-white) !important;
}
/* Headings */
h1, h2, h3, h4, h5, h6 {
font-family: 'Press Start 2P', monospace !important;
letter-spacing: 0.5px !important;
line-height: 1.8 !important;
}
h1 { font-size: 18px !important; color: var(--nes-light-green) !important; }
h2 { font-size: 14px !important; color: var(--nes-sky) !important; }
h3 { font-size: 12px !important; color: var(--nes-yellow) !important; }
/* Header/Navbar */
header,
nav,
.navbar,
[class*="header"],
[class*="Header"] {
background: var(--nes-black) !important;
border-bottom: 3px solid var(--nes-gray) !important;
}
/* Sidebar */
aside,
[class*="sidebar"],
[class*="Sidebar"] {
background: var(--nes-black) !important;
border-right: 3px solid var(--nes-gray) !important;
}
/* Main content area */
main,
[class*="content"],
[class*="Content"] {
background: transparent !important;
}
/* Cards and panels */
[class*="card"],
[class*="Card"],
[class*="panel"],
[class*="Panel"],
[class*="box"],
[class*="Box"] {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Buttons */
button,
[class*="btn"],
[class*="Btn"],
[class*="button"],
[class*="Button"] {
font-family: 'IBM Plex Mono', monospace !important;
font-weight: 500 !important;
border-radius: 4px !important;
transition: all 0.15s ease !important;
}
/* Primary buttons */
[class*="primary"],
[class*="Primary"],
button[type="submit"] {
background: var(--nes-mid-blue) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-light-blue) !important;
}
[class*="primary"]:hover,
[class*="Primary"]:hover,
button[type="submit"]:hover {
background: var(--nes-light-blue) !important;
color: var(--nes-black) !important;
}
/* Success/green buttons */
[class*="success"],
[class*="Success"],
[class*="green"],
[class*="Green"] {
background: var(--nes-green) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-light-green) !important;
}
[class*="success"]:hover,
[class*="Success"]:hover {
background: var(--nes-light-green) !important;
color: var(--nes-black) !important;
}
/* Danger/red buttons */
[class*="danger"],
[class*="Danger"],
[class*="error"],
[class*="Error"],
[class*="red"],
[class*="Red"] {
background: var(--nes-red) !important;
color: var(--nes-white) !important;
}
/* Forms */
input,
textarea,
select {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--nes-dark-gray) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
padding: 8px 12px !important;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--nes-light-blue) !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(112, 160, 224, 0.3) !important;
}
/* Tables */
table {
background: var(--nes-black) !important;
border: 2px solid var(--nes-gray) !important;
border-collapse: collapse !important;
}
th {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
font-family: 'Press Start 2P', monospace !important;
font-size: 10px !important;
text-transform: uppercase !important;
padding: 10px 12px !important;
border: 1px solid var(--nes-mid-blue) !important;
}
td {
background: var(--nes-black) !important;
color: var(--nes-cream) !important;
padding: 8px 12px !important;
border: 1px solid var(--nes-dark-gray) !important;
}
tr:nth-child(even) td {
background: var(--nes-dark-gray) !important;
}
tr:hover td {
background: var(--nes-dark-blue) !important;
}
/* Pipeline/build status colors */
[class*="running"],
[class*="Running"],
[class*="pending"],
[class*="Pending"] {
color: var(--nes-yellow) !important;
}
[class*="success"],
[class*="Success"],
[class*="passed"],
[class*="Passed"] {
color: var(--nes-light-green) !important;
}
[class*="failure"],
[class*="Failure"],
[class*="failed"],
[class*="Failed"],
[class*="error"],
[class*="Error"] {
color: var(--nes-red) !important;
}
/* Build logs */
pre,
code,
[class*="log"],
[class*="Log"],
[class*="console"],
[class*="Console"] {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--nes-black) !important;
color: var(--nes-cream) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Inline code */
code {
background: var(--nes-dark-gray) !important;
color: var(--nes-light-green) !important;
padding: 2px 6px !important;
border: none !important;
}
/* Links */
a {
color: var(--nes-sky) !important;
text-decoration: none !important;
}
a:hover {
color: var(--nes-white) !important;
text-decoration: underline !important;
}
/* Dropdowns */
[class*="dropdown"],
[class*="Dropdown"],
[class*="menu"],
[class*="Menu"],
[class*="popover"],
[class*="Popover"] {
background: var(--nes-dark-gray) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
[class*="dropdown"] li:hover,
[class*="menu"] li:hover,
[role="menuitem"]:hover,
[role="option"]:hover {
background: var(--nes-blue) !important;
}
/* Badges and tags */
[class*="badge"],
[class*="Badge"],
[class*="tag"],
[class*="Tag"],
[class*="label"],
[class*="Label"] {
font-family: 'IBM Plex Mono', monospace !important;
font-size: 11px !important;
border-radius: 4px !important;
}
/* Tooltips */
[role="tooltip"],
[class*="tooltip"],
[class*="Tooltip"] {
font-family: 'IBM Plex Mono', monospace !important;
background: var(--nes-black) !important;
color: var(--nes-white) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Progress bars */
progress,
[class*="progress"],
[class*="Progress"] {
background: var(--nes-dark-gray) !important;
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
progress::-webkit-progress-bar {
background: var(--nes-dark-gray) !important;
}
progress::-webkit-progress-value {
background: var(--nes-green) !important;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background: var(--nes-dark-gray);
}
::-webkit-scrollbar-thumb {
background: var(--nes-gray);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--nes-light-gray);
}
/* Selection */
::selection {
background: var(--nes-blue) !important;
color: var(--nes-white) !important;
}
/* Icons - ensure visibility */
svg,
[class*="icon"],
[class*="Icon"] {
fill: currentColor;
}
/* Avatar images */
img[class*="avatar"],
[class*="avatar"] img {
border: 2px solid var(--nes-gray) !important;
border-radius: 4px !important;
}
/* Repo/pipeline list items */
[class*="item"],
[class*="Item"],
[class*="list-item"],
[class*="ListItem"] {
border-bottom: 1px solid var(--nes-dark-gray) !important;
}
[class*="item"]:hover,
[class*="Item"]:hover {
background: rgba(64, 96, 192, 0.1) !important;
}