Initial commit

This commit is contained in:
icecheng 2025-11-12 08:37:55 +00:00
commit b8245ccdfd
72 changed files with 8984 additions and 0 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
# API Settings
APP_VERSION=1.0.0
ENVIRONMENT=dev
# Server Settings
UVICORN_HOST=0.0.0.0
UVICORN_PORT=8888
# CORS Settings
BACKEND_CORS_ORIGINS=http://localhost:3000,http://localhost:8080,http://localhost:5173
# Application Settings
PROJECT_NAME=template-test4
# Logging
LOGGING_LEVEL=INFO
FREELEAPS_PRODUCT_ID=#{{FREELEAPS_PRODUCT_ID}}

View File

@ -0,0 +1,34 @@
# Use Python 3.10 slim image as base
FROM python:3.10-slim
# Set working directory
WORKDIR /app
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
POETRY_VERSION=1.7.1
# Install system dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gcc \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements file
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose the port the app runs on
EXPOSE 8888
# Command to run the application
# Must use port 8888. Using a different port will cause Prometheus metrics collection to fail,
# and metrics data will not be visible on the ops page of the freeleaps.com platform.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8888"]

View File

@ -0,0 +1,6 @@
apiVersion: v2
name: template-test4
description: A Helm chart for FastAPI and Vue.js application
type: application
version: 0.1.0
appVersion: "1.0.0"

View File

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "app.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "app.labels" -}}
helm.sh/chart: {{ include "app.chart" . }}
{{ include "app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,88 @@
{{ namespace := .Release.Namespace }}
{{ appVersion := .Chart.AppVersion | quote }}
{{ releaseService := .Release.Service }}
{{ releaseName := .Release.Name }}
{{- if .Values.templateTest4ApiServer.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-api-server
labels:
{{- include "app.labels" . | nindent 4 }}
component: api-server
spec:
replicas: {{ .Values.templateTest4ApiServer.replicas }}
selector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
component: api-server
template:
metadata:
labels:
{{- include "app.selectorLabels" . | nindent 8 }}
component: api-server
spec:
serviceAccountName: {{ include "app.serviceAccountName" . }}
containers:
- name: api-server
image: "{{ .Values.templateTest4ApiServer.image.registry | default .Values.global.registry }}/{{ .Values.templateTest4ApiServer.image.repository | default .Values.global.repository }}/{{ .Values.templateTest4ApiServer.image.name }}:{{ .Values.templateTest4ApiServer.image.tag }}"
imagePullPolicy: {{ .Values.templateTest4ApiServer.image.imagePullPolicy }}
ports:
{{- toYaml .Values.templateTest4ApiServer.ports | nindent 12 }}
resources:
{{- toYaml .Values.templateTest4ApiServer.resources | nindent 12 }}
env:
{{- toYaml .Values.templateTest4ApiServer.env | nindent 12 }}
{{- with .Values.templateTest4ApiServer.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.templateTest4ApiServer.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: template-test4-api-server
labels:
{{- include "app.labels" . | nindent 4 }}
component: api-server
spec:
type: {{ .Values.service.type }}
ports:
- port: 8888
targetPort: http
protocol: TCP
name: api-server
selector:
{{- include "app.selectorLabels" . | nindent 4 }}
component: api-server
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ .Values.templateTest4ApiServer.serviceMonitor.name }}
namespace: freeleaps-monitoring-system
labels:
app.kubernetes.io/version: {{ appVersion }}
app.kubernetes.io/name: {{ .Values.templateTest4ApiServer.serviceMonitor.name }}
app.kubernetes.io/managed-by: {{ releaseService }}
app.kubernetes.io/instance: {{ releaseName }}
release: kube-prometheus-stack
spec:
endpoints:
- path: /api/_/metrics
targetPort: 8888
interval: 30s
scrapeTimeout: ''
namespaceSelector:
matchNames:
- {{ namespace | quote }}
selector:
matchLabels:
app.kubernetes.io/name: template-test4
app.kubernetes.io/instance: {{ releaseName }}
component: api-server
{{- end }}

View File

@ -0,0 +1,8 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "app.serviceAccountName" . }}
labels:
{{- include "app.labels" . | nindent 4 }}
{{- end }}

View File

@ -0,0 +1,58 @@
{{- if .Values.templateTest4WebServer.enabled -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-web-server
labels:
{{- include "app.labels" . | nindent 4 }}
component: web-server
spec:
replicas: {{ .Values.templateTest4WebServer.replicas }}
selector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
component: web-server
template:
metadata:
labels:
{{- include "app.selectorLabels" . | nindent 8 }}
component: web-server
spec:
serviceAccountName: {{ include "app.serviceAccountName" . }}
containers:
- name: web-server
image: "{{ .Values.templateTest4WebServer.image.registry | default .Values.global.registry }}/{{ .Values.templateTest4WebServer.image.repository | default .Values.global.repository }}/{{ .Values.templateTest4WebServer.image.name }}:{{ .Values.templateTest4WebServer.image.tag }}"
imagePullPolicy: {{ .Values.templateTest4WebServer.image.imagePullPolicy }}
ports:
{{- toYaml .Values.templateTest4WebServer.ports | nindent 12 }}
resources:
{{- toYaml .Values.templateTest4WebServer.resources | nindent 12 }}
env:
{{- toYaml .Values.templateTest4WebServer.env | nindent 12 }}
{{- with .Values.templateTest4WebServer.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.templateTest4WebServer.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: template-test4-web-server
labels:
{{- include "app.labels" . | nindent 4 }}
component: web-server
spec:
type: {{ .Values.service.type }}
ports:
- port: 80
targetPort: http
protocol: TCP
name: web-server
selector:
{{- include "app.selectorLabels" . | nindent 4 }}
component: web-server
{{- end }}

View File

@ -0,0 +1,106 @@
# Default values for the Helm chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Global settings
global:
registry: docker.io
repository: freeleapsdevops
nodeSelector: {}
# Name override settings
nameOverride: ""
fullnameOverride: ""
templateTest4ApiServer:
enabled: true
name: template-test4-api-server
replicas: 1
image:
registry: ""
repository: ""
name: api-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8888
protocol: TCP
resources:
requests:
cpu: "0.2"
memory: "256Mi"
limits:
cpu: "0.5"
memory: "512Mi"
env:
- name: PYTHONUNBUFFERED
value: "1"
- name: DEBUG
value: "false"
- name: FREELEAPS_PRODUCT_ID
value: #{{FREELEAPS_PRODUCT_ID}}
- name: ENVIRONMENT
value: alpha
livenessProbe:
httpGet:
path: /api/v1/_/livez
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/_/readyz
port: http
initialDelaySeconds: 5
periodSeconds: 5
serviceMonitor:
name: template-test4-api-server-alpha-monitor
templateTest4WebServer:
enabled: true
name: template-test4-web-server
replicas: 1
image:
registry: ""
repository: ""
name: web-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
cpu: "0.1"
memory: "128Mi"
limits:
cpu: "0.3"
memory: "256Mi"
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: API_SERVER_URL
value: "http://template-test4-api-server.#{{FREELEAPS_PRODUCT_ID}}-alpha.svc.freeleaps.cluster:8888"
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
serviceAccount:
create: true
name: ""
service:
type: ClusterIP

View File

@ -0,0 +1,106 @@
# Default values for the Helm chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Global settings
global:
registry: docker.io
repository: freeleapsdevops
nodeSelector: {}
# Name override settings
nameOverride: ""
fullnameOverride: ""
templateTest4ApiServer:
enabled: true
name: template-test4-api-server
replicas: 1
image:
registry: ""
repository: ""
name: api-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8888
protocol: TCP
resources:
requests:
cpu: "0.2"
memory: "256Mi"
limits:
cpu: "0.5"
memory: "512Mi"
env:
- name: PYTHONUNBUFFERED
value: "1"
- name: DEBUG
value: "false"
- name: FREELEAPS_PRODUCT_ID
value: #{{FREELEAPS_PRODUCT_ID}}
- name: ENVIRONMENT
value: prod
livenessProbe:
httpGet:
path: /api/v1/_/livez
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/_/readyz
port: http
initialDelaySeconds: 5
periodSeconds: 5
templateTest4WebServer:
enabled: true
name: template-test4-web-server
replicas: 1
image:
registry: ""
repository: ""
name: web-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
cpu: "0.1"
memory: "128Mi"
limits:
cpu: "0.3"
memory: "256Mi"
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: API_SERVER_URL
value: "http://template-test4-api-server.#{{FREELEAPS_PRODUCT_ID}}-prod.svc.freeleaps.cluster:8888"
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
serviceMonitor:
name: template-test4-api-server-prod-monitor
serviceAccount:
create: true
name: ""
service:
type: ClusterIP

View File

@ -0,0 +1,110 @@
# Default values for the Helm chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Global settings
global:
registry: docker.io
repository: freeleapsdevops
nodeSelector: {}
# Name override settings
nameOverride: ""
fullnameOverride: ""
templateTest4ApiServer:
enabled: true
name: template-test4-api-server
replicas: 1
image:
registry: ""
repository: ""
name: api-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8888
protocol: TCP
resources:
requests:
cpu: "0.2"
memory: "256Mi"
limits:
cpu: "0.5"
memory: "512Mi"
env:
- name: PYTHONUNBUFFERED
value: "1"
- name: DEBUG
value: "false"
- name: FREELEAPS_PRODUCT_ID
value: FREELEAPS_PRODUCT_ID
- name: ENVIRONMENT
value: ""
livenessProbe:
httpGet:
path: /api/v1/_/livez
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/v1/_/readyz
port: http
initialDelaySeconds: 5
periodSeconds: 5
serviceMonitor:
name: template-test4-api-server-monitor
templateTest4WebServer:
enabled: true
name: template-test4-web-server
replicas: 1
image:
registry: ""
repository: ""
name: web-server
tag: latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
requests:
cpu: "0.1"
memory: "128Mi"
limits:
cpu: "0.3"
memory: "256Mi"
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: API_SERVER_URL
value: ""
- name: FREELEAPS_PRODUCT_ID
value: #{{FREELEAPS_PRODUCT_ID}}
- name: ENVIRONMENT
value: alpha
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
serviceAccount:
create: true
name: ""
service:
type: ClusterIP

View File

@ -0,0 +1,44 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_proxied any;
# Cache control for static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, no-transform";
access_log off;
}
# Handle Vue router history mode
location / {
try_files $uri $uri/ /index.html;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
# Proxy API requests to the backend
location /api/ {
proxy_pass ${API_SERVER_URL};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@ -0,0 +1,11 @@
#!/bin/sh
set -e
# Default value for API_SERVER_URL if not provided
API_SERVER_URL=${API_SERVER_URL:-http://api-server:8888/}
# Replace the environment variable in the nginx config
envsubst '${API_SERVER_URL}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# Start nginx
exec nginx -g 'daemon off;'

View File

@ -0,0 +1,21 @@
# Use nginx alpine as base image
FROM nginx:stable-alpine
# Install envsubst
RUN apk add --no-cache gettext
# Copy pre-built dist files into nginx
COPY dist /usr/share/nginx/html
# Copy nginx configuration template and entry script
COPY nginx/default.conf /etc/nginx/conf.d/default.conf.template
COPY docker-entrypoint.sh /docker-entrypoint.sh
# Make the entry script executable
RUN chmod ug+x /docker-entrypoint.sh
# Expose port 80
EXPOSE 80
# Use the entry script as the entrypoint
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@ -0,0 +1,48 @@
apiVersion: freeleaps.com/v1alpha1
kind: DevOpsProjectDotfile
metadata:
name: template-test4
annotations:
freeleaps.com/ref-devops-project-uid: ""
spec:
layout: FAST_API_VUE_3
serviceName: template-test4
executeMode: on-demand
branch: main
components:
- name: templateTest4ApiServer
root: '.'
language: python
dependenciesManager: pip
requirementsFile: requirements.txt
buildCacheEnabled: true
buildAgentImage: python:3.10-slim-buster
buildArtifacts:
- '.'
imageBuilder: dind
dockerfilePath: .freeleaps/devops/api-server.Dockerfile
imageName: template-test4-api-server
imageBuildRoot: '.'
imageReleaseArchitectures:
- linux/amd64
- linux/arm64/v8
- name: templateTest4WebServer
root: 'web'
language: javascript
dependenciesManager: pnpm
pnpmPackageJsonFile: package.json
buildCacheEnabled: true
buildAgentImage: node:lts
buildCommand: 'pnpm -r build'
buildArtifacts:
- 'dist'
- 'public'
imageBuilder: dind
dockerfilePath: ../.freeleaps/devops/web-server.Dockerfile
imageName: template-test4-web-server
imageBuildRoot: '.'
imageReleaseArchitectures:
- linux/amd64
- linux/arm64/v8

View File

@ -0,0 +1,48 @@
apiVersion: freeleaps.com/v1alpha1
kind: DevOpsProjectDotfile
metadata:
name: template-test4
annotations:
freeleaps.com/ref-devops-project-uid: ""
spec:
layout: FAST_API_VUE_3
serviceName: template-test4
executeMode: on-demand
branch: main
components:
- name: templateTest4ApiServer
root: '.'
language: python
dependenciesManager: pip
requirementsFile: requirements.txt
buildCacheEnabled: true
buildAgentImage: python:3.10-slim-buster
buildArtifacts:
- '.'
imageBuilder: dind
dockerfilePath: .freeleaps/devops/api-server.Dockerfile
imageName: template-test4-api-server
imageBuildRoot: '.'
imageReleaseArchitectures:
- linux/amd64
- linux/arm64/v8
- name: templateTest4WebServer
root: 'web'
language: javascript
dependenciesManager: pnpm
pnpmPackageJsonFile: package.json
buildCacheEnabled: true
buildAgentImage: node:lts
buildCommand: 'pnpm -r build'
buildArtifacts:
- 'dist'
- 'public'
imageBuilder: dind
dockerfilePath: ../.freeleaps/devops/web-server.Dockerfile
imageName: template-test4-web-server
imageBuildRoot: '.'
imageReleaseArchitectures:
- linux/amd64
- linux/arm64/v8

54
.gitignore vendored Normal file
View File

@ -0,0 +1,54 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
.env
.venv
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# FastAPI
.pytest_cache/
coverage.xml
.coverage
htmlcov/
# Logs
*.log
logs/
# Local development
.env.local
.env.development.local
.env.test.local
.env.production.local
# Docker
docker-compose.override.yml

68
README.md Normal file
View File

@ -0,0 +1,68 @@
# template-test4
This repo create with `FastAPI` and `Vue 3`, powered by `freeleaps.com`.
> **Please do not delete files under `${PROJECT_ROOT}/.freeleaps`**, these files used to supports DevOps workflow.
## Project Layout
```
├── .freeleaps/ # Freeleaps configuration
│ ├── devops/ # DevOps related configurations
│ └── project.yaml # Project configuration file
├── app/ # FastAPI backend application
│ ├── modules/ # Application modules
│ │ ├── sys/ # System related modules
│ │ └── __init__.py # Module initialization
│ ├── utils/ # Utility functions
│ ├── routes.py # API route definitions
│ ├── schema.py # Pydantic schemas
│ └── setup_app.py # Application setup and configuration
├── web/ # Vue 3 frontend application
│ ├── src/ # Source code
│ │ ├── assets/ # Static assets
│ │ ├── components/ # Vue components
│ │ ├── router/ # Vue router configuration
│ │ ├── stores/ # Pinia stores
│ │ ├── views/ # Page views
│ │ ├── App.vue # Root Vue component
│ │ └── main.ts # Application entry point
│ ├── public/ # Public static files
│ ├── e2e/ # End-to-end tests
│ ├── package.json # Node.js dependencies
│ ├── vite.config.ts # Vite configuration
│ ├── tsconfig.json # TypeScript configuration
│ └── README.md # Frontend specific documentation
├── main.py # FastAPI application entry point
├── requirements.txt # Python dependencies
├── .env.example # Environment variables template
├── .gitignore # Git ignore rules
└── README.md # This file
```
### Backend (FastAPI)
- **main.py**: Application entry point and server startup
- **app/**: Contains all backend application code
- **modules/**: Feature-based modules organization
- **routes.py**: API endpoint definitions
- **schema.py**: Data validation schemas using Pydantic
- **setup_app.py**: Application configuration and middleware setup
- **utils/**: Shared utility functions
### Frontend (Vue 3)
- **web/**: Complete Vue 3 application with TypeScript
- **src/**: Source code with modern Vue 3 composition API
- **components/**: Reusable Vue components
- **views/**: Page-level components
- **router/**: Client-side routing configuration
- **stores/**: State management using Pinia
- **assets/**: Static assets like images, styles
- **Vite**: Fast build tool and development server
- **TypeScript**: Type-safe JavaScript development
- **ESLint & Prettier**: Code linting and formatting
### Configuration
- **.freeleaps/**: Platform-specific configurations
- **.env.example**: Environment variables template
- **requirements.txt**: Python package dependencies
- **package.json**: Node.js dependencies and scripts

20
app/modules/__init__.py Normal file
View File

@ -0,0 +1,20 @@
import pathlib
import pkgutil
from importlib import import_module
from importlib.util import find_spec
def _modules(postfix="") -> list:
"""
Get all modules in the current package.
"""
return [
import_module(f".{name}{postfix}", package=__name__)
for (_, name, _) in pkgutil.iter_modules([str(pathlib.Path(__file__).parent)])
if find_spec(f".{name}{postfix}", package=__name__)
]
def detect_modules() -> list:
"""
Detect all modules in the current package.
"""
return _modules(".modules")

View File

View File

@ -0,0 +1,29 @@
import asyncio
import random
from fastapi import APIRouter, status
from starlette.responses import JSONResponse
from app.schema import Response
router = APIRouter()
@router.get('/demo', status_code=status.HTTP_200_OK, response_model=Response)
async def demo() -> JSONResponse:
"""
Demo API endpoint that randomly sleeps for 0-500ms before responding.
"""
# Random sleep between 0 and 500ms
sleep_time = random.uniform(0, 0.5)
await asyncio.sleep(sleep_time)
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': status.HTTP_200_OK,
'msg': 'ok',
'payload': {
'sleep_time_ms': round(sleep_time * 1000, 2)
}
}
)

View File

View File

@ -0,0 +1,26 @@
import logging
from fastapi import FastAPI
from app.utils.config import settings
def register_metrics(app: FastAPI):
"""
Register Prometheus metrics endpoint.
"""
try:
from prometheus_fastapi_instrumentator import Instrumentator
instrumentator = (
Instrumentator().instrument(
app,
metric_namespace="freeleaps_{}".format(settings.FREELEAPS_PRODUCT_ID),
metric_subsystem=settings.ENVIRONMENT
)
)
instrumentator.expose(app, endpoint="/api/_/metrics", should_gzip=True)
logging.info("Metrics endpoint exposed at /api/_/metrics")
except ImportError:
logging.warning("prometheus-fastapi-instrumentator not installed, metrics endpoint will not be available")

41
app/modules/sys/routes.py Normal file
View File

@ -0,0 +1,41 @@
from fastapi import APIRouter, status
from starlette.responses import JSONResponse
from app.utils.config import settings
from app.schema import Response
router = APIRouter()
@router.get('/_/livez', status_code=status.HTTP_200_OK, response_model=Response)
async def liveness() -> JSONResponse:
"""
Liveness check probe endpoint.
You can modify the logic here to check the health of your application.
But do not modify the response format or remove this endpoint.
Its will break the health check of the deployment.
"""
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': status.HTTP_200_OK,
'msg': 'ok',
'payload': None
}
)
@router.get('/_/readyz', status_code=status.HTTP_200_OK, response_model=Response)
async def readiness() -> JSONResponse:
"""
Readiness check probe endpoint.
You can modify the logic here to check the health of your application.
But do not modify the response format or remove this endpoint.
Its will break the health check of the deployment.
"""
return JSONResponse(
status_code=status.HTTP_200_OK,
content={
'code': status.HTTP_200_OK,
'msg': 'ok',
'payload': None
}
)

30
app/routes.py Normal file
View File

@ -0,0 +1,30 @@
from fastapi import APIRouter, status
from app.schema import Response
from app.modules.sys.routes import router as sys_router
from app.modules.demo.routes import router as demo_router
from app.utils.config import settings
api_router = APIRouter()
root_router = APIRouter()
api_router.include_router(
sys_router
)
api_router.include_router(
demo_router,
tags=["Demo"],
)
@root_router.get('/', status_code=status.HTTP_200_OK, response_model=Response)
def root() -> dict:
return {
'code': status.HTTP_200_OK,
'msg': 'ok',
'payload': {
'name': settings.PROJECT_NAME,
'version': settings.APP_VERSION,
'environment': settings.ENVIRONMENT,
}
}

8
app/schema.py Normal file
View File

@ -0,0 +1,8 @@
from typing import Any
from pydantic import BaseModel
class Response(BaseModel):
code: int
msg: str
payload: dict[Any, Any] | None = None

54
app/setup_app.py Normal file
View File

@ -0,0 +1,54 @@
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from app.modules.sys.registers import register_metrics
from app.utils.config import settings
from app.utils.logger import logger
from app.routes import api_router, root_router
def setup_routers(app: FastAPI) -> None:
# Register root router without prefix to handle root level routes
app.include_router(root_router)
# Register API router with configured prefix
app.include_router(
api_router,
prefix=settings.API_V1_STR,
)
def setup_cors(app: FastAPI) -> None:
origins = []
if settings.BACKEND_CORS_ORIGINS:
origins_raw = settings.BACKEND_CORS_ORIGINS.split(",")
for origin in origins_raw:
use_origin = origin.strip()
origins.append(use_origin)
logger.info(f"Allowed CORS origins: {origins}")
app.user_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def create_app() -> FastAPI:
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.APP_VERSION,
docs_url=None if settings.is_production() else "/docs",
redoc_url=None if settings.is_production() else "/redoc",
openapi_url=f"{settings.API_V1_STR}/openapi.json",
)
setup_routers(app)
setup_cors(app)
# Register Prometheus metrics collection endpoint.
# Do not delete this line. Removing it will cause Prometheus metrics collection to fail,
# and metrics data will not be visible on the ops page of the freeleaps.com platform.
register_metrics(app)
return app

0
app/utils/__init__.py Normal file
View File

38
app/utils/config.py Normal file
View File

@ -0,0 +1,38 @@
from enum import Enum
from pydantic_settings import BaseSettings
class AppEnvironment(str, Enum):
PRODUCTION = "prod"
DEVELOPMENT = "dev"
ALPHA = "alpha"
class Config(BaseSettings):
API_V1_STR: str = "/api/v1"
APP_VERSION: str = "Unversioned API"
UVICORN_HOST: str = "0.0.0.0"
UVICORN_PORT: int = 8888
BACKEND_CORS_ORIGINS: str = ""
PROJECT_NAME: str = "template-test4"
LOGGING_LEVEL: str = "INFO"
FREELEAPS_PRODUCT_ID: str = "#{{FREELEAPS_PRODUCT_ID}}"
ENVIRONMENT: str = "alpha"
def is_development(self) -> bool:
return self.ENVIRONMENT == AppEnvironment.DEVELOPMENT.value
def is_alpha(self) -> bool:
return self.ENVIRONMENT == AppEnvironment.ALPHA.value
def is_production(self) -> bool:
return self.ENVIRONMENT == AppEnvironment.PRODUCTION.value
settings = Config(_env_file=".env", _env_file_encoding="utf-8")

8
app/utils/logger.py Normal file
View File

@ -0,0 +1,8 @@
import logging
from app.utils.config import settings
formatter = "%(levelname)s: %(asctime)s - %(module)s - %(funcName)s - %(message)s"
logger = logging.getLogger(__name__)
logging.basicConfig(level=settings.LOGGING_LEVEL, format=formatter)

16
main.py Normal file
View File

@ -0,0 +1,16 @@
from app.setup_app import create_app
from app.utils.config import settings
from app.utils.logger import logger
app = create_app()
if __name__ == "__main__":
import uvicorn
logger.info(f"Starting server on {settings.UVICORN_HOST}:{settings.UVICORN_PORT}...")
uvicorn.run(
'main:app',
host=settings.UVICORN_HOST,
port=settings.UVICORN_PORT,
reload=settings.is_development(),
)

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
fastapi==0.115.12
starlette==0.46.2
pydantic_settings==2.9.1
uvicorn==0.34.2
prometheus-fastapi-instrumentator==6.1.0

9
web/.editorconfig Normal file
View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
web/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

33
web/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
test-results/
playwright-report/

6
web/.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

3
web/docker-entrypoint.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/sh
# Start nginx
exec nginx -g 'daemon off;'

4
web/e2e/tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": ["./**/*"]
}

8
web/e2e/vue.spec.ts Normal file
View File

@ -0,0 +1,8 @@
import { test, expect } from '@playwright/test';
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toHaveText('You did it!');
})

1
web/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

34
web/eslint.config.ts Normal file
View File

@ -0,0 +1,34 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginVitest from '@vitest/eslint-plugin'
import pluginPlaywright from 'eslint-plugin-playwright'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
{
...pluginVitest.configs.recommended,
files: ['src/**/__tests__/*'],
},
{
...pluginPlaywright.configs['flat/recommended'],
files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'],
},
skipFormatting,
)

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

44
web/nginx/default.conf Normal file
View File

@ -0,0 +1,44 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
gzip_proxied any;
# Cache control for static assets
location /assets/ {
expires 1y;
add_header Cache-Control "public, no-transform";
access_log off;
}
# Handle Vue router history mode
location / {
try_files $uri $uri/ /index.html;
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
# Proxy API requests to the backend
location /api/ {
proxy_pass ${API_SERVER_URL};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

6676
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
web/package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "template-test4",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"test:e2e": "playwright test",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"format": "prettier --write src/"
},
"dependencies": {
"pinia": "^3.0.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@playwright/test": "^1.51.1",
"@tsconfig/node22": "^22.0.1",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.14.0",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"@vitest/eslint-plugin": "^1.1.39",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.22.0",
"eslint-plugin-playwright": "^2.2.0",
"eslint-plugin-vue": "~10.0.0",
"jiti": "^2.4.2",
"jsdom": "^26.0.0",
"npm-run-all2": "^7.0.2",
"prettier": "3.5.3",
"typescript": "~5.8.0",
"vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2",
"vitest": "^3.1.1",
"vue-tsc": "^2.2.8"
}
}

110
web/playwright.config.ts Normal file
View File

@ -0,0 +1,110 @@
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
* Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'npm run preview' : 'npm run dev',
port: process.env.CI ? 4173 : 5173,
reuseExistingServer: !process.env.CI,
},
})

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

86
web/src/App.vue Normal file
View File

@ -0,0 +1,86 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/demo">Demo</RouterLink>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</div>
</header>
<RouterView />
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>

86
web/src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
web/src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

35
web/src/assets/main.css Normal file
View File

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener">Vue - Official</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

14
web/src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

31
web/src/router/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/demo',
name: 'demo',
// route level code-splitting
// this generates a separate chunk (Demo.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/DemoView.vue'),
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue'),
},
],
})
export default router

