Compare commits

..

1 Commits

Author SHA1 Message Date
ribardej
405be381fc feat(backend): fixed a naming conflict in app.py 2025-10-13 14:09:39 +02:00
197 changed files with 1002 additions and 5739 deletions

View File

@@ -15,7 +15,7 @@ on:
context: context:
description: "Docker build context path" description: "Docker build context path"
required: false required: false
default: "7project/src/backend" default: "7project/backend"
type: string type: string
pr_number: pr_number:
description: "PR number (required when mode=pr)" description: "PR number (required when mode=pr)"
@@ -94,7 +94,7 @@ jobs:
tags: | tags: |
${{ env.IMAGE_REPO }}:${{ env.TAG1 }} ${{ env.IMAGE_REPO }}:${{ env.TAG1 }}
${{ env.IMAGE_REPO }}:${{ env.TAG2 }} ${{ env.IMAGE_REPO }}:${{ env.TAG2 }}
platforms: linux/arm64,linux/amd64 platforms: linux/amd64
- name: Set outputs - name: Set outputs
id: set id: set

View File

@@ -9,11 +9,6 @@ permissions:
pull-requests: write pull-requests: write
jobs: jobs:
test:
name: Run Python Tests
if: github.event.action != 'closed'
uses: ./.github/workflows/run-tests.yml
build: build:
if: github.event.action != 'closed' if: github.event.action != 'closed'
name: Build and push image (reusable) name: Build and push image (reusable)
@@ -21,30 +16,17 @@ jobs:
with: with:
mode: pr mode: pr
image_repo: lukastrkan/cc-app-demo image_repo: lukastrkan/cc-app-demo
context: 7project/src/backend context: 7project/backend
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
secrets: inherit secrets: inherit
get_urls:
if: github.event.action != 'closed'
name: Generate Preview URLs
uses: ./.github/workflows/url_generator.yml
with:
runner: vhs
mode: pr
pr_number: ${{ github.event.pull_request.number }}
base_domain: ${{ vars.PROD_DOMAIN }}
secrets: inherit
frontend: frontend:
if: github.event.action != 'closed' if: github.event.action != 'closed'
name: Frontend - Build and Deploy to Cloudflare Pages (PR) name: Frontend - Build and Deploy to Cloudflare Pages (PR)
needs: [get_urls]
uses: ./.github/workflows/frontend-pages.yml uses: ./.github/workflows/frontend-pages.yml
with: with:
mode: pr mode: pr
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
backend_url_scheme: ${{ needs.get_urls.outputs.backend_url_scheme }}
secrets: inherit secrets: inherit
deploy: deploy:
@@ -54,7 +36,7 @@ jobs:
concurrency: concurrency:
group: pr-${{ github.event.pull_request.number }} group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: false cancel-in-progress: false
needs: [build, frontend, get_urls] needs: [build, frontend]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -77,48 +59,48 @@ jobs:
- name: Helm upgrade/install PR preview - name: Helm upgrade/install PR preview
env: env:
DEV_BASE_DOMAIN: ${{ vars.BASE_DOMAIN }} DEV_BASE_DOMAIN: ${{ secrets.BASE_DOMAIN }}
RABBITMQ_PASSWORD: ${{ secrets.PROD_RABBITMQ_PASSWORD }} RABBITMQ_PASSWORD: ${{ secrets.PROD_RABBITMQ_PASSWORD }}
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
IMAGE_REPO: ${{ needs.build.outputs.image_repo }}
DIGEST: ${{ needs.build.outputs.digest }} DIGEST: ${{ needs.build.outputs.digest }}
DOMAIN: "${{ needs.get_urls.outputs.backend_url }}"
DOMAIN_SCHEME: "${{ needs.get_urls.outputs.backend_url_scheme }}"
FRONTEND_DOMAIN: "${{ needs.get_urls.outputs.frontend_url }}"
FRONTEND_DOMAIN_SCHEME: "${{ needs.get_urls.outputs.frontend_url_scheme }}"
UNIRATE_API_KEY: ${{ secrets.UNIRATE_API_KEY }}
run: | run: |
PR=${{ github.event.pull_request.number }} PR=${{ github.event.pull_request.number }}
if [ -z "$PR" ]; then echo "PR number missing"; exit 1; fi
if [ -z "$DEV_BASE_DOMAIN" ]; then echo "Secret DEV_BASE_DOMAIN is required (e.g., dev.example.com)"; exit 1; fi
if [ -z "$RABBITMQ_PASSWORD" ]; then echo "Secret DEV_RABBITMQ_PASSWORD is required"; exit 1; fi
if [ -z "$DB_PASSWORD" ]; then echo "Secret DEV_DB_PASSWORD is required"; exit 1; fi
RELEASE=myapp-pr-$PR RELEASE=myapp-pr-$PR
NAMESPACE=pr-$PR NAMESPACE=pr-$PR
helm upgrade --install "$RELEASE" ./7project/src/charts/myapp-chart \ DOMAIN=pr-$PR.$DEV_BASE_DOMAIN
if [ -z "$IMAGE_REPO" ]; then IMAGE_REPO="lukastrkan/cc-app-demo"; fi
helm upgrade --install "$RELEASE" ./7project/charts/myapp-chart \
-n "$NAMESPACE" --create-namespace \ -n "$NAMESPACE" --create-namespace \
-f 7project/src/charts/myapp-chart/values-dev.yaml \ -f 7project/charts/myapp-chart/values-dev.yaml \
--set prNumber="$PR" \ --set prNumber="$PR" \
--set deployment="pr-$PR" \ --set deployment="pr-$PR" \
--set domain="$DOMAIN" \ --set domain="$DOMAIN" \
--set domain_scheme="$DOMAIN_SCHEME" \ --set image.repository="$IMAGE_REPO" \
--set frontend_domain="$FRONTEND_DOMAIN" \
--set frontend_domain_scheme="$FRONTEND_DOMAIN_SCHEME" \
--set image.digest="$DIGEST" \ --set image.digest="$DIGEST" \
--set-string rabbitmq.password="$RABBITMQ_PASSWORD" \ --set-string rabbitmq.password="$RABBITMQ_PASSWORD" \
--set-string database.password="$DB_PASSWORD" \ --set-string database.password="$DB_PASSWORD"
--set-string database.encryptionSecret="$PR" \
--set-string app.name="finance-tracker-pr-$PR" \
--set-string unirate.key="$UNIRATE_API_KEY"
- name: Post preview URLs as PR comment - name: Post preview URLs as PR comment
uses: actions/github-script@v7 uses: actions/github-script@v7
env: env:
BACKEND_URL: ${{ needs.get_urls.outputs.backend_url_scheme }} DEV_BASE_DOMAIN: ${{ secrets.BASE_DOMAIN }}
FRONTEND_URL: ${{ needs.get_urls.outputs.frontend_url_scheme }} FRONTEND_URL: ${{ needs.frontend.outputs.deployed_url }}
with: with:
script: | script: |
const pr = context.payload.pull_request; const pr = context.payload.pull_request;
if (!pr) { core.setFailed('No pull_request context'); return; } if (!pr) { core.setFailed('No pull_request context'); return; }
const prNumber = pr.number; const prNumber = pr.number;
const backendUrl = process.env.BACKEND_URL || '(not available)'; const domainBase = process.env.DEV_BASE_DOMAIN;
if (!domainBase) { core.setFailed('DEV_BASE_DOMAIN is required'); return; }
const backendDomain = `pr-${prNumber}.${domainBase}`;
const backendUrl = `https://${backendDomain}`;
const frontendUrl = process.env.FRONTEND_URL || '(not available)'; const frontendUrl = process.env.FRONTEND_URL || '(not available)';
const marker = '<!-- preview-comment-marker -->'; const marker = '<!-- preview-link -->';
const body = `${marker}\nPreview environment is running\n- Frontend: ${frontendUrl}\n- Backend: ${backendUrl}\n`; const body = `${marker}\nPreview environment is running\n- Frontend: ${frontendUrl}\n- Backend: ${backendUrl}\n`;
const { owner, repo } = context.repo; const { owner, repo } = context.repo;
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 100 }); const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: prNumber, per_page: 100 });
@@ -157,4 +139,4 @@ jobs:
NAMESPACE=pr-$PR NAMESPACE=pr-$PR
helm uninstall "$RELEASE" -n "$NAMESPACE" || true helm uninstall "$RELEASE" -n "$NAMESPACE" || true
# Optionally delete the namespace if empty # Optionally delete the namespace if empty
kubectl delete namespace "$NAMESPACE" --ignore-not-found=true || true kubectl delete namespace "$NAMESPACE" --ignore-not-found=true || true

View File

