Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
4d5944e
feat(a11y): wrap decorative HTML entities in aria-hidden
pboachie Mar 24, 2026
9715b88
🛡️ Sentinel: [HIGH] Fix missing rate limiting on invite_accept endpoint
pboachie Mar 25, 2026
04e2305
🎨 Palette: Improve button accessibility with contextual ARIA labels
pboachie Mar 25, 2026
f0b66a4
I have added a global loading state to standard (non-HTMX) forms for …
pboachie Mar 28, 2026
f24959f
🎨 Palette: Fix screen reader output for empty states
pboachie Mar 30, 2026
5bdad06
🛡️ Sentinel: [HIGH] Add missing rate limiting to authentication bound…
pboachie Apr 1, 2026
9aa329d
🎨 Palette: Add aria-label to progress elements for a11y
pboachie Apr 3, 2026
a70e49c
🛡️ Sentinel: [HIGH] Add rate limiting to portal authentication endpoints
pboachie Apr 3, 2026
3736987
Add three Outcome Insights parking lot items from old qual-analysis r…
gilliankerr Apr 3, 2026
827cb91
Implement INSIGHTS-DQ1, INSIGHTS-CONF1, and INSIGHTS-LANG1
gilliankerr Apr 3, 2026
3cc7f48
Mark INSIGHTS-DQ1, INSIGHTS-CONF1, INSIGHTS-LANG1 as done in TODO.md
gilliankerr Apr 3, 2026
4798804
Fix duplicate voice coverage sentence in Participant Voice section
gilliankerr Apr 3, 2026
7981b0a
Clean up redundant regex flag and AI prompt formatting
gilliankerr Apr 3, 2026
4d9af03
🛡️ Sentinel: [MEDIUM] Add missing rate limit to goal builder save end…
pboachie Apr 4, 2026
a875216
🛡️ Sentinel: [HIGH] Add rate limiting to portal auth endpoints
pboachie Apr 6, 2026
55bc499
Fix decorative HTML entities a11y issues
pboachie Apr 6, 2026
646eb5b
Add DRR and implementation prompt for de-identified evaluation microd…
gilliankerr Apr 7, 2026
e04058c
Merge pull request #604 from LogicalOutcomes/feat/evaluation-microdat…
gilliankerr Apr 7, 2026
04f117f
Add session prompt for evaluation export and TODO entry
gilliankerr Apr 7, 2026
7f98f59
Merge pull request #605 from LogicalOutcomes/chore/evaluation-export-…
gilliankerr Apr 7, 2026
6070164
Add report.evaluation_export permission for de-identified evaluation …
gilliankerr Apr 7, 2026
da5ce89
Add de-identification pipeline engine for evaluation microdata export
gilliankerr Apr 7, 2026
8b6ae9c
Add evaluation export form, view, and templates (EVAL-FORM1)
gilliankerr Apr 7, 2026
8e5e2c7
Wire evaluation export into navigation and SecureExportLink (EVAL-NAV1)
gilliankerr Apr 7, 2026
7bb361b
Add is_evaluation_exportable to CustomFieldGroup (EVAL-ADMIN1)
gilliankerr Apr 7, 2026
bbabe71
Mark EVAL-EXPORT1 as done in TODO.md
gilliankerr Apr 7, 2026
edc3e1f
Simplify: fix N+1 queries, remove dead state, deduplicate code
gilliankerr Apr 7, 2026
8e718e8
Fix critical issues from session review
gilliankerr Apr 7, 2026
30ac100
Add migration for is_evaluation_exportable + fix metric name collisions
gilliankerr Apr 7, 2026
a43e91b
Merge pull request #606 from LogicalOutcomes/feat/evaluation-microdat…
gilliankerr Apr 7, 2026
27b2e5f
Update program filter to use 'status="active"' instead of 'is_active=…
pboachie Apr 8, 2026
e177bf9
Fix browser title showing agency name instead of KoNote
gilliankerr Apr 8, 2026
bbbb062
Merge pull request #564 from LogicalOutcomes/palette-aria-hidden-enti…
pboachie Apr 8, 2026
b6a677b
Merge pull request #568 from LogicalOutcomes/sentinel/invite-rate-lim…
pboachie Apr 8, 2026
84f1f45
Merge pull request #569 from LogicalOutcomes/palette-add-contextual-a…
pboachie Apr 8, 2026
68dd760
Merge pull request #573 from LogicalOutcomes/palette-form-loading-sta…
pboachie Apr 8, 2026
98d5be3
Merge pull request #576 from LogicalOutcomes/palette-fix-empty-state-…
pboachie Apr 8, 2026
a811c2e
Merge pull request #580 from LogicalOutcomes/sentinel-rate-limit-5855…
pboachie Apr 8, 2026
99235aa
Merge pull request #591 from LogicalOutcomes/palette-ux-progress-aria…
pboachie Apr 8, 2026
ff847d1
Merge pull request #592 from LogicalOutcomes/sentinel-add-ratelimit-t…
pboachie Apr 8, 2026
6ec653e
Merge pull request #596 from LogicalOutcomes/security/fix-missing-ai-…
pboachie Apr 8, 2026
7b074f4
Merge pull request #600 from LogicalOutcomes/security/rate-limit-port…
pboachie Apr 8, 2026
8923385
Merge pull request #601 from LogicalOutcomes/fix-decorative-entities-…
pboachie Apr 8, 2026
b69258d
Merge pull request #609 from LogicalOutcomes/worktree/session-2026040…
pboachie Apr 8, 2026
71e6ef4
Add evaluation export governance tasks and documentation plan
gilliankerr Apr 8, 2026
a66b568
Merge pull request #610 from LogicalOutcomes/feat/eval-export-governa…
gilliankerr Apr 8, 2026
ebe138a
Add per-user evaluation export permission + demo seed command
gilliankerr Apr 8, 2026
e279eaa
Merge pull request #611 from LogicalOutcomes/feat/eval-export-demo-seed
gilliankerr Apr 8, 2026
8a4c62c
Add REVIEW.md for Claude Code Review integration
gilliankerr Apr 8, 2026
f3cb416
Fix review issues in eval export seed command
gilliankerr Apr 8, 2026
c82a584
Merge pull request #614 from LogicalOutcomes/fix/eval-export-review-f…
gilliankerr Apr 8, 2026
4eb87c0
Add TODO comment linking evaluation_export_granted to EVAL-GOV1
gilliankerr Apr 8, 2026
ce46716
Merge pull request #615 from LogicalOutcomes/fix/eval-export-todo-com…
gilliankerr Apr 8, 2026
8ec4290
Rename Evaluation Export to Evaluator Export (Confidential)
gilliankerr Apr 8, 2026
f0a9e3c
Merge pull request #616 from LogicalOutcomes/fix/rename-evaluator-export
gilliankerr Apr 8, 2026
fe2b5b7
Merge pull request #613 from LogicalOutcomes/worktree/session-2026040…
pboachie Apr 8, 2026
546cda4
Enforce per-user grant for Evaluator Export, fix admin menu, wire dem…
gilliankerr Apr 9, 2026
2cefa21
Merge pull request #617 from LogicalOutcomes/fix/eval-export-admin-by…
gilliankerr Apr 9, 2026
dc4b33a
Enhance metrics and custom fields handling, add financial coaching su…
pboachie Apr 9, 2026
da53dfc
Merge pull request #618 from LogicalOutcomes/pb-konote-updates
pboachie Apr 9, 2026
edb0dff
Merge pull request #619 from LogicalOutcomes/develop
pboachie Apr 9, 2026
92394bf
Untrack .Jules/ files so .gitignore rule takes effect
gilliankerr Apr 9, 2026
40a8486
Merge pull request #621 from LogicalOutcomes/worktree/session-2026040…
gilliankerr Apr 9, 2026
307e500
Merge main into staging to resolve PR #620 conflicts
pboachie Apr 9, 2026
5b3acca
Simplify per-user eval export check and short-circuit demo seed
gilliankerr Apr 9, 2026
922f407
Merge pull request #622 from LogicalOutcomes/fix/eval-export-admin-by…
gilliankerr Apr 9, 2026
9c5b03b
Add regression test and atomicity comment for Evaluator Export
gilliankerr Apr 9, 2026
a20629b
Restructure eval export tests: helper unit tests + focused view test
gilliankerr Apr 9, 2026
333150c
Merge pull request #623 from LogicalOutcomes/test/eval-export-admin-r…
gilliankerr Apr 9, 2026
40fd72f
Add EVAL-GOV1 implementation prompt for a new session
gilliankerr Apr 9, 2026
9114ff6
Merge pull request #624 from LogicalOutcomes/docs/eval-gov1-prompt
gilliankerr Apr 9, 2026
9e61cbf
Record Evaluator Export bypass fix in TODO.md
gilliankerr Apr 9, 2026
dceb8ba
Merge pull request #625 from LogicalOutcomes/chore/todo-eval-gov-bypa…
gilliankerr Apr 9, 2026
b56fa50
fix: use backdate instead of created_at in tests and add evaluation_e…
pboachie Apr 9, 2026
4df6477
Merge pull request #626 from LogicalOutcomes/fix/test-date-backdate-a…
pboachie Apr 9, 2026
8607340
Merge pull request #627 from LogicalOutcomes/develop
pboachie Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions .Jules/palette.md