12
web/src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

22
web/src/utils/api.ts Normal file
View File

@ -0,0 +1,22 @@
const API_BASE_URL = '/api/v1'
export interface ApiResponse<T = unknown> {
code: number
msg: string
payload: T | null
}
export interface DemoResponse {
sleep_time_ms: number
}
export function callDemoApi(): Promise<ApiResponse<DemoResponse>> {
return fetch(`${API_BASE_URL}/demo`)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json() as Promise<ApiResponse<DemoResponse>>
})
}

View File

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

119
web/src/views/DemoView.vue Normal file
View File

@ -0,0 +1,119 @@
<script setup lang="ts">
import { ref } from 'vue'
import { callDemoApi, type ApiResponse, type DemoResponse } from '../utils/api'
const loading = ref(false)
const result = ref<ApiResponse<DemoResponse> | null>(null)
const error = ref<string | null>(null)
const handleCallDemo = async () => {
loading.value = true
error.value = null
result.value = null
try {
const response = await callDemoApi()
result.value = response
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error occurred'
} finally {
loading.value = false
}
}
</script>
<template>
<main>
<div class="demo-section">
<h2>Demo API Test</h2>
<button
@click="handleCallDemo"
:disabled="loading"
class="demo-button"
>
{{ loading ? 'Calling...' : 'Call Demo API' }}
</button>
<div v-if="result" class="result">
<h3>Response:</h3>
<pre>{{ JSON.stringify(result, null, 2) }}</pre>
<p v-if="result.payload" class="sleep-time">
Sleep time: <strong>{{ result.payload.sleep_time_ms }}ms</strong>
</p>
</div>
<div v-if="error" class="error">
<h3>Error:</h3>
<p>{{ error }}</p>
</div>
</div>
</main>
</template>
<style scoped>
.demo-section {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
}
.demo-section h2 {
margin-bottom: 1rem;
}
.demo-button {
padding: 0.75rem 1.5rem;
font-size: 1rem;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.demo-button:hover:not(:disabled) {
background-color: #35a372;
}
.demo-button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 1.5rem;
padding: 1rem;
background-color: #f5f5f5;
border-radius: 4px;
}
.result h3 {
margin-top: 0;
}
.result pre {
background-color: #fff;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
.sleep-time {
margin-top: 1rem;
font-size: 1.1rem;
}
.error {
margin-top: 1.5rem;
padding: 1rem;
background-color: #fee;
border-radius: 4px;
color: #c33;
}
.error h3 {
margin-top: 0;
}
</style>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

12
web/tsconfig.app.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
web/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}

19
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

11
web/tsconfig.vitest.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}

50
web/vite.config.ts Normal file
View File

@ -0,0 +1,50 @@
import { fileURLToPath, URL } from 'node:url'
import { ConfigEnv, defineConfig, loadEnv, type UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig((result: ConfigEnv) => {
const { command, mode } = result
const env = loadEnv(mode, process.cwd())
if (command === 'serve') {
// dev specific config
console.log(
'loading environment variables from .env and .env.' + mode + ' under ' + process.cwd()
)
}
console.log('backend api url: ' + (env.VITE_PROXY_API_URL || 'http://localhost:8888'))
return {
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'^/api/': {
target: env.VITE_PROXY_API_URL || 'http://localhost:8888',
changeOrigin: true,
secure: true,
ws: true,
rewrite: (path) => {
console.info(path)
return path
}
}
}
}
} as UserConfig
})

14
web/vitest.config.ts Normal file
View File

@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)