@@ -4,9 +4,9 @@ on:
push: push:
branches: [ "main" ] branches: [ "main" ]
paths: paths:
- ../../7project/src/backend/** - 7project/backend/**
- ../../7project/src/frontend/** - 7project/frontend/**
- ../../7project/src/charts/myapp-chart/** - 7project/charts/myapp-chart/**
- .github/workflows/deploy-prod.yaml - .github/workflows/deploy-prod.yaml
- .github/workflows/build-image.yaml - .github/workflows/build-image.yaml
- .github/workflows/frontend-pages.yml - .github/workflows/frontend-pages.yml
@@ -21,43 +21,26 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
test:
name: Run Python Tests
uses: ./.github/workflows/run-tests.yml
build: build:
name: Build and push image (reusable) name: Build and push image (reusable)
needs: [test]
uses: ./.github/workflows/build-image.yaml uses: ./.github/workflows/build-image.yaml
with: with:
mode: prod mode: prod
image_repo: lukastrkan/cc-app-demo image_repo: lukastrkan/cc-app-demo
context: 7project/src/backend context: 7project/backend
secrets: inherit
get_urls:
name: Generate Production URLs
needs: [test]
uses: ./.github/workflows/url_generator.yml
with:
mode: prod
runner: vhs
base_domain: ${{ vars.PROD_DOMAIN }}
secrets: inherit secrets: inherit
frontend: frontend:
name: Frontend - Build and Deploy to Cloudflare Pages (prod) name: Frontend - Build and Deploy to Cloudflare Pages (prod)
needs: [get_urls]
uses: ./.github/workflows/frontend-pages.yml uses: ./.github/workflows/frontend-pages.yml
with: with:
mode: prod mode: prod
backend_url_scheme: ${{ needs.get_urls.outputs.backend_url_scheme }}
secrets: inherit secrets: inherit
deploy: deploy:
name: Helm upgrade/install (prod) name: Helm upgrade/install (prod)
runs-on: vhs runs-on: vhs
needs: [build, frontend, get_urls] needs: [build, frontend]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -80,53 +63,25 @@ jobs:
- name: Helm upgrade/install prod - name: Helm upgrade/install prod
env: env:
DOMAIN: ${{ needs.get_urls.outputs.backend_url }} DOMAIN: ${{ secrets.PROD_DOMAIN }}
DOMAIN_SCHEME: ${{ needs.get_urls.outputs.backend_url_scheme }}
FRONTEND_DOMAIN: ${{ needs.get_urls.outputs.frontend_url }}
FRONTEND_DOMAIN_SCHEME: ${{ needs.get_urls.outputs.frontend_url_scheme }}
RABBITMQ_PASSWORD: ${{ secrets.PROD_RABBITMQ_PASSWORD }} RABBITMQ_PASSWORD: ${{ secrets.PROD_RABBITMQ_PASSWORD }}
DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
IMAGE_REPO: ${{ needs.build.outputs.image_repo }}
DIGEST: ${{ needs.build.outputs.digest }} DIGEST: ${{ needs.build.outputs.digest }}
BANKID_CLIENT_ID: ${{ secrets.BANKID_CLIENT_ID }}
BANKID_CLIENT_SECRET: ${{ secrets.BANKID_CLIENT_SECRET }}
MOJEID_CLIENT_ID: ${{ secrets.MOJEID_CLIENT_ID }}
MOJEID_CLIENT_SECRET: ${{ secrets.MOJEID_CLIENT_SECRET }}
CSAS_CLIENT_ID: ${{ secrets.CSAS_CLIENT_ID }}
CSAS_CLIENT_SECRET: ${{ secrets.CSAS_CLIENT_SECRET }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
SMTP_USE_TLS: ${{ secrets.SMTP_USE_TLS }}
SMTP_USE_SSL: ${{ secrets.SMTP_USE_SSL }}
SMTP_FROM: ${{ secrets.SMTP_FROM }}
UNIRATE_API_KEY: ${{ secrets.UNIRATE_API_KEY }}
run: | run: |
helm upgrade --install myapp ./7project/src/charts/myapp-chart \ if [ -z "$DOMAIN" ]; then
echo "Secret PROD_DOMAIN is required (e.g., app.example.com)"; exit 1; fi
if [ -z "$RABBITMQ_PASSWORD" ]; then
echo "Secret PROD_RABBITMQ_PASSWORD is required"; exit 1; fi
if [ -z "$DB_PASSWORD" ]; then
echo "Secret PROD_DB_PASSWORD is required"; exit 1; fi
if [ -z "$IMAGE_REPO" ]; then IMAGE_REPO="lukastrkan/cc-app-demo"; fi
helm upgrade --install myapp ./7project/charts/myapp-chart \
-n prod --create-namespace \ -n prod --create-namespace \
-f 7project/src/charts/myapp-chart/values-prod.yaml \ -f 7project/charts/myapp-chart/values-prod.yaml \
--set deployment="prod" \ --set deployment="prod" \
--set domain="$DOMAIN" \ --set domain="$DOMAIN" \
--set domain_scheme="$DOMAIN_SCHEME" \ --set image.repository="$IMAGE_REPO" \
--set frontend_domain="$FRONTEND_DOMAIN" \
--set frontend_domain_scheme="$FRONTEND_DOMAIN_SCHEME" \
--set image.digest="$DIGEST" \ --set image.digest="$DIGEST" \
--set-string rabbitmq.password="$RABBITMQ_PASSWORD" \ --set-string rabbitmq.password="$RABBITMQ_PASSWORD" \
--set-string database.password="$DB_PASSWORD" \ --set-string database.password="$DB_PASSWORD"
--set-string oauth.bankid.clientId="$BANKID_CLIENT_ID" \
--set-string oauth.bankid.clientSecret="$BANKID_CLIENT_SECRET" \
--set-string oauth.mojeid.clientId="$MOJEID_CLIENT_ID" \
--set-string oauth.mojeid.clientSecret="$MOJEID_CLIENT_SECRET" \
--set-string oauth.csas.clientId="$CSAS_CLIENT_ID" \
--set-string oauth.csas.clientSecret="$CSAS_CLIENT_SECRET" \
--set-string sentry_dsn="$SENTRY_DSN" \
--set-string database.encryptionSecret="${{ secrets.PROD_DB_ENCRYPTION_KEY }}" \
--set-string smtp.host="$SMTP_HOST" \
--set smtp.port="$SMTP_PORT" \
--set-string smtp.username="$SMTP_USERNAME" \
--set-string smtp.password="$SMTP_PASSWORD" \
--set-string smtp.tls="$SMTP_USE_TLS" \
--set-string smtp.ssl="$SMTP_USE_SSL" \
--set-string smtp.from="$SMTP_FROM" \
--set-string unirate.key="$UNIRATE_API_KEY"

View File

@@ -15,10 +15,6 @@ on:
description: 'Cloudflare Pages project name (overrides default)' description: 'Cloudflare Pages project name (overrides default)'
required: false required: false
type: string type: string
backend_url_scheme:
description: 'The full scheme URL for the backend (e.g., https://api.example.com)'
required: true
type: string
secrets: secrets:
CLOUDFLARE_API_TOKEN: CLOUDFLARE_API_TOKEN:
required: true required: true
@@ -29,13 +25,21 @@ on:
description: 'URL of deployed frontend' description: 'URL of deployed frontend'
value: ${{ jobs.deploy.outputs.deployed_url }} value: ${{ jobs.deploy.outputs.deployed_url }}
# Required repository secrets:
# CLOUDFLARE_API_TOKEN - API token with Pages:Edit (or Account:Workers Scripts:Edit) permissions
# CLOUDFLARE_ACCOUNT_ID - Your Cloudflare account ID
# Optional repository variables:
# CF_PAGES_PROJECT_NAME - Default Cloudflare Pages project name
# PROD_DOMAIN - App domain for prod releases (e.g., api.example.com or https://api.example.com)
# BACKEND_URL_PR_TEMPLATE - Template for PR backend URL. Use {PR} placeholder for PR number (e.g., https://api-pr-{PR}.example.com)
jobs: jobs:
build: build:
name: Build frontend name: Build frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
working-directory: 7project/src/frontend working-directory: 7project/frontend
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -45,14 +49,55 @@ jobs:
with: with:
node-version: '20' node-version: '20'
cache: 'npm' cache: 'npm'
cache-dependency-path: 7project/src/frontend/package-lock.json cache-dependency-path: 7project/frontend/package-lock.json
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Set backend URL from workflow input - name: Compute backend URL for Vite
id: be
env:
EVENT_NAME: ${{ github.event_name }}
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
PR_TEMPLATE: ${{ vars.BACKEND_URL_PR_TEMPLATE }}
DEV_BASE_DOMAIN: ${{ secrets.BASE_DOMAIN }}
PROD_DOMAIN_VAR: ${{ vars.PROD_DOMAIN }}
PROD_DOMAIN_SECRET: ${{ secrets.PROD_DOMAIN }}
BACKEND_URL_OVERRIDE: ${{ vars.BACKEND_URL || secrets.BACKEND_URL }}
MODE: ${{ inputs.mode }}
run: | run: |
echo "VITE_BACKEND_URL=${{ inputs.backend_url_scheme }}" >> $GITHUB_ENV set -euo pipefail
URL=""
# 1) Explicit override wins (from repo var or secret)
if [ -n "${BACKEND_URL_OVERRIDE:-}" ]; then
if echo "$BACKEND_URL_OVERRIDE" | grep -Eiq '^https?://'; then
URL="$BACKEND_URL_OVERRIDE"
else
URL="https://${BACKEND_URL_OVERRIDE}"
fi
else
# 2) PR-specific URL when building for PR
if [ "${MODE:-}" = "pr" ] || [ "${EVENT_NAME}" = "pull_request" ]; then
if [ -n "${PR_TEMPLATE:-}" ] && [ -n "${PR_NUMBER:-}" ] ; then
URL="${PR_TEMPLATE//\{PR\}/${PR_NUMBER}}"
elif [ -n "${DEV_BASE_DOMAIN:-}" ] && [ -n "${PR_NUMBER:-}" ]; then
URL="https://pr-${PR_NUMBER}.${DEV_BASE_DOMAIN}"
fi
fi
# 3) Fallback to PROD_DOMAIN (prefer repo var, then secret)
if [ -z "$URL" ]; then
PROD_DOMAIN="${PROD_DOMAIN_VAR:-${PROD_DOMAIN_SECRET:-}}"
if [ -n "$PROD_DOMAIN" ]; then
if echo "$PROD_DOMAIN" | grep -Eiq '^https?://'; then
URL="$PROD_DOMAIN"
else
URL="https://${PROD_DOMAIN}"
fi
fi
fi
fi
echo "Using backend URL: ${URL:-<empty>}"
echo "VITE_BACKEND_URL=${URL}" >> $GITHUB_ENV
- name: Build - name: Build
run: npm run build run: npm run build
@@ -61,7 +106,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: frontend-dist name: frontend-dist
path: 7project/src/frontend/dist path: 7project/frontend/dist
deploy: deploy:
name: Deploy to Cloudflare Pages name: Deploy to Cloudflare Pages
@@ -132,4 +177,4 @@ jobs:
else else
URL="https://${PBRANCH}.${PNAME}.pages.dev" URL="https://${PBRANCH}.${PNAME}.pages.dev"
fi fi
echo "deployed_url=$URL" >> $GITHUB_OUTPUT echo "deployed_url=$URL" >> $GITHUB_OUTPUT

View File

@@ -1,66 +0,0 @@
name: Run Python Tests
permissions:
contents: read
on:
workflow_call:
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
mariadb:
image: mariadb:11.4
env:
MARIADB_ROOT_PASSWORD: rootpw
MARIADB_DATABASE: group_project
MARIADB_USER: appuser
MARIADB_PASSWORD: apppass
ports:
- 3306:3306
options: >-
--health-cmd="mariadb-admin ping -h 127.0.0.1 -u root -prootpw --silent"
--health-interval=5s
--health-timeout=2s
--health-retries=20
env:
MARIADB_HOST: 127.0.0.1
MARIADB_PORT: "3306"
MARIADB_DB: group_project
MARIADB_USER: appuser
MARIADB_PASSWORD: apppass
# Ensure the application uses MariaDB (async) during tests
DATABASE_URL: mysql+asyncmy://appuser:apppass@127.0.0.1:3306/group_project
DISABLE_METRICS: "1"
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Add test dependencies to requirements
run: |
echo "pytest==8.4.2" >> ./7project/src/backend/requirements.txt
echo "pytest-asyncio==1.2.0" >> ./7project/src/backend/requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ./7project/src/backend/requirements.txt
- name: Run Alembic migrations
run: |
alembic upgrade head
working-directory: ./7project/src/backend
- name: Run tests with pytest
env:
PYTEST_RUN_CONFIG: "True"
run: pytest
working-directory: ./7project/src/backend

View File

@@ -1,74 +0,0 @@
name: Generate Preview or Production URLs
on:
workflow_call:
inputs:
mode:
description: "Build mode: 'prod' or 'pr'"
required: true
type: string
pr_number:
description: 'PR number (required when mode=pr)'
required: false
type: string
runner:
description: 'The runner to use for this job'
required: false
type: string
default: 'ubuntu-latest'
base_domain:
description: 'The base domain for production URLs (e.g., example.com)'
required: true
type: string
outputs:
backend_url:
description: "The backend URL without scheme (e.g., api.example.com)"
value: ${{ jobs.generate-urls.outputs.backend_url }}
frontend_url:
description: "The frontend URL without scheme (e.g., app.example.com)"
value: ${{ jobs.generate-urls.outputs.frontend_url }}
backend_url_scheme:
description: "The backend URL with scheme (e.g., https://api.example.com)"
value: ${{ jobs.generate-urls.outputs.backend_url_scheme }}
frontend_url_scheme:
description: "The frontend URL with scheme (e.g., https://app.example.com)"
value: ${{ jobs.generate-urls.outputs.frontend_url_scheme }}
jobs:
generate-urls:
permissions:
contents: none
runs-on: ${{ inputs.runner }}
outputs:
backend_url: ${{ steps.set_urls.outputs.backend_url }}
frontend_url: ${{ steps.set_urls.outputs.frontend_url }}
backend_url_scheme: ${{ steps.set_urls.outputs.backend_url_scheme }}
frontend_url_scheme: ${{ steps.set_urls.outputs.frontend_url_scheme }}
steps:
- name: Generate URLs
id: set_urls
env:
BASE_DOMAIN: ${{ inputs.base_domain }}
run: |
set -euo pipefail
if [ "${{ inputs.mode }}" = "prod" ]; then
BACKEND_URL="api.${BASE_DOMAIN}"
FRONTEND_URL="finance.${BASE_DOMAIN}"
else
# This is your current logic
FRONTEND_URL="pr-${{ inputs.pr_number }}.group-8-frontend.pages.dev"
BACKEND_URL="api-pr-${{ inputs.pr_number }}.${BASE_DOMAIN}"
fi
FRONTEND_URL_SCHEME="https://$FRONTEND_URL"
BACKEND_URL_SCHEME="https://$BACKEND_URL"
# This part correctly writes to GITHUB_OUTPUT for the step
echo "backend_url_scheme=$BACKEND_URL_SCHEME" >> $GITHUB_OUTPUT
echo "frontend_url_scheme=$FRONTEND_URL_SCHEME" >> $GITHUB_OUTPUT
echo "backend_url=$BACKEND_URL" >> $GITHUB_OUTPUT
echo "frontend_url=$FRONTEND_URL" >> $GITHUB_OUTPUT

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

16
7project/.gitignore vendored
View File

@@ -1,8 +1,8 @@
/src/tofu/controlplane.yaml /tofu/controlplane.yaml
/src/tofu/kubeconfig /tofu/kubeconfig
/src/tofu/talosconfig /tofu/talosconfig
/src/tofu/terraform.tfstate /tofu/terraform.tfstate
/src/tofu/terraform.tfstate.backup /tofu/terraform.tfstate.backup
/src/tofu/worker.yaml /tofu/worker.yaml
/src/tofu/.terraform.lock.hcl /tofu/.terraform.lock.hcl
/src/tofu/.terraform/ /tofu/.terraform/

8
7project/.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -1,6 +1,43 @@
# Personal Finance Tracker # Lab 6: Design Document for Course Project
## Folder Structure
- meetings: Contains note from meetings | Lab 6: | Design Document for Course Project |
- scr: Source code for the project | ----------- | ---------------------------------- |
- checklist: Project checklist and self assessment tracking | Subject: | DAT515 Cloud Computing |
- report.md: Detailed report of the project | Deadline: | **September 19, 2025 23:59** |
| Grading: | No Grade |
| Submission: | Group |
## Table of Contents
- [Table of Contents](#table-of-contents)
- [1. Design Document (design.md)](#1-design-document-designmd)
The design document is the first deliverable for your project.
We separated this out as a separate deliverable, with its own deadline, to ensure that you have a clear plan before you start coding.
This part only needs a cursory review by the teaching staff to ensure it is sufficiently comprehensive, while still realistic.
The teaching staff will assign you to a project mentor who will provide guidance and support throughout the development process.
## 1. Design Document (design.md)
You are required to prepare a design document for your application.
The design doc should be brief, well-organized and easy to understand.
The design doc should be prepared in markdown format and named `design.md` and submitted in the project group's repository.
Remember that you can use [mermaid diagrams](https://github.com/mermaid-js/mermaid#readme) in markdown files.
The design doc **should include** the following sections:
- **Overview**: A brief description of the application and its purpose.
- **Architecture**: The high-level architecture of the application, including components, interactions, and data flow.
- **Technologies**: The cloud computing technologies or services used in the application.
- **Deployment**: The deployment strategy for the application, including any infrastructure requirements.
The design document should be updated throughout the development process and reflect the final implementation of your project.
Optional sections may include:
- Security: The security measures implemented in the application to protect data and resources.
- Scalability: The scalability considerations for the application, including load balancing and auto-scaling.
- Monitoring: The monitoring and logging strategy for the application to track performance and detect issues.
- Disaster Recovery: The disaster recovery plan for the application to ensure business continuity in case of failures.
- Cost Analysis: The cost analysis of running the application on the cloud, including pricing models and cost-saving strategies.
- References: Any external sources or references used in the design document.

View File

@@ -1,4 +1,4 @@
FROM python:3.11-trixie FROM python:3.11-slim
WORKDIR /app WORKDIR /app
COPY requirements.txt . COPY requirements.txt .

View File

@@ -25,8 +25,7 @@ if not DATABASE_URL:
SYNC_DATABASE_URL = DATABASE_URL.replace("+asyncmy", "+pymysql") SYNC_DATABASE_URL = DATABASE_URL.replace("+asyncmy", "+pymysql")
host_env = os.getenv("MARIADB_HOST", "localhost") ssl_enabled = os.getenv("MARIADB_HOST", "localhost") != "localhost"
ssl_enabled = host_env not in {"localhost", "127.0.0.1"}
connect_args = {"ssl": {"ssl": True}} if ssl_enabled else {} connect_args = {"ssl": {"ssl": True}} if ssl_enabled else {}
def run_migrations_offline() -> None: def run_migrations_offline() -> None:

View File

@@ -0,0 +1,31 @@
from fastapi import APIRouter
from app.schemas.user import UserCreate, UserRead, UserUpdate
from app.services.user_service import auth_backend, fastapi_users
router = APIRouter()
# Keep existing paths as-is under /auth/* and /users/*
router.include_router(
fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"]
)
router.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_reset_password_router(),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_verify_router(UserRead),
prefix="/auth",
tags=["auth"],
)
router.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)

View File

@@ -5,7 +5,7 @@ from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.models.categories import Category from app.models.categories import Category
from app.schemas.category import CategoryCreate, CategoryRead, CategoryUpdate from app.schemas.category import CategoryCreate, CategoryRead
from app.services.db import get_async_session from app.services.db import get_async_session
from app.services.user_service import current_active_user from app.services.user_service import current_active_user
from app.models.user import User from app.models.user import User
@@ -43,37 +43,6 @@ async def list_categories(
return list(res.scalars()) return list(res.scalars())
@router.patch("/{category_id}", response_model=CategoryRead)
async def update_category(
category_id: int,
payload: CategoryUpdate,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
res = await session.execute(
select(Category).where(Category.id == category_id, Category.user_id == user.id)
)
category = res.scalar_one_or_none()
if not category:
raise HTTPException(status_code=404, detail="Category not found")
# If name changed, check uniqueness per user
if payload.name is not None and payload.name != category.name:
dup = await session.execute(
select(Category.id).where(Category.user_id == user.id, Category.name == payload.name)
)
if dup.scalar_one_or_none() is not None:
raise HTTPException(status_code=409, detail="Category with this name already exists")
category.name = payload.name
if payload.description is not None:
category.description = payload.description
await session.commit()
await session.refresh(category)
return category
@router.get("/{category_id}", response_model=CategoryRead) @router.get("/{category_id}", response_model=CategoryRead)
async def get_category( async def get_category(
category_id: int, category_id: int,

View File

@@ -1,8 +1,7 @@
from typing import List, Optional from typing import List, Optional
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select, and_, func from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.models.transaction import Transaction from app.models.transaction import Transaction
@@ -24,7 +23,6 @@ def _to_read_model(tx: Transaction) -> TransactionRead:
id=tx.id, id=tx.id,
amount=tx.amount, amount=tx.amount,
description=tx.description, description=tx.description,
date=tx.date,
category_ids=[c.id for c in (tx.categories or [])], category_ids=[c.id for c in (tx.categories or [])],
) )
@@ -35,21 +33,7 @@ async def create_transaction(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
): ):
# Build transaction; set `date` only if provided to let DB default apply otherwise tx = Transaction(amount=payload.amount, description=payload.description, user_id=user.id)
tx_kwargs = dict(
amount=payload.amount,
description=payload.description,
user_id=user.id,
)
if payload.date is not None:
parsed_date = payload.date
if isinstance(parsed_date, str):
try:
parsed_date = date.fromisoformat(parsed_date)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format, expected YYYY-MM-DD")
tx_kwargs["date"] = parsed_date
tx = Transaction(**tx_kwargs)
# Attach categories if provided (and owned by user) # Attach categories if provided (and owned by user)
if payload.category_ids: if payload.category_ids:
@@ -76,18 +60,11 @@ async def create_transaction(
@router.get("/", response_model=List[TransactionRead]) @router.get("/", response_model=List[TransactionRead])
async def list_transactions( async def list_transactions(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
): ):
cond = [Transaction.user_id == user.id]
if start_date is not None:
cond.append(Transaction.date >= start_date)
if end_date is not None:
cond.append(Transaction.date <= end_date)
res = await session.execute( res = await session.execute(
select(Transaction).where(and_(*cond)).order_by(Transaction.date, Transaction.id) select(Transaction).where(Transaction.user_id == user.id).order_by(Transaction.id)
) )
txs = list(res.scalars()) txs = list(res.scalars())
# Eagerly load categories for each transaction # Eagerly load categories for each transaction
@@ -96,36 +73,6 @@ async def list_transactions(
return [_to_read_model(tx) for tx in txs] return [_to_read_model(tx) for tx in txs]
@router.get("/balance_series")
async def get_balance_series(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
cond = [Transaction.user_id == user.id]
if start_date is not None:
cond.append(Transaction.date >= start_date)
if end_date is not None:
cond.append(Transaction.date <= end_date)
res = await session.execute(
select(Transaction).where(and_(*cond)).order_by(Transaction.date, Transaction.id)
)
txs = list(res.scalars())
# Group by date and accumulate
daily = {}
for tx in txs:
key = tx.date.isoformat() if hasattr(tx.date, 'isoformat') else str(tx.date)
daily[key] = daily.get(key, 0.0) + float(tx.amount)
# Build cumulative series sorted by date
series = []
running = 0.0
for d in sorted(daily.keys()):
running += daily[d]
series.append({"date": d, "balance": running})
return series
@router.get("/{transaction_id}", response_model=TransactionRead) @router.get("/{transaction_id}", response_model=TransactionRead)
async def get_transaction( async def get_transaction(
transaction_id: int, transaction_id: int,
@@ -164,14 +111,6 @@ async def update_transaction(
tx.amount = payload.amount tx.amount = payload.amount
if payload.description is not None: if payload.description is not None:
tx.description = payload.description tx.description = payload.description
if payload.date is not None:
new_date = payload.date
if isinstance(new_date, str):
try:
new_date = date.fromisoformat(new_date)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid date format, expected YYYY-MM-DD")
tx.date = new_date
if payload.category_ids is not None: if payload.category_ids is not None:
# Preload categories to avoid async lazy-load during assignment # Preload categories to avoid async lazy-load during assignment

View File

@@ -0,0 +1,61 @@
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.models.user import User
from app.services.user_service import current_active_verified_user
from app.api.auth import router as auth_router
from app.api.categories import router as categories_router
from app.api.transactions import router as transactions_router
from app.services.user_service import auth_backend, current_active_verified_user, fastapi_users, get_oauth_provider
fastApi = FastAPI()
# CORS for frontend dev server
fastApi.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:5173",
"http://127.0.0.1:5173",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
fastApi.include_router(auth_router)
fastApi.include_router(categories_router)
fastApi.include_router(transactions_router)
fastApi.include_router(
fastapi_users.get_oauth_router(
get_oauth_provider("MojeID"),
auth_backend,
"SECRET",
associate_by_email=True,
),
prefix="/auth/mojeid",
tags=["auth"],
)
fastApi.include_router(
fastapi_users.get_oauth_router(
get_oauth_provider("BankID"),
auth_backend,
"SECRET",
associate_by_email=True,
),
prefix="/auth/bankid",
tags=["auth"],
)
# Liveness/root endpoint
@fastApi.get("/", include_in_schema=False)
async def root():
return {"status": "ok"}
@fastApi.get("/authenticated-route")
async def authenticated_route(user: User = Depends(current_active_verified_user)):
return {"message": f"Hello {user.email}!"}

View File

@@ -1,7 +1,5 @@
import os import os
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.base import Base from app.core.base import Base
DATABASE_URL = os.getenv("DATABASE_URL") DATABASE_URL = os.getenv("DATABASE_URL")
@@ -21,11 +19,9 @@ from app.models.user import User
from app.models.transaction import Transaction from app.models.transaction import Transaction
from app.models.categories import Category from app.models.categories import Category
host_env = os.getenv("MARIADB_HOST", "localhost") ssl_enabled = os.getenv("MARIADB_HOST", "localhost") != "localhost"
ssl_enabled = host_env not in {"localhost", "127.0.0.1"}
connect_args = {"ssl": {"ssl": True}} if ssl_enabled else {} connect_args = {"ssl": {"ssl": True}} if ssl_enabled else {}
# Async engine/session for the async parts of the app
engine = create_async_engine( engine = create_async_engine(
DATABASE_URL, DATABASE_URL,
pool_pre_ping=True, pool_pre_ping=True,
@@ -33,13 +29,3 @@ engine = create_async_engine(
connect_args=connect_args, connect_args=connect_args,
) )
async_session_maker = async_sessionmaker(engine, expire_on_commit=False) async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
# Synchronous engine/session for sync utilities (e.g., bank_scraper)
SYNC_DATABASE_URL = DATABASE_URL.replace("+asyncmy", "+pymysql")
engine_sync = create_engine(
SYNC_DATABASE_URL,
pool_pre_ping=True,
echo=os.getenv("SQL_ECHO", "0") == "1",
connect_args=connect_args,
)
sync_session_maker = sessionmaker(bind=engine_sync, expire_on_commit=False)

View File

@@ -7,8 +7,8 @@ from app.core.base import Base
association_table = Table( association_table = Table(
"category_transaction", "category_transaction",
Base.metadata, Base.metadata,
Column("category_id", Integer, ForeignKey("categories.id", ondelete="CASCADE"), primary_key=True), Column("id_category", Integer, ForeignKey("categories.id")),
Column("transaction_id", Integer, ForeignKey("transaction.id", ondelete="CASCADE"), primary_key=True) Column("id_transaction", Integer, ForeignKey("transaction.id"))
) )

View File

@@ -1,24 +1,17 @@
import os
from fastapi_users_db_sqlalchemy import GUID from fastapi_users_db_sqlalchemy import GUID
from sqlalchemy import Column, Integer, String, Float, ForeignKey, Date, func from sqlalchemy import Column, Integer, String, Float, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy_utils import EncryptedType
from sqlalchemy_utils.types.encrypted.encrypted_type import FernetEngine
from app.core.base import Base from app.core.base import Base
from app.models.categories import association_table from app.models.categories import association_table
SECRET_KEY = os.environ.get("DB_ENCRYPTION_KEY", "localdev")
class Transaction(Base): class Transaction(Base):
__tablename__ = "transaction" __tablename__ = "transaction"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
amount = Column(EncryptedType(Float, SECRET_KEY, engine=FernetEngine), nullable=False) amount = Column(Float, nullable=False)
description = Column(EncryptedType(String(length=255), SECRET_KEY, engine=FernetEngine), nullable=True) description = Column(String(length=255), nullable=True)
date = Column(Date, nullable=False, server_default=func.current_date())
user_id = Column(GUID, ForeignKey("user.id"), nullable=False) user_id = Column(GUID, ForeignKey("user.id"), nullable=False)
# Relationship # Relationship
user = relationship("User", back_populates="transactions") user = relationship("User", back_populates="transactions")
categories = relationship("Category", secondary=association_table, back_populates="transactions", passive_deletes=True) categories = relationship("Category", secondary=association_table, back_populates="transactions")

View File

@@ -1,8 +1,6 @@
from sqlalchemy import Column, String from sqlalchemy import Column, String
from sqlalchemy.orm import relationship, mapped_column, Mapped from sqlalchemy.orm import relationship, mapped_column, Mapped
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyBaseOAuthAccountTableUUID from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyBaseOAuthAccountTableUUID
from sqlalchemy.sql.sqltypes import JSON
from app.core.base import Base from app.core.base import Base
@@ -15,7 +13,6 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
first_name = Column(String(length=100), nullable=True) first_name = Column(String(length=100), nullable=True)
last_name = Column(String(length=100), nullable=True) last_name = Column(String(length=100), nullable=True)
oauth_accounts = relationship("OAuthAccount", lazy="joined") oauth_accounts = relationship("OAuthAccount", lazy="joined")
config = Column(JSON, default={})
# Relationship # Relationship
transactions = relationship("Transaction", back_populates="user") transactions = relationship("Transaction", back_populates="user")

View File

@@ -11,7 +11,7 @@ class MojeIDOAuth(CustomOpenID):
super().__init__( super().__init__(
client_id, client_id,
client_secret, client_secret,
"https://mojeid.cz/.well-known/openid-configuration/", "https://mojeid.regtest.nic.cz/.well-known/openid-configuration/",
"MojeID", "MojeID",
base_scopes=["openid", "email", "profile"], base_scopes=["openid", "email", "profile"],
) )

View File

@@ -11,11 +11,6 @@ class CategoryCreate(CategoryBase):
pass pass
class CategoryUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
class CategoryRead(CategoryBase): class CategoryRead(CategoryBase):
id: int id: int
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View File

@@ -1,13 +1,10 @@
from typing import List, Optional, Union from typing import List, Optional
from datetime import date
from pydantic import BaseModel, Field, ConfigDict from pydantic import BaseModel, Field, ConfigDict
class TransactionBase(BaseModel): class TransactionBase(BaseModel):
amount: float = Field(..., gt=-1e18, lt=1e18) amount: float = Field(..., gt=-1e18, lt=1e18)
description: Optional[str] = None description: Optional[str] = None
# accept either ISO date string or date object
date: Optional[Union[date, str]] = None
class TransactionCreate(TransactionBase): class TransactionCreate(TransactionBase):
category_ids: Optional[List[int]] = None category_ids: Optional[List[int]] = None
@@ -15,12 +12,10 @@ class TransactionCreate(TransactionBase):
class TransactionUpdate(BaseModel): class TransactionUpdate(BaseModel):
amount: Optional[float] = Field(None, gt=-1e18, lt=1e18) amount: Optional[float] = Field(None, gt=-1e18, lt=1e18)
description: Optional[str] = None description: Optional[str] = None
# accept either ISO date string or date object
date: Optional[Union[date, str]] = None
category_ids: Optional[List[int]] = None category_ids: Optional[List[int]] = None
class TransactionRead(TransactionBase): class TransactionRead(TransactionBase):
id: int id: int
category_ids: List[int] = [] category_ids: List[int] = []
date: Optional[Union[date, str]]
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)

View File

@@ -1,11 +1,10 @@
import uuid import uuid
from typing import Optional, Dict, Any from typing import Optional
from fastapi_users import schemas from fastapi_users import schemas
class UserRead(schemas.BaseUser[uuid.UUID]): class UserRead(schemas.BaseUser[uuid.UUID]):
first_name: Optional[str] = None first_name: Optional[str] = None
last_name: Optional[str] = None last_name: Optional[str] = None
config: Optional[Dict[str, Any]] = None
class UserCreate(schemas.BaseUserCreate): class UserCreate(schemas.BaseUserCreate):
first_name: Optional[str] = None first_name: Optional[str] = None

View File

@@ -14,7 +14,6 @@ from httpx_oauth.oauth2 import BaseOAuth2
from app.models.user import User from app.models.user import User
from app.oauth.bank_id import BankID from app.oauth.bank_id import BankID
from app.oauth.csas import CSASOAuth
from app.oauth.custom_openid import CustomOpenID from app.oauth.custom_openid import CustomOpenID
from app.oauth.moje_id import MojeIDOAuth from app.oauth.moje_id import MojeIDOAuth
from app.services.db import get_user_db from app.services.db import get_user_db
@@ -33,7 +32,7 @@ providers = {
"BankID": BankID( "BankID": BankID(
os.getenv("BANKID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"), os.getenv("BANKID_CLIENT_ID", "CHANGE_ME_CLIENT_ID"),
os.getenv("BANKID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"), os.getenv("BANKID_CLIENT_SECRET", "CHANGE_ME_CLIENT_SECRET"),
), )
} }
@@ -102,7 +101,7 @@ bearer_transport = BearerTransport(tokenUrl="auth/jwt/login")
def get_jwt_strategy() -> JWTStrategy: def get_jwt_strategy() -> JWTStrategy:
return JWTStrategy(secret=SECRET, lifetime_seconds=604800) return JWTStrategy(secret=SECRET, lifetime_seconds=3600)
auth_backend = AuthenticationBackend( auth_backend = AuthenticationBackend(

View File

@@ -0,0 +1,19 @@
import logging
from celery import shared_task
logger = logging.getLogger("celery_tasks")
if not logger.handlers:
_h = logging.StreamHandler()
logger.addHandler(_h)
logger.setLevel(logging.INFO)
@shared_task(name="workers.send_email")
def send_email(to: str, subject: str, body: str) -> None:
if not (to and subject and body):
logger.error("Email task missing fields. to=%r subject=%r body_len=%r", to, subject, len(body) if body else 0)
return
# Placeholder for real email sending logic
logger.info("[Celery] Email sent | to=%s | subject=%s | body_len=%d", to, subject, len(body))

4
7project/backend/main.py Normal file
View File

@@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", log_level="info")

View File

@@ -38,8 +38,6 @@ MarkupSafe==3.0.2
multidict==6.6.4 multidict==6.6.4
packaging==25.0 packaging==25.0
pamqp==3.3.0 pamqp==3.3.0
prometheus-fastapi-instrumentator==7.1.0
prometheus_client==0.23.1
prompt_toolkit==3.0.52 prompt_toolkit==3.0.52
propcache==0.3.2 propcache==0.3.2
pwdlib==0.2.1 pwdlib==0.2.1
@@ -52,17 +50,14 @@ python-dateutil==2.9.0.post0
python-dotenv==1.1.1 python-dotenv==1.1.1
python-multipart==0.0.20 python-multipart==0.0.20
PyYAML==6.0.2 PyYAML==6.0.2
sentry-sdk==2.42.0
six==1.17.0 six==1.17.0
sniffio==1.3.1 sniffio==1.3.1
SQLAlchemy==2.0.43 SQLAlchemy==2.0.43
SQLAlchemy-Utils==0.42.0
starlette==0.48.0 starlette==0.48.0
tomli==2.2.1 tomli==2.2.1
typing-inspection==0.4.1 typing-inspection==0.4.1
typing_extensions==4.15.0 typing_extensions==4.15.0
tzdata==2025.2 tzdata==2025.2
urllib3==2.5.0
uvicorn==0.37.0 uvicorn==0.37.0
uvloop==0.21.0 uvloop==0.21.0
vine==5.1.0 vine==5.1.0
@@ -70,4 +65,3 @@ watchfiles==1.1.0
wcwidth==0.2.14 wcwidth==0.2.14
websockets==15.0.1 websockets==15.0.1
yarl==1.20.1 yarl==1.20.1
python-json-logger==2.0.7

View File

@@ -0,0 +1,54 @@
Thank you for installing myapp-chart.
This chart packages all Kubernetes manifests from the original deployment directory and parameterizes environment, database name (with optional PR suffix), image, and domain for external access.
Namespaces per developer (important):
- Install each developer's environment into their own namespace using Helm's -n/--namespace flag.
- No hardcoded namespace is used in templates; resources are created in .Release.Namespace.
- Example namespaces: dev-alice, dev-bob, pr-123, etc.
Key values:
- deployment -> used as Database CR name and DB username (MARIADB_DB and MARIADB_USER)
- image.repository/tag or image.digest -> container image
- domain -> public FQDN used by TunnelBinding (required to expose app)
- app/worker names, replicas, ports
Examples:
- Dev install (Alice):
helm upgrade --install myapp ./7project/charts/myapp-chart \
-n dev-alice --create-namespace \
-f values-dev.yaml \
--set domain=alice.demo.example.com \
--set-string rabbitmq.password="$RABBITMQ_PASSWORD" \
--set-string database.password="$DB_PASSWORD"
- Dev install (Bob):
helm upgrade --install myapp ./7project/charts/myapp-chart \
-n dev-bob --create-namespace \
-f values-dev.yaml \
--set domain=bob.demo.example.com
- Prod install (different cleanupPolicy):
helm upgrade --install myapp ./7project/charts/myapp-chart \
-n prod --create-namespace \
-f values-prod.yaml \
--set domain=app.example.com
- PR (preview) install with DB name containing PR number (also its own namespace):
PR=123
helm upgrade --install myapp-pr-$PR ./7project/charts/myapp-chart \
-n pr-$PR --create-namespace \
-f values-dev.yaml \
--set prNumber=$PR \
--set deployment=preview-$PR \
--set domain=pr-$PR.example.com
- Use a custom deployment identifier to suffix DB name, DB username and Secret name:
helm upgrade --install myapp ./7project/charts/myapp-chart \
-n dev-alice --create-namespace \
-f values-dev.yaml \
--set deployment=alice \
--set domain=alice.demo.example.com
Render locally (dry run):
helm template ./7project/charts/myapp-chart -f values-dev.yaml --set prNumber=456 --set deployment=test --set domain=demo.example.com --namespace dev-test | sed -n '/kind: Database/,$p' | head -n 30

View File

@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.app.name }}
spec:
replicas: {{ .Values.app.replicas }}
revisionHistoryLimit: 3
selector:
matchLabels:
app: {{ .Values.app.name }}
template:
metadata:
labels:
app: {{ .Values.app.name }}
spec:
containers:
- name: {{ .Values.app.name }}
image: "{{- if .Values.image.digest -}}{{ .Values.image.repository }}@{{ .Values.image.digest }}{{- else -}}{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}{{- end -}}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
ports:
- containerPort: {{ .Values.app.port }}
env:
- name: MARIADB_HOST
value: "mariadb-repl-maxscale-internal.mariadb-operator.svc.cluster.local"
- name: MARIADB_PORT
value: '3306'
- name: MARIADB_DB
value: {{ required "Set .Values.deployment" .Values.deployment | quote }}
- name: MARIADB_USER
value: {{ required "Set .Values.deployment" .Values.deployment | quote }}
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ required "Set .Values.database.secretName" .Values.database.secretName }}
key: password
- name: RABBITMQ_USERNAME
value: {{ .Values.rabbitmq.username | quote }}
- name: RABBITMQ_PASSWORD
valueFrom:
secretKeyRef:
name: {{ printf "%s-user-credentials" (.Values.rabbitmq.username | default "app-user") }}
key: password
- name: RABBITMQ_HOST
value: {{ printf "%s.%s.svc.cluster.local" "rabbitmq-cluster" .Release.Namespace | quote }}
- name: RABBITMQ_PORT
value: {{ .Values.rabbitmq.port | quote }}
- name: RABBITMQ_VHOST
value: {{ .Values.rabbitmq.vhost | default "/" | quote }}
- name: MAIL_QUEUE
value: {{ .Values.worker.mailQueueName | default "mail_queue" | quote }}
livenessProbe:
httpGet:
path: /
port: {{ .Values.app.port }}
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: {{ .Values.app.port }}
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3

View File

@@ -2,12 +2,9 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: {{ .Values.app.name }} name: {{ .Values.app.name }}
labels:
app: {{ .Values.app.name }}
spec: spec:
ports: ports:
- name: http - port: {{ .Values.service.port }}
port: {{ .Values.service.port }}
targetPort: {{ .Values.app.port }} targetPort: {{ .Values.app.port }}
selector: selector:
app: {{ .Values.app.name }} app: {{ .Values.app.name }}

View File

@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ printf "%s-worker" .Values.app.name }}
spec:
replicas: {{ .Values.worker.replicas }}
revisionHistoryLimit: 3
selector:
matchLabels:
app: {{ printf "%s-worker" .Values.app.name }}
template:
metadata:
labels:
app: {{ printf "%s-worker" .Values.app.name }}
spec:
containers:
- name: {{ printf "%s-worker" .Values.app.name }}
image: "{{- if .Values.image.digest -}}{{ .Values.image.repository }}@{{ .Values.image.digest }}{{- else -}}{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}{{- end -}}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
command:
- celery
- -A
- app.celery_app
- worker
- -Q
- $(MAIL_QUEUE)
- --loglevel
- INFO
env:
- name: RABBITMQ_USERNAME
value: {{ .Values.rabbitmq.username | quote }}
- name: RABBITMQ_PASSWORD
valueFrom:
secretKeyRef:
name: {{ printf "%s-user-credentials" (.Values.rabbitmq.username | default "app-user") }}
key: password
- name: RABBITMQ_HOST
value: {{ printf "%s.%s.svc.cluster.local" "rabbitmq-cluster" .Release.Namespace | quote }}
- name: RABBITMQ_PORT
value: {{ .Values.rabbitmq.port | quote }}
- name: RABBITMQ_VHOST
value: {{ .Values.rabbitmq.vhost | default "/" | quote }}
- name: MAIL_QUEUE
value: {{ .Values.worker.mailQueueName | default "mail_queue" | quote }}

View File

@@ -5,6 +5,3 @@ app:
worker: worker:
replicas: 3 replicas: 3
cron:
enabled: true

View File

@@ -11,15 +11,6 @@ deployment: ""
# Public domain to expose the app under (used by TunnelBinding fqdn) # Public domain to expose the app under (used by TunnelBinding fqdn)
# Set at install time: --set domain=example.com # Set at install time: --set domain=example.com
domain: "" domain: ""
domain_scheme: ""
unirate:
key: ""
frontend_domain: ""
frontend_domain_scheme: ""
sentry_dsn: ""
image: image:
repository: lukastrkan/cc-app-demo repository: lukastrkan/cc-app-demo
@@ -38,38 +29,10 @@ worker:
# Queue name for Celery worker and for CRD Queue # Queue name for Celery worker and for CRD Queue
mailQueueName: "mail_queue" mailQueueName: "mail_queue"
cron:
enabled: false
schedule: "*/5 * * * *" # every 5 minutes
scheme: "http"
endpoint: "/_cron"
concurrencyPolicy: "Forbid"
smtp:
host:
port: 587
username: ""
password: ""
tls: false
ssl: false
from: ""
service: service:
port: 80 port: 80
oauth:
bankid:
clientId: ""
clientSecret: ""
mojeid:
clientId: ""
clientSecret: ""
csas:
clientId: ""
clientSecret: ""
rabbitmq: rabbitmq:
create: true create: true
replicas: 1 replicas: 1
@@ -95,4 +58,3 @@ database:
userName: app-demo-user userName: app-demo-user
secretName: app-demo-database-secret secretName: app-demo-database-secret
password: "" password: ""
encryptionSecret: ""