This file was deleted.

4 changes: 0 additions & 4 deletions .Jules/sentinel.md

This file was deleted.

4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
## 2025-03-10 - Add ARIA Labels to Copy Buttons
**Learning:** Many "Copy" buttons in the app use the `copy-btn` class but lack `aria-label`s. Screen readers might just read "Copy" out of context, which can be confusing (e.g., "Copy what?").
**Action:** Add descriptive `aria-label` attributes to these buttons (e.g., `aria-label="{% trans 'Copy calendar link' %}"`) to improve accessibility.

## 2025-03-10 - Add Contextual ARIA Labels to Action Buttons in Lists
**Learning:** Action buttons like "Approve" and "Dismiss" in dynamically generated lists (like survey assignments) lack context for screen readers when they don't explicitly reference the target item. A user tabbing through the page would hear "Approve, button", "Dismiss, button", etc., multiple times without knowing *what* is being approved.
**Action:** Always add descriptive `aria-label` attributes to generic action buttons inside iterative loops, using `{% blocktrans %}` to include the item's name dynamically (e.g., `aria-label="{% blocktrans with name=a.survey.name %}Approve {{ name }}{% endblocktrans %}"`).
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
**Vulnerability:** The `demo_portal_login` view was missing rate limiting, making it vulnerable to brute-force or DoS attacks.
**Learning:** Even endpoints designed for demo purposes need to be protected. The `django-ratelimit` decorator with `block=True` should be applied uniformly to all authentication-related endpoints.
**Prevention:** Always ensure the `@ratelimit(key="ip", rate="...", method="POST", block=True)` decorator is present on any view that processes login or authentication requests. Also make sure the import is present: `from django_ratelimit.decorators import ratelimit`.

