mirror of
https://github.com/tmdinosaurcenter/kiosk-guestbook.git
synced 2026-06-04 00:10:16 -06:00
fix: correct marquee scroll speed and add code TODOs
- Fixed scrolling marquee to use a fixed px/s speed via JS instead of a fixed duration, preventing it from speeding up as entries are added - Added inline TODO comments throughout codebase to track known issues (rate limiting, CSRF, unbounded queries, deprecated Flask decorator, PII logging, schema versioning, Docker non-root user, etc.) - Added todo-to-issue GitHub Action to auto-create Issues from TODOs on push to main - Added .claude/ to .gitignore
This commit is contained in:
@@ -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 }}
|
||||||
@@ -184,5 +184,8 @@ cython_debug/
|
|||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
.env
|
.env
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|||||||
@@ -24,5 +24,10 @@ ENV FLASK_ENV=production
|
|||||||
# Expose the port (Gunicorn will run on 8000)
|
# Expose the port (Gunicorn will run on 8000)
|
||||||
EXPOSE 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
|
# Use the entrypoint script as the container's command
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def load_banned_words():
|
|||||||
BANNED_WORDS = load_banned_words()
|
BANNED_WORDS = load_banned_words()
|
||||||
|
|
||||||
def contains_banned_words(text):
|
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()
|
words = text.lower().split()
|
||||||
for word in words:
|
for word in words:
|
||||||
word_clean = word.strip(".,!?;:\"'")
|
word_clean = word.strip(".,!?;:\"'")
|
||||||
@@ -41,6 +42,7 @@ def contains_banned_words(text):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def init_db():
|
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)
|
conn = sqlite3.connect(DATABASE)
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''
|
c.execute('''
|
||||||
@@ -60,14 +62,19 @@ def init_db():
|
|||||||
logger.info("Database initialized.")
|
logger.info("Database initialized.")
|
||||||
|
|
||||||
def is_valid_email(email):
|
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+$'
|
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
|
||||||
return re.match(pattern, email)
|
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
|
@app.before_first_request
|
||||||
def initialize_database():
|
def initialize_database():
|
||||||
init_db()
|
init_db()
|
||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST'])
|
@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():
|
def index():
|
||||||
error = None
|
error = None
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@@ -92,6 +99,8 @@ def index():
|
|||||||
if error:
|
if error:
|
||||||
conn = sqlite3.connect(DATABASE)
|
conn = sqlite3.connect(DATABASE)
|
||||||
c = conn.cursor()
|
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')
|
c.execute('SELECT first_name, location FROM guests ORDER BY id DESC')
|
||||||
guests = c.fetchall()
|
guests = c.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -108,11 +117,14 @@ def index():
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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)
|
logger.info("Added guest: %s %s from %s", first_name, last_name, location)
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
conn = sqlite3.connect(DATABASE)
|
conn = sqlite3.connect(DATABASE)
|
||||||
c = conn.cursor()
|
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')
|
c.execute('SELECT first_name, location FROM guests ORDER BY id DESC')
|
||||||
guests = c.fetchall()
|
guests = c.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
envsubst < /app/templates/index.html.template > /app/templates/index.html
|
envsubst < /app/templates/index.html.template > /app/templates/index.html
|
||||||
|
|
||||||
# Start Gunicorn; using an environment variable for workers (default is 3)
|
# 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}
|
exec gunicorn --bind 0.0.0.0:8000 app:app --workers ${WORKERS:-3}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import csv
|
import csv
|
||||||
import sqlite3
|
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'
|
DATABASE = 'guestbook.db'
|
||||||
EXPORT_FILE = 'mailchimp_export.csv'
|
EXPORT_FILE = 'mailchimp_export.csv'
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
.scrolling-content {
|
.scrolling-content {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
animation: scroll-left 20s linear infinite;
|
animation: scroll-left linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scroll-left {
|
@keyframes scroll-left {
|
||||||
0% {
|
0% {
|
||||||
transform: translateX(100%);
|
transform: translateX(100vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
@@ -107,6 +107,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Set scrolling speed to a fixed pixels-per-second rate -->
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const pixelsPerSecond = 80;
|
||||||
|
const content = document.querySelector(".scrolling-content");
|
||||||
|
|
||||||
|
function updateScrollSpeed() {
|
||||||
|
const totalDistance = window.innerWidth + content.offsetWidth;
|
||||||
|
content.style.animationDuration = (totalDistance / pixelsPerSecond) + "s";
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScrollSpeed();
|
||||||
|
window.addEventListener("resize", updateScrollSpeed);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- JavaScript to reveal the comment field -->
|
<!-- JavaScript to reveal the comment field -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user