View File

@@ -7,64 +7,64 @@ Focus on areas that align with your project goals and interests.
The core deliverables are required. The core deliverables are required.
This means that you must get at least 2 points for each item in this category. This means that you must get at least 2 points for each item in this category.
| **Category** | **Item** | **Max Points** | **Points** | **Comments** | | **Category** | **Item** | **Max Points** | **Points** |
|----------------------------------| --------------------------------------- | -------------- |-------------------------------------------------| | | -------------------------------- | --------------------------------------- | -------------- | ---------------- |
| **Core Deliverables (Required)** | | | | | | **Core Deliverables (Required)** | | | |
| Codebase & Organization | Well-organized project structure | 5 | 5 | | | Codebase & Organization | Well-organized project structure | 5 | |
| | Clean, readable code | 5 | 4 | | | | Clean, readable code | 5 | |
| | Use planning tool (e.g., GitHub issues) | 5 | 4 | | | | Use planning tool (e.g., GitHub issues) | 5 | |
| | Proper version control usage | 5 | 5 | | | | Proper version control usage | 5 | |
| 23 | Complete source code | 5 | 5 | | | | Complete source code | 5 | |
| Documentation | Comprehensive reproducibility report | 10 | 4-5 | | | Documentation | Comprehensive reproducibility report | 10 | |
| | Updated design document | 5 | 2 | | | | Updated design document | 5 | |
| | Clear build/deployment instructions | 5 | 2 | | | | Clear build/deployment instructions | 5 | |
| | Troubleshooting guide | 5 | 1 | | | | Troubleshooting guide | 5 | |
| | Completed self-assessment table | 5 | 2 | | | | Completed self-assessment table | 5 | |
| 14 | Hour sheets for all members | 5 | 3 | | | | Hour sheets for all members | 5 | |
| Presentation Video | Project demonstration | 5 | 0 | | | Presentation Video | Project demonstration | 5 | |
| | Code walk-through | 5 | 0 | | | | Code walk-through | 5 | |
| 0 | Deployment showcase | 5 | 0 | | | | Deployment showcase | 5 | |
| **Technical Implementation** | | | | | | **Technical Implementation** | | | |
| Application Functionality | Basic functionality works | 10 | 8 | | | Application Functionality | Basic functionality works | 10 | |
| | Advanced features implemented | 10 | 0 | | | | Advanced features implemented | 10 | |
| | Error handling & robustness | 10 | 4 | | | | Error handling & robustness | 10 | |
| 16 | User-friendly interface | 5 | 4 | | | | User-friendly interface | 5 | |
| Backend & Architecture | Stateless web server | 5 | 5 | | | Backend & Architecture | Stateless web server | 5 | |
| | Stateful application | 10 | ? WHAT DOES THIS MEAN | | | | Stateful application | 10 | |
| | Database integration | 10 | 10 | | | | Database integration | 10 | |
| | API design | 5 | 5 | | | | API design | 5 | |
| 20 | Microservices architecture | 10 | 0 | | | | Microservices architecture | 10 | |
| Cloud Integration | Basic cloud deployment | 10 | 10 | | | Cloud Integration | Basic cloud deployment | 10 | |
| | Cloud APIs usage | 10 | ? WHAT DOES THIS MEAN | | | | Cloud APIs usage | 10 | |
| | Serverless components | 10 | 0 | | | | Serverless components | 10 | |
| 10 | Advanced cloud services | 5 | 0 | | | | Advanced cloud services | 5 | |
| **DevOps & Deployment** | | | | | | **DevOps & Deployment** | | | |
| Containerization | Basic Dockerfile | 5 | 5 | | | Containerization | Basic Dockerfile | 5 | |
| | Optimized Dockerfile | 5 | 0 | | | | Optimized Dockerfile | 5 | |
| | Docker Compose | 5 | 5 - dev only | | | | Docker Compose | 5 | |
| 15 | Persistent storage | 5 | 5 | | | | Persistent storage | 5 | |
| Deployment & Scaling | Manual deployment | 5 | 5 | | | Deployment & Scaling | Manual deployment | 5 | |
| | Automated deployment | 5 | 5 | | | | Automated deployment | 5 | |
| | Multiple replicas | 5 | 5 | | | | Multiple replicas | 5 | |
| 20 | Kubernetes deployment | 10 | 10 | | | | Kubernetes deployment | 10 | |
| **Quality Assurance** | | | | | | **Quality Assurance** | | | |
| Testing | Unit tests | 5 | 2 | | | Testing | Unit tests | 5 | |
| | Integration tests | 5 | 2 | | | | Integration tests | 5 | |
| | End-to-end tests | 5 | 5 | | | | End-to-end tests | 5 | |
| 9 | Performance testing | 5 | 0 | | | | Performance testing | 5 | |
| Monitoring & Operations | Health checks | 5 | 5 | | | Monitoring & Operations | Health checks | 5 | |
| | Logging | 5 | 2 - only to terminal add logstash | | | | Logging | 5 | |
| 9 | Metrics/Monitoring | 5 | 2 - only DB, need to create Prometheus endpoint | | | | Metrics/Monitoring | 5 | |
| Security | HTTPS/TLS | 5 | 5 | | | Security | HTTPS/TLS | 5 | |
| | Authentication | 5 | 5 | | | | Authentication | 5 | |
| 15 | Authorization | 5 | 5 | | | | Authorization | 5 | |
| **Innovation & Excellence** | | | | | | **Innovation & Excellence** | | | |
| Advanced Features and | AI/ML Integration | 10 | 0 | | | Advanced Features and | AI/ML Integration | 10 | |
| Technical Excellence | Real-time features | 10 | 0 | | | Technical Excellence | Real-time features | 10 | |
| | Creative problem solving | 10 | ? | | | | Creative problem solving | 10 | |
| | Performance optimization | 5 | 2 | | | | Performance optimization | 5 | |
| 2 | Exceptional user experience | 5 | 0 | | | | Exceptional user experience | 5 | |
| **Total** | | **255** | **153** | | | **Total** | | **255** | **[Your Total]** |
## Grading Scale ## Grading Scale

View File

@@ -0,0 +1,20 @@
apiVersion: k8s.mariadb.com/v1alpha1
kind: Grant
metadata:
name: grant
spec:
mariaDbRef:
name: mariadb-repl
namespace: mariadb-operator
privileges:
- "ALL PRIVILEGES"
database: "app-demo-database"
table: "*"
username: "app-demo-user"
grantOption: true
host: "%"
# Delete the resource in the database whenever the CR gets deleted.
# Alternatively, you can specify Skip in order to omit deletion.
cleanupPolicy: Skip
requeueInterval: 10h
retryInterval: 30s

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: app-demo-database-secret
type: kubernetes.io/basic-auth
stringData:
password: "strongpassword"

View File

@@ -0,0 +1,20 @@
apiVersion: k8s.mariadb.com/v1alpha1
kind: User
metadata:
name: app-demo-user
spec:
# If you want the user to be created with a different name than the resource name
# name: user-custom
mariaDbRef:
name: mariadb-repl
namespace: mariadb-operator
passwordSecretKeyRef:
name: app-demo-database-secret
key: password
maxUserConnections: 20
host: "%"
# Delete the resource in the database whenever the CR gets deleted.
# Alternatively, you can specify Skip in order to omit deletion.
cleanupPolicy: Skip
requeueInterval: 10h
retryInterval: 30s