## 2024-03-05 - [Missing Rate Limiting on Invite Endpoints]
**Vulnerability:** The `invite_accept` endpoint in `apps/auth_app/invite_views.py` allowed unauthenticated users to accept invites and register without rate limits.
**Learning:** This made it vulnerable to brute force and DoS attacks by making it easy to test combinations of `code` or flood the endpoint with POST requests.
**Prevention:** As with all public/authentication-related endpoints, ensure the `@ratelimit(key="ip", rate="...", method=["GET", "POST"], block=True)` decorator is applied. When using `key="ip"`, note that it assumes correct proxy settings are present (e.g. `X-Forwarded-For`) so it doesn't just block the proxy IP.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ Current DRRs (all in `tasks/design-rationale/` — see `README.md` there for the
- `data-access-residency-policy.md` — Canadian residency by data access level
- `document-integration.md` — SharePoint + Google Drive integration
- `encryption-key-rotation.md` — Key rotation procedures and custody
- `evaluation-microdata-export.md` — De-identified microdata export for external evaluators, k-anonymity pipeline
- `executive-dashboard-redesign.md` — Dashboard UX with accessibility focus
- `fhir-informed-modelling.md` — FHIR concepts without FHIR compliance
- `funder-reporting-profiles.md` — Template-based funder reporting (Parking Lot)
Expand Down
46 changes: 46 additions & 0 deletions REVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Code Review Rules for KoNote

