Initial add

This commit is contained in:
2026-05-19 18:14:39 -04:00
commit 86e6adfe2e
12 changed files with 526 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.pyo
*.pyd
venv/
.env
.git/

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Created by venv; see https://docs.python.org/3/library/venv.html
*~

6
Chart.yaml Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: timeclock
description: Flask app with SQLite persistence
type: application
version: 0.1.0
appVersion: "1.0"

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM python:3.12-slim
# Prevent Python from buffering stdout/stderr
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create database directory if needed
RUN mkdir -p database
# Expose Flask/Gunicorn port
EXPOSE 5000
# Start app
CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"]

298
app.py Normal file
View File

@@ -0,0 +1,298 @@
#!/usr/bin/env python3
"""
Minimal Time Clock Application
Requirements:
- Python 3
- Flask
- sqlite3
- Linux compatible
- No JavaScript
- Simple HTML UI
Features:
- User selection
- Clock in
- Clock out
- Report generation
- CRUD API
- Automatic database initialization
"""
import sqlite3
from datetime import datetime, timedelta
from flask import Flask, request, redirect, url_for, render_template, jsonify
# -----------------------------------------------------------------------------
# Flask Configuration
# -----------------------------------------------------------------------------
app = Flask(__name__)
DATABASE = "database/timeclock.db"
# -----------------------------------------------------------------------------
# Database Helpers
# -----------------------------------------------------------------------------
def get_db_connection():
"""
Create and return a sqlite3 database connection.
"""
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def initialize_database():
"""
Create database tables if they do not already exist.
"""
conn = get_db_connection()
cursor = conn.cursor()
# Users table
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)
""")
# Entries table
#
# user_id:
# References users.id
#
# entrytype:
# Must be either 'in' or 'out'
#
# ts:
# Timestamp stored as ISO datetime string
#
cursor.execute("""
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
entrytype TEXT NOT NULL CHECK(entrytype IN ('in', 'out')),
ts DATETIME NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
)
""")
conn.commit()
# Add sample users if table is empty
cursor.execute("SELECT COUNT(*) AS count FROM users")
result = cursor.fetchone()
if result["count"] == 0:
cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
cursor.execute("INSERT INTO users (name) VALUES ('Charlie')")
conn.commit()
conn.close()
# -----------------------------------------------------------------------------
# Utility Functions
# -----------------------------------------------------------------------------
def create_entry(user_id, entry_type):
"""
Create a clock in/out entry for the specified user.
"""
conn = get_db_connection()
cursor = conn.cursor()
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute("""
INSERT INTO entries (user_id, entrytype, ts)
VALUES (?, ?, ?)
""", (user_id, entry_type, timestamp))
conn.commit()
conn.close()
def generate_report(user_id, begin_date, end_date):
"""
Generate total worked hours for a user between two dates.
Assumptions:
- Entries alternate correctly between 'in' and 'out'
- Missing pairs are ignored
"""
conn = get_db_connection()
cursor = conn.cursor()
# Inclusive date range
begin_dt = datetime.strptime(begin_date, "%Y-%m-%dT%H:%M")
end_dt = datetime.strptime(end_date, "%Y-%m-%dT%H:%M")
# Include entire ending day
end_dt = end_dt + timedelta(days=1)
cursor.execute("""
SELECT entrytype, ts
FROM entries
WHERE user_id = ?
AND ts >= ?
AND ts < ?
ORDER BY ts ASC
""", (
user_id,
begin_dt.strftime("%Y-%m-%d %H:%M"),
end_dt.strftime("%Y-%m-%d %H:%M")
))
rows = cursor.fetchall()
conn.close()
total_seconds = 0
clock_in_time = None
for row in rows:
entry_type = row["entrytype"]
timestamp = datetime.strptime(row["ts"], "%Y-%m-%d %H:%M:%S")
if entry_type == "in":
clock_in_time = timestamp
elif entry_type == "out" and clock_in_time is not None:
delta = timestamp - clock_in_time
total_seconds += delta.total_seconds()
clock_in_time = None
total_hours = total_seconds / 3600.0
return round(total_hours, 2)
# -----------------------------------------------------------------------------
# Web UI Routes
# -----------------------------------------------------------------------------
@app.route("/", methods=["GET", "POST"])
def index():
"""
Main application page.
"""
conn = get_db_connection()
users = conn.execute("""
SELECT id, name
FROM users
ORDER BY name
""").fetchall()
conn.close()
report_hours = None
if request.method == "POST":
user_id = request.form.get("user_id")
action = request.form.get("action")
if action == "clock_in":
create_entry(user_id, "in")
elif action == "clock_out":
create_entry(user_id, "out")
elif action == "report":
begin_date = request.form.get("begin_date")
end_date = request.form.get("end_date")
report_hours = generate_report(
user_id,
begin_date,
end_date
)
return render_template(
"index.html",
users=users,
report_hours=report_hours
)
# -----------------------------------------------------------------------------
# CRUD API
# -----------------------------------------------------------------------------
@app.route("/api/users", methods=["GET"])
def api_get_users():
"""
Return all users.
"""
conn = get_db_connection()
users = conn.execute("""
SELECT id, name
FROM users
""").fetchall()
conn.close()
return jsonify([dict(user) for user in users])
@app.route("/api/entries", methods=["GET"])
def api_get_entries():
"""
Return all entries.
"""
conn = get_db_connection()
entries = conn.execute("""
SELECT id, user_id, entrytype, ts
FROM entries
ORDER BY ts DESC
""").fetchall()
conn.close()
return jsonify([dict(entry) for entry in entries])
@app.route("/api/entries", methods=["POST"])
def api_create_entry():
"""
Create a clock entry.
"""
data = request.get_json()
user_id = data.get("user_id")
entry_type = data.get("entrytype")
if entry_type not in ("in", "out"):
return jsonify({"error": "Invalid entrytype"}), 400
create_entry(user_id, entry_type)
return jsonify({"status": "success"}), 201
# -----------------------------------------------------------------------------
# Application Entry Point
# -----------------------------------------------------------------------------
if __name__ == "__main__":
initialize_database()
app.run(
host="0.0.0.0",
port=5000,
debug=False
)

View File

@@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "timeclock.fullname" . }}
namespace: {{ .Values.namespace }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "timeclock.name" . }}
template:
metadata:
labels:
app: {{ include "timeclock.name" . }}
spec:
containers:
- name: flask-app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 5000
volumeMounts:
- name: sqlite-data
mountPath: {{ .Values.app.dbPath }}
volumes:
- name: sqlite-data
persistentVolumeClaim:
claimName: {{ include "timeclock.fullname" . }}-pvc

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace }}

14
chart/templates/pvc.yaml Normal file
View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "timeclock.fullname" . }}-pvc
namespace: {{ .Values.namespace }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
storageClassName: {{ .Values.persistence.storageClass }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "timeclock.fullname" . }}
namespace: {{ .Values.namespace }}
spec:
type: {{ .Values.service.type }}
selector:
app: {{ include "timeclock.name" . }}
ports:
- protocol: TCP
port: {{ .Values.service.port }}
targetPort: 5000

21
chart/values.yaml Normal file
View File

@@ -0,0 +1,21 @@
namespace: flask-app
replicaCount: 1
image:
repository: registry.home.aklabs.net/timeclock
tag: latest
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 5000
persistence:
enabled: true
accessMode: ReadWriteOnce
size: 1Gi
storageClass: ""
app:
dbPath: /app/database

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
flask
gunicorn

101
templates/index.html Normal file
View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head>
<title>Time Clock</title>
</head>
<body>
<h1>Simple Time Clock</h1>
<form method="POST">
<table border="1" cellpadding="5">
<tr>
<td>User</td>
<td>
<select name="user_id">
{% for user in users %}
<option value="{{ user.id }}">
{{ user.name }}
</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td>Clock Actions</td>
<td>
<button type="submit" name="action" value="clock_in">
Clock In
</button>
<button type="submit" name="action" value="clock_out">
Clock Out
</button>
</td>
</tr>
<tr>
<td>Begin Date</td>
<td>
<input
type="datetime-local"
name="begin_date"
value=""
>
</td>
</tr>
<tr>
<td>End Date</td>
<td>
<input
type="datetime-local"
name="end_date"
value=""
>
</td>
</tr>
<tr>
<td>Report</td>
<td>
<button type="submit" name="action" value="report">
Generate Report
</button>
</td>
</tr>
</table>
</form>
<br>
{% if report_hours is not none %}
<table border="1" cellpadding="5">
<tr>
<th>Total Hours Worked</th>
</tr>
<tr>
<td>{{ report_hours }}</td>
</tr>
</table>
{% endif %}
</body>
</html>