View File

@@ -0,0 +1,15 @@
apiVersion: k8s.mariadb.com/v1alpha1
kind: Database
metadata:
name: app-demo-database
spec:
mariaDbRef:
name: mariadb-repl
namespace: mariadb-operator
characterSet: utf8
collate: utf8_general_ci
# Delete the resource in the database whenever the CR gets deleted.
# Alternatively, you can specify Skip in order to omit deletion.
cleanupPolicy: Skip
requeueInterval: 10h
retryInterval: 30s

View File

@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-demo
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: app-demo
template:
metadata:
labels:
app: app-demo
spec:
containers:
- image: lukastrkan/cc-app-demo@sha256:75634b4d97282b6b8424fe17767c81adf44af5f7359c1d25883073b5629b3e05
name: app-demo
ports:
- containerPort: 8000
env:
- name: MARIADB_HOST
value: mariadb-repl.mariadb-operator.svc.cluster.local
- name: MARIADB_PORT
value: '3306'
- name: MARIADB_DB
value: app-demo-database
- name: MARIADB_USER
value: app-demo-user
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: app-demo-database-secret
key: password
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: Service
metadata:
name: app-demo
spec:
ports:
- port: 80
targetPort: 8000
selector:
app: app-demo

View File

@@ -0,0 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-demo-worker
spec:
replicas: 3
revisionHistoryLimit: 3
selector:
matchLabels:
app: app-demo-worker
template:
metadata:
labels:
app: app-demo-worker
spec:
containers:
- image: lukastrkan/cc-app-demo@sha256:75634b4d97282b6b8424fe17767c81adf44af5f7359c1d25883073b5629b3e05
name: app-demo-worker
command:
- celery
- -A
- app.celery_app
- worker
- -Q
- $(MAIL_QUEUE)
- --loglevel
- INFO
env:
- name: RABBITMQ_USERNAME
value: demo-app
- name: RABBITMQ_PASSWORD
valueFrom:
secretKeyRef:
name: demo-app-user-credentials
key: password
- name: RABBITMQ_HOST
value: rabbitmq.rabbitmq.svc.cluster.local
- name: RABBITMQ_PORT
value: '5672'
- name: RABBITMQ_VHOST
value: "/"

