diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index 53fadb0..b215626 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -47,15 +47,23 @@ jobs:
- name: Run tests
run: pnpm test
- build-deploy:
- name: Build & Deploy
+ publish:
+ name: Build & Publish Images
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- - name: Build Docker images
- run: docker compose build web api
+ - name: Log in to Gitea Container Registry
+ run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.vicolab.de -u ${{ gitea.actor }} --password-stdin
- - name: Deploy services
- run: docker compose up -d web api
+ - name: Build web image
+ run: docker build -t git.vicolab.de/schalli/tessera-ctl/web:latest -f apps/web/Dockerfile .
+
+ - name: Build api image
+ run: docker build -t git.vicolab.de/schalli/tessera-ctl/api:latest -f apps/api/Dockerfile .
+
+ - name: Push images
+ run: |
+ docker push git.vicolab.de/schalli/tessera-ctl/web:latest
+ docker push git.vicolab.de/schalli/tessera-ctl/api:latest
diff --git a/apps/web/src/components/layout/sidebar.test.tsx b/apps/web/src/components/layout/sidebar.test.tsx
index a04d556..95ae563 100644
--- a/apps/web/src/components/layout/sidebar.test.tsx
+++ b/apps/web/src/components/layout/sidebar.test.tsx
@@ -140,6 +140,12 @@ describe('Sidebar', () => {
const Sidebar = await importSidebar();
render();
+ // Categories are collapsed by default — wait for category header, then expand
+ await waitFor(() => {
+ expect(screen.getByText('Domain Tools')).toBeInTheDocument();
+ });
+ await userEvent.click(screen.getByText('Domain Tools'));
+
await waitFor(() => {
expect(screen.getByText('Domaincheck')).toBeInTheDocument();
});
@@ -156,8 +162,9 @@ describe('Sidebar', () => {
const Sidebar = await importSidebar();
const { rerender } = render();
+ // Wait for category header to confirm initial fetch completed
await waitFor(() => {
- expect(screen.getByText('Domaincheck')).toBeInTheDocument();
+ expect(screen.getByText('Domain Tools')).toBeInTheDocument();
});
const initialCallCount = (globalThis.fetch as ReturnType).mock.calls.length;
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..c18709d
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,79 @@
+services:
+ web:
+ image: git.vicolab.de/schalli/tessera-ctl/web:latest
+ restart: unless-stopped
+ ports:
+ - "3000:3000"
+ environment:
+ HOSTNAME: "0.0.0.0"
+ NEXT_PUBLIC_API_URL: ${APP_URL:-http://localhost:3001}
+ API_INTERNAL_URL: "http://api:3001"
+ JWT_SECRET: ${JWT_SECRET}
+ networks:
+ - frontend-net
+ - backend-net
+ depends_on:
+ api:
+ condition: service_healthy
+
+ api:
+ image: git.vicolab.de/schalli/tessera-ctl/api:latest
+ restart: unless-stopped
+ ports:
+ - "3001:3001"
+ networks:
+ - backend-net
+ - data-net
+ depends_on:
+ db:
+ condition: service_healthy
+ environment:
+ DATABASE_URL: ${DATABASE_URL}
+ JWT_SECRET: ${JWT_SECRET}
+ TESSERA_ADMIN_USER: ${TESSERA_ADMIN_USER:-admin}
+ TESSERA_ADMIN_EMAIL: ${TESSERA_ADMIN_EMAIL}
+ TESSERA_ADMIN_PASSWORD: ${TESSERA_ADMIN_PASSWORD}
+ TESSERA_FORCE_CHANGE: ${TESSERA_FORCE_CHANGE:-true}
+ TESSERA_SMTP_HOST: ${TESSERA_SMTP_HOST:-}
+ TESSERA_SMTP_PORT: ${TESSERA_SMTP_PORT:-587}
+ TESSERA_SMTP_SECURE: ${TESSERA_SMTP_SECURE:-false}
+ TESSERA_SMTP_USER: ${TESSERA_SMTP_USER:-}
+ TESSERA_SMTP_PASSWORD: ${TESSERA_SMTP_PASSWORD:-}
+ TESSERA_SMTP_FROM: ${TESSERA_SMTP_FROM:-Tessera }
+ TESSERA_APP_URL: ${APP_URL:-http://localhost:3000}
+ CALENDAR_ENCRYPTION_KEY: ${CALENDAR_ENCRYPTION_KEY}
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
+ interval: 10s
+ timeout: 5s
+ retries: 3
+ start_period: 20s
+
+ db:
+ image: postgres:16-alpine
+ restart: unless-stopped
+ networks:
+ - data-net
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: tessera
+ POSTGRES_PASSWORD: ${DB_PASSWORD}
+ POSTGRES_DB: tessera
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U tessera"]
+ interval: 5s
+ timeout: 3s
+ retries: 5
+
+networks:
+ frontend-net:
+ driver: bridge
+ backend-net:
+ driver: bridge
+ data-net:
+ driver: bridge
+ internal: true
+
+volumes:
+ pgdata: