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
32 changes: 32 additions & 0 deletions .github/workflows/web-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,35 @@ jobs:
- run: npm run lint
- run: npm run test
- run: npm run build

docker:
name: Web image build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t fincore-web:ci web
- name: Assert nonroot runtime user
run: |
user=$(docker inspect -f '{{.Config.User}}' fincore-web:ci)
echo "runtime user: $user"
[ -n "$user" ] && [ "$user" != "root" ] && [ "$user" != "0" ] || { echo "FAIL: runtime user is root/empty ($user)"; exit 1; }
- name: Smoke run (serves index and falls back to it for client routes)
run: |
docker run -d --name web -p 8080:8080 fincore-web:ci
for _ in $(seq 1 15); do curl -fsS http://localhost:8080/ >/dev/null 2>&1 && break || sleep 2; done
curl -fsS http://localhost:8080/ >/dev/null 2>&1 || { echo "FAIL: container never became ready"; docker logs web; docker rm -f web; exit 1; }
curl -fsS http://localhost:8080/ | grep -qi '<!doctype html'
curl -fsS http://localhost:8080/payments | grep -qi '<!doctype html'
docker rm -f web
- name: Trivy image scan (no CRITICAL)
uses: aquasecurity/trivy-action@master
with:
scan-type: image
image-ref: fincore-web:ci
severity: CRITICAL
exit-code: "1"
ignore-unfixed: true
env:
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ services:
FINCORE_PAYMENTS_BANK_SANDBOX_ENABLED: "true"
OTLP_TRACING_ENDPOINT: http://otel-collector:4318/v1/traces

web:
build: ./web
ports:
- "8082:8080"

# Opt-in telemetry backends. Started only with `docker compose --profile observability up`;
# the default stack (and the CI compose smoke) never starts these.
otel-collector:
Expand Down
7 changes: 7 additions & 0 deletions web/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
.git
coverage
*.log
.env
.env.*
17 changes: 17 additions & 0 deletions web/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: BUSL-1.1
# SPDX-FileCopyrightText: 2026 FinCore Engine Authors

FROM node:22-alpine AS builder
WORKDIR /web
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginxinc/nginx-unprivileged:1.27-alpine AS runtime
USER root
RUN apk upgrade --no-cache libcrypto3 libssl3
USER 101
COPY --from=builder /web/dist /usr/share/nginx/html
COPY default.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
18 changes: 18 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,21 @@ npm run build # typecheck + production bundle

The Overview landing runs on a typed mock-data layer (`src/mock/`). Each screen migrates from mock to the
REST API as its backend lands.

## Run as a container

The SPA is served as static files by a non-root nginx image (multi-stage build, `web/Dockerfile`).

```bash
docker build -t fincore-web web # build the image
docker run --rm -p 8082:8080 fincore-web # serve on http://localhost:8082
```

Or via the umbrella stack (served alongside the backend):

```bash
docker compose up web
```

The API base URL is baked at build time from `VITE_API_BASE_URL` (defaults to a relative path); point it at the
ledger/payments/compliance/decision services as needed.
25 changes: 25 additions & 0 deletions web/default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
server {
listen 8080;
server_name _;
server_tokens off;

root /usr/share/nginx/html;
index index.html;

gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024;

location = /index.html {
add_header Cache-Control "no-cache";
}

location ~* \.(?:js|css|woff2?|png|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

location / {
try_files $uri $uri/ /index.html;
}
}
Loading