View File

@@ -0,0 +1,14 @@
apiVersion: networking.cfargotunnel.com/v1alpha1
kind: TunnelBinding
metadata:
name: guestbook-tunnel-binding
namespace: group-project
subjects:
- name: app-server
spec:
target: http://app-demo.group-project.svc.cluster.local
fqdn: demo.ltrk.cz
noTlsVerify: true
tunnelRef:
kind: ClusterTunnel
name: cluster-tunnel

View File

@@ -9,8 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1"
"recharts": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
@@ -1048,32 +1047,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@reduxjs/toolkit": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.1.tgz",
"integrity": "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.38", "version": "1.0.0-beta.38",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz",
@@ -1389,18 +1362,6 @@
"win32" "win32"
] ]
}, },
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1446,69 +1407,6 @@
"@babel/types": "^7.28.2" "@babel/types": "^7.28.2"
} }
}, },
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1537,7 +1435,7 @@
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
"integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1553,12 +1451,6 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.45.0", "version": "8.45.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
@@ -2037,15 +1929,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2099,130 +1982,9 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -2241,12 +2003,6 @@
} }
} }
}, },
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2261,16 +2017,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/es-toolkit": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz",
"integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.10", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
@@ -2514,12 +2260,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2723,16 +2463,6 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz",
"integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2760,15 +2490,6 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3204,36 +2925,6 @@
"react": "^19.2.0" "react": "^19.2.0"
} }
}, },
"node_modules/react-is": {
"version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz",
"integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
"license": "MIT",
"peer": true
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -3244,54 +2935,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/recharts": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz",
"integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==",
"license": "MIT",
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3454,12 +3097,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -3633,41 +3270,10 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "7.1.11", "version": "7.1.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -11,8 +11,7 @@
}, },
"dependencies": { "dependencies": {
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1"
"recharts": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -0,0 +1,39 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { BACKEND_URL } from './config'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
<p style={{ fontSize: 12, color: '#888' }}>
Backend URL: <code>{BACKEND_URL || '(not configured)'}</code>
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,2 @@
export const BACKEND_URL: string =
import.meta.env.VITE_BACKEND_URL ?? '';

View File

@@ -24,6 +24,8 @@ a:hover {
body { body {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
} }

View File

@@ -1,11 +1,7 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import './index.css' import './index.css'
import './ui.css'
import App from './App.tsx' import App from './App.tsx'
import { applyAppearanceFromStorage } from './appearance'
applyAppearanceFromStorage()
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>

View File

@@ -3,5 +3,5 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) })

