Compare commits

..

4 Commits

17 changed files with 238 additions and 59 deletions

123
AGENTS.md
View File

@@ -2,49 +2,60 @@
## Project Overview ## 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: This is a Flask web application - a "Jeopardy-style" quiz game about the Russian short story "Уроки французского" by Valentin Rasputin.
- **Backend**: Python 3.13 with Flask
- **Frontend**: HTML, CSS, JavaScript (vanilla) **Tech Stack:**
- **Data Storage**: JSON file (`data/questions.json`) - Backend: Python 3.13 with Flask
- **State**: Browser localStorage (client-side) - Frontend: HTML, CSS, JavaScript (vanilla)
- Data Storage: JSON file (`data/questions.json`)
- State: Browser localStorage (client-side)
- Container: Docker
## Build/Test Commands ## Build/Test Commands
### Running the Application ### Running Locally (without Docker)
```bash ```bash
# Activate virtual environment # Activate virtual environment
source venv/bin/activate source venv/bin/activate
# Run Flask app (default port 5000) # Run Flask app (default port 5000)
python app.py python src/app.py
# Run on specific port # 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: This project does not have a formal test suite. For manual testing:
- Use `curl` to test API endpoints - Use `curl` to test API endpoints
- Test in browser at http://localhost:5000 (or configured port) - Test in browser at http://localhost:5000
### Linting/Type Checking
No formal linter is configured. The codebase uses basic Python and JavaScript.
## Code Style Guidelines ## Code Style Guidelines
### Python (app.py) ### Python (app.py)
**Imports** **Imports** - Standard library first, then third-party:
- Standard library first, then third-party ```python
- Use absolute imports import json
- Example: from flask import Flask, render_template, request, redirect, url_for, make_response
```python ```
import json
from flask import Flask, render_template, request, redirect, url_for, make_response
```
**Formatting** **Formatting**
- Use 4 spaces for indentation - Use 4 spaces for indentation
@@ -71,20 +82,18 @@ No formal linter is configured. The codebase uses basic Python and JavaScript.
**General** **General**
- Use vanilla JavaScript (no frameworks) - Use vanilla JavaScript (no frameworks)
- Prefer `var` over `let/const` for compatibility - 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** **Event Handling**
- Use `onclick` directly in HTML or `element.onclick = function()` - Use `onclick` directly in HTML or `element.onclick = function()`
- Avoid `addEventListener` for simplicity - Avoid `addEventListener` for simplicity
**DOM Manipulation** **DOM Manipulation**
- Use `document.getElementById` and `document.querySelector` - 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 - Use `JSON.parse()` and `JSON.stringify()` for localStorage
**Naming** **Naming** - camelCase for variables and functions
- camelCase for variables and functions
- Descriptive names (e.g., `currentTeam`, `selectAnswer`)
### HTML/CSS (templates/*.html) ### 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 - Keep responsive design in mind
**Structure** **Structure**
- Inline CSS in `<style>` tags (simple project) - Inline CSS in `<style>` tags
- Inline JS in `<script>` tags at end of body - Inline JS in `<script>` tags at end of body
## File Organization ## File Organization
``` ```
/home/eof/dev/roma/sigra/ /home/eof/dev/roma/sigra/
├── app.py # Flask application ├── Dockerfile # Docker image definition
├── data/ ├── build.sh # Build script
│ └── questions.json # Game questions data ├── run.sh # Run script
├── templates/ ├── .dockerignore # Docker ignore
│ ├── index.html # Main game page ├── src/ # Application source (copied to container)
│ ├── admin.html # Admin panel (edit questions) │ ├── app.py # Flask application
│ ├── edit.html # Question editor │ ├── data/
│ └── login.html # Admin login │ └── questions.json # Game questions
├── static/ # Static assets (empty currently) │ ├── templates/ # Jinja2 templates
├── venv/ # Virtual environment │ └── static/ # Static assets
├── SPEC.md # Project specification ├── helm/ # Kubernetes Helm chart
└── AGENTS.md # This file ├── venv/ # Virtual environment
├── SPEC.md # Project specification
├── AGENTS.md # This file
└── .gitignore # Git ignore rules
``` ```
## Common Patterns ## Common Patterns
### Adding a New Question ### Adding a New Question
1. Edit `data/questions.json` manually or via admin panel at `/admin` 1. Edit `data/questions.json` manually or via admin panel at `/admin`
2. Follow existing structure: 2. Structure:
```json ```json
{ {
"cost": 600, "cost": 600,
"question": "Question text?", "question": "Question text?",
"options": ["Option 1", "Option 2", "Option 3", "Option 4"], "options": ["Option 1", "Option 2", "Option 3", "Option 4"],
"answer": 0 // 0-3 index of correct answer "answer": 0
} }
``` ```
### Adding a New Route ### Adding a New Route
1. Add route in `app.py` using `@app.route()` 1. Add route in `app.py` using `@app.route()`
2. Return `render_template()` or `jsonify()` 2. Return `render_template()` or `jsonify()`
3. For API routes, use proper HTTP methods
### Modifying Game Logic ### Game State (localStorage)
- Game state stored in browser's localStorage
- Key: `francuzskiy_game` - Key: `francuzskiy_game`
- Structure: `{ score: 0, answered: [], userAnswers: {}, teams: [] }` - 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 ## Security Notes
- Admin panel is password-protected (password in `app.py`) - Admin panel password: stored in `app.py` (`ADMIN_PASSWORD`)
- No database - data is in JSON file - No database - data in JSON file
- No SQL injection risk (no database) - No SQL injection risk
- XSS: Be careful with `innerHTML` - user input in questions is trusted - XSS: user input in questions is trusted (admin-only)
## Dependencies ## Dependencies
@@ -159,6 +176,8 @@ Jinja2==3.1.6
Werkzeug==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

