Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/server/cover.out
/server/certs/*.crt
/server/certs/*.key
/server/.env
/server/**/*.sql
/report.json
.vscode/settings.json
193 changes: 192 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,192 @@
# triangle-cms
# Triangle CMS (Delta)

## Foreword

Triangle CMS (Delta) is the Drexel Triangle's solution for a minimal WordPress replacement. Its core is a headless CMS (Content Management System) that serves and edits content from a local database. WordPress content can be migrated into Delta via the `wordpress-etl` tool. Delta also offers a minimal frontend similar to WordPress, and because the core CMS is headless, users are welcome to build their own. Delta also includes built-in logging, accessible through a Grafana dashboard, to enable easy monitoring of API calls and internal errors.

## Developer Guide

### Project Structure

Triangle CMS is split into:
- `server/`: Go backend
- `frontend/`: React frontend
- `observability/`: Loki + Promtail + Grafana config for logging purposes
- `scripts/`: setup scripts

### API Specification and Data Models

API specification and response/data model documentation live in the project wiki:
- https://github.com/DrexelTriangle/triangle-cms/wiki
- Endpoints: https://github.com/DrexelTriangle/triangle-cms/wiki/Endpoints
- Response shapes/models: https://github.com/DrexelTriangle/triangle-cms/wiki/Response-Shapes

### Prerequisites

- Docker and Docker Compose
- Go 1.24+
- Node.js 20+ and npm
- Local clone of `wordpress-etl`: https://github.com/DrexelTriangle/wordpress-etl

### Quick Start

1. Run the WordPress ETL pipeline locally in `wordpress-etl` to generate source SQL files.
2. From `triangle-cms` repo root, generate CMS SQL files:

```bash
./scripts/generate_wordpress_sql.sh
```

By default, the script auto-detects ETL SQL if the `wordpress-etl` repository is in the same directory or the parent directory of `triangle-cms`:
- `../wordpress-etl/logs/sql`
- `./wordpress-etl/logs/sql`

3. Start only MariaDB in Docker (recommended for local backend development):

```bash
docker compose up -d mariadb
```

This starts:
- MariaDB (`mariadb`) on `localhost:${MARIADB_PORT_FORWARD:-3306}` (container port `3306`), populated from the ETL pipeline output

If you want the full Docker stack (CMS + observability), run:

```bash
./scripts/setup-containers.sh
```

This starts:
- MariaDB (`mariadb`) on `localhost:${MARIADB_PORT_FORWARD:-3306}` (container port `3306`), populated from the ETL pipeline output
- CMS backend (`cms`) on `https://localhost:8080` which exposes the API
- Promtail (`promtail`) which collects logs from Docker containers
- Loki (`loki`) on port `3100`, which indexes logs from Promtail
- Grafana (`http://localhost:3000`, default `User:admin, Password:admin`) which allows you to explore and query logs from Loki

Important:
- Logging/observability (Promtail, Loki, Grafana) is only available when running the full Docker stack.
- If you run the backend locally via `go run` (instead of the `cms` Docker container), those Docker logging pipelines do not apply to your local process.

Reset all compose volumes and rebuild:

> [!WARNING]
> Running this command will erase all logs and database entries, equivalent to a fresh install

```bash
./scripts/setup-containers.sh --reset-data
```

### Local Development

#### Backend (Go API)

1. Ensure MariaDB is running (via Docker or local instance).
2. If using Docker MariaDB and running the backend via `go run`, configure `server/.env` with the same values used by your compose `MARIADB_DATABASE`, `MARIADB_USER`, and `MARIADB_PASSWORD`:

```env
DB_NAME=triangle
DB_USER=triangle_user
DB_PASSWORD=triangle_password
DB_HOST=127.0.0.1
DB_PORT=3306
```

If using a separate local MariaDB instance, configure `server/.env` for that instance instead.

3. If the Docker `cms` service is running, stop it first to avoid port conflict on `:8080`:

```bash
docker compose stop cms
```

4. Run backend:

```bash
cd server
go run ./main.go
```

The backend serves HTTPS on `https://localhost:8080` using (the certs are just there to keep Postman happy):
- `server/certs/localhost.crt`
- `server/certs/localhost.key`

#### Apply Backend Changes to Docker CMS

When you change Go backend code under `server/` and want those changes reflected in the Docker `cms` container, rebuild and restart that service image:

```bash
docker compose up -d --build cms
```

If your change is to MariaDB bootstrap SQL files in `server/internal/database/wordpress_etl/`, those are only applied on fresh DB initialization. Recreate volumes for those to take effect:

> [!WARNING]
> Running this command will erase all logs and database entries, equivalent to a fresh install

```bash
./scripts/setup-containers.sh --reset-data
```

#### Frontend (Vite)

```bash
cd frontend
npm install
npm run dev
```

Vite dev server starts on `http://localhost:5173`.

### WordPress SQL ETL Flow

SQL files for bootstrap imports live in:
- `server/internal/database/wordpress_etl/`

Before running this step, you must have already run the ETL pipeline in:
- https://github.com/DrexelTriangle/wordpress-etl