View File

@@ -1,53 +0,0 @@
# Weekly Meeting Notes
- Group 8 - Personal finance tracker
- Mentor: Jaychander
Keep all meeting notes in the `meetings.md` file in your project folder.
Just copy the template below for each weekly meeting and fill in the details.
## Administrative Info
- Date: 2025-10-16
- Attendees: Dejan Ribarovski, Lukas Trkan
- Notetaker: Dejan Ribarovski
## Progress Update (Before Meeting)
Summary of what has been accomplished since the last meeting in the following categories.
## Action Items from Last Week (During Meeting)
- [x] start coding the app logic
- [x] start writing the report so it matches the actual progress
- [x] redo the system diagram so it includes a response flow
### Coding
Implemented initial functioning version of the app, added OAuth with BankId and MojeID,
added database snapshots.
### Documentation
report.md is up to date
## Questions and Topics for Discussion (Before Meeting)
Prepare 3-5 questions and topics you want to discuss with your mentor.
1. What other functionality should be added to the app
2. Priority for the next week (Testing maybe?)
3. Question 3
## Discussion Notes (During Meeting)
## Action Items for Next Week (During Meeting)
Last 3 minutes of the meeting, summarize action items.
- [x] OAuth
- [x] CI/CD fix
- [ ] Database local (multiple bank accounts)
- [ ] Add tests and set up github pipeline
- [ ] Frontend imporvment - user experience
- [ ] make the report more clear
---