View File

@@ -4,7 +4,7 @@ WORKDIR /app
RUN pip install --no-cache-dir flask==3.1.3 jinja2==3.1.6 werkzeug==3.1.6 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_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0 ENV FLASK_RUN_HOST=0.0.0.0

View File

@@ -2,5 +2,8 @@
set -e set -e
echo "Building Docker image: sigra" echo "Building Docker image: sigra"
docker build -t sigra . docker build \
-t sigra \
-t git.itphx.ru/eka/sigra:latest \
.
echo "Build complete!" echo "Build complete!"

13
fleet.yaml Normal file
View 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
View 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"

View 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 }}

View 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 }}

View File

@@ -0,0 +1,20 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "sigra.fullname" . }}
labels:
{{- include "sigra.labels" . | nindent 4 }}
spec:
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 }}

View 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 }}

24
helm/sigra/values.yaml Normal file
View File

@@ -0,0 +1,24 @@
replicaCount: 1
image:
repository: git.itphx.ru/eka/sigra
pullPolicy: Always
tag: "latest"
service:
type: ClusterIP
port: 5000
ingress:
enabled: true
host: sigra.k3s.itphx.loc
path: /
pathType: Prefix
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi

4
run.sh
View File

@@ -2,6 +2,6 @@
set -e set -e
echo "Starting sigra container..." 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 "Container 'sigra' is running!"
echo "Open http://localhost:5000" echo "Open http://localhost:5002"

View File

@@ -1,17 +1,20 @@
import json import json
import os
from flask import Flask, render_template, request, redirect, url_for, make_response 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 = Flask(__name__)
app.secret_key = 'your_secret_key_here' app.secret_key = 'your_secret_key_here'
ADMIN_PASSWORD = 'RaMaZaNoV2013' ADMIN_PASSWORD = 'RaMaZaNoV2013'
def load_questions(): 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) return json.load(f)
def save_questions(data): 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) json.dump(data, f, ensure_ascii=False, indent=4)
@app.context_processor @app.context_processor
@@ -70,4 +73,5 @@ def edit_question(cat, q):
return render_template('edit.html', question=question, cat=cat, q=q) return render_template('edit.html', question=question, cat=cat, q=q)
if __name__ == '__main__': if __name__ == '__main__':
app.run(debug=True, port=5002) port = int(os.environ.get('FLASK_RUN_PORT', 5000))
app.run(debug=True, host='0.0.0.0', port=port)