Generate CMS SQL files from the ETL pipeline output:

```bash
./scripts/generate_wordpress_sql.sh [source_sql_dir] [output_dir]
```

Defaults:
- source: auto-detected (`../wordpress-etl`, `../wordpress-etl/logs/sql`, `./wordpress-etl`, `./wordpress-etl/logs/sql`)
- output: default (`server/internal/database/wordpress_etl`)

Overrides:
- Pass explicit args:
`./scripts/generate_wordpress_sql.sh ../wordpress-etl/logs/sql server/internal/database/wordpress_etl`
- Or use env vars:
`WP_ETL_SQL_DIR=../wordpress-etl ./scripts/generate_wordpress_sql.sh`
`WP_ETL_OUT_DIR=server/internal/database/wordpress_etl ./scripts/generate_wordpress_sql.sh`
`WP_ETL_SQL_DIR=../wordpress-etl WP_ETL_OUT_DIR=server/internal/database/wordpress_etl ./scripts/generate_wordpress_sql.sh`

Generated files:
- `01-authors.sql`
- `02-articles.sql`
- `03-articles-authors.sql`
- `04-seo.sql`

These are mounted into MariaDB init at container startup through `docker-compose.yml`.

### Testing

Backend tests:

```bash
cd server
go test ./...
```

Test coverage:

Run to see the percentage of code each test covers

```bash
cd server
go test -coverprofile=cover.out ./...
```

This also generates the `cover.out` file, showing exactly which lines are run during tests.
84 changes: 84 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
services:
mariadb:
image: mariadb:11.7
restart: unless-stopped
environment:
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-rootpassword}
MARIADB_DATABASE: ${MARIADB_DATABASE:-triangle}
MARIADB_USER: ${MARIADB_USER:-triangle_user}
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-triangle_password}
volumes:
- mariadb_data:/var/lib/mysql
- ./server/internal/database/wordpress_etl/01-authors.sql:/docker-entrypoint-initdb.d/01-authors.sql:ro,z
- ./server/internal/database/wordpress_etl/02-articles.sql:/docker-entrypoint-initdb.d/02-articles.sql:ro,z
- ./server/internal/database/wordpress_etl/03-articles-authors.sql:/docker-entrypoint-initdb.d/03-articles-authors.sql:ro,z
- ./server/internal/database/wordpress_etl/04-seo.sql:/docker-entrypoint-initdb.d/04-seo.sql:ro,z
ports:
- "${MARIADB_PORT_FORWARD:-3306}:3306"
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s

cms:
build:
context: ./server
dockerfile: Dockerfile
restart: unless-stopped
environment:
DB_NAME: ${MARIADB_DATABASE:-triangle}
DB_USER: ${MARIADB_USER:-triangle_user}
DB_PASSWORD: ${MARIADB_PASSWORD:-triangle_password}
DB_HOST: mariadb
DB_PORT: 3306
TLS_CERT_FILE: /app/certs/localhost.crt
TLS_KEY_FILE: /app/certs/localhost.key
depends_on:
mariadb:
condition: service_healthy
ports:
- "8080:8080"

loki:
image: grafana/loki:3.5.6
restart: unless-stopped
command: ["-config.file=/etc/loki/config.yaml"]
volumes:
- ./observability/loki-config.yml:/etc/loki/config.yaml:ro,z
- loki_data:/loki
ports:
- "3100:3100"

promtail:
image: grafana/promtail:3.5.6
restart: unless-stopped
user: "0:0"
command: ["-config.file=/etc/promtail/config.yml"]
depends_on:
- loki
volumes:
- ./observability/promtail-config.yml:/etc/promtail/config.yml:ro,z
- promtail_positions:/tmp
- /var/lib/docker/containers:/var/lib/docker/containers:ro,z

grafana:
image: grafana/grafana:12.2.0
restart: unless-stopped
depends_on:
- loki
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_ADMIN_USER:-admin}
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin}
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./observability/grafana/provisioning/datasources/loki.yml:/etc/grafana/provisioning/datasources/loki.yml:ro,z

volumes:
mariadb_data:
loki_data:
promtail_positions:
grafana_data:
File renamed without changes.
9 changes: 9 additions & 0 deletions observability/grafana/provisioning/datasources/loki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: 1

datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: false
44 changes: 44 additions & 0 deletions observability/loki-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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

schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h

ruler:
alertmanager_url: http://localhost:9093

limits_config:
allow_structured_metadata: true
volume_enabled: true

pattern_ingester:
enabled: true

query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
27 changes: 27 additions & 0 deletions observability/promtail-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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:
- job_name: docker
static_configs:
- targets: [localhost]
labels:
job: docker
__path__: /var/lib/docker/containers/*/*-json.log
pipeline_stages:
- docker: {}
- regex:
expression: ".*(?:service=|\\\"service\\\":\\\")(?P<service>[a-zA-Z0-9_-]+).*"
- labels:
service:
relabel_configs:
- source_labels: [__path__]
regex: /var/lib/docker/containers/([a-f0-9]{64})/.*-json.log
target_label: container
Loading