View File

@@ -1,54 +0,0 @@
# Weekly Meeting Notes
- Group 8 - Personal finance tracker
- Mentor: Jaychander
Keep all meeting notes in the `meetings.md` file in your project folder.
Just copy the template below for each weekly meeting and fill in the details.
## Administrative Info
- Date: 2025-10-23
- Attendees: Dejan
- Notetaker: Dejan
## Progress Update (Before Meeting)
Last 3 minutes of the meeting, summarize action items.
- [x] OAuth (BankID)
- [x] CI/CD fix
- [X] Database local (multiple bank accounts)
- [X] Add tests and set up github pipeline
- [X] Frontend imporvment - user experience
- [ ] make the report more clear - partly
Summary of what has been accomplished since the last meeting in the following categories.
### Coding
Improved Frontend, added Mock Bank, fixed deployment, fixed OAuth(BankID) on production, added basic tests
### Documentation
Not much - just updated the work done
## Questions and Topics for Discussion (Before Meeting)
This was not prepared, I planned to do it right before meeting, but Jaychander needed to go somewhere earlier.
1. Question 1
2. Question 2
3. Question 3
## Discussion Notes (During Meeting)
The tracker should not store the transactions in the database - security vulnerability.
## Action Items for Next Week (During Meeting)
Last 3 minutes of the meeting, summarize action items.
- [ ] Dont store data in database (security) - Load it on login (from CSAS API and local database), load automatically with email
- [ ] Go through the checklist
- [ ] Look for possible APIs (like stocks or financial details whatever)
- [ ] Report
---

View File

@@ -1,51 +0,0 @@
# Weekly Meeting Notes
- Group 8 - Personal finance tracker
- Mentor: Jaychander
Keep all meeting notes in the `meetings.md` file in your project folder.
Just copy the template below for each weekly meeting and fill in the details.
## Administrative Info
- Date: 2025-10-30
- Attendees: Dejan, Lukas
- Notetaker: Dejan
## Progress Update (Before Meeting)
Last 3 minutes of the meeting, summarize action items.
- [ ] Dont store data in database (security) - Load it on login (from CSAS API and local database), load automatically with email
- [X] Go through the checklist
- [X] Look for possible APIs (like stocks or financial details whatever)
- [ ] Report - partly
Summary of what has been accomplished since the last meeting in the following categories.
### Coding
Implemented CSAS API transactions fetch, Added tests with testing database on github actions, redone UI,
added currency exchange rate with CNB API
### Documentation
Not much - just updated the work done
## Questions and Topics for Discussion (Before Meeting)
1. Security regarding storing transactions - possibility of encryption
2. Realisticaly what needs to be done for us to be done
3. Question 3
## Discussion Notes (During Meeting)
The tracker should not store the transactions in the database - security vulnerability.
## Action Items for Next Week (During Meeting)
Last 3 minutes of the meeting, summarize action items.
- [x] Change the name on frontend from 7project
- [x] Finalize the funcionality and everyting in the code part
- [ ] Try to finalize report with focus on reproducibility
- [ ] More high level explanation of the workflow in the report
---

View File

@@ -1,47 +0,0 @@
# Weekly Meeting Notes
- Group 8 - Personal finance tracker
- Mentor: Jaychander
Keep all meeting notes in the `meetings.md` file in your project folder.
Just copy the template below for each weekly meeting and fill in the details.
## Administrative Info
- Date: 2025-10-30
- Attendees: Dejan, Lukas
- Notetaker: Dejan
## Progress Update (Before Meeting)
Last 3 minutes of the meeting, summarize action items.
- [x] Change the name on frontend from 7project
- [x] Finalize the funcionality and everyting in the code part
- [x] Try to finalize report with focus on reproducibility
- [x] More high level explanation of the workflow in the report
Summary of what has been accomplished since the last meeting in the following categories.
### Coding
### Documentation
## Questions and Topics for Discussion (Before Meeting)
## Discussion Notes (During Meeting)
The tracker should not store the transactions in the database - security vulnerability.
## Action Items for Next Week (During Meeting)
Last 3 minutes of the meeting, summarize action items.
- [ ] video
- [ ] highlight the optional stuff in the report
---

Some files were not shown because too many files have changed in this diff Show More