12 Commits
Author SHA1 Message Date
ryan ed6bb95451 feat: update BASE_VERSION to 1.1.0 for upcoming release
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m10s
2026-09-03 16:10:43 -04:00
ryan db42f81780 feat: update tutorial functionality and improve tutorial step management
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m56s
2026-08-01 01:57:24 -04:00
ryan f54833e642 feat: remove MongoDB test database reset step from deployment workflow
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m23s
2026-08-01 01:18:23 -04:00
ryan 59adf22d42 feat: implement MongoDB test database reset for clean test sessions
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m22s
2026-07-31 17:47:06 -04:00
ryan 4f9b01ce38 feat: add USE_MONGODB environment variable for test services
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m19s
2026-07-31 17:02:11 -04:00
ryan d69d2226a3 feat: enhance test data cleanup by dropping MongoDB test database if applicable
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m22s
2026-07-31 16:23:15 -04:00
ryan 0b89833c32 feat: update environment variables to set DATA_ENV and DB_ENV to 'prod'
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m39s
2026-07-31 15:59:19 -04:00
ryan 95dee37b60 feat: update Gunicorn configuration to use gthread and remove wsgi.py
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m20s
2026-07-30 00:40:09 -04:00
ryan 7e4a08d415 feat: update Gunicorn command to use wsgi:app and add wsgi.py for application entry point
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m18s
2026-07-30 00:29:41 -04:00
ryan 0f903f57bb feat: add MONGO_URI to environment variable setup in build workflow
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m22s
2026-07-30 00:21:12 -04:00
ryan 7e71446cc7 feat: add MONGO_URI to environment variable setup in build workflow
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m17s
2026-07-29 17:52:47 -04:00
ryan c9cc424dda feat: add MONGO_URI to environment variable setup in build workflow
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m18s
2026-07-29 17:03:10 -04:00
17 changed files with 182 additions and 56 deletions
+3
View File
@@ -151,9 +151,12 @@ jobs:
ADMIN_PASSWORD=${{ secrets.ADMIN_PASSWORD }}
ADMIN_FIRST_NAME=${{ secrets.ADMIN_FIRST_NAME }}
ADMIN_LAST_NAME=${{ secrets.ADMIN_LAST_NAME }}
MONGO_URI=${{ secrets.MONGO_URI }}
USE_MONGODB=true
EOF
echo "SECRET_KEY is set: $(grep -q 'SECRET_KEY=' .env && echo YES || echo NO)"
echo "MONGO_URI is set: $(grep -q 'MONGO_URI=' .env && echo YES || echo NO)"
echo "Bringing down previous test environment..."
docker-compose -f docker-compose.test.yml down --volumes --remove-orphans || true
+1 -2
View File
@@ -2,8 +2,7 @@
name: Developer
description: Implements core application features across Python backends and Vue frontends.
mode: subagent
model: moonshotai/kimi-k2.7-code
temperature: 0.2
model: "deepseek/deepseek-v4-pro"
maxSteps: 50
permission:
edit: allow
+1 -1
View File
@@ -1,7 +1,7 @@
---
description: "Drafts and updates technical documentation, architecture guides, and API specs."
mode: "subagent"
model: "deepseek/deepseek/deepseek-v4-flash"
model: "deepseek/deepseek-v4-flash"
permission:
edit: allow
bash: deny
+1 -1
View File
@@ -15,4 +15,4 @@ ENV PYTHONIOENCODING=utf-8
VOLUME ["/app/data"]
# Use Gunicorn instead of python main.py
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gevent", "--workers", "1", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "-c", "gunicorn.conf.py", "main:app"]
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gthread", "--threads", "8", "--workers", "1", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "-c", "gunicorn.conf.py", "main:app"]
+4 -4
View File
@@ -212,10 +212,10 @@ def create_test_digest_token():
def send_test_digest():
"""Trigger a digest email for a specific user by email address.
Only active when DB_ENV is not 'production'. Requires admin authentication.
Only active when DB_ENV is not 'prod'. Requires admin authentication.
Note: actual email delivery is skipped in e2e mode by email_sender.
"""
if os.environ.get('DB_ENV') == 'production':
if os.environ.get('DB_ENV') == 'prod':
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
user_id = get_validated_user_id()
@@ -253,10 +253,10 @@ def send_test_digest():
def trigger_test_chore_expiry():
"""Trigger the chore expiry notification check for a specific user by email address.
Only active when DB_ENV is not 'production'. Requires admin authentication.
Only active when DB_ENV is not 'prod'. Requires admin authentication.
Note: actual push delivery requires VAPID keys to be configured.
"""
if os.environ.get('DB_ENV') == 'production':
if os.environ.get('DB_ENV') == 'prod':
return jsonify({'error': 'Not found', 'code': 'NOT_FOUND'}), 404
user_id = get_validated_user_id()
+1 -1
View File
@@ -2,7 +2,7 @@
# file: config/version.py
import os
BASE_VERSION = "1.0.16" # update manually when releasing features
BASE_VERSION = "1.1.0" # update manually when releasing features
def get_full_version() -> str:
"""
+16 -1
View File
@@ -580,7 +580,8 @@ def ensure_mongodb_indexes(client=None, db_name=None):
# Clear test collections at import time so tests start with a clean slate.
if os.environ.get('DB_ENV', 'prod') == 'test':
# Only TinyDB path — MongoDB cleanup is handled explicitly via reset_mongo_db().
if os.environ.get('DB_ENV', 'prod') == 'test' and not USE_MONGODB:
child_db.truncate()
task_db.truncate()
routine_db.truncate()
@@ -599,3 +600,17 @@ if os.environ.get('DB_ENV', 'prod') == 'test':
refresh_tokens_db.truncate()
push_subscriptions_db.truncate()
digest_action_tokens_db.truncate()
def reset_mongo_db():
"""Drop the MongoDB test/e2e database for a totally fresh slate.
Only acts on database names ending with ``_test`` or ``_e2e`` as a
safety guard against accidentally dropping production data. Call this
once at the start of a test session or deployment seed step.
"""
if not USE_MONGODB:
return
if not _mongo_db_name or not _mongo_db_name.endswith(('_test', '_e2e')):
return
get_mongo_client().drop_database(_mongo_db_name)
+2
View File
@@ -22,6 +22,8 @@ _mongo_client = None
def _create_mongo_client():
"""Build a fail-fast MongoClient from environment variables."""
uri = os.environ.get('MONGO_URI')
if not uri:
raise RuntimeError(
'MONGO_URI environment variable is required when USE_MONGODB=true.'
+4 -3
View File
@@ -24,7 +24,8 @@ def set_test_db_env():
os.environ['MONGO_URI'] = 'mongomock'
os.environ['SECRET_KEY'] = TEST_SECRET_KEY
os.environ['REFRESH_TOKEN_EXPIRY_DAYS'] = str(TEST_REFRESH_TOKEN_EXPIRY_DAYS)
# Ensure indexes are created once for the test session. This is safe to
# call repeatedly because MongoDB treats index creation as idempotent.
from db.db import ensure_mongodb_indexes
# Drop the MongoDB test database once at the start of the session so every
# test begins from a clean slate. Indexes are recreated afterward.
from db.db import reset_mongo_db, ensure_mongodb_indexes
reset_mongo_db()
ensure_mongodb_indexes()
+4
View File
@@ -23,6 +23,10 @@ services:
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- ADMIN_FIRST_NAME=${ADMIN_FIRST_NAME}
- ADMIN_LAST_NAME=${ADMIN_LAST_NAME}
- MONGO_URI=${MONGO_URI}
- USE_MONGODB=true
- DATA_ENV=test
- DB_ENV=test
volumes:
- chores-test-app-backend-data:/app/data
+3
View File
@@ -15,6 +15,9 @@ services:
- DIGEST_TOKEN_SECRET=${DIGEST_TOKEN_SECRET}
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
- MONGO_URI=${MONGO_URI}
- DATA_ENV=prod
- DB_ENV=prod
volumes:
- chores-app-backend-data:/app/data # Assuming backend data storage; adjust path as needed
networks:
+6 -6
View File
@@ -2,20 +2,20 @@
"cookies": [
{
"name": "refresh_token",
"value": "5gEK_iLdGl1BAGfetT8hEf6SJK2lQVbYxQjH0BdKWwU",
"value": "WGHWfbkdu5a4GEsBQ4exR8DG4W39iOm1b71weDg6RWU",
"domain": "localhost",
"path": "/api/auth",
"expires": 1792813397.168935,
"expires": 1793338810.330124,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
},
{
"name": "access_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkODk0ZjA0OS0xODU3LTQwM2UtYjgzZC1jMTY2NTNmZDU2MmEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTd9.S7frWOrZwXC5xVgtG2RDioXGok04qK_jLCYWvfedD7s",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJlMzIxNzNmNy1lODM5LTQxNjMtOGUyYS04YmRhZGY5ZTZiZjEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MTB9.W08NeVvTAftfz4VQVopd8U38mOEiqUW9qb-EhbttWJk",
"domain": "localhost",
"path": "/",
"expires": 1785048197.167997,
"expires": 1785573610.329316,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
@@ -27,11 +27,11 @@
"localStorage": [
{
"name": "authSyncEvent",
"value": "{\"type\":\"logout\",\"at\":1785037396813}"
"value": "{\"type\":\"logout\",\"at\":1785562810109}"
},
{
"name": "parentAuth",
"value": "{\"expiresAt\":1785210197388}"
"value": "{\"expiresAt\":1785735610529}"
}
]
}
+6 -6
View File
@@ -2,20 +2,20 @@
"cookies": [
{
"name": "refresh_token",
"value": "Ke6qDj_0RYyrMSI1Z9C69DPtvYo26oUUiLZr_pDXZv4",
"value": "nT_T0nZ7ZXthfVaOIvMOa-9Z8IJAkT8hByHcv9yxyno",
"domain": "localhost",
"path": "/api/auth",
"expires": 1792813392.716776,
"expires": 1793338807.10011,
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
},
{
"name": "access_token",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI3NTYwZDBlNy1iMmVlLTRiMjktYTYwNi1lZTM3NmIzN2Y3ODciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwNDgxOTJ9.QB4bXo88KC7jr94QSjJI3Y1eVWeU4KNKdHRi1_7zcD0",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkZmQ4ZjI1NS0yMzkwLTRiOTgtODJmYi1jZGRmOTBjODYwYWMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODU1NzM2MDd9.Wn8RdLZGFifPe3LwAjljmHIoiFOOIzqeDCfQWvvy-_I",
"domain": "localhost",
"path": "/",
"expires": 1785048192.71579,
"expires": 1785573607.099308,
"httpOnly": true,
"secure": true,
"sameSite": "Lax"
@@ -27,11 +27,11 @@
"localStorage": [
{
"name": "authSyncEvent",
"value": "{\"type\":\"logout\",\"at\":1785037392502}"
"value": "{\"type\":\"logout\",\"at\":1785562806880}"
},
{
"name": "parentAuth",
"value": "{\"expiresAt\":1785210192904}"
"value": "{\"expiresAt\":1785735607328}"
}
]
}
+1 -17
View File
@@ -445,16 +445,8 @@ function openRoutineMenu(routineId: string, e: MouseEvent) {
'routine-kebab-menu',
() => document.querySelector('.kebab-menu') as HTMLElement | null,
)
tutorialMaybeShow(
'kebab-edit-points-cost',
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
)
tutorialMaybeShow(
'routine-schedule',
() => document.querySelector('[data-tutorial="routine-schedule"]') as HTMLElement | null,
)
})
const items = childRoutineListRef.value?.items ?? []
const items: ChildRoutine[] = childRoutineListRef.value?.items ?? []
const routine = items.find((r) => r.id === routineId)
if (routine) {
if (isRoutineExpired(routine)) {
@@ -740,14 +732,6 @@ function openChoreMenu(taskId: string, e: MouseEvent) {
'chore-kebab-menu',
() => document.querySelector('.kebab-menu') as HTMLElement | null,
)
tutorialMaybeShow(
'kebab-edit-points-cost',
() => document.querySelector('[data-tutorial~="kebab-edit-points-cost"]') as HTMLElement | null,
)
tutorialMaybeShow(
'chore-schedule',
() => document.querySelector('[data-tutorial="chore-schedule"]') as HTMLElement | null,
)
})
const items: ChildTask[] = childChoreListRef.value?.items ?? []
const task = items.find((t) => t.id === taskId)
@@ -1,17 +1,37 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import {
shouldShowStep,
clearChainProgress,
tutorialEnabled,
tutorialProgress,
sessionSkipped,
maybeShow,
dismissActive,
activeStep,
} from '../controller'
function setKebabAnchorHtml() {
document.body.innerHTML = `
<div class="kebab-menu"></div>
<button data-tutorial="chore-edit-points kebab-edit-points-cost">Edit Points</button>
<button data-tutorial="chore-schedule">Schedule</button>
`
}
describe('tutorial controller - shouldShowStep', () => {
beforeEach(() => {
tutorialEnabled.value = true
tutorialProgress.value = {}
sessionSkipped.value = false
activeStep.value = null
document.body.innerHTML = ''
global.fetch = vi.fn().mockResolvedValue({ ok: true })
})
afterEach(() => {
activeStep.value = null
document.body.innerHTML = ''
vi.restoreAllMocks()
})
it('returns true when tips are enabled, the step is unseen, and the session is not skipped', () => {
@@ -46,3 +66,103 @@ describe('tutorial controller - shouldShowStep', () => {
expect(shouldShowStep('status-pending', true)).toBe(true)
})
})
describe('tutorial controller - maybeShow', () => {
beforeEach(() => {
tutorialEnabled.value = true
tutorialProgress.value = {}
sessionSkipped.value = false
activeStep.value = null
setKebabAnchorHtml()
global.fetch = vi.fn().mockResolvedValue({ ok: true })
})
afterEach(() => {
activeStep.value = null
document.body.innerHTML = ''
vi.restoreAllMocks()
})
it('promotes a step when the anchor is present', () => {
maybeShow('chore-kebab-menu')
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
})
it('does not promote a step that has already been seen', () => {
tutorialProgress.value = { 'chore-kebab-menu': true }
maybeShow('chore-kebab-menu')
expect(activeStep.value).toBeNull()
})
it('is idempotent for the currently active step', () => {
maybeShow('chore-kebab-menu')
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
maybeShow('chore-kebab-menu')
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
})
it('drops a step whose anchor is missing', () => {
document.body.innerHTML = ''
maybeShow('chore-kebab-menu')
expect(activeStep.value).toBeNull()
})
})
describe('tutorial controller - dismissActive', () => {
beforeEach(() => {
tutorialEnabled.value = true
tutorialProgress.value = {}
sessionSkipped.value = false
activeStep.value = null
setKebabAnchorHtml()
global.fetch = vi.fn().mockResolvedValue({ ok: true })
})
afterEach(() => {
activeStep.value = null
document.body.innerHTML = ''
vi.restoreAllMocks()
})
it('marks the active step seen and chains the next step', () => {
maybeShow('chore-kebab-menu')
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
dismissActive(true)
expect(tutorialProgress.value['chore-kebab-menu']).toBe(true)
expect(activeStep.value?.def.id).toBe('chore-edit-points')
})
it('does not duplicate a chained step that is already queued', () => {
// Set up: open the chore kebab menu, then queue the next step early.
maybeShow('chore-kebab-menu')
maybeShow('chore-edit-points')
expect(activeStep.value?.def.id).toBe('chore-kebab-menu')
// Dismissing the menu should chain to chore-edit-points only once.
dismissActive(true)
expect(activeStep.value?.def.id).toBe('chore-edit-points')
// If a duplicate had been added, we would still be on chore-edit-points.
dismissActive(true)
expect(activeStep.value?.def.id).toBe('chore-schedule')
})
it('does not re-chain a step that is currently active', () => {
maybeShow('chore-kebab-menu')
dismissActive(true)
expect(activeStep.value?.def.id).toBe('chore-edit-points')
// Dismissing chore-edit-points should move to chore-schedule, not loop.
dismissActive(true)
expect(activeStep.value?.def.id).toBe('chore-schedule')
})
it('does not mark the step seen when markSeen is false', () => {
maybeShow('chore-kebab-menu')
dismissActive(false)
expect(tutorialProgress.value['chore-kebab-menu']).toBeUndefined()
expect(activeStep.value).toBeNull()
})
})
+1 -1
View File
@@ -147,7 +147,7 @@ export function dismissActive(markSeen = true) {
}
if (current.def.next) {
const nextDef = stepRegistry[current.def.next]
if (nextDef && shouldShowStep(nextDef.id)) {
if (nextDef && shouldShowStep(nextDef.id) && !queue.some((q) => q.def.id === nextDef.id)) {
queue.unshift({ def: nextDef, anchor: null })
}
}
+7 -12
View File
@@ -5,20 +5,15 @@
"enabled": true,
"type": "local",
"command": [
"docker",
"run",
"-i",
"--rm",
"-e",
"GITEA_ACCESS_TOKEN",
"-e",
"GITEA_HOST",
"docker.gitea.com/gitea-mcp-server"
"ssh",
"-T",
"devserver.lan",
"/home/ryan/gitea-mcp-wrapper.sh"
],
"environment": {
"GITEA_HOST": "https://git.ryankegel.com",
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
}
"GITEA_HOST": "https://git.ryankegel.com"
},
"timeout": 30000
}
}
}