diff --git a/.github/workflows/todo-to-issue.yml b/.github/workflows/todo-to-issue.yml
new file mode 100644
index 0000000..833ae44
--- /dev/null
+++ b/.github/workflows/todo-to-issue.yml
@@ -0,0 +1,14 @@
+name: TODO to Issue
+
+on:
+ push:
+ branches: [ "main" ]
+
+jobs:
+ todo:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: alstr/todo-to-issue-action@v5
+ with:
+ TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 8a8f590..b5e8d5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -184,5 +184,8 @@ cython_debug/
# VS Code
.vscode/
+# Claude Code
+.claude/
+
.env
docker-compose.yml
diff --git a/Dockerfile b/Dockerfile
index c99dae2..5d07de8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,5 +24,10 @@ ENV FLASK_ENV=production
# Expose the port (Gunicorn will run on 8000)
EXPOSE 8000
+# TODO: No USER directive — container runs as root. Add a non-root user for security.
+# example.env has PID/GID=1000 vars suggesting this was intended. e.g.:
+# RUN useradd -u 1000 -g 1000 appuser && chown -R appuser /app /data
+# USER appuser
+
# Use the entrypoint script as the container's command
CMD ["/entrypoint.sh"]
diff --git a/app.py b/app.py
index 510b8af..b3f1103 100644
--- a/app.py
+++ b/app.py
@@ -33,6 +33,7 @@ def load_banned_words():
BANNED_WORDS = load_banned_words()
def contains_banned_words(text):
+ # TODO: This filter is easily bypassed (spacing, leet-speak, numbers). Consider a more robust NLP-based approach.
words = text.lower().split()
for word in words:
word_clean = word.strip(".,!?;:\"'")
@@ -41,6 +42,7 @@ def contains_banned_words(text):
return False
def init_db():
+ # TODO: No schema versioning — adding columns in the future requires manual DB updates. Consider a migration tool (e.g. Alembic).
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute('''
@@ -60,14 +62,19 @@ def init_db():
logger.info("Database initialized.")
def is_valid_email(email):
+ # TODO: This regex allows edge cases like consecutive dots and leading/trailing hyphens. Consider using the `email-validator` package.
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return re.match(pattern, email)
+# TODO: @before_first_request is deprecated in Flask 2.2 and removed in Flask 3.0.
+# Replace with: with app.app_context(): init_db() at module level, or use a CLI command.
@app.before_first_request
def initialize_database():
init_db()
@app.route('/', methods=['GET', 'POST'])
+# TODO: No rate limiting — form can be spammed. Add Flask-Limiter (e.g. @limiter.limit("10/minute")).
+# TODO: No CSRF protection. Add Flask-WTF for CSRF tokens.
def index():
error = None
if request.method == 'POST':
@@ -92,6 +99,8 @@ def index():
if error:
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
+ # TODO: No LIMIT — returns all rows. Add LIMIT 100 or similar.
+ # TODO: No error handling — a locked/corrupted DB returns an unhandled 500. Wrap in try/except.
c.execute('SELECT first_name, location FROM guests ORDER BY id DESC')
guests = c.fetchall()
conn.close()
@@ -108,11 +117,14 @@ def index():
)
conn.commit()
conn.close()
+ # TODO: Logging full name and location is PII. Consider omitting or hashing before logging.
logger.info("Added guest: %s %s from %s", first_name, last_name, location)
return redirect(url_for('index'))
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
+ # TODO: No LIMIT — returns all rows forever. Add LIMIT 100 or similar to avoid memory growth.
+ # TODO: No indexes on this table — full table scan on every page load. Add index on id/timestamp.
c.execute('SELECT first_name, location FROM guests ORDER BY id DESC')
guests = c.fetchall()
conn.close()
diff --git a/entrypoint.sh b/entrypoint.sh
index cc32e1a..4ac9629 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -4,4 +4,5 @@
envsubst < /app/templates/index.html.template > /app/templates/index.html
# Start Gunicorn; using an environment variable for workers (default is 3)
+# TODO: Variable mismatch — example.env sets GUNICORN_WORKERS but this reads WORKERS. Change to ${GUNICORN_WORKERS:-3}.
exec gunicorn --bind 0.0.0.0:8000 app:app --workers ${WORKERS:-3}
diff --git a/scripts/guestbook_export.py b/scripts/guestbook_export.py
index 2cc11a2..310d024 100644
--- a/scripts/guestbook_export.py
+++ b/scripts/guestbook_export.py
@@ -1,7 +1,8 @@
import csv
import sqlite3
-# Update the database file path if needed.
+# TODO: Hardcoded relative path — breaks if script is run from a different directory.
+# Replace with: DATABASE = os.environ.get('DATABASE_PATH', 'guestbook.db') and import os.
DATABASE = 'guestbook.db'
EXPORT_FILE = 'mailchimp_export.csv'
diff --git a/templates/index.html.template b/templates/index.html.template
index 9aa28b9..dcdbdce 100644
--- a/templates/index.html.template
+++ b/templates/index.html.template
@@ -23,12 +23,12 @@
.scrolling-content {
display: inline-block;
padding: 10px;
- animation: scroll-left 20s linear infinite;
+ animation: scroll-left linear infinite;
}
@keyframes scroll-left {
0% {
- transform: translateX(100%);
+ transform: translateX(100vw);
}
100% {
@@ -107,6 +107,22 @@
+
+
+