## Privacy & Consent

- Any view that queries `ProgressNote` and displays note content to staff **must** apply PHIPA consent filtering. List views: `apply_consent_filter()`. Single-note views: `check_note_consent_or_403()`. See `tasks/design-rationale/phipa-consent-enforcement.md`.
- PII fields use encrypted property accessors (`client.first_name`, not `_first_name_encrypted`). Never query encrypted fields in SQL — filter in Python.
- Audit log writes must use the audit database: `AuditLog.objects.using("audit")`.

## Forms & Validation

- Always use Django `ModelForm` in `forms.py` for validation. Never use raw `request.POST.get()` directly in views.

## Templates & Frontend

- Use `{{ term.client }}` for terminology — never hardcode "client", "participant", etc.
- Use `{{ features.programs }}` for feature toggles.
- No React, Vue, webpack, or npm. Server-rendered Django templates + HTMX + Pico CSS only.
- HTMX responses must have global `htmx:responseError` handling so errors don't fail silently.
- Checkboxes must be **inside** the `<label>` tag (Pico CSS requirement). Use `{% include "includes/_form_field.html" %}` in form loops.

## Accessibility

- WCAG 2.2 AA: semantic HTML, colour contrast, alt text, keyboard navigation.

## Translations

- New or modified templates with user-visible text must use `{% trans %}` or `{% blocktrans %}` tags.
- Check that corresponding French translations exist in `locale/fr/LC_MESSAGES/django.po`.

## Spelling

- Canadian English: colour, centre, behaviour, organisation, but **-ize** not -ise (organize, optimize, analyze). Use "program" not "programme" in English.

## Design Rationale

- Before approving changes to features covered by a Design Rationale Record (`tasks/design-rationale/`), check that the PR does not re-introduce a rejected anti-pattern.

## Testing & QA

- New or changed views should have corresponding tests (permissions, form validation, happy path).
- New URL routes should have a matching entry in `konote-qa-scenarios/pages/page-inventory.yaml`.
- Migrations must be included when models change.

## Admin Routes

