Restructure project for Docker: move app to src/, add Helm chart and Fleet config
This commit is contained in:
123
AGENTS.md
123
AGENTS.md
@@ -2,49 +2,60 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a simple Flask web application - a "Jeopardy-style" quiz game about the Russian short story "Уроки французского" by Valentin Rasputin. The project uses:
|
||||
- **Backend**: Python 3.13 with Flask
|
||||
- **Frontend**: HTML, CSS, JavaScript (vanilla)
|
||||
- **Data Storage**: JSON file (`data/questions.json`)
|
||||
- **State**: Browser localStorage (client-side)
|
||||
This is a Flask web application - a "Jeopardy-style" quiz game about the Russian short story "Уроки французского" by Valentin Rasputin.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Python 3.13 with Flask
|
||||
- Frontend: HTML, CSS, JavaScript (vanilla)
|
||||
- Data Storage: JSON file (`data/questions.json`)
|
||||
- State: Browser localStorage (client-side)
|
||||
- Container: Docker
|
||||
|
||||
## Build/Test Commands
|
||||
|
||||
### Running the Application
|
||||
### Running Locally (without Docker)
|
||||
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Run Flask app (default port 5000)
|
||||
python app.py
|
||||
python src/app.py
|
||||
|
||||
# Run on specific port
|
||||
python -c "from app import app; app.run(port=5002)"
|
||||
python -c "from src.app import app; app.run(port=5002)"
|
||||
```
|
||||
|
||||
### No Formal Tests
|
||||
### Running with Docker
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
./build.sh
|
||||
|
||||
# Run container
|
||||
./run.sh
|
||||
|
||||
# Stop container
|
||||
docker stop sigra && docker rm sigra
|
||||
```
|
||||
|
||||
**Note:** Dockerfile copies only `src/` directory into the container (app.py, data/, templates/, static/).
|
||||
|
||||
### Manual Testing
|
||||
|
||||
This project does not have a formal test suite. For manual testing:
|
||||
- Use `curl` to test API endpoints
|
||||
- Test in browser at http://localhost:5000 (or configured port)
|
||||
|
||||
### Linting/Type Checking
|
||||
|
||||
No formal linter is configured. The codebase uses basic Python and JavaScript.
|
||||
- Test in browser at http://localhost:5000
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Python (app.py)
|
||||
|
||||
**Imports**
|
||||
- Standard library first, then third-party
|
||||
- Use absolute imports
|
||||
- Example:
|
||||
```python
|
||||
import json
|
||||
from flask import Flask, render_template, request, redirect, url_for, make_response
|
||||
```
|
||||
**Imports** - Standard library first, then third-party:
|
||||
```python
|
||||
import json
|
||||
from flask import Flask, render_template, request, redirect, url_for, make_response
|
||||
```
|
||||
|
||||
**Formatting**
|
||||
- Use 4 spaces for indentation
|
||||
@@ -71,20 +82,18 @@ No formal linter is configured. The codebase uses basic Python and JavaScript.
|
||||
**General**
|
||||
- Use vanilla JavaScript (no frameworks)
|
||||
- Prefer `var` over `let/const` for compatibility
|
||||
- Use `function` keyword instead of arrow functions where possible
|
||||
- Use `function` keyword instead of arrow functions
|
||||
|
||||
**Event Handling**
|
||||
- Use `onclick` directly in HTML or `element.onclick = function()`
|
||||
- Use `onclick` directly in HTML or `element.onclick = function()`
|
||||
- Avoid `addEventListener` for simplicity
|
||||
|
||||
**DOM Manipulation**
|
||||
- Use `document.getElementById` and `document.querySelector`
|
||||
- Template literals for dynamic content: `` `string ${variable}` ``
|
||||
- Template literals for dynamic content
|
||||
- Use `JSON.parse()` and `JSON.stringify()` for localStorage
|
||||
|
||||
**Naming**
|
||||
- camelCase for variables and functions
|
||||
- Descriptive names (e.g., `currentTeam`, `selectAnswer`)
|
||||
**Naming** - camelCase for variables and functions
|
||||
|
||||
### HTML/CSS (templates/*.html)
|
||||
|
||||
@@ -99,57 +108,65 @@ No formal linter is configured. The codebase uses basic Python and JavaScript.
|
||||
- Keep responsive design in mind
|
||||
|
||||
**Structure**
|
||||
- Inline CSS in `<style>` tags (simple project)
|
||||
- Inline CSS in `<style>` tags
|
||||
- Inline JS in `<script>` tags at end of body
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
/home/eof/dev/roma/sigra/
|
||||
├── app.py # Flask application
|
||||
├── data/
|
||||
│ └── questions.json # Game questions data
|
||||
├── templates/
|
||||
│ ├── index.html # Main game page
|
||||
│ ├── admin.html # Admin panel (edit questions)
|
||||
│ ├── edit.html # Question editor
|
||||
│ └── login.html # Admin login
|
||||
├── static/ # Static assets (empty currently)
|
||||
├── venv/ # Virtual environment
|
||||
├── SPEC.md # Project specification
|
||||
└── AGENTS.md # This file
|
||||
├── Dockerfile # Docker image definition
|
||||
├── build.sh # Build script
|
||||
├── run.sh # Run script
|
||||
├── .dockerignore # Docker ignore
|
||||
├── src/ # Application source (copied to container)
|
||||
│ ├── app.py # Flask application
|
||||
│ ├── data/
|
||||
│ │ └── questions.json # Game questions
|
||||
│ ├── templates/ # Jinja2 templates
|
||||
│ └── static/ # Static assets
|
||||
├── helm/ # Kubernetes Helm chart
|
||||
├── venv/ # Virtual environment
|
||||
├── SPEC.md # Project specification
|
||||
├── AGENTS.md # This file
|
||||
└── .gitignore # Git ignore rules
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a New Question
|
||||
1. Edit `data/questions.json` manually or via admin panel at `/admin`
|
||||
2. Follow existing structure:
|
||||
2. Structure:
|
||||
```json
|
||||
{
|
||||
"cost": 600,
|
||||
"question": "Question text?",
|
||||
"options": ["Option 1", "Option 2", "Option 3", "Option 4"],
|
||||
"answer": 0 // 0-3 index of correct answer
|
||||
"answer": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Route
|
||||
1. Add route in `app.py` using `@app.route()`
|
||||
2. Return `render_template()` or `jsonify()`
|
||||
3. For API routes, use proper HTTP methods
|
||||
|
||||
### Modifying Game Logic
|
||||
- Game state stored in browser's localStorage
|
||||
### Game State (localStorage)
|
||||
- Key: `francuzskiy_game`
|
||||
- Structure: `{ score: 0, answered: [], userAnswers: {}, teams: [] }`
|
||||
- `answered`: array of "cat_q" keys (e.g., ["0_0", "1_2"])
|
||||
- `userAnswers`: object with keys and `{team, answer}` values
|
||||
|
||||
### Multi-team Support
|
||||
- Teams: 1-3 teams supported
|
||||
- Questions: 30 (always, regardless of team count)
|
||||
- Auto-switch to next team after answering
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Admin panel is password-protected (password in `app.py`)
|
||||
- No database - data is in JSON file
|
||||
- No SQL injection risk (no database)
|
||||
- XSS: Be careful with `innerHTML` - user input in questions is trusted
|
||||
- Admin panel password: stored in `app.py` (`ADMIN_PASSWORD`)
|
||||
- No database - data in JSON file
|
||||
- No SQL injection risk
|
||||
- XSS: user input in questions is trusted (admin-only)
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -159,6 +176,8 @@ Jinja2==3.1.6
|
||||
Werkzeug==3.1.6
|
||||
```
|
||||
|
||||
## Contact
|
||||
## Git Conventions
|
||||
|
||||
For questions about this codebase, refer to SPEC.md or the original requirements.
|
||||
- Do NOT commit without explicit user request
|
||||
- Run lint/typecheck before committing if available
|
||||
- Keep commits focused and descriptive
|
||||
|
||||
@@ -4,12 +4,12 @@ WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir flask==3.1.3 jinja2==3.1.6 werkzeug==3.1.6
|
||||
|
||||
COPY . .
|
||||
COPY src /app
|
||||
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
ENV FLASK_RUN_PORT=5000
|
||||
ENV FLASK_RUN_PORT=5002
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 5002
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
|
||||
6
build.sh
6
build.sh
@@ -2,6 +2,8 @@
|
||||
set -e
|
||||
|
||||
echo "Building Docker image: sigra"
|
||||
docker build -t sigra .
|
||||
docker build -t git.itphx.ru/eka/sigra:latest .
|
||||
docker build \
|
||||
-t sigra \
|
||||
-t git.itphx.ru/eka/sigra:latest \
|
||||
.
|
||||
echo "Build complete!"
|
||||
|
||||
13
fleet.yaml
Normal file
13
fleet.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace: default
|
||||
|
||||
helm:
|
||||
chart: ./helm/sigra
|
||||
releaseName: sigra
|
||||
values:
|
||||
image:
|
||||
repository: git.itphx.ru/eka/sigra
|
||||
tag: latest
|
||||
ingress:
|
||||
enabled: true
|
||||
host: sigra.k3s.itphx.loc
|
||||
|
||||
6
helm/sigra/Chart.yaml
Normal file
6
helm/sigra/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: sigra
|
||||
description: A Flask web application - Jeopardy-style quiz game
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
49
helm/sigra/templates/_helpers.tpl
Normal file
49
helm/sigra/templates/_helpers.tpl
Normal file
@@ -0,0 +1,49 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "sigra.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "sigra.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 "sigra.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "sigra.labels" -}}
|
||||
helm.sh/chart: {{ include "sigra.chart" . }}
|
||||
{{ include "sigra.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "sigra.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "sigra.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
26
helm/sigra/templates/deployment.yaml
Normal file
26
helm/sigra/templates/deployment.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "sigra.fullname" . }}
|
||||
labels:
|
||||
{{- include "sigra.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "sigra.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "sigra.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
21
helm/sigra/templates/ingress.yaml
Normal file
21
helm/sigra/templates/ingress.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "sigra.fullname" . }}
|
||||
labels:
|
||||
{{- include "sigra.labels" . | nindent 4 }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
rules:
|
||||
- host: {{ .Values.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.path }}
|
||||
pathType: {{ .Values.ingress.pathType }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "sigra.fullname" . }}
|
||||
port:
|
||||
number: {{ .Values.service.port }}
|
||||
{{- end }}
|
||||
15
helm/sigra/templates/service.yaml
Normal file
15
helm/sigra/templates/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "sigra.fullname" . }}
|
||||
labels:
|
||||
{{- include "sigra.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "sigra.selectorLabels" . | nindent 4 }}
|
||||
25
helm/sigra/values.yaml
Normal file
25
helm/sigra/values.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: git.itphx.ru/eka/sigra
|
||||
pullPolicy: Always
|
||||
tag: "latest"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 5002
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
host: sigra.k3s.itphx.loc
|
||||
path: /
|
||||
pathType: Prefix
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
4
run.sh
4
run.sh
@@ -2,6 +2,6 @@
|
||||
set -e
|
||||
|
||||
echo "Starting sigra container..."
|
||||
docker run -d --name sigra -p 5000:5000 sigra
|
||||
docker run --rm --name sigra -p 5002:5002 sigra
|
||||
echo "Container 'sigra' is running!"
|
||||
echo "Open http://localhost:5000"
|
||||
echo "Open http://localhost:5002"
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import json
|
||||
import os
|
||||
from flask import Flask, render_template, request, redirect, url_for, make_response
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'your_secret_key_here'
|
||||
|
||||
ADMIN_PASSWORD = 'RaMaZaNoV2013'
|
||||
|
||||
def load_questions():
|
||||
with open('data/questions.json', 'r', encoding='utf-8') as f:
|
||||
with open(os.path.join(BASE_DIR, 'data', 'questions.json'), 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_questions(data):
|
||||
with open('data/questions.json', 'w', encoding='utf-8') as f:
|
||||
with open(os.path.join(BASE_DIR, 'data', 'questions.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
@app.context_processor
|
||||
Reference in New Issue
Block a user