Compare commits
4 Commits
8fa2dbcb06
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35061cbd3 | ||
|
|
9f4d38a1fe | ||
|
|
649ec54886 | ||
|
|
b8b822e578 |
109
AGENTS.md
109
AGENTS.md
@@ -2,45 +2,56 @@
|
|||||||
|
|
||||||
## 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
|
|
||||||
- Use absolute imports
|
|
||||||
- Example:
|
|
||||||
```python
|
```python
|
||||||
import json
|
import json
|
||||||
from flask import Flask, render_template, request, redirect, url_for, make_response
|
from flask import Flask, render_template, request, redirect, url_for, make_response
|
||||||
@@ -71,7 +82,7 @@ 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()`
|
||||||
@@ -79,12 +90,10 @@ No formal linter is configured. The codebase uses basic Python and JavaScript.
|
|||||||
|
|
||||||
**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
|
||||||
|
│ └── static/ # Static assets
|
||||||
|
├── helm/ # Kubernetes Helm chart
|
||||||
├── venv/ # Virtual environment
|
├── venv/ # Virtual environment
|
||||||
├── SPEC.md # Project specification
|
├── SPEC.md # Project specification
|
||||||
└── AGENTS.md # This file
|
├── 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
5
build.sh
5
build.sh
@@ -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
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 }}
|
||||||
20
helm/sigra/templates/ingress.yaml
Normal file
20
helm/sigra/templates/ingress.yaml
Normal 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 }}
|
||||||
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 }}
|
||||||
24
helm/sigra/values.yaml
Normal file
24
helm/sigra/values.yaml
Normal 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
4
run.sh
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user