- All `/admin/*` routes are admin-only, enforced by RBAC middleware. New admin routes must follow this pattern.
20 changes: 20 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ Step-by-step commands for each task are in [tasks/recurring-tasks.md](tasks/recu
- [ ] Push Circle/CircleMember Entity lists — depends on above (FIELD-ODK-CIRCLES1)
- [ ] Agency-facing documentation — ODK Collect setup, device loss protocol (FIELD-ODK-DOC1)

### Phase: Evaluation Export Governance & Documentation (see tasks/eval-export-governance.md)

**Code & UI:**
- [ ] Add reason field to evaluation export permission grant — free-text logged when `report.evaluation_export` is granted. Detailed implementation prompt in [tasks/phase-eval-gov1-prompt.md](tasks/phase-eval-gov1-prompt.md) — a new session can pick this up cold (EVAL-GOV1)
- [ ] Admin dashboard card for evaluation export — shows N authorised users, last export date, click-through to permission list (EVAL-GOV2)
- [ ] Permission audit list page — who has the permission, granted by whom, when, why, last used, revoke button (EVAL-GOV3)
- [ ] Export history view — past evaluation exports with program, evaluator, counts, status (EVAL-GOV4)
- [ ] Agreement expiry warning — banner on export history when evaluator agreement has passed (EVAL-GOV5)
- [ ] Wire up `is_evaluation_exportable` custom field groups — form dynamically shows exportable groups as QI columns (EVAL-GOV6)
- [ ] Pipeline test suite — consent filtering, k-anonymity, blocking, pseudonymous IDs, CSV output (EVAL-GOV7)

**Documentation:**
- [ ] Update admin reporting guide with evaluation export section — `docs/admin/reporting.md` (EVAL-DOC1)
- [ ] Update deployment protocol — add evaluation export to permissions interview — `tasks/agency-permissions-interview.md` Section 7 (EVAL-DOC2)
- [ ] Add evaluation export section to user guide — `docs/help.md` (EVAL-DOC3)
- [ ] ED-facing one-page evaluation export reference — `docs/evaluation-export-guide.md` (EVAL-DOC4)

### Phase: Documentation & Website Updates

_All documentation tasks completed — see Recently Done._
Expand Down Expand Up @@ -129,6 +146,9 @@ Not yet clear we should build these, or the design isn't settled. May be too com

## Recently Done

- [x] Close Evaluator Export admin bypass — removed `is_admin` bypass in `can_create_evaluation_export` + nav check, added missing Team Members link to admin menu, wired `seed_eval_export_demo` into container-startup orchestrator with Casey/Morgan/Eva granted, added fast-path short-circuit, hoisted `EVAL_EXPORT_GRANTEES` constant, added regression tests (`EvaluatorExportPermissionTest` in `tests/test_export_permissions.py`), wrote EVAL-GOV1 implementation prompt — PRs #617, #622, #623, #624 — 2026-04-09 (EVAL-GOV-BYPASS1)
- [x] De-identified evaluation microdata export — 10-step de-identification pipeline with k-anonymity (k=5), pseudonymous IDs, generalised demographics, population thresholds, enhanced audit trail, preview/confirm flow, permission-gated nav — 2026-04-07 (EVAL-EXPORT1)
- [x] Insights quality & language features — DQ1: practice signal contextual sentence + month count in summary bar; CONF1: raised theme auto-link threshold from 2→3 words; LANG1: EN/FR language detection on quotes, AI prompt language awareness, FR pills on mixed-language Insights pages — PR #595 — 2026-04-03 (INSIGHTS-DQ1, INSIGHTS-CONF1, INSIGHTS-LANG1)
- [x] Dashboard & Insights enrichment — FHIR metadata features: program summary sentences, practice indicators, goal source distribution on Insights; stale episodes attention signal on Dashboard; batch query optimization; simplified both pages (removed funder stats, cohort comparison, cross-tab, dashboard bloat — 8 sections → 5 on Insights, ~20 rows → ~8 per program card) — PRs #529-#533 — 2026-03-16 (ENRICH1)
- [x] Expand accessibility tests to cover portal flow (dashboard, journal, goals) and report/chart flow (outcome insights) — axe-core tests in test_a11y_ci.py — 2026-03-12 (REV26-A11Y1)
- [x] AI provider configuration guide for operators — docs/ai-provider-guide.md covering cloud vs self-hosted, data residency, configuration, costs — 2026-03-12 (REV26-AI4)
Expand Down
2 changes: 1 addition & 1 deletion apps/admin_settings/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def check_demo_data_health(app_configs, **kwargs):
if not ClientFile.objects.filter(is_demo=True).exists():
return warnings

programs = Program.objects.filter(is_active=True)
programs = Program.objects.filter(status="active")
low_programs = []

for prog in programs:
Expand Down
99 changes: 79 additions & 20 deletions apps/admin_settings/management/commands/apply_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,17 +223,31 @@ def _apply_metrics(enabled_names, disabled_names, stdout):

enabled_count = 0
disabled_count = 0

for name in enabled_names:
updated = MetricDefinition.objects.filter(name=name).update(is_enabled=True)
configured_count = 0

for entry in enabled_names:
# Support both plain names and dicts with properties
if isinstance(entry, dict):
name = entry["name"]
updates = {"is_enabled": True}
if "cadence_sessions" in entry:
updates["cadence_sessions"] = entry["cadence_sessions"]
updated = MetricDefinition.objects.filter(name=name).update(**updates)
if updated:
configured_count += 1
else:
updated = MetricDefinition.objects.filter(name=entry).update(is_enabled=True)
enabled_count += updated

for name in disabled_names:
updated = MetricDefinition.objects.filter(name=name).update(is_enabled=False)
disabled_count += updated

_log(stdout, f" Metrics: {enabled_count} enabled, {disabled_count} disabled.")
return {"Metrics": f"{enabled_count} enabled, {disabled_count} disabled"}
msg = f" Metrics: {enabled_count} enabled, {disabled_count} disabled"
if configured_count:
msg += f", {configured_count} configured"
_log(stdout, msg + ".")
return {"Metrics": f"{enabled_count} enabled, {disabled_count} disabled, {configured_count} configured"}



Expand Down Expand Up @@ -284,32 +298,77 @@ def _apply_custom_fields(groups_data, stdout):

group_count = 0
field_count = 0
updated_count = 0

for i, grp in enumerate(groups_data):
grp_defaults = {
"sort_order": grp.get("sort_order", i),
"admin_only": grp.get("admin_only", False),
"collapsed_by_default": grp.get("collapsed_by_default", False),
}
if "status" in grp:
grp_defaults["status"] = grp["status"]

group, grp_created = CustomFieldGroup.objects.get_or_create(
title=grp["title"],
defaults={
"sort_order": i,
"admin_only": grp.get("admin_only", False),
},
defaults=grp_defaults,
)
if grp_created:
group_count += 1
else:
# Update group properties that are explicitly set in the config
changed = False
for attr in ("admin_only", "collapsed_by_default", "sort_order", "status"):
if attr in grp and getattr(group, attr) != grp[attr]:
setattr(group, attr, grp[attr])
changed = True
if changed:
group.save()
updated_count += 1

for j, fld in enumerate(grp.get("fields", [])):
_, fld_created = CustomFieldDefinition.objects.get_or_create(
fld_defaults = {
"input_type": fld.get("input_type", "text"),
"is_required": fld.get("is_required", False),
"is_sensitive": fld.get("is_sensitive", False),
"options_json": fld.get("options", []),
"sort_order": fld.get("sort_order", j),
"front_desk_access": fld.get("front_desk_access", "none"),
"show_on_create": fld.get("show_on_create", False),
"is_dv_sensitive": fld.get("is_dv_sensitive", False),
}
if fld.get("placeholder"):
fld_defaults["placeholder"] = fld["placeholder"]
if "status" in fld:
fld_defaults["status"] = fld["status"]

field, fld_created = CustomFieldDefinition.objects.get_or_create(
group=group,
name=fld["name"],
defaults={
"input_type": fld.get("input_type", "text"),
"is_required": fld.get("is_required", False),
"is_sensitive": fld.get("is_sensitive", False),
"options_json": fld.get("options", []),
"sort_order": j,
},
defaults=fld_defaults,
)
if fld_created:
field_count += 1

_log(stdout, f" Custom fields: {group_count} groups, {field_count} fields.")
return {"Custom fields": f"{group_count} group(s), {field_count} field(s)"}
else:
# Update field properties that are explicitly set in the config
changed = False
for attr, config_key in (
("front_desk_access", "front_desk_access"),
("show_on_create", "show_on_create"),
("is_dv_sensitive", "is_dv_sensitive"),
("status", "status"),
("sort_order", "sort_order"),
("is_required", "is_required"),
("options_json", "options"),
):
if config_key in fld:
val = fld[config_key]
if getattr(field, attr) != val:
setattr(field, attr, val)
changed = True
if changed:
field.save()
updated_count += 1

_log(stdout, f" Custom fields: {group_count} groups, {field_count} fields created, {updated_count} updated.")
return {"Custom fields": f"{group_count} group(s), {field_count} field(s) created, {updated_count} updated"}
Loading
Loading