Rene De Ren 7501e1b7a4 fix(helix): alternate cards L/R by index to prevent overlap
Side now flips strictly by slot index (even=left, odd=right) instead
of by strand. With strand-driven side assignment, several consecutive
same-strand projects would stack on one side and overlap vertically.

Belt: SLOT_HEIGHT trimmed to 140 (was 180). Same-side cards are now
2×140 = 280 px apart centre-to-centre. Suspenders: card .slide gets
max-height: 230 px so a runaway title can never grow past that
budget. Together they guarantee a ≥50 px gap on same-side cards.

Strand identity remains visible via stripe colour, badge, and node
fill — the L/R position is no longer a strand signal.

Helix section for 3 seeded projects now renders at 560 px (was 960).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:52:55 +02:00

HELIX

EVOLV and every R&D strand, one helix.

The R&D showcase platform of Waterschap Brabantse Delta. EVOLV at its core, every innovation along the strands.

HELIX collects projects, innovations, and updates from across the R&D team in one place — with deep links to repos, dashboards, and demos. Sign-in is gated to the RnD Gitea organisation; viewing is open.

Stack

  • SvelteKit 2 + Svelte 5 + TypeScript
  • Tailwind v4 (CSS-first design tokens)
  • SQLite (single file) + Drizzle ORM
  • Gitea OAuth2 for authoring (no passwords stored)
  • Pure SVG + CSS helix animation — no WebGL

Run it locally — two ways

Mirrors how the EVOLV stack is exercised locally. One command, named volume for SQLite, healthcheck wired:

cd /path/to/helix
docker compose up -d --build         # build image + start container
docker compose logs -f helix         # tail logs
# → http://localhost:3000

If port 3000 is in use (Node-RED, another dev server, …) pick another:

HELIX_PORT=3030 \
  ORIGIN=http://localhost:3030 \
  GITEA_REDIRECT_URI=http://localhost:3030/auth/gitea/callback \
  docker compose up -d --build
# → http://localhost:3030

Useful commands:

docker compose ps                                 # status + health
docker inspect helix --format '{{.State.Health.Status}}'
docker compose exec helix sh                      # shell into the container
docker compose exec helix sqlite3 /data/helix.db  # inspect the DB
docker compose down                               # stop (keeps the volume)
docker compose down -v                            # stop + DELETE all data

The SQLite database lives in the helix_helix-data named volume at /data/helix.db. Migrations + the idempotent seed both run on every boot. Once you have real content, set SEED_ON_BOOT=false in your env to stop re-seeding the demo entries.

2. Native Node (faster dev loop, hot reload)

nvm use                # .nvmrc → Node 20 (Tailwind v3 also works on 18)
cp .env.example .env   # fill in Gitea OAuth — see below

npm install
npm run db:generate    # generate drizzle/0000_*.sql from the schema
npm run db:migrate     # apply migrations to ./helix.db
npm run db:seed        # idempotent demo content
npm run dev            # http://localhost:5173

Gitea OAuth setup

HELIX uses your existing Gitea identity. Create an OAuth2 application once:

  1. Open https://gitea.wbd-rd.nl/-/user/settings/applications
  2. Click Create new OAuth2 application
  3. Application name: HELIX (local) (or HELIX (production))
  4. Redirect URI:
    • local: http://localhost:3000/auth/gitea/callback
    • prod: https://<your-helix-host>/auth/gitea/callback
  5. Click Create application
  6. Copy the Client ID and Client Secret into your .env:
    GITEA_CLIENT_ID=...
    GITEA_CLIENT_SECRET=...
    

GITEA_ALLOWED_ORG=RnD restricts authoring (sign-in) to members of the RnD organisation. Leave it empty to allow any authenticated Gitea user.

Adding a dashboard embed allowlist host

src/lib/config.tsDASHBOARD_ALLOWED_HOSTS controls which Grafana / dashboard origins can be embedded inline on a project page. Add a hostname (no scheme, no path) and redeploy. Hosts not on the list render as a "Open in new tab" card instead — never blindly iframed.

Production deploy

Same compose file as local. On the production host:

cp .env.example .env
# Set: ORIGIN=https://your-host, GITEA_REDIRECT_URI=https://your-host/auth/gitea/callback,
#      GITEA_CLIENT_ID, GITEA_CLIENT_SECRET (from a separate prod OAuth app)
docker compose up -d --build

Put a TLS-terminating reverse proxy (nginx/caddy/traefik) in front of port 3000. Back up the volume periodically:

docker compose exec helix sqlite3 /data/helix.db .dump | gzip > helix-backup-$(date +%F).sql.gz

Project layout

helix/
├── src/
│   ├── lib/
│   │   ├── components/         # Helix.svelte, ProjectCard, PostCard, Nav, Footer, DashboardEmbed
│   │   ├── server/
│   │   │   ├── db/             # Drizzle schema + client (better-sqlite3)
│   │   │   ├── auth.ts         # session token + cookie helpers
│   │   │   └── gitea.ts        # OAuth2 dance + user fetch
│   │   ├── config.ts           # site name, tagline, dashboard allowlist
│   │   └── markdown.ts         # marked wrapper + slug/id helpers
│   ├── routes/
│   │   ├── +page.svelte        # landing (helix hero + recent projects/posts)
│   │   ├── projects/           # /projects, /[slug], /new
│   │   ├── posts/              # /posts, /[slug], /new
│   │   ├── login/              # sign-in page (kicks off Gitea OAuth)
│   │   ├── logout/+server.ts   # POST → invalidates session
│   │   └── auth/gitea/         # OAuth start + callback
│   ├── app.html
│   ├── app.css                 # Tailwind v4 entry + design tokens
│   ├── app.d.ts                # Locals types
│   └── hooks.server.ts         # session hydration
├── drizzle/                    # generated migrations
├── scripts/
│   ├── migrate.js              # run pending migrations
│   └── seed.js                 # idempotent example data
├── static/favicon.svg
├── Dockerfile
├── docker-compose.yml
├── drizzle.config.ts
├── svelte.config.js
├── vite.config.ts
├── tailwind.config.ts (none — Tailwind v4 uses @theme in app.css)
├── tsconfig.json
└── package.json

How to post

After signing in (top-right → Sign in → Gitea), use:

  • + New in the nav (or /projects/new) — a project: title, summary, markdown body, links
  • /posts/new — a shorter update / write-up

Both are auth-gated. Anyone in RnD on Gitea can post.

Contributing

This is internal R&D — open a PR against main on gitea.wbd-rd.nl/RnD/helix.

License

Internal — Waterschap Brabantse Delta R&D.

Description
landing page R&D projects
Readme 195 KiB
Languages
Svelte 64.2%
TypeScript 23.3%
JavaScript 6.9%
CSS 3.8%
Dockerfile 1.2%
Other 0.6%