Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e7b0ef6
Add lastmod to sitemap group listings and homepage
timujinne Mar 18, 2026
072fd0e
Merge remote-tracking branch 'upstream/dev' into dev
timujinne Mar 19, 2026
d20da8c
Add email provider behaviour, refactor Mailer and UserNotifier
timujinne Mar 19, 2026
af6af2e
Add AdminEditHelper for universal admin edit links in public views
timujinne Mar 19, 2026
0d45a28
Remove Emails module from core, extract to phoenix_kit_emails package
timujinne Mar 20, 2026
0451081
Fix cookie consent: dynamic legal links, theme-aware backdrop, daisyU…
timujinne Mar 20, 2026
1e03f22
Remove hardcoded Emails block from Modules page — now rendered as ext…
timujinne Mar 20, 2026
e3b8cf0
Enrich external module cards with config stats, settings link, and mo…
timujinne Mar 21, 2026
0edeefd
Add system_prompt field to AI prompts and AI Playground page
mdon Mar 23, 2026
be42da0
Merge pull request #443 from mdon/dev
ddon Mar 23, 2026
f42de62
Add PR review docs for #443 AI system prompt and playground
ddon Mar 23, 2026
5153873
Add database connection check to install and update tasks
construct-d Mar 23, 2026
6f28089
Fix connection check
construct-d Mar 23, 2026
2bd4ec7
Fix warning
construct-d Mar 23, 2026
a1f3996
Fix module_card to render hero-* icons properly
timujinne Mar 23, 2026
4073072
Fix media selector modal z-index to appear above all overlays
Mar 23, 2026
bc7c842
Merge pull request #445 from construct-d/dev
ddon Mar 23, 2026
6b43427
Merge remote-tracking branch 'upstream/dev' into dev
timujinne Mar 23, 2026
a36e300
Update Leaf dependency to v0.2.5
Mar 23, 2026
328c797
Merge pull request #446 from alexdont/dev
ddon Mar 23, 2026
d91df44
Fix DbConnectionCheck: correct spec, naming, and remove hard exit fro…
ddon Mar 23, 2026
977c64f
Add PR review docs for #446 Leaf editor v0.2.5 and z-index fix
ddon Mar 23, 2026
545b0e7
Fix external_plugin_view? to recognize PhoenixKit.Modules.*.Web as ex…
timujinne Mar 23, 2026
4c38e91
Fix extract_admin_links: skip parent tabs, deduplicate paths
timujinne Mar 23, 2026
6b0df80
Merge pull request #447 from timujinne/dev
ddon Mar 23, 2026
d1bdbad
Fix post-merge issues from #447 Emails extraction
ddon Mar 24, 2026
0c79605
Extract Publishing module into external phoenix_kit_publishing package
mdon Mar 24, 2026
53cec97
Guard all Publishing references behind Code.ensure_loaded? for extern…
mdon Mar 24, 2026
150efba
Extract Entities module into external phoenix_kit_entities package
mdon Mar 24, 2026
2833857
Extract AI module into external phoenix_kit_ai package
mdon Mar 24, 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
67 changes: 14 additions & 53 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,68 +12,23 @@
{"lib/phoenix_kit/install/migration_strategy.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.status.ex", :unknown_function},
{"lib/phoenix_kit/migrations/postgres.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/email_cleanup.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/email_export.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/email_stats.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/email_test_webhook.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/email_verify_config.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/entities/export.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit/entities/import.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.email.debug_sqs.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.email.process_dlq.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.email.send_test.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.configure_aws_ses.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.process_dlq.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.process_sqs_queue.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.sync_email_status.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.seed_templates.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.fix_missing_events.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.process_sqs.ex", :unknown_function},
{"lib/mix/tasks/phoenix_kit.cleanup_orphaned_files.ex", :unknown_function},

# Mix.Task behaviour callbacks (expected in Mix tasks)
# Note: Mix.Task behaviour info is not available to Dialyzer (compile-time only)
# Adding @impl Mix.Task does not fix this warning
{"lib/mix/tasks/phoenix_kit.doctor.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.gen.migration.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.seed_templates.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.install.ex", :callback_info_missing, 2},
{"lib/mix/tasks/phoenix_kit.update.ex", :callback_info_missing, 3},
{"lib/mix/tasks/phoenix_kit.gen.admin_page.ex", :callback_info_missing},
{"lib/mix/tasks/phoenix_kit.gen.dashboard_tab.ex", :callback_info_missing},
{"lib/mix/tasks/phoenix_kit.modernize_layouts.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.assets.rebuild.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.status.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/email_cleanup.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/email_export.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/email_stats.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/email_test_webhook.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/email_verify_config.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/entities/export.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit/entities/import.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.email.debug_sqs.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.email.process_dlq.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.email.send_test.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.configure_aws_ses.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.process_dlq.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.process_sqs_queue.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.sync_email_status.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.fix_missing_events.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.process_sqs.ex", :callback_info_missing, 1},
{"lib/mix/tasks/phoenix_kit.cleanup_orphaned_files.ex", :callback_info_missing, 1},

# False positive pattern match warnings (runtime behavior differs from static analysis)
{"lib/mix/tasks/phoenix_kit/email_cleanup.ex", :pattern_match, 1},

# Publishing module defensive fallbacks and settings_call dynamic dispatch
{"lib/modules/publishing/publishing.ex", :guard_fail},
{"lib/modules/publishing/publishing.ex", :pattern_match_cov},
{"lib/modules/publishing/publishing.ex", :pattern_match},
{"lib/modules/publishing/shared.ex", :guard_fail},
# ExAws library type definition issues (false positives from incomplete type specs)
~r/lib\/modules\/emails\/archiver\.ex:.*pattern_match/,
~r/lib\/modules\/emails\/archiver\.ex:.*unused_fun/,

# Publishing module (extracted) — dynamic dispatch through publishing_module() helper
# Ecto.Multi opaque type false positives (code works correctly)
~r/lib\/phoenix_kit\/users\/auth\.ex:.*call_without_opaque/,

Expand All @@ -86,11 +41,7 @@
~r/lib\/modules\/legal\/schemas\/consent_log\.ex:.*no_return/,
~r/lib\/modules\/legal\/schemas\/consent_log\.ex:.*call/,

# Publishing Editor submodules - with-chain type inference false positives
~r/lib\/modules\/publishing\/web\/editor\/.*\.ex:.*pattern_match/,
~r/lib\/modules\/publishing\/web\/editor\/.*\.ex:.*pattern_match_cov/,

# Pages module - same type inference false positives as Publishing (copied codebase)
# Pages module - type inference false positives
~r/lib\/modules\/pages\/listing_cache\.ex:.*pattern_match/,
~r/lib\/modules\/pages\/storage\/.*\.ex:.*pattern_match/,
~r/lib\/modules\/pages\/storage\/.*\.ex:.*call/,
Expand Down Expand Up @@ -129,18 +80,28 @@
# Entity form - defensive catch-all clauses for mb_to_bytes and parse_accept_list
# Dialyzer proves previous clauses cover all actual call-site types but
# catch-alls are kept intentionally for safety with dynamic form params
{"lib/modules/entities/web/entity_form.ex", :pattern_match_cov},

# tab_callback_context/1 has a :user_dashboard_tabs clause for future use
# but compile_module_admin_routes only passes :admin_tabs and :settings_tabs currently
{"lib/phoenix_kit_web/integration.ex", :pattern_match},

# External optional modules guarded by Code.ensure_loaded? at runtime
{"lib/modules/sitemap/sources/posts.ex", :unknown_function},
{"lib/modules/sitemap/sources/publishing.ex", :unknown_function},
{"lib/modules/pages/renderer.ex", :unknown_function},
{"lib/modules/pages/page_builder/renderer.ex", :unknown_function},
{"lib/phoenix_kit/dashboard/registry.ex", :unknown_function},
{"lib/phoenix_kit/install/css_integration.ex", :unknown_function},
{"lib/phoenix_kit/scheduled_jobs/workers/process_scheduled_jobs_worker.ex", :unknown_function},

# ExUnit internal functions — false positives when test/support is compiled in MIX_ENV=test
# Dialyzer cannot resolve ExUnit private macros expanded at compile time
{"test/support/conn_case.ex", :unknown_function},
{"test/support/data_case.ex", :unknown_function}
{"test/support/data_case.ex", :unknown_function},

# Sync connections_live - MapSet opaque type false positives in topo_sort/visit_node
# Same pattern as context_selector.ex - MapSet.t() opaque type through recursive functions
# Matches both standard Dialyxir format (call_without_opaque) and legacy format (opaque term)
~r/lib\/modules\/sync\/web\/connections_live\.ex:.*call_without_opaque/,
~r/lib\/modules\/sync\/web\/connections_live\.ex:.*opaque term/
]
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ git status
PhoenixKit has two levels of tests:

1. **Unit tests** (`test/phoenix_kit/`, `test/modules/`) — Pure logic, no DB required
2. **Integration tests** (`test/integration/`, `test/modules/publishing/integration/`) — Real PostgreSQL via Ecto sandbox
2. **Integration tests** (`test/integration/`) — Real PostgreSQL via Ecto sandbox

#### Test database setup

Expand Down
4 changes: 0 additions & 4 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ config :phoenix_kit,
session_fingerprint_enabled: true,
session_fingerprint_strict: false

# Future: Configure FakeSettings when publishing tests are implemented
# config :phoenix_kit,
# publishing_settings_module: PhoenixKit.Test.FakeSettings

# Configure logger for tests
config :logger, level: :warning

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PhoenixKit modules can be independently installable as separate hex packages. Th

### PhoenixKit.Module Behaviour

All 21 internal modules implement `use PhoenixKit.Module`, which provides:
All internal modules implement `use PhoenixKit.Module`, which provides:

**Required callbacks:**
- `module_key/0` — unique string identifier (e.g., `"tickets"`)
Expand All @@ -33,14 +33,14 @@ All 21 internal modules implement `use PhoenixKit.Module`, which provides:
### PhoenixKit.ModuleRegistry

GenServer using `:persistent_term` for zero-cost reads. Loads modules from two sources:
1. **Internal modules** — hardcoded list in `internal_modules/0` (the ONE place that enumerates all 21 bundled modules)
1. **Internal modules** — hardcoded list in `internal_modules/0` (the ONE place that enumerates all bundled modules)
2. **External modules** — auto-discovered from beam files via `PhoenixKit.ModuleDiscovery`, with `Application.get_env(:phoenix_kit, :modules, [])` as fallback

Provides aggregated queries: `all_admin_tabs/0`, `all_settings_tabs/0`, `all_permission_metadata/0`, `feature_enabled_checks/0`, `get_by_key/1`, etc.

### Core Files Refactored

Seven core files that previously hardcoded references to all 21 modules now use the registry:
Seven core files that previously hardcoded references to all modules now use the registry:

| File | What Changed |
|------|-------------|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Claude Review — PR #443: AI System Prompt & Playground

**Verdict:** Approve with minor observations

## What's Good

1. **Clean system_prompt integration.** The field slots naturally into the existing Prompt schema. Variable extraction from both fields is handled by combining strings before regex, which keeps the logic simple and avoids duplicate extraction paths.

2. **`render_system_prompt/2` follows existing patterns.** Pattern-matched on `nil` and `""` returning `{:ok, nil}`, consistent with how `render/2` works. The `with` chain in `ask_with_prompt` integrates cleanly.

3. **Playground LiveView is well-structured.** Separation of concerns is clear: `apply_form_changes` delegates to focused `maybe_update_*` helpers, request execution is split into `execute_prompt_request` and `execute_freeform_request`, and the async `handle_info(:do_send, ...)` pattern avoids blocking the LiveView process.

4. **Migration is idempotent.** V85 checks both table and column existence with `information_schema` queries before altering — safe for re-runs and partial migrations.

5. **Good test coverage.** 44 unit tests covering the Prompt module's public API comprehensively, including edge cases (nil, empty strings, atom vs string keys, deduplication).

## Issues Found

### Minor: Missing `tab-active` class on Playground tab in endpoints page

In `endpoints.html.heex`, the Playground tab link (lines 145-150 of the diff) lacks the conditional `tab-active` class that other tabs use:

```heex
# Current (missing active state):
<.link navigate={...} class="tab">

# Expected pattern (matching other tabs):
<.link navigate={...} class={"tab #{if @active_tab == "playground", do: "tab-active"}"}>
```

The same issue exists in `prompts.html.heex` — the Playground tab there also has no active state conditional. Only `playground.html.heex` itself correctly shows the active state (hardcoded as `tab tab-active`).

**Impact:** The Playground tab won't highlight in the navigation when navigating from Endpoints or Prompts pages. Cosmetic only.

### Minor: Playground doesn't extract variables from system_prompt when switching prompts

In `maybe_update_prompt/2` (playground.ex), when a prompt is selected, variables are extracted only from `edited_content`:

```elixir
variables = if prompt, do: Prompt.extract_variables(edited_content || ""), else: []
```

If the prompt's `system_prompt` also contains variables, they won't appear as input fields until the user edits the content template. Should combine both fields:

```elixir
variables = if prompt do
system_vars = Prompt.extract_variables(prompt.system_prompt || "")
content_vars = Prompt.extract_variables(edited_content || "")
Enum.uniq(system_vars ++ content_vars)
else
[]
end
```

**Impact:** Variables defined only in the system prompt won't have input fields in the Playground. They'll be sent as-is with `{{VarName}}` unreplaced.

### Observation: `phx-update="ignore"` on content textarea

The edited content textarea uses `phx-update="ignore"` to preserve user edits across re-renders. This is correct for the use case, but means the textarea won't update if the user selects a different prompt — the `maybe_update_prompt` code sets `edited_content` in assigns but the DOM won't reflect it because of the ignore directive.

This is mitigated by the wrapping div having a dynamic ID (`edited-content-wrap-#{@selected_prompt.uuid}`), which forces a new DOM element when the prompt changes. Clever solution.

### Observation: Blocking AI call in handle_info

The `handle_info(:do_send, ...)` handler calls `execute_request/1` synchronously. While this is better than doing it in `handle_event` (the UI updates with the loading state first), a very slow AI response will still block other messages to this LiveView process.

For an admin-only tool this is acceptable. If it becomes an issue, wrapping in `Task.Supervisor.async_nolink` would be the standard improvement.

## Testing Notes

- Tests are all unit-level (no DB required) — they test `Prompt` struct functions directly
- No integration tests for the Playground LiveView itself, which is reasonable for an initial PR
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# PR #443 — Add system_prompt field to AI prompts and AI Playground page

**Author:** Max (mdon)
**Base:** dev
**Date:** 2026-03-23
**Impact:** +1,099 lines / -7 lines

## Summary

Adds a `system_prompt` field to AI prompts (enabling separate system-level instructions) and introduces a new AI Playground LiveView for interactively testing endpoints and prompts from the admin dashboard.

## Key Changes

### 1. System Prompt Field

| File | Change |
|------|--------|
| `migrations/postgres/v85.ex` | New migration: adds `system_prompt` TEXT column to `phoenix_kit_ai_prompts` |
| `prompt.ex` | Schema field, changeset cast, `render_system_prompt/2` function |
| `ai.ex` | `ask_with_prompt/4` auto-includes system prompt via `:system` opt |
| `prompt_form.html.heex` | System prompt textarea in prompt create/edit form |

- Variables are now extracted from **both** `system_prompt` and `content` fields (combined then deduplicated)
- `render_system_prompt/2` returns `{:ok, nil}` for nil/empty, `{:ok, rendered}` otherwise
- `ask_with_prompt` uses `Keyword.put_new` so callers can still override `:system`

### 2. AI Playground LiveView

| File | Change |
|------|--------|
| `web/playground.ex` | New LiveView (300 lines) — endpoint/prompt selection, variable inputs, freeform mode |
| `web/playground.html.heex` | Template (336 lines) — configuration card, input card, response area with usage stats |
| `integration.ex` | Route: `/admin/ai/playground` |
| `ai.ex` | Playground tab registered in navigation (priority 553) |

**Two modes:**
- **Prompt mode** — Select a prompt, fill in variables, edit the template inline, send
- **Freeform mode** — Type a message + optional system prompt directly

**Features:**
- Loading skeleton with auto-scroll to response
- Usage stats display (tokens in/out, total, cost)
- Async request via `handle_info(:do_send, ...)` to avoid blocking the LiveView

### 3. Navigation Updates

Playground tab added to all AI admin page tab bars:
- `endpoints.html.heex`
- `prompts.html.heex`
- `playground.html.heex` (active state)

### 4. Tests

- 44 unit tests in `test/modules/ai/prompt_test.exs` covering `extract_variables`, `render`, `render_system_prompt`, changeset variable extraction, `validate_variables`, `has_variables?`, `valid_content?`, `content_preview`, `generate_slug`, and `format_variables_for_display`

## Migration

**V85** — Idempotent `ALTER TABLE` adding `system_prompt` TEXT column. Checks for both table and column existence before executing. Bumps version comment to 85.
Loading
Loading