Compare commits

...
55 Commits
Author SHA1 Message Date
Firehose Bot f522895e4e fixed link to tag and image 2026-08-12 23:48:12 +02:00
Firehose Bot 3308299d3c pi.dev and open weights 2026-08-11 08:19:47 +02:00
Firehose Bot 4fa0f85847 Merge branch 'main' of ssh://gitea.apps.sustainabledelivery.com:3022/mostalive/firehose 2026-08-08 12:57:50 +02:00
mostalive a426a2b61b retort blog post 2026-07-30 10:15:43 +01:00
Firehose Bot d6dddbc9fb session transcript in pi 2026-07-28 13:15:24 +01:00
Firehose Bot 20de33720a freewriting 2026-07-27 16:18:09 +01:00
Firehose Bot f22a8cbf37 swim the test part two 2026-07-27 16:16:49 +01:00
Firehose Bot d7c6bea7fa good enough for now 2026-07-27 11:14:22 +01:00
Firehose Bot 419abc6b92 swim the test zeroth draft 2026-07-27 10:47:17 +01:00
Firehose Bot 17d7ad760d fix sigil error - no comma needed in word list 2026-07-19 08:52:31 +01:00
Firehose Bot 2e3187597d does ai assistance support tiny open source projects? 2026-07-18 10:14:26 +01:00
Firehose Bot 1554773544 pi coding agent skill 2026-07-16 18:29:32 +01:00
Firehose Bot 761d6fe2ce draft for pi skill 2026-07-16 13:21:03 +01:00
Firehose Bot 4ead5702fb Merge branch 'main' of dokku:firehose.apps.sustainabledelivery.com 2026-07-13 15:02:22 +01:00
Firehose Bot 4885c94b72 old conflicts and version update 2026-07-13 15:01:19 +01:00
Firehose Bot 7af61a917f Merge branch 'main' of ssh://gitea.apps.sustainabledelivery.com:3022/mostalive/firehose 2026-07-13 14:58:40 +01:00
Firehose Bot 337a0a61d8 Synthetic TDD - Vocabulary 2026-07-13 14:49:01 +01:00
mostalive 990af18ef8 v0.2.0 — RSS subscribe links
- Add release note for v0.2.0
- Bump version from 0.1.0 to 0.2.0
2026-07-08 17:00:11 +01:00
mostalive e385710498 Swap to classic RSS waveguide icon for better recognizability
Replace the broadcast-style SVG icon with the traditional RSS
symbol (dot + two concentric arcs) — the standard feed icon
used across browsers and feed readers.
2026-07-08 17:00:11 +01:00
mostalive 855b6cb590 Add RSS subscribe links with icon across blog pages, footer, and head
- Add <.rss_icon> inline SVG component in core_components.ex
- Inject RSS <link> tags into <head> on blog pages for auto-discovery
- Show RSS icon next to blog title on index and tag pages
- Show RSS icon on post pages next to back-link
- Add Subscribe section in footer with links to both feeds
- Wire feed URL through blog controller (index, show, tag actions)

174 tests pass, credo clean.
2026-07-08 17:00:11 +01:00
mostalive 4c8d1dcee3 add serve target to Makefile 2026-07-08 14:23:39 +01:00
mostalive 001bf684b7 add description 2026-07-08 14:23:39 +01:00
mostalive 9238ecb7f3 add serve target to Makefile 2026-07-08 14:22:09 +01:00
mostalive 10c02f5c6a add description 2026-07-07 13:48:41 +01:00
mostalive 63ae78129f fixed test - clicking on line instead of parts in svg 2026-07-07 13:36:09 +01:00
mostalive a1d1107129 Run tests when resetting pi, so we do not forget to run tests 2026-07-07 13:36:09 +01:00
Firehose Bot 32f75d63d3 upgrade nimble_publisher to 2.0 for mdex_native (replaces deprecated earmark) 2026-07-07 13:36:09 +01:00
Firehose Bot 29754ff651 fix microprints_live tests: syntax error, brittle selectors, and layout assertions
- Add missing 'do' keyword on describe block (SyntaxError)
- Replace brittle SVG rect DOM selector with direct render_click event
  trigger to avoid AmbiguousError from multi-segment rects
- Fix layout test: render/1 returns LV inner HTML only, not the
  layout wrapper — check for content actually present in output
2026-07-07 13:36:09 +01:00
Firehose Bot 9839899eae ignore logs 2026-07-07 13:36:09 +01:00
Firehose Bot 5afc935ed8 delete log 2026-07-07 13:36:09 +01:00
Firehose Bot 6c3051d618 Reapply "Fix dashboard dark mode readability by using theme-aware text colors"
This reverts commit 76255c7641.
2026-07-07 13:36:09 +01:00
Firehose Bot 7552dd85ee Revert "add clean build"
This reverts commit 679147dcd5.
2026-07-07 13:36:09 +01:00
Firehose Bot 639ff5e781 Revert "ignore log files"
This reverts commit c53fd9d441.
2026-07-07 13:36:09 +01:00
Firehose Bot 97dea4b544 ignore log files 2026-07-07 13:36:09 +01:00
Firehose Bot d2152dbcea add clean build 2026-07-07 13:36:09 +01:00
Firehose Bot b71f4649e2 Revert "Fix dashboard dark mode readability by using theme-aware text colors"
This reverts commit 2a0634b820.
2026-07-07 13:36:09 +01:00
Firehose Bot 07aca93e7c Fix dashboard dark mode readability by using theme-aware text colors
- Use  and  instead of hardcoded zinc colors in EditorDashboardLive
- Apply same fix to MicroprintsLive description paragraph
- All titles now respect the active theme and remain readable in both light and dark modes
2026-07-07 13:36:09 +01:00
Firehose Bot 188c556233 Fix demo user confirmation to allow password login
The seeds file was creating an unconfirmed user and then setting a password,
which conflicts with Phoenix's auth system (magic link login is not allowed
for unconfirmed users with passwords).

This fix ensures the demo user is confirmed before setting their password,
so they can log in via both magic link and password.

Demo credentials: demo@example.com / password123!
2026-07-07 13:36:09 +01:00
Firehose Bot efbd314d33 Add app layout to live_session for dashboard menu
Fix the dashboard missing menu by explicitly setting layout: {FirehoseWeb.Layouts, :app} in the live_session.
This ensures the navigation menu is rendered on the authenticated editor dashboard LiveView.
2026-07-07 13:36:09 +01:00
mostalive c8864c5b3b fixed test - clicking on line instead of parts in svg 2026-07-07 13:33:50 +01:00
mostalive 716bfb4b12 Run tests when resetting pi, so we do not forget to run tests 2026-07-07 13:32:55 +01:00
mostalive 6b5329bd75 moralistic programming 2026-07-07 13:32:55 +01:00
Firehose Bot 51ba85d492 upgrade nimble_publisher to 2.0 for mdex_native (replaces deprecated earmark) 2026-07-06 18:18:12 +01:00
Firehose Bot 39973c3414 fix microprints_live tests: syntax error, brittle selectors, and layout assertions
- Add missing 'do' keyword on describe block (SyntaxError)
- Replace brittle SVG rect DOM selector with direct render_click event
  trigger to avoid AmbiguousError from multi-segment rects
- Fix layout test: render/1 returns LV inner HTML only, not the
  layout wrapper — check for content actually present in output
2026-07-06 18:09:26 +01:00
Firehose Bot b55de0a677 ignore logs 2026-07-06 18:08:09 +01:00
Firehose Bot 85f0d86016 delete log 2026-07-06 18:07:43 +01:00
Firehose Bot 2ba8457b14 Reapply "Fix dashboard dark mode readability by using theme-aware text colors"
This reverts commit 76255c7641.
2026-07-06 17:02:13 +01:00
Firehose Bot e0c7abc417 Revert "add clean build"
This reverts commit 679147dcd5.
2026-07-06 17:02:11 +01:00
Firehose Bot f100d8d211 Revert "ignore log files"
This reverts commit c53fd9d441.
2026-07-06 17:02:10 +01:00
Firehose Bot c53fd9d441 ignore log files 2026-07-06 16:57:02 +01:00
Firehose Bot 679147dcd5 add clean build 2026-07-06 16:56:54 +01:00
Firehose Bot 76255c7641 Revert "Fix dashboard dark mode readability by using theme-aware text colors"
This reverts commit 2a0634b820.
2026-07-06 11:54:56 +01:00
Firehose Bot 2a0634b820 Fix dashboard dark mode readability by using theme-aware text colors
- Use  and  instead of hardcoded zinc colors in EditorDashboardLive
- Apply same fix to MicroprintsLive description paragraph
- All titles now respect the active theme and remain readable in both light and dark modes
2026-07-06 11:14:13 +01:00
Firehose Bot 2be39c3273 Fix demo user confirmation to allow password login
The seeds file was creating an unconfirmed user and then setting a password,
which conflicts with Phoenix's auth system (magic link login is not allowed
for unconfirmed users with passwords).

This fix ensures the demo user is confirmed before setting their password,
so they can log in via both magic link and password.

Demo credentials: demo@example.com / password123!
2026-07-06 10:55:15 +01:00
Firehose Bot ccb52be9db Add app layout to live_session for dashboard menu
Fix the dashboard missing menu by explicitly setting layout: {FirehoseWeb.Layouts, :app} in the live_session.
This ensures the navigation menu is rendered on the authenticated editor dashboard LiveView.
2026-07-06 10:37:08 +01:00
47 changed files with 1032 additions and 319 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
((markdown-mode . ((qwan-markdown-attachment-dir . "app/priv/static/images/blog/2026")))) ((markdown-mode . ((qwan-markdown-attachment-dir . "/Users/willem/dev/elixir/firehose/app/priv/static/images/blog/2026"))))
+1
View File
@@ -9,3 +9,4 @@ transcripts/
.pi/skills/demo/chrome .pi/skills/demo/chrome
.rodney/ .rodney/
.dir-locals.el .dir-locals.el
*.log
+37
View File
@@ -0,0 +1,37 @@
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
try {
const { stdout, stderr, code } = await pi.exec("make", ["test"]);
const output = stdout + stderr;
if (code === 0) {
ctx.ui.notify("make test passed", "info");
} else {
ctx.ui.notify(`make test failed (exit ${code})`, "error");
}
// Also send the output as a message so it's visible in the session
pi.sendMessage({
customType: "run-make-test",
content: [
{
type: "text",
text: `Here's the result of running \`make test\`:\n\n\`\`\`\n${output}\`\`\``,
},
],
display: true,
}, { deliverAs: "followUp" });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
pi.sendMessage({
customType: "run-make-test",
content: [
{ type: "text", text: `Error running make test: ${message}` },
],
display: true,
}, { deliverAs: "followUp" });
}
});
}
-89
View File
@@ -1,89 +0,0 @@
{"timestamp":"2026-04-29T13:29:50.834Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":3898,"outputTokens":60,"totalTokens":3958,"prefillTokensPerSec":162.25,"generationTokensPerSec":61.04,"combinedTokensPerSec":158.27,"totalDurationMs":25008,"timeToFirstTokenMs":24025,"rawTimestamps":{"ttftMs":24025,"allTtftMs":[24025],"generationDurationMs":983,"turns":[{"turnId":"turn-0","durationMs":25008,"ttftMs":24025}]}}
{"timestamp":"2026-04-29T13:41:50.149Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":7,"inputTokens":5667,"outputTokens":960,"totalTokens":6627,"prefillTokensPerSec":500.44,"generationTokensPerSec":11.09,"combinedTokensPerSec":67.69,"totalDurationMs":97903,"timeToFirstTokenMs":11324,"rawTimestamps":{"ttftMs":11324,"allTtftMs":[11324],"generationDurationMs":86579,"turns":[{"turnId":"turn-0","durationMs":6146},{"turnId":"turn-1","durationMs":2996},{"turnId":"turn-2","durationMs":4988},{"turnId":"turn-3","durationMs":5554},{"turnId":"turn-4","durationMs":29939},{"turnId":"turn-5","durationMs":3521},{"turnId":"turn-6","durationMs":44759,"ttftMs":11324}]}}
{"timestamp":"2026-04-29T15:50:22.720Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":3895,"outputTokens":81,"totalTokens":3976,"prefillTokensPerSec":165.89,"generationTokensPerSec":37.09,"combinedTokensPerSec":154.93,"totalDurationMs":25664,"timeToFirstTokenMs":23480,"rawTimestamps":{"ttftMs":23480,"allTtftMs":[23480],"generationDurationMs":2184,"turns":[{"turnId":"turn-0","durationMs":25664,"ttftMs":23480}]}}
{"timestamp":"2026-04-29T15:52:02.780Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":3,"inputTokens":61,"outputTokens":381,"totalTokens":442,"prefillTokensPerSec":8.43,"generationTokensPerSec":21.42,"combinedTokensPerSec":17.66,"totalDurationMs":25023,"timeToFirstTokenMs":7240,"rawTimestamps":{"ttftMs":7240,"allTtftMs":[7240,1429],"generationDurationMs":17783,"turns":[{"turnId":"turn-0","durationMs":15773,"ttftMs":7240},{"turnId":"turn-1","durationMs":7547,"ttftMs":1429},{"turnId":"turn-2","durationMs":1703}]}}
{"timestamp":"2026-04-29T15:54:09.944Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":6,"inputTokens":3978,"outputTokens":1388,"totalTokens":5366,"prefillTokensPerSec":924.04,"generationTokensPerSec":12.1,"combinedTokensPerSec":45.09,"totalDurationMs":119004,"timeToFirstTokenMs":4305,"rawTimestamps":{"ttftMs":4305,"allTtftMs":[4305,8638,7219,29433,6874],"generationDurationMs":114699,"turns":[{"turnId":"turn-0","durationMs":12420,"ttftMs":4305},{"turnId":"turn-1","durationMs":12723,"ttftMs":8638},{"turnId":"turn-2","durationMs":23125,"ttftMs":7219},{"turnId":"turn-3","durationMs":50368,"ttftMs":29433},{"turnId":"turn-4","durationMs":5165},{"turnId":"turn-5","durationMs":15203,"ttftMs":6874}]}}
{"timestamp":"2026-04-29T15:57:10.210Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":33,"outputTokens":318,"totalTokens":351,"prefillTokensPerSec":2.19,"generationTokensPerSec":28.27,"combinedTokensPerSec":13.35,"totalDurationMs":26294,"timeToFirstTokenMs":15045,"rawTimestamps":{"ttftMs":15045,"allTtftMs":[15045],"generationDurationMs":11249,"turns":[{"turnId":"turn-0","durationMs":26294,"ttftMs":15045}]}}
{"timestamp":"2026-04-29T15:58:56.626Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":4,"inputTokens":2132,"outputTokens":1231,"totalTokens":3363,"prefillTokensPerSec":376.28,"generationTokensPerSec":12.52,"combinedTokensPerSec":32.34,"totalDurationMs":103985,"timeToFirstTokenMs":5666,"rawTimestamps":{"ttftMs":5666,"allTtftMs":[5666,39799,19167],"generationDurationMs":98319,"turns":[{"turnId":"turn-0","durationMs":6587},{"turnId":"turn-1","durationMs":11102,"ttftMs":5666},{"turnId":"turn-2","durationMs":54948,"ttftMs":39799},{"turnId":"turn-3","durationMs":31348,"ttftMs":19167}]}}
{"timestamp":"2026-04-29T16:05:02.148Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":2,"inputTokens":205,"outputTokens":571,"totalTokens":776,"prefillTokensPerSec":74.49,"generationTokensPerSec":15.29,"combinedTokensPerSec":19.35,"totalDurationMs":40104,"timeToFirstTokenMs":2752,"rawTimestamps":{"ttftMs":2752,"allTtftMs":[2752],"generationDurationMs":37352,"turns":[{"turnId":"turn-0","durationMs":30227},{"turnId":"turn-1","durationMs":9877,"ttftMs":2752}]}}
{"timestamp":"2026-04-29T16:07:50.115Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":2,"inputTokens":234,"outputTokens":199,"totalTokens":433,"prefillTokensPerSec":36.8,"generationTokensPerSec":25.9,"combinedTokensPerSec":30.84,"totalDurationMs":14042,"timeToFirstTokenMs":6358,"rawTimestamps":{"ttftMs":6358,"allTtftMs":[6358],"generationDurationMs":7684,"turns":[{"turnId":"turn-0","durationMs":4488},{"turnId":"turn-1","durationMs":9554,"ttftMs":6358}]}}
{"timestamp":"2026-04-29T20:33:28.606Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":4,"inputTokens":313,"outputTokens":377,"totalTokens":690,"prefillTokensPerSec":97.48,"generationTokensPerSec":15.28,"combinedTokensPerSec":24.75,"totalDurationMs":27880,"timeToFirstTokenMs":3211,"rawTimestamps":{"ttftMs":3211,"allTtftMs":[3211],"generationDurationMs":24669,"turns":[{"turnId":"turn-0","durationMs":8848},{"turnId":"turn-1","durationMs":9567},{"turnId":"turn-2","durationMs":4669},{"turnId":"turn-3","durationMs":4796,"ttftMs":3211}]}}
{"timestamp":"2026-04-29T20:34:21.540Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":4,"inputTokens":641,"outputTokens":497,"totalTokens":1138,"prefillTokensPerSec":80.86,"generationTokensPerSec":16.65,"combinedTokensPerSec":30.12,"totalDurationMs":37779,"timeToFirstTokenMs":7927,"rawTimestamps":{"ttftMs":7927,"allTtftMs":[7927,7791],"generationDurationMs":29852,"turns":[{"turnId":"turn-0","durationMs":13323,"ttftMs":7927},{"turnId":"turn-1","durationMs":5975},{"turnId":"turn-2","durationMs":7432},{"turnId":"turn-3","durationMs":11049,"ttftMs":7791}]}}
{"timestamp":"2026-04-29T21:00:14.163Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":130,"outputTokens":754,"totalTokens":884,"prefillTokensPerSec":18.2,"generationTokensPerSec":22.65,"combinedTokensPerSec":21.87,"totalDurationMs":40429,"timeToFirstTokenMs":7141,"rawTimestamps":{"ttftMs":7141,"allTtftMs":[7141],"generationDurationMs":33288,"turns":[{"turnId":"turn-0","durationMs":40429,"ttftMs":7141}]}}
{"timestamp":"2026-04-30T08:14:53.444Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":19,"inputTokens":12970,"outputTokens":3205,"totalTokens":16175,"prefillTokensPerSec":1449.65,"generationTokensPerSec":44.97,"combinedTokensPerSec":201.65,"totalDurationMs":80212,"timeToFirstTokenMs":8947,"rawTimestamps":{"ttftMs":8947,"allTtftMs":[8947,3426,526,1162],"generationDurationMs":71265,"turns":[{"turnId":"turn-0","durationMs":6818},{"turnId":"turn-1","durationMs":1695},{"turnId":"turn-2","durationMs":1595},{"turnId":"turn-3","durationMs":3043},{"turnId":"turn-4","durationMs":2759},{"turnId":"turn-5","durationMs":3602},{"turnId":"turn-6","durationMs":4147},{"turnId":"turn-7","durationMs":3449},{"turnId":"turn-8","durationMs":7692},{"turnId":"turn-9","durationMs":5670},{"turnId":"turn-10","durationMs":1713},{"turnId":"turn-11","durationMs":1593},{"turnId":"turn-12","durationMs":4806},{"turnId":"turn-13","durationMs":1737},{"turnId":"turn-14","durationMs":4030},{"turnId":"turn-15","durationMs":12934,"ttftMs":8947},{"turnId":"turn-16","durationMs":6340,"ttftMs":3426},{"turnId":"turn-17","durationMs":2325,"ttftMs":526},{"turnId":"turn-18","durationMs":4264,"ttftMs":1162}]}}
{"timestamp":"2026-05-04T11:46:24.369Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":6,"inputTokens":54016,"outputTokens":1558,"totalTokens":55574,"prefillTokensPerSec":12304.33,"generationTokensPerSec":21.46,"combinedTokensPerSec":721.94,"totalDurationMs":76979,"timeToFirstTokenMs":4390,"rawTimestamps":{"ttftMs":4390,"allTtftMs":[4390,4350,5857,8964,12347,14311],"generationDurationMs":72589,"turns":[{"turnId":"turn-0","durationMs":7310,"ttftMs":4390},{"turnId":"turn-1","durationMs":7610,"ttftMs":4350},{"turnId":"turn-2","durationMs":10860,"ttftMs":5857},{"turnId":"turn-3","durationMs":15082,"ttftMs":8964},{"turnId":"turn-4","durationMs":17713,"ttftMs":12347},{"turnId":"turn-5","durationMs":18404,"ttftMs":14311}]}}
{"timestamp":"2026-05-04T11:47:12.784Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":1,"inputTokens":15483,"outputTokens":527,"totalTokens":16010,"prefillTokensPerSec":915.45,"generationTokensPerSec":77.75,"combinedTokensPerSec":675.78,"totalDurationMs":23691,"timeToFirstTokenMs":16913,"rawTimestamps":{"ttftMs":16913,"allTtftMs":[16913],"generationDurationMs":6778,"turns":[{"turnId":"turn-0","durationMs":23691,"ttftMs":16913}]}}
{"timestamp":"2026-05-04T11:48:28.522Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":2,"inputTokens":33498,"outputTokens":1509,"totalTokens":35007,"prefillTokensPerSec":1806.99,"generationTokensPerSec":35.09,"combinedTokensPerSec":568.89,"totalDurationMs":61536,"timeToFirstTokenMs":18538,"rawTimestamps":{"ttftMs":18538,"allTtftMs":[18538,19230],"generationDurationMs":42998,"turns":[{"turnId":"turn-0","durationMs":21065,"ttftMs":18538},{"turnId":"turn-1","durationMs":40471,"ttftMs":19230}]}}
{"timestamp":"2026-05-04T11:55:38.472Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":2,"inputTokens":39338,"outputTokens":570,"totalTokens":39908,"prefillTokensPerSec":1902.59,"generationTokensPerSec":22.16,"combinedTokensPerSec":860.14,"totalDurationMs":46397,"timeToFirstTokenMs":20676,"rawTimestamps":{"ttftMs":20676,"allTtftMs":[20676,19136],"generationDurationMs":25721,"turns":[{"turnId":"turn-0","durationMs":25951,"ttftMs":20676},{"turnId":"turn-1","durationMs":20446,"ttftMs":19136}]}}
{"timestamp":"2026-05-04T11:56:06.171Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":1,"inputTokens":20697,"outputTokens":122,"totalTokens":20819,"prefillTokensPerSec":1032.58,"generationTokensPerSec":64.14,"combinedTokensPerSec":948.65,"totalDurationMs":21946,"timeToFirstTokenMs":20044,"rawTimestamps":{"ttftMs":20044,"allTtftMs":[20044],"generationDurationMs":1902,"turns":[{"turnId":"turn-0","durationMs":21946,"ttftMs":20044}]}}
{"timestamp":"2026-05-06T16:39:45.645Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":1,"inputTokens":4174,"outputTokens":325,"totalTokens":4499,"prefillTokensPerSec":792.63,"generationTokensPerSec":66.8,"combinedTokensPerSec":444.08,"totalDurationMs":10131,"timeToFirstTokenMs":5266,"rawTimestamps":{"ttftMs":5266,"allTtftMs":[5266],"generationDurationMs":4865,"turns":[{"turnId":"turn-0","durationMs":10131,"ttftMs":5266}]}}
{"timestamp":"2026-05-06T16:42:07.071Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":7,"inputTokens":9779,"outputTokens":2117,"totalTokens":11896,"prefillTokensPerSec":4159.51,"generationTokensPerSec":41.52,"combinedTokensPerSec":223.01,"totalDurationMs":53342,"timeToFirstTokenMs":2351,"rawTimestamps":{"ttftMs":2351,"allTtftMs":[2351,9533],"generationDurationMs":50991,"turns":[{"turnId":"turn-0","durationMs":6041,"ttftMs":2351},{"turnId":"turn-1","durationMs":5709},{"turnId":"turn-2","durationMs":4712},{"turnId":"turn-3","durationMs":10280},{"turnId":"turn-4","durationMs":4285},{"turnId":"turn-5","durationMs":5823},{"turnId":"turn-6","durationMs":16492,"ttftMs":9533}]}}
{"timestamp":"2026-05-06T16:49:48.369Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":7,"inputTokens":311,"outputTokens":1074,"totalTokens":1385,"prefillTokensPerSec":36.18,"generationTokensPerSec":61.74,"combinedTokensPerSec":53.29,"totalDurationMs":25992,"timeToFirstTokenMs":8597,"rawTimestamps":{"ttftMs":8597,"allTtftMs":[8597],"generationDurationMs":17395,"turns":[{"turnId":"turn-0","durationMs":11317,"ttftMs":8597},{"turnId":"turn-1","durationMs":2369},{"turnId":"turn-2","durationMs":2161},{"turnId":"turn-3","durationMs":2403},{"turnId":"turn-4","durationMs":4231},{"turnId":"turn-5","durationMs":2551},{"turnId":"turn-6","durationMs":960}]}}
{"timestamp":"2026-05-06T16:56:43.506Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":11,"inputTokens":1781,"outputTokens":1747,"totalTokens":3528,"prefillTokensPerSec":495.82,"generationTokensPerSec":49.55,"combinedTokensPerSec":90.81,"totalDurationMs":38852,"timeToFirstTokenMs":3592,"rawTimestamps":{"ttftMs":3592,"allTtftMs":[3592,694,1252],"generationDurationMs":35260,"turns":[{"turnId":"turn-0","durationMs":4830,"ttftMs":3592},{"turnId":"turn-1","durationMs":2331},{"turnId":"turn-2","durationMs":3344},{"turnId":"turn-3","durationMs":2239},{"turnId":"turn-4","durationMs":4882,"ttftMs":694},{"turnId":"turn-5","durationMs":4150},{"turnId":"turn-6","durationMs":5061},{"turnId":"turn-7","durationMs":3934},{"turnId":"turn-8","durationMs":1503},{"turnId":"turn-9","durationMs":2303},{"turnId":"turn-10","durationMs":4275,"ttftMs":1252}]}}
{"timestamp":"2026-05-06T16:59:33.370Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":8,"inputTokens":357,"outputTokens":949,"totalTokens":1306,"prefillTokensPerSec":222.57,"generationTokensPerSec":46.43,"combinedTokensPerSec":59.25,"totalDurationMs":22044,"timeToFirstTokenMs":1604,"rawTimestamps":{"ttftMs":1604,"allTtftMs":[1604],"generationDurationMs":20440,"turns":[{"turnId":"turn-0","durationMs":2025},{"turnId":"turn-1","durationMs":3837},{"turnId":"turn-2","durationMs":1860},{"turnId":"turn-3","durationMs":3942},{"turnId":"turn-4","durationMs":1846},{"turnId":"turn-5","durationMs":3096},{"turnId":"turn-6","durationMs":1495},{"turnId":"turn-7","durationMs":3943,"ttftMs":1604}]}}
{"timestamp":"2026-05-06T17:31:10.634Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":92,"inputTokens":80382,"outputTokens":40770,"totalTokens":121152,"prefillTokensPerSec":34827.56,"generationTokensPerSec":21.57,"combinedTokensPerSec":64.01,"totalDurationMs":1892815,"timeToFirstTokenMs":2308,"rawTimestamps":{"ttftMs":2308,"allTtftMs":[2308,5231,2214,13471,19063,46574,152271,61194,48786,7235,28151,26069,9052,1985,1952,1437,10416,14830,1134,16993,12759,52435,92275,13142,123464,5492,21599,22940,67932,2646,19419,9988,11058,6667,3583,6608,1500],"generationDurationMs":1890507,"turns":[{"turnId":"turn-0","durationMs":4003,"ttftMs":2308},{"turnId":"turn-1","durationMs":41405},{"turnId":"turn-2","durationMs":9163,"ttftMs":5231},{"turnId":"turn-3","durationMs":15549},{"turnId":"turn-4","durationMs":24263,"ttftMs":2214},{"turnId":"turn-5","durationMs":6306},{"turnId":"turn-6","durationMs":58263,"ttftMs":13471},{"turnId":"turn-7","durationMs":2270},{"turnId":"turn-8","durationMs":28312,"ttftMs":19063},{"turnId":"turn-9","durationMs":15839},{"turnId":"turn-10","durationMs":3190},{"turnId":"turn-11","durationMs":62636,"ttftMs":46574},{"turnId":"turn-12","durationMs":29406},{"turnId":"turn-13","durationMs":13332},{"turnId":"turn-14","durationMs":162150,"ttftMs":152271},{"turnId":"turn-15","durationMs":9085},{"turnId":"turn-16","durationMs":67115,"ttftMs":61194},{"turnId":"turn-17","durationMs":12812},{"turnId":"turn-18","durationMs":3990},{"turnId":"turn-19","durationMs":55146,"ttftMs":48786},{"turnId":"turn-20","durationMs":21407},{"turnId":"turn-21","durationMs":21800,"ttftMs":7235},{"turnId":"turn-22","durationMs":2180},{"turnId":"turn-23","durationMs":41363,"ttftMs":28151},{"turnId":"turn-24","durationMs":1791},{"turnId":"turn-25","durationMs":32993,"ttftMs":26069},{"turnId":"turn-26","durationMs":6916},{"turnId":"turn-27","durationMs":1731},{"turnId":"turn-28","durationMs":22931,"ttftMs":9052},{"turnId":"turn-29","durationMs":1821},{"turnId":"turn-30","durationMs":4633,"ttftMs":1985},{"turnId":"turn-31","durationMs":4862,"ttftMs":1952},{"turnId":"turn-32","durationMs":4595,"ttftMs":1437},{"turnId":"turn-33","durationMs":13428,"ttftMs":10416},{"turnId":"turn-34","durationMs":25682,"ttftMs":14830},{"turnId":"turn-35","durationMs":17933,"ttftMs":1134},{"turnId":"turn-36","durationMs":20492,"ttftMs":16993},{"turnId":"turn-37","durationMs":31332,"ttftMs":12759},{"turnId":"turn-38","durationMs":56269,"ttftMs":52435},{"turnId":"turn-39","durationMs":8502},{"turnId":"turn-40","durationMs":6420},{"turnId":"turn-41","durationMs":25318},{"turnId":"turn-42","durationMs":9494},{"turnId":"turn-43","durationMs":95912,"ttftMs":92275},{"turnId":"turn-44","durationMs":34948,"ttftMs":13142},{"turnId":"turn-45","durationMs":8642},{"turnId":"turn-46","durationMs":130233,"ttftMs":123464},{"turnId":"turn-47","durationMs":38380},{"turnId":"turn-48","durationMs":9214},{"turnId":"turn-49","durationMs":11870,"ttftMs":5492},{"turnId":"turn-50","durationMs":6218},{"turnId":"turn-51","durationMs":7383},{"turnId":"turn-52","durationMs":4064},{"turnId":"turn-53","durationMs":27853,"ttftMs":21599},{"turnId":"turn-54","durationMs":11404},{"turnId":"turn-55","durationMs":17718},{"turnId":"turn-56","durationMs":32005,"ttftMs":22940},{"turnId":"turn-57","durationMs":14470},{"turnId":"turn-58","durationMs":76489,"ttftMs":67932},{"turnId":"turn-59","durationMs":7167},{"turnId":"turn-60","durationMs":21798,"ttftMs":2646},{"turnId":"turn-61","durationMs":5036},{"turnId":"turn-62","durationMs":27297,"ttftMs":19419},{"turnId":"turn-63","durationMs":2917},{"turnId":"turn-64","durationMs":18622},{"turnId":"turn-65","durationMs":4121},{"turnId":"turn-66","durationMs":17758,"ttftMs":9988},{"turnId":"turn-67","durationMs":14293},{"turnId":"turn-68","durationMs":10149},{"turnId":"turn-69","durationMs":21838,"ttftMs":11058},{"turnId":"turn-70","durationMs":33425,"ttftMs":6667},{"turnId":"turn-71","durationMs":8877},{"turnId":"turn-72","durationMs":4817},{"turnId":"turn-73","durationMs":14984},{"turnId":"turn-74","durationMs":3183},{"turnId":"turn-75","durationMs":13013},{"turnId":"turn-76","durationMs":4295},{"turnId":"turn-77","durationMs":11146},{"turnId":"turn-78","durationMs":4254},{"turnId":"turn-79","durationMs":22982},{"turnId":"turn-80","durationMs":2358},{"turnId":"turn-81","durationMs":9871},{"turnId":"turn-82","durationMs":2366},{"turnId":"turn-83","durationMs":10509},{"turnId":"turn-84","durationMs":2354},{"turnId":"turn-85","durationMs":7532,"ttftMs":3583},{"turnId":"turn-86","durationMs":11922,"ttftMs":6608},{"turnId":"turn-87","durationMs":6071},{"turnId":"turn-88","durationMs":7534},{"turnId":"turn-89","durationMs":4058},{"turnId":"turn-90","durationMs":6229},{"turnId":"turn-91","durationMs":15108,"ttftMs":1500}]}}
{"timestamp":"2026-05-06T21:04:32.534Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":1,"inputTokens":29,"outputTokens":507,"totalTokens":536,"prefillTokensPerSec":4.5,"generationTokensPerSec":43.58,"combinedTokensPerSec":29.64,"totalDurationMs":18082,"timeToFirstTokenMs":6448,"rawTimestamps":{"ttftMs":6448,"allTtftMs":[6448],"generationDurationMs":11634,"turns":[{"turnId":"turn-0","durationMs":18082,"ttftMs":6448}]}}
{"timestamp":"2026-05-06T21:27:50.071Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":1,"inputTokens":45,"outputTokens":546,"totalTokens":591,"prefillTokensPerSec":5.14,"generationTokensPerSec":43.29,"combinedTokensPerSec":27.67,"totalDurationMs":21362,"timeToFirstTokenMs":8748,"rawTimestamps":{"ttftMs":8748,"allTtftMs":[8748],"generationDurationMs":12614,"turns":[{"turnId":"turn-0","durationMs":21362,"ttftMs":8748}]}}
{"timestamp":"2026-05-06T21:31:47.562Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":4,"inputTokens":331,"outputTokens":712,"totalTokens":1043,"prefillTokensPerSec":127.6,"generationTokensPerSec":24.75,"combinedTokensPerSec":33.26,"totalDurationMs":31358,"timeToFirstTokenMs":2594,"rawTimestamps":{"ttftMs":2594,"allTtftMs":[2594],"generationDurationMs":28764,"turns":[{"turnId":"turn-0","durationMs":7885},{"turnId":"turn-1","durationMs":13632},{"turnId":"turn-2","durationMs":3642},{"turnId":"turn-3","durationMs":6199,"ttftMs":2594}]}}
{"timestamp":"2026-05-06T21:34:33.835Z","provider":"faster-mlx","model":"mlx-community/Qwen3.6-35B-A3B-8bit","turnCount":2,"inputTokens":537,"outputTokens":457,"totalTokens":994,"prefillTokensPerSec":186.07,"generationTokensPerSec":36.49,"combinedTokensPerSec":64.5,"totalDurationMs":15410,"timeToFirstTokenMs":2886,"rawTimestamps":{"ttftMs":2886,"allTtftMs":[2886,4622],"generationDurationMs":12524,"turns":[{"turnId":"turn-0","durationMs":10034,"ttftMs":2886},{"turnId":"turn-1","durationMs":5376,"ttftMs":4622}]}}
{"timestamp":"2026-05-07T09:58:26.890Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":8,"inputTokens":6451,"outputTokens":1301,"totalTokens":7752,"prefillTokensPerSec":1185.41,"generationTokensPerSec":5.62,"combinedTokensPerSec":32.73,"totalDurationMs":236828,"timeToFirstTokenMs":5442,"rawTimestamps":{"ttftMs":5442,"allTtftMs":[5442,1468],"generationDurationMs":231386,"turns":[{"turnId":"turn-0","durationMs":110526},{"turnId":"turn-1","durationMs":7296},{"turnId":"turn-2","durationMs":14916,"ttftMs":5442},{"turnId":"turn-3","durationMs":12938},{"turnId":"turn-4","durationMs":11185},{"turnId":"turn-5","durationMs":8236},{"turnId":"turn-6","durationMs":10174},{"turnId":"turn-7","durationMs":61557,"ttftMs":1468}]}}
{"timestamp":"2026-05-07T10:53:17.649Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":32,"inputTokens":18704,"outputTokens":5545,"totalTokens":24249,"prefillTokensPerSec":2125.45,"generationTokensPerSec":15.7,"combinedTokensPerSec":66.97,"totalDurationMs":362085,"timeToFirstTokenMs":8800,"rawTimestamps":{"ttftMs":8800,"allTtftMs":[8800,19803,2],"generationDurationMs":353285,"turns":[{"turnId":"turn-0","durationMs":4811},{"turnId":"turn-1","durationMs":6333},{"turnId":"turn-2","durationMs":18593},{"turnId":"turn-3","durationMs":17961,"ttftMs":8800},{"turnId":"turn-4","durationMs":13068},{"turnId":"turn-5","durationMs":14593},{"turnId":"turn-6","durationMs":12481},{"turnId":"turn-7","durationMs":9793},{"turnId":"turn-8","durationMs":21642},{"turnId":"turn-9","durationMs":22537},{"turnId":"turn-10","durationMs":9008},{"turnId":"turn-11","durationMs":12334},{"turnId":"turn-12","durationMs":39276},{"turnId":"turn-13","durationMs":17312},{"turnId":"turn-14","durationMs":19481},{"turnId":"turn-15","durationMs":10121},{"turnId":"turn-16","durationMs":6445},{"turnId":"turn-17","durationMs":6207},{"turnId":"turn-18","durationMs":9938},{"turnId":"turn-19","durationMs":16812},{"turnId":"turn-20","durationMs":27727,"ttftMs":19803},{"turnId":"turn-21","durationMs":18264},{"turnId":"turn-22","durationMs":16482},{"turnId":"turn-23","durationMs":10849},{"turnId":"turn-24","durationMs":4},{"turnId":"turn-25","durationMs":0},{"turnId":"turn-26","durationMs":5},{"turnId":"turn-27","durationMs":0},{"turnId":"turn-28","durationMs":3},{"turnId":"turn-29","durationMs":0},{"turnId":"turn-30","durationMs":2},{"turnId":"turn-31","durationMs":3,"ttftMs":2}]}}
{"timestamp":"2026-05-07T10:56:15.344Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":7,"inputTokens":1120,"outputTokens":1046,"totalTokens":2166,"prefillTokensPerSec":50.27,"generationTokensPerSec":15.14,"combinedTokensPerSec":23.71,"totalDurationMs":91350,"timeToFirstTokenMs":22281,"rawTimestamps":{"ttftMs":22281,"allTtftMs":[22281],"generationDurationMs":69069,"turns":[{"turnId":"turn-0","durationMs":6803},{"turnId":"turn-1","durationMs":9002},{"turnId":"turn-2","durationMs":9423},{"turnId":"turn-3","durationMs":8839},{"turnId":"turn-4","durationMs":21020},{"turnId":"turn-5","durationMs":7403},{"turnId":"turn-6","durationMs":28860,"ttftMs":22281}]}}
{"timestamp":"2026-05-07T10:57:51.083Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":11,"inputTokens":847,"outputTokens":1039,"totalTokens":1886,"prefillTokensPerSec":458.83,"generationTokensPerSec":13.33,"combinedTokensPerSec":23.63,"totalDurationMs":79819,"timeToFirstTokenMs":1846,"rawTimestamps":{"ttftMs":1846,"allTtftMs":[1846],"generationDurationMs":77973,"turns":[{"turnId":"turn-0","durationMs":4602},{"turnId":"turn-1","durationMs":4772},{"turnId":"turn-2","durationMs":9277},{"turnId":"turn-3","durationMs":7327},{"turnId":"turn-4","durationMs":6075},{"turnId":"turn-5","durationMs":11215},{"turnId":"turn-6","durationMs":5261},{"turnId":"turn-7","durationMs":7154},{"turnId":"turn-8","durationMs":8922},{"turnId":"turn-9","durationMs":5088},{"turnId":"turn-10","durationMs":10126,"ttftMs":1846}]}}
{"timestamp":"2026-05-07T11:44:31.762Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":29,"outputTokens":1453,"totalTokens":1482,"prefillTokensPerSec":0.63,"generationTokensPerSec":25.21,"combinedTokensPerSec":14.34,"totalDurationMs":103354,"timeToFirstTokenMs":45712,"rawTimestamps":{"ttftMs":45712,"allTtftMs":[45712],"generationDurationMs":57642,"turns":[{"turnId":"turn-0","durationMs":103354,"ttftMs":45712}]}}
{"timestamp":"2026-05-07T11:49:17.424Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":20,"inputTokens":2908,"outputTokens":2259,"totalTokens":5167,"prefillTokensPerSec":576.64,"generationTokensPerSec":11.38,"combinedTokensPerSec":25.39,"totalDurationMs":203478,"timeToFirstTokenMs":5043,"rawTimestamps":{"ttftMs":5043,"allTtftMs":[5043],"generationDurationMs":198435,"turns":[{"turnId":"turn-0","durationMs":7406},{"turnId":"turn-1","durationMs":8731},{"turnId":"turn-2","durationMs":18904},{"turnId":"turn-3","durationMs":10628},{"turnId":"turn-4","durationMs":7590},{"turnId":"turn-5","durationMs":8434},{"turnId":"turn-6","durationMs":6380},{"turnId":"turn-7","durationMs":11269},{"turnId":"turn-8","durationMs":31805},{"turnId":"turn-9","durationMs":11914},{"turnId":"turn-10","durationMs":9418},{"turnId":"turn-11","durationMs":8427},{"turnId":"turn-12","durationMs":5672},{"turnId":"turn-13","durationMs":8463},{"turnId":"turn-14","durationMs":6268},{"turnId":"turn-15","durationMs":7554},{"turnId":"turn-16","durationMs":6114},{"turnId":"turn-17","durationMs":7812},{"turnId":"turn-18","durationMs":6089},{"turnId":"turn-19","durationMs":14600,"ttftMs":5043}]}}
{"timestamp":"2026-05-07T12:23:00.698Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":52,"inputTokens":26465,"outputTokens":20704,"totalTokens":47169,"prefillTokensPerSec":9398.08,"generationTokensPerSec":11.03,"combinedTokensPerSec":25.1,"totalDurationMs":1879573,"timeToFirstTokenMs":2816,"rawTimestamps":{"ttftMs":2816,"allTtftMs":[2816,10170,65028,235220,117161,353893,91713,2451,2498,2927,1877,61437,41723,17718,6349,4740,1],"generationDurationMs":1876757,"turns":[{"turnId":"turn-0","durationMs":30454},{"turnId":"turn-1","durationMs":5727},{"turnId":"turn-2","durationMs":10687,"ttftMs":2816},{"turnId":"turn-3","durationMs":17901},{"turnId":"turn-4","durationMs":14927,"ttftMs":10170},{"turnId":"turn-5","durationMs":4974},{"turnId":"turn-6","durationMs":70205,"ttftMs":65028},{"turnId":"turn-7","durationMs":10421},{"turnId":"turn-8","durationMs":250712,"ttftMs":235220},{"turnId":"turn-9","durationMs":121868,"ttftMs":117161},{"turnId":"turn-10","durationMs":26319},{"turnId":"turn-11","durationMs":10498},{"turnId":"turn-12","durationMs":8040},{"turnId":"turn-13","durationMs":357629,"ttftMs":353893},{"turnId":"turn-14","durationMs":119802,"ttftMs":91713},{"turnId":"turn-15","durationMs":17411,"ttftMs":2451},{"turnId":"turn-16","durationMs":20892,"ttftMs":2498},{"turnId":"turn-17","durationMs":96951,"ttftMs":2927},{"turnId":"turn-18","durationMs":36436,"ttftMs":1877},{"turnId":"turn-19","durationMs":13108},{"turnId":"turn-20","durationMs":11605},{"turnId":"turn-21","durationMs":9548},{"turnId":"turn-22","durationMs":7530},{"turnId":"turn-23","durationMs":17564},{"turnId":"turn-24","durationMs":1129},{"turnId":"turn-25","durationMs":7810},{"turnId":"turn-26","durationMs":7152},{"turnId":"turn-27","durationMs":66842,"ttftMs":61437},{"turnId":"turn-28","durationMs":127759,"ttftMs":41723},{"turnId":"turn-29","durationMs":7755},{"turnId":"turn-30","durationMs":16499},{"turnId":"turn-31","durationMs":14999},{"turnId":"turn-32","durationMs":12641},{"turnId":"turn-33","durationMs":28267},{"turnId":"turn-34","durationMs":14505},{"turnId":"turn-35","durationMs":6545},{"turnId":"turn-36","durationMs":45348},{"turnId":"turn-37","durationMs":11660},{"turnId":"turn-38","durationMs":15989},{"turnId":"turn-39","durationMs":41281},{"turnId":"turn-40","durationMs":12805},{"turnId":"turn-41","durationMs":32023,"ttftMs":17718},{"turnId":"turn-42","durationMs":6866},{"turnId":"turn-43","durationMs":24151},{"turnId":"turn-44","durationMs":36240},{"turnId":"turn-45","durationMs":11290},{"turnId":"turn-46","durationMs":9438},{"turnId":"turn-47","durationMs":17588,"ttftMs":6349},{"turnId":"turn-48","durationMs":11779,"ttftMs":4740},{"turnId":"turn-49","durationMs":1},{"turnId":"turn-50","durationMs":0},{"turnId":"turn-51","durationMs":2,"ttftMs":1}]}}
{"timestamp":"2026-05-07T12:24:17.200Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":3,"inputTokens":237,"outputTokens":285,"totalTokens":522,"prefillTokensPerSec":129.08,"generationTokensPerSec":10.96,"combinedTokensPerSec":18.76,"totalDurationMs":27832,"timeToFirstTokenMs":1836,"rawTimestamps":{"ttftMs":1836,"allTtftMs":[1836],"generationDurationMs":25996,"turns":[{"turnId":"turn-0","durationMs":7168},{"turnId":"turn-1","durationMs":16917},{"turnId":"turn-2","durationMs":3747,"ttftMs":1836}]}}
{"timestamp":"2026-05-07T21:26:10.637Z","provider":"mtplx","model":"mtplx-qwen36-27b-optimized-speed","turnCount":1,"inputTokens":4235,"outputTokens":74,"totalTokens":4309,"prefillTokensPerSec":197.46,"generationTokensPerSec":29.67,"combinedTokensPerSec":179.98,"totalDurationMs":23941,"timeToFirstTokenMs":21447,"rawTimestamps":{"ttftMs":21447,"allTtftMs":[21447],"generationDurationMs":2494,"turns":[{"turnId":"turn-0","durationMs":23941,"ttftMs":21447}]}}
{"timestamp":"2026-05-09T19:38:05.353Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":3107,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":3107}]}}
{"timestamp":"2026-05-09T19:48:18.966Z","provider":"llama.cpp","model":"unsloth/Qwen3.6-27B-GGUF:Q4_K_M","turnCount":4,"inputTokens":1670,"outputTokens":761,"totalTokens":2431,"prefillTokensPerSec":2275.2,"generationTokensPerSec":50.15,"combinedTokensPerSec":152.83,"totalDurationMs":15907,"timeToFirstTokenMs":734,"rawTimestamps":{"ttftMs":734,"allTtftMs":[734],"generationDurationMs":15173,"turns":[{"turnId":"turn-0","durationMs":3512},{"turnId":"turn-1","durationMs":3412},{"turnId":"turn-2","durationMs":3367},{"turnId":"turn-3","durationMs":5616,"ttftMs":734}]}}
{"timestamp":"2026-05-13T20:46:56.858Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":13,"inputTokens":7828,"outputTokens":2680,"totalTokens":10508,"prefillTokensPerSec":1055.7,"generationTokensPerSec":47.56,"combinedTokensPerSec":164.81,"totalDurationMs":63760,"timeToFirstTokenMs":7415,"rawTimestamps":{"ttftMs":7415,"allTtftMs":[7415,1600,2500,1244,955,1149,2365,1985],"generationDurationMs":56345,"turns":[{"turnId":"turn-0","durationMs":11164,"ttftMs":7415},{"turnId":"turn-1","durationMs":6978,"ttftMs":1600},{"turnId":"turn-2","durationMs":2824},{"turnId":"turn-3","durationMs":3851,"ttftMs":2500},{"turnId":"turn-4","durationMs":7489},{"turnId":"turn-5","durationMs":1959},{"turnId":"turn-6","durationMs":4377,"ttftMs":1244},{"turnId":"turn-7","durationMs":1598},{"turnId":"turn-8","durationMs":3951,"ttftMs":955},{"turnId":"turn-9","durationMs":1577},{"turnId":"turn-10","durationMs":7759,"ttftMs":1149},{"turnId":"turn-11","durationMs":3915,"ttftMs":2365},{"turnId":"turn-12","durationMs":6318,"ttftMs":1985}]}}
{"timestamp":"2026-05-13T20:47:50.561Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":647,"outputTokens":831,"totalTokens":1478,"prefillTokensPerSec":172.21,"generationTokensPerSec":64.9,"combinedTokensPerSec":89.24,"totalDurationMs":16562,"timeToFirstTokenMs":3757,"rawTimestamps":{"ttftMs":3757,"allTtftMs":[3757],"generationDurationMs":12805,"turns":[{"turnId":"turn-0","durationMs":9204},{"turnId":"turn-1","durationMs":2557},{"turnId":"turn-2","durationMs":4801,"ttftMs":3757}]}}
{"timestamp":"2026-05-13T20:48:25.529Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":4,"inputTokens":680,"outputTokens":332,"totalTokens":1012,"prefillTokensPerSec":888.89,"generationTokensPerSec":43.33,"combinedTokensPerSec":120.09,"totalDurationMs":8427,"timeToFirstTokenMs":765,"rawTimestamps":{"ttftMs":765,"allTtftMs":[765],"generationDurationMs":7662,"turns":[{"turnId":"turn-0","durationMs":2549},{"turnId":"turn-1","durationMs":1491},{"turnId":"turn-2","durationMs":2700},{"turnId":"turn-3","durationMs":1687,"ttftMs":765}]}}
{"timestamp":"2026-05-13T20:48:35.789Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":241,"outputTokens":251,"totalTokens":492,"prefillTokensPerSec":632.55,"generationTokensPerSec":48.33,"combinedTokensPerSec":88.27,"totalDurationMs":5574,"timeToFirstTokenMs":381,"rawTimestamps":{"ttftMs":381,"allTtftMs":[381],"generationDurationMs":5193,"turns":[{"turnId":"turn-0","durationMs":2389},{"turnId":"turn-1","durationMs":2251},{"turnId":"turn-2","durationMs":934,"ttftMs":381}]}}
{"timestamp":"2026-05-14T13:11:31.541Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":5399,"outputTokens":887,"totalTokens":6286,"prefillTokensPerSec":4499.17,"generationTokensPerSec":40.34,"combinedTokensPerSec":271.07,"totalDurationMs":23190,"timeToFirstTokenMs":1200,"rawTimestamps":{"ttftMs":1200,"allTtftMs":[1200,1279],"generationDurationMs":21990,"turns":[{"turnId":"turn-0","durationMs":6605},{"turnId":"turn-1","durationMs":7004,"ttftMs":1200},{"turnId":"turn-2","durationMs":9581,"ttftMs":1279}]}}
{"timestamp":"2026-05-14T14:34:22.286Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":32,"inputTokens":28376,"outputTokens":5699,"totalTokens":34075,"prefillTokensPerSec":5253.84,"generationTokensPerSec":38.75,"combinedTokensPerSec":223.5,"totalDurationMs":152463,"timeToFirstTokenMs":5401,"rawTimestamps":{"ttftMs":5401,"allTtftMs":[5401,3585,370,1212,1288,1704,1129,3603,1962,0,1,0,0],"generationDurationMs":147062,"turns":[{"turnId":"turn-0","durationMs":8626,"ttftMs":5401},{"turnId":"turn-1","durationMs":6065},{"turnId":"turn-2","durationMs":3000},{"turnId":"turn-3","durationMs":19385,"ttftMs":3585},{"turnId":"turn-4","durationMs":1200},{"turnId":"turn-5","durationMs":5050,"ttftMs":370},{"turnId":"turn-6","durationMs":2453,"ttftMs":1212},{"turnId":"turn-7","durationMs":7630,"ttftMs":1288},{"turnId":"turn-8","durationMs":2868},{"turnId":"turn-9","durationMs":6154,"ttftMs":1704},{"turnId":"turn-10","durationMs":8455},{"turnId":"turn-11","durationMs":3484,"ttftMs":1129},{"turnId":"turn-12","durationMs":3632},{"turnId":"turn-13","durationMs":3091},{"turnId":"turn-14","durationMs":28599},{"turnId":"turn-15","durationMs":4515},{"turnId":"turn-16","durationMs":7130},{"turnId":"turn-17","durationMs":4364},{"turnId":"turn-18","durationMs":3361},{"turnId":"turn-19","durationMs":9264,"ttftMs":3603},{"turnId":"turn-20","durationMs":5422},{"turnId":"turn-21","durationMs":3263},{"turnId":"turn-22","durationMs":3531,"ttftMs":1962},{"turnId":"turn-23","durationMs":1909},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":0},{"turnId":"turn-26","durationMs":1},{"turnId":"turn-27","durationMs":1,"ttftMs":0},{"turnId":"turn-28","durationMs":0},{"turnId":"turn-29","durationMs":7,"ttftMs":1},{"turnId":"turn-30","durationMs":1,"ttftMs":0},{"turnId":"turn-31","durationMs":0,"ttftMs":0}]}}
{"timestamp":"2026-05-14T14:35:37.325Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":116,"outputTokens":284,"totalTokens":400,"prefillTokensPerSec":32.95,"generationTokensPerSec":70.95,"combinedTokensPerSec":53.16,"totalDurationMs":7524,"timeToFirstTokenMs":3521,"rawTimestamps":{"ttftMs":3521,"allTtftMs":[3521],"generationDurationMs":4003,"turns":[{"turnId":"turn-0","durationMs":1970},{"turnId":"turn-1","durationMs":5036,"ttftMs":3521},{"turnId":"turn-2","durationMs":518}]}}
{"timestamp":"2026-05-15T10:49:34.264Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":34,"inputTokens":11209,"outputTokens":4755,"totalTokens":15964,"prefillTokensPerSec":1523.17,"generationTokensPerSec":62.18,"combinedTokensPerSec":190.42,"totalDurationMs":83835,"timeToFirstTokenMs":7359,"rawTimestamps":{"ttftMs":7359,"allTtftMs":[7359,478,623,648,2511,1286,2844,2715,2402,801,0,0,1,0,0],"generationDurationMs":76476,"turns":[{"turnId":"turn-0","durationMs":9931,"ttftMs":7359},{"turnId":"turn-1","durationMs":1589},{"turnId":"turn-2","durationMs":3336,"ttftMs":478},{"turnId":"turn-3","durationMs":2417,"ttftMs":623},{"turnId":"turn-4","durationMs":1862,"ttftMs":648},{"turnId":"turn-5","durationMs":1945},{"turnId":"turn-6","durationMs":1150},{"turnId":"turn-7","durationMs":4422,"ttftMs":2511},{"turnId":"turn-8","durationMs":1893},{"turnId":"turn-9","durationMs":1987},{"turnId":"turn-10","durationMs":3645},{"turnId":"turn-11","durationMs":2576},{"turnId":"turn-12","durationMs":3972,"ttftMs":1286},{"turnId":"turn-13","durationMs":2337},{"turnId":"turn-14","durationMs":3141},{"turnId":"turn-15","durationMs":3070},{"turnId":"turn-16","durationMs":2305},{"turnId":"turn-17","durationMs":7110,"ttftMs":2844},{"turnId":"turn-18","durationMs":5955},{"turnId":"turn-19","durationMs":3529},{"turnId":"turn-20","durationMs":5359,"ttftMs":2715},{"turnId":"turn-21","durationMs":4366,"ttftMs":2402},{"turnId":"turn-22","durationMs":3139,"ttftMs":801},{"turnId":"turn-23","durationMs":2786},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":1},{"turnId":"turn-26","durationMs":0,"ttftMs":0},{"turnId":"turn-27","durationMs":1},{"turnId":"turn-28","durationMs":0,"ttftMs":0},{"turnId":"turn-29","durationMs":3},{"turnId":"turn-30","durationMs":3},{"turnId":"turn-31","durationMs":1,"ttftMs":1},{"turnId":"turn-32","durationMs":1,"ttftMs":0},{"turnId":"turn-33","durationMs":1,"ttftMs":0}]}}
{"timestamp":"2026-05-15T10:51:47.217Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":6,"inputTokens":332,"outputTokens":803,"totalTokens":1135,"prefillTokensPerSec":225.24,"generationTokensPerSec":52.52,"combinedTokensPerSec":67.7,"totalDurationMs":16764,"timeToFirstTokenMs":1474,"rawTimestamps":{"ttftMs":1474,"allTtftMs":[1474],"generationDurationMs":15290,"turns":[{"turnId":"turn-0","durationMs":2480},{"turnId":"turn-1","durationMs":1555},{"turnId":"turn-2","durationMs":2223},{"turnId":"turn-3","durationMs":5185},{"turnId":"turn-4","durationMs":2690},{"turnId":"turn-5","durationMs":2631,"ttftMs":1474}]}}
{"timestamp":"2026-05-15T10:52:22.219Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":212,"outputTokens":217,"totalTokens":429,"prefillTokensPerSec":201.14,"generationTokensPerSec":53.82,"combinedTokensPerSec":84.35,"totalDurationMs":5086,"timeToFirstTokenMs":1054,"rawTimestamps":{"ttftMs":1054,"allTtftMs":[1054],"generationDurationMs":4032,"turns":[{"turnId":"turn-0","durationMs":2312},{"turnId":"turn-1","durationMs":2774,"ttftMs":1054}]}}
{"timestamp":"2026-05-15T10:53:14.017Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":6,"inputTokens":2752,"outputTokens":611,"totalTokens":3363,"prefillTokensPerSec":2564.77,"generationTokensPerSec":33.42,"combinedTokensPerSec":173.75,"totalDurationMs":19355,"timeToFirstTokenMs":1073,"rawTimestamps":{"ttftMs":1073,"allTtftMs":[1073,2916,890],"generationDurationMs":18282,"turns":[{"turnId":"turn-0","durationMs":2568},{"turnId":"turn-1","durationMs":2126},{"turnId":"turn-2","durationMs":4607,"ttftMs":1073},{"turnId":"turn-3","durationMs":5843,"ttftMs":2916},{"turnId":"turn-4","durationMs":1926},{"turnId":"turn-5","durationMs":2285,"ttftMs":890}]}}
{"timestamp":"2026-05-15T13:58:01.754Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":8,"inputTokens":9084,"outputTokens":1515,"totalTokens":10599,"prefillTokensPerSec":964.84,"generationTokensPerSec":51.2,"combinedTokensPerSec":271.76,"totalDurationMs":39002,"timeToFirstTokenMs":9415,"rawTimestamps":{"ttftMs":9415,"allTtftMs":[9415,4643,1692],"generationDurationMs":29587,"turns":[{"turnId":"turn-0","durationMs":12718,"ttftMs":9415},{"turnId":"turn-1","durationMs":3001},{"turnId":"turn-2","durationMs":2771},{"turnId":"turn-3","durationMs":4305},{"turnId":"turn-4","durationMs":1971},{"turnId":"turn-5","durationMs":8398,"ttftMs":4643},{"turnId":"turn-6","durationMs":1356},{"turnId":"turn-7","durationMs":4482,"ttftMs":1692}]}}
{"timestamp":"2026-05-15T13:59:22.964Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":490,"outputTokens":469,"totalTokens":959,"prefillTokensPerSec":135.36,"generationTokensPerSec":79.93,"combinedTokensPerSec":101.08,"totalDurationMs":9488,"timeToFirstTokenMs":3620,"rawTimestamps":{"ttftMs":3620,"allTtftMs":[3620,821],"generationDurationMs":5868,"turns":[{"turnId":"turn-0","durationMs":6452,"ttftMs":3620},{"turnId":"turn-1","durationMs":1650},{"turnId":"turn-2","durationMs":1386,"ttftMs":821}]}}
{"timestamp":"2026-05-18T16:04:53.686Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1493,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1493}]}}
{"timestamp":"2026-05-18T16:04:59.425Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1282,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1282}]}}
{"timestamp":"2026-05-18T16:05:07.244Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1373,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1373}]}}
{"timestamp":"2026-05-18T16:05:18.879Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":0,"outputTokens":0,"totalTokens":0,"prefillTokensPerSec":0,"generationTokensPerSec":0,"combinedTokensPerSec":0,"totalDurationMs":1222,"rawTimestamps":{"allTtftMs":[],"turns":[{"turnId":"turn-0","durationMs":1222}]}}
{"timestamp":"2026-05-18T16:05:39.663Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":6049,"outputTokens":520,"totalTokens":6569,"prefillTokensPerSec":2581.73,"generationTokensPerSec":41.42,"combinedTokensPerSec":440.93,"totalDurationMs":14898,"timeToFirstTokenMs":2343,"rawTimestamps":{"ttftMs":2343,"allTtftMs":[2343],"generationDurationMs":12555,"turns":[{"turnId":"turn-0","durationMs":5221},{"turnId":"turn-1","durationMs":9677,"ttftMs":2343}]}}
{"timestamp":"2026-05-18T16:06:49.310Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":24,"outputTokens":951,"totalTokens":975,"prefillTokensPerSec":2.88,"generationTokensPerSec":109.29,"combinedTokensPerSec":57.23,"totalDurationMs":17037,"timeToFirstTokenMs":8335,"rawTimestamps":{"ttftMs":8335,"allTtftMs":[8335],"generationDurationMs":8702,"turns":[{"turnId":"turn-0","durationMs":17037,"ttftMs":8335}]}}
{"timestamp":"2026-05-18T16:14:33.629Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":10,"inputTokens":549,"outputTokens":1110,"totalTokens":1659,"prefillTokensPerSec":683.69,"generationTokensPerSec":48.38,"combinedTokensPerSec":69.87,"totalDurationMs":23745,"timeToFirstTokenMs":803,"rawTimestamps":{"ttftMs":803,"allTtftMs":[803],"generationDurationMs":22942,"turns":[{"turnId":"turn-0","durationMs":2273},{"turnId":"turn-1","durationMs":2599},{"turnId":"turn-2","durationMs":1569},{"turnId":"turn-3","durationMs":2705},{"turnId":"turn-4","durationMs":1677},{"turnId":"turn-5","durationMs":3140},{"turnId":"turn-6","durationMs":1769},{"turnId":"turn-7","durationMs":3296},{"turnId":"turn-8","durationMs":1226},{"turnId":"turn-9","durationMs":3491,"ttftMs":803}]}}
{"timestamp":"2026-05-18T16:25:00.013Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":36,"inputTokens":8805,"outputTokens":7546,"totalTokens":16351,"prefillTokensPerSec":13587.96,"generationTokensPerSec":51.5,"combinedTokensPerSec":111.1,"totalDurationMs":147178,"timeToFirstTokenMs":648,"rawTimestamps":{"ttftMs":648,"allTtftMs":[648,1264,2424,1730,2366,1,6141,2028,1209,3327],"generationDurationMs":146530,"turns":[{"turnId":"turn-0","durationMs":3100},{"turnId":"turn-1","durationMs":1611},{"turnId":"turn-2","durationMs":2836},{"turnId":"turn-3","durationMs":1701},{"turnId":"turn-4","durationMs":7045},{"turnId":"turn-5","durationMs":5537},{"turnId":"turn-6","durationMs":9904,"ttftMs":648},{"turnId":"turn-7","durationMs":2297},{"turnId":"turn-8","durationMs":3572},{"turnId":"turn-9","durationMs":6941,"ttftMs":1264},{"turnId":"turn-10","durationMs":4770},{"turnId":"turn-11","durationMs":5741,"ttftMs":2424},{"turnId":"turn-12","durationMs":2288},{"turnId":"turn-13","durationMs":2018},{"turnId":"turn-14","durationMs":2203},{"turnId":"turn-15","durationMs":1905},{"turnId":"turn-16","durationMs":2054},{"turnId":"turn-17","durationMs":1797},{"turnId":"turn-18","durationMs":1620},{"turnId":"turn-19","durationMs":2448},{"turnId":"turn-20","durationMs":2930},{"turnId":"turn-21","durationMs":5691,"ttftMs":1730},{"turnId":"turn-22","durationMs":5988},{"turnId":"turn-23","durationMs":7813,"ttftMs":2366},{"turnId":"turn-24","durationMs":1},{"turnId":"turn-25","durationMs":1099,"ttftMs":1},{"turnId":"turn-26","durationMs":1652},{"turnId":"turn-27","durationMs":3354},{"turnId":"turn-28","durationMs":14059,"ttftMs":6141},{"turnId":"turn-29","durationMs":1673},{"turnId":"turn-30","durationMs":3813,"ttftMs":2028},{"turnId":"turn-31","durationMs":1979},{"turnId":"turn-32","durationMs":2149},{"turnId":"turn-33","durationMs":7972,"ttftMs":1209},{"turnId":"turn-34","durationMs":4344},{"turnId":"turn-35","durationMs":11273,"ttftMs":3327}]}}
{"timestamp":"2026-05-18T16:26:14.186Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":7,"inputTokens":2011,"outputTokens":1232,"totalTokens":3243,"prefillTokensPerSec":1028.64,"generationTokensPerSec":26.77,"combinedTokensPerSec":67.61,"totalDurationMs":47969,"timeToFirstTokenMs":1955,"rawTimestamps":{"ttftMs":1955,"allTtftMs":[1955,2520,1853,694],"generationDurationMs":46014,"turns":[{"turnId":"turn-0","durationMs":5149},{"turnId":"turn-1","durationMs":10442,"ttftMs":1955},{"turnId":"turn-2","durationMs":6081},{"turnId":"turn-3","durationMs":10493,"ttftMs":2520},{"turnId":"turn-4","durationMs":5028,"ttftMs":1853},{"turnId":"turn-5","durationMs":6746},{"turnId":"turn-6","durationMs":4030,"ttftMs":694}]}}
{"timestamp":"2026-05-18T16:28:35.851Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":87,"outputTokens":301,"totalTokens":388,"prefillTokensPerSec":129.08,"generationTokensPerSec":42.92,"combinedTokensPerSec":50.47,"totalDurationMs":7687,"timeToFirstTokenMs":674,"rawTimestamps":{"ttftMs":674,"allTtftMs":[674],"generationDurationMs":7013,"turns":[{"turnId":"turn-0","durationMs":5103},{"turnId":"turn-1","durationMs":2584,"ttftMs":674}]}}
{"timestamp":"2026-05-18T16:29:33.286Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":40,"outputTokens":531,"totalTokens":571,"prefillTokensPerSec":17.44,"generationTokensPerSec":57.31,"combinedTokensPerSec":49.4,"totalDurationMs":11558,"timeToFirstTokenMs":2293,"rawTimestamps":{"ttftMs":2293,"allTtftMs":[2293],"generationDurationMs":9265,"turns":[{"turnId":"turn-0","durationMs":11558,"ttftMs":2293}]}}
{"timestamp":"2026-05-18T16:31:22.157Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":58,"outputTokens":618,"totalTokens":676,"prefillTokensPerSec":7.43,"generationTokensPerSec":134.11,"combinedTokensPerSec":54.44,"totalDurationMs":12417,"timeToFirstTokenMs":7809,"rawTimestamps":{"ttftMs":7809,"allTtftMs":[7809],"generationDurationMs":4608,"turns":[{"turnId":"turn-0","durationMs":12417,"ttftMs":7809}]}}
{"timestamp":"2026-05-18T16:32:43.573Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":408,"outputTokens":1498,"totalTokens":1906,"prefillTokensPerSec":75.25,"generationTokensPerSec":58.97,"combinedTokensPerSec":61.84,"totalDurationMs":30823,"timeToFirstTokenMs":5422,"rawTimestamps":{"ttftMs":5422,"allTtftMs":[5422,14765],"generationDurationMs":25401,"turns":[{"turnId":"turn-0","durationMs":7357,"ttftMs":5422},{"turnId":"turn-1","durationMs":23466,"ttftMs":14765}]}}
{"timestamp":"2026-05-18T16:50:30.294Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":44,"outputTokens":2533,"totalTokens":2577,"prefillTokensPerSec":0.93,"generationTokensPerSec":485.99,"combinedTokensPerSec":48.86,"totalDurationMs":52746,"timeToFirstTokenMs":47534,"rawTimestamps":{"ttftMs":47534,"allTtftMs":[47534],"generationDurationMs":5212,"turns":[{"turnId":"turn-0","durationMs":52746,"ttftMs":47534}]}}
{"timestamp":"2026-05-18T16:57:06.934Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":17,"inputTokens":9721,"outputTokens":6329,"totalTokens":16050,"prefillTokensPerSec":513.66,"generationTokensPerSec":44.57,"combinedTokensPerSec":99.73,"totalDurationMs":160936,"timeToFirstTokenMs":18925,"rawTimestamps":{"ttftMs":18925,"allTtftMs":[18925,16190,4022,7901,1873,10528,1481],"generationDurationMs":142011,"turns":[{"turnId":"turn-0","durationMs":21666,"ttftMs":18925},{"turnId":"turn-1","durationMs":25748,"ttftMs":16190},{"turnId":"turn-2","durationMs":18958,"ttftMs":4022},{"turnId":"turn-3","durationMs":2499},{"turnId":"turn-4","durationMs":14503,"ttftMs":7901},{"turnId":"turn-5","durationMs":7021},{"turnId":"turn-6","durationMs":8323,"ttftMs":1873},{"turnId":"turn-7","durationMs":8202},{"turnId":"turn-8","durationMs":8887},{"turnId":"turn-9","durationMs":2925},{"turnId":"turn-10","durationMs":2921},{"turnId":"turn-11","durationMs":3410},{"turnId":"turn-12","durationMs":2903},{"turnId":"turn-13","durationMs":5754},{"turnId":"turn-14","durationMs":14057,"ttftMs":10528},{"turnId":"turn-15","durationMs":7970},{"turnId":"turn-16","durationMs":5189,"ttftMs":1481}]}}
{"timestamp":"2026-05-18T17:00:32.909Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":1,"inputTokens":25,"outputTokens":715,"totalTokens":740,"prefillTokensPerSec":2.76,"generationTokensPerSec":94.53,"combinedTokensPerSec":44.48,"totalDurationMs":16637,"timeToFirstTokenMs":9073,"rawTimestamps":{"ttftMs":9073,"allTtftMs":[9073],"generationDurationMs":7564,"turns":[{"turnId":"turn-0","durationMs":16637,"ttftMs":9073}]}}
{"timestamp":"2026-05-18T17:03:32.883Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":4,"inputTokens":728,"outputTokens":1603,"totalTokens":2331,"prefillTokensPerSec":477.69,"generationTokensPerSec":49.97,"combinedTokensPerSec":69.36,"totalDurationMs":33605,"timeToFirstTokenMs":1524,"rawTimestamps":{"ttftMs":1524,"allTtftMs":[1524],"generationDurationMs":32081.000000000004,"turns":[{"turnId":"turn-0","durationMs":3655},{"turnId":"turn-1","durationMs":17547},{"turnId":"turn-2","durationMs":8416},{"turnId":"turn-3","durationMs":3987,"ttftMs":1524}]}}
{"timestamp":"2026-05-18T17:04:51.598Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":91,"outputTokens":302,"totalTokens":393,"prefillTokensPerSec":120.69,"generationTokensPerSec":42.08,"combinedTokensPerSec":49.56,"totalDurationMs":7930,"timeToFirstTokenMs":754,"rawTimestamps":{"ttftMs":754,"allTtftMs":[754],"generationDurationMs":7176,"turns":[{"turnId":"turn-0","durationMs":5537},{"turnId":"turn-1","durationMs":2393,"ttftMs":754}]}}
{"timestamp":"2026-05-18T17:06:35.090Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":5,"inputTokens":595,"outputTokens":991,"totalTokens":1586,"prefillTokensPerSec":107.67,"generationTokensPerSec":50.99,"combinedTokensPerSec":63.53,"totalDurationMs":24963,"timeToFirstTokenMs":5526,"rawTimestamps":{"ttftMs":5526,"allTtftMs":[5526],"generationDurationMs":19437,"turns":[{"turnId":"turn-0","durationMs":2342},{"turnId":"turn-1","durationMs":8629},{"turnId":"turn-2","durationMs":3748},{"turnId":"turn-3","durationMs":1905},{"turnId":"turn-4","durationMs":8339,"ttftMs":5526}]}}
{"timestamp":"2026-05-18T17:09:58.845Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":9,"inputTokens":2111,"outputTokens":3124,"totalTokens":5235,"prefillTokensPerSec":316.73,"generationTokensPerSec":38.79,"combinedTokensPerSec":60.03,"totalDurationMs":87211,"timeToFirstTokenMs":6665,"rawTimestamps":{"ttftMs":6665,"allTtftMs":[6665,8492,4911,5143,2588,740],"generationDurationMs":80546,"turns":[{"turnId":"turn-0","durationMs":9004,"ttftMs":6665},{"turnId":"turn-1","durationMs":21348,"ttftMs":8492},{"turnId":"turn-2","durationMs":8350},{"turnId":"turn-3","durationMs":10895,"ttftMs":4911},{"turnId":"turn-4","durationMs":8784},{"turnId":"turn-5","durationMs":11053,"ttftMs":5143},{"turnId":"turn-6","durationMs":8565},{"turnId":"turn-7","durationMs":7853,"ttftMs":2588},{"turnId":"turn-8","durationMs":1359,"ttftMs":740}]}}
{"timestamp":"2026-05-18T17:13:46.526Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":3,"inputTokens":5057,"outputTokens":641,"totalTokens":5698,"prefillTokensPerSec":2095.73,"generationTokensPerSec":32.47,"combinedTokensPerSec":257.16,"totalDurationMs":22157,"timeToFirstTokenMs":2413,"rawTimestamps":{"ttftMs":2413,"allTtftMs":[2413,4376],"generationDurationMs":19744,"turns":[{"turnId":"turn-0","durationMs":7565},{"turnId":"turn-1","durationMs":4846,"ttftMs":2413},{"turnId":"turn-2","durationMs":9746,"ttftMs":4376}]}}
{"timestamp":"2026-05-18T17:15:51.225Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":10,"inputTokens":4304,"outputTokens":1913,"totalTokens":6217,"prefillTokensPerSec":435.67,"generationTokensPerSec":57.84,"combinedTokensPerSec":144.73,"totalDurationMs":42955,"timeToFirstTokenMs":9879,"rawTimestamps":{"ttftMs":9879,"allTtftMs":[9879,3442,2613,3964,856],"generationDurationMs":33076,"turns":[{"turnId":"turn-0","durationMs":3346},{"turnId":"turn-1","durationMs":11767,"ttftMs":9879},{"turnId":"turn-2","durationMs":5287,"ttftMs":3442},{"turnId":"turn-3","durationMs":3421},{"turnId":"turn-4","durationMs":4258,"ttftMs":2613},{"turnId":"turn-5","durationMs":2424},{"turnId":"turn-6","durationMs":6178,"ttftMs":3964},{"turnId":"turn-7","durationMs":1727},{"turnId":"turn-8","durationMs":2407},{"turnId":"turn-9","durationMs":2140,"ttftMs":856}]}}
{"timestamp":"2026-05-18T17:26:11.786Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":34,"inputTokens":43395,"outputTokens":12858,"totalTokens":56253,"prefillTokensPerSec":1200.32,"generationTokensPerSec":25.66,"combinedTokensPerSec":104.72,"totalDurationMs":537177,"timeToFirstTokenMs":36153,"rawTimestamps":{"ttftMs":36153,"allTtftMs":[36153,2689,1366,4236,6406,5356,790,1651,13306,66825,29393,119886,40627,5585,1105,2249],"generationDurationMs":501024,"turns":[{"turnId":"turn-0","durationMs":37128,"ttftMs":36153},{"turnId":"turn-1","durationMs":16635,"ttftMs":2689},{"turnId":"turn-2","durationMs":4975,"ttftMs":1366},{"turnId":"turn-3","durationMs":3817},{"turnId":"turn-4","durationMs":9210,"ttftMs":4236},{"turnId":"turn-5","durationMs":4367},{"turnId":"turn-6","durationMs":6681},{"turnId":"turn-7","durationMs":9766,"ttftMs":6406},{"turnId":"turn-8","durationMs":7955},{"turnId":"turn-9","durationMs":8770,"ttftMs":5356},{"turnId":"turn-10","durationMs":1848},{"turnId":"turn-11","durationMs":2371},{"turnId":"turn-12","durationMs":2166},{"turnId":"turn-13","durationMs":3484},{"turnId":"turn-14","durationMs":2771},{"turnId":"turn-15","durationMs":1892},{"turnId":"turn-16","durationMs":2189},{"turnId":"turn-17","durationMs":2412},{"turnId":"turn-18","durationMs":3483,"ttftMs":790},{"turnId":"turn-19","durationMs":3791,"ttftMs":1651},{"turnId":"turn-20","durationMs":5764},{"turnId":"turn-21","durationMs":19747,"ttftMs":13306},{"turnId":"turn-22","durationMs":69825,"ttftMs":66825},{"turnId":"turn-23","durationMs":37360,"ttftMs":29393},{"turnId":"turn-24","durationMs":2},{"turnId":"turn-25","durationMs":3310},{"turnId":"turn-26","durationMs":133998,"ttftMs":119886},{"turnId":"turn-27","durationMs":21354},{"turnId":"turn-28","durationMs":34613},{"turnId":"turn-29","durationMs":47182,"ttftMs":40627},{"turnId":"turn-30","durationMs":6697},{"turnId":"turn-31","durationMs":7977,"ttftMs":5585},{"turnId":"turn-32","durationMs":9916,"ttftMs":1105},{"turnId":"turn-33","durationMs":3721,"ttftMs":2249}]}}
{"timestamp":"2026-05-18T17:28:46.844Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":8,"inputTokens":2243,"outputTokens":1970,"totalTokens":4213,"prefillTokensPerSec":132.06,"generationTokensPerSec":42.23,"combinedTokensPerSec":66.2,"totalDurationMs":63639,"timeToFirstTokenMs":16985,"rawTimestamps":{"ttftMs":16985,"allTtftMs":[16985,1337],"generationDurationMs":46654,"turns":[{"turnId":"turn-0","durationMs":12673},{"turnId":"turn-1","durationMs":20344,"ttftMs":16985},{"turnId":"turn-2","durationMs":4137},{"turnId":"turn-3","durationMs":9417},{"turnId":"turn-4","durationMs":5762},{"turnId":"turn-5","durationMs":2149},{"turnId":"turn-6","durationMs":7088},{"turnId":"turn-7","durationMs":2069,"ttftMs":1337}]}}
{"timestamp":"2026-05-18T17:29:26.902Z","provider":"llama.cpp","model":"Qwen3.6-35B-A3B-MXFP4_MOE.gguf","turnCount":2,"inputTokens":416,"outputTokens":265,"totalTokens":681,"prefillTokensPerSec":109.85,"generationTokensPerSec":55.78,"combinedTokensPerSec":79.76,"totalDurationMs":8538,"timeToFirstTokenMs":3787,"rawTimestamps":{"ttftMs":3787,"allTtftMs":[3787],"generationDurationMs":4751,"turns":[{"turnId":"turn-0","durationMs":3102},{"turnId":"turn-1","durationMs":5436,"ttftMs":3787}]}}
{"timestamp":"2026-05-18T19:02:24.350Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":2,"inputTokens":68503,"outputTokens":4616,"totalTokens":73119,"prefillTokensPerSec":8757.73,"generationTokensPerSec":152.37,"combinedTokensPerSec":1918.33,"totalDurationMs":38116,"timeToFirstTokenMs":7822,"rawTimestamps":{"ttftMs":7822,"allTtftMs":[7822],"generationDurationMs":30294,"turns":[{"turnId":"turn-0","durationMs":8502,"ttftMs":7822},{"turnId":"turn-1","durationMs":29614}]}}
{"timestamp":"2026-05-18T19:11:08.129Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":36,"inputTokens":692307,"outputTokens":15656,"totalTokens":707963,"prefillTokensPerSec":11579.75,"generationTokensPerSec":76.25,"combinedTokensPerSec":2670.5,"totalDurationMs":265105,"timeToFirstTokenMs":59786,"rawTimestamps":{"ttftMs":59786,"allTtftMs":[59786,3861,4337,12069,4515,3773,3224,11472,4712,3959,2260,3896,60110,4270,5140,4180,0,1,0,0,1,0,0,0],"generationDurationMs":205319,"turns":[{"turnId":"turn-0","durationMs":5495},{"turnId":"turn-1","durationMs":59996,"ttftMs":59786},{"turnId":"turn-2","durationMs":14978,"ttftMs":3861},{"turnId":"turn-3","durationMs":5894},{"turnId":"turn-4","durationMs":4717,"ttftMs":4337},{"turnId":"turn-5","durationMs":12969,"ttftMs":12069},{"turnId":"turn-6","durationMs":4223},{"turnId":"turn-7","durationMs":7194,"ttftMs":4515},{"turnId":"turn-8","durationMs":6171},{"turnId":"turn-9","durationMs":4150},{"turnId":"turn-10","durationMs":6994,"ttftMs":3773},{"turnId":"turn-11","durationMs":4149,"ttftMs":3224},{"turnId":"turn-12","durationMs":12091,"ttftMs":11472},{"turnId":"turn-13","durationMs":4703},{"turnId":"turn-14","durationMs":5047,"ttftMs":4712},{"turnId":"turn-15","durationMs":3886},{"turnId":"turn-16","durationMs":5228,"ttftMs":3959},{"turnId":"turn-17","durationMs":3955,"ttftMs":2260},{"turnId":"turn-18","durationMs":9010,"ttftMs":3896},{"turnId":"turn-19","durationMs":61482,"ttftMs":60110},{"turnId":"turn-20","durationMs":4774,"ttftMs":4270},{"turnId":"turn-21","durationMs":5580,"ttftMs":5140},{"turnId":"turn-22","durationMs":3583},{"turnId":"turn-23","durationMs":8814,"ttftMs":4180},{"turnId":"turn-24","durationMs":4,"ttftMs":0},{"turnId":"turn-25","durationMs":5},{"turnId":"turn-26","durationMs":5,"ttftMs":1},{"turnId":"turn-27","durationMs":1,"ttftMs":0},{"turnId":"turn-28","durationMs":0,"ttftMs":0},{"turnId":"turn-29","durationMs":2,"ttftMs":1},{"turnId":"turn-30","durationMs":2},{"turnId":"turn-31","durationMs":1},{"turnId":"turn-32","durationMs":1},{"turnId":"turn-33","durationMs":1,"ttftMs":0},{"turnId":"turn-34","durationMs":0,"ttftMs":0},{"turnId":"turn-35","durationMs":0,"ttftMs":0}]}}
{"timestamp":"2026-05-18T19:31:54.781Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":9,"inputTokens":89552,"outputTokens":3597,"totalTokens":93149,"prefillTokensPerSec":21495.92,"generationTokensPerSec":67.54,"combinedTokensPerSec":1622.18,"totalDurationMs":57422,"timeToFirstTokenMs":4166,"rawTimestamps":{"ttftMs":4166,"allTtftMs":[4166,3529,3655,6510],"generationDurationMs":53256,"turns":[{"turnId":"turn-0","durationMs":6647,"ttftMs":4166},{"turnId":"turn-1","durationMs":5466},{"turnId":"turn-2","durationMs":3693},{"turnId":"turn-3","durationMs":4031},{"turnId":"turn-4","durationMs":5936,"ttftMs":3529},{"turnId":"turn-5","durationMs":9830},{"turnId":"turn-6","durationMs":5425},{"turnId":"turn-7","durationMs":4325,"ttftMs":3655},{"turnId":"turn-8","durationMs":12069,"ttftMs":6510}]}}
{"timestamp":"2026-05-18T19:37:04.141Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":6,"inputTokens":113452,"outputTokens":3752,"totalTokens":117204,"prefillTokensPerSec":26015.13,"generationTokensPerSec":69.59,"combinedTokensPerSec":2011.22,"totalDurationMs":58275,"timeToFirstTokenMs":4361,"rawTimestamps":{"ttftMs":4361,"allTtftMs":[4361,17827,5563,7617,3362,5079],"generationDurationMs":53914,"turns":[{"turnId":"turn-0","durationMs":5018,"ttftMs":4361},{"turnId":"turn-1","durationMs":18740,"ttftMs":17827},{"turnId":"turn-2","durationMs":6211,"ttftMs":5563},{"turnId":"turn-3","durationMs":15178,"ttftMs":7617},{"turnId":"turn-4","durationMs":4283,"ttftMs":3362},{"turnId":"turn-5","durationMs":8845,"ttftMs":5079}]}}
{"timestamp":"2026-05-18T19:38:21.928Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":4,"inputTokens":8546,"outputTokens":1786,"totalTokens":10332,"prefillTokensPerSec":984.45,"generationTokensPerSec":72.01,"combinedTokensPerSec":308.57,"totalDurationMs":33484,"timeToFirstTokenMs":8681,"rawTimestamps":{"ttftMs":8681,"allTtftMs":[8681,5603],"generationDurationMs":24803,"turns":[{"turnId":"turn-0","durationMs":8109},{"turnId":"turn-1","durationMs":5687},{"turnId":"turn-2","durationMs":9773,"ttftMs":8681},{"turnId":"turn-3","durationMs":9915,"ttftMs":5603}]}}
{"timestamp":"2026-05-18T19:46:17.168Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":5,"inputTokens":68753,"outputTokens":2154,"totalTokens":70907,"prefillTokensPerSec":9984.46,"generationTokensPerSec":65.6,"combinedTokensPerSec":1785.04,"totalDurationMs":39723,"timeToFirstTokenMs":6886,"rawTimestamps":{"ttftMs":6886,"allTtftMs":[6886,4833],"generationDurationMs":32837,"turns":[{"turnId":"turn-0","durationMs":8479},{"turnId":"turn-1","durationMs":4208},{"turnId":"turn-2","durationMs":16657,"ttftMs":6886},{"turnId":"turn-3","durationMs":4345},{"turnId":"turn-4","durationMs":6034,"ttftMs":4833}]}}
{"timestamp":"2026-05-18T19:56:09.763Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":28,"inputTokens":1595354,"outputTokens":25752,"totalTokens":1621106,"prefillTokensPerSec":525305.89,"generationTokensPerSec":69.18,"combinedTokensPerSec":4319.55,"totalDurationMs":375295,"timeToFirstTokenMs":3037,"rawTimestamps":{"ttftMs":3037,"allTtftMs":[3037,13673,28033,9202,4807,4528,3654,5502,3886,4456,3250,13080,12854,3427,4212,19933,24411,9703,16122,1],"generationDurationMs":372258,"turns":[{"turnId":"turn-0","durationMs":3834},{"turnId":"turn-1","durationMs":2495},{"turnId":"turn-2","durationMs":4310,"ttftMs":3037},{"turnId":"turn-3","durationMs":15225,"ttftMs":13673},{"turnId":"turn-4","durationMs":29096,"ttftMs":28033},{"turnId":"turn-5","durationMs":9805,"ttftMs":9202},{"turnId":"turn-6","durationMs":13204,"ttftMs":4807},{"turnId":"turn-7","durationMs":5250,"ttftMs":4528},{"turnId":"turn-8","durationMs":4470,"ttftMs":3654},{"turnId":"turn-9","durationMs":5782,"ttftMs":5502},{"turnId":"turn-10","durationMs":7596,"ttftMs":3886},{"turnId":"turn-11","durationMs":7902,"ttftMs":4456},{"turnId":"turn-12","durationMs":4203,"ttftMs":3250},{"turnId":"turn-13","durationMs":5008},{"turnId":"turn-14","durationMs":3844},{"turnId":"turn-15","durationMs":14795,"ttftMs":13080},{"turnId":"turn-16","durationMs":40603,"ttftMs":12854},{"turnId":"turn-17","durationMs":5471,"ttftMs":3427},{"turnId":"turn-18","durationMs":11695,"ttftMs":4212},{"turnId":"turn-19","durationMs":26970,"ttftMs":19933},{"turnId":"turn-20","durationMs":25652,"ttftMs":24411},{"turnId":"turn-21","durationMs":11640},{"turnId":"turn-22","durationMs":10598,"ttftMs":9703},{"turnId":"turn-23","durationMs":33673,"ttftMs":16122},{"turnId":"turn-24","durationMs":3,"ttftMs":1},{"turnId":"turn-25","durationMs":5},{"turnId":"turn-26","durationMs":51430},{"turnId":"turn-27","durationMs":20736}]}}
{"timestamp":"2026-05-18T19:58:52.915Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":6,"inputTokens":10435,"outputTokens":2461,"totalTokens":12896,"prefillTokensPerSec":693.95,"generationTokensPerSec":19.21,"combinedTokensPerSec":90.07,"totalDurationMs":143171,"timeToFirstTokenMs":15037,"rawTimestamps":{"ttftMs":15037,"allTtftMs":[15037,16187,26633],"generationDurationMs":128133.99999999999,"turns":[{"turnId":"turn-0","durationMs":18505},{"turnId":"turn-1","durationMs":23335,"ttftMs":15037},{"turnId":"turn-2","durationMs":25587,"ttftMs":16187},{"turnId":"turn-3","durationMs":31798,"ttftMs":26633},{"turnId":"turn-4","durationMs":32046},{"turnId":"turn-5","durationMs":11900}]}}
{"timestamp":"2026-05-18T20:08:44.940Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":21,"inputTokens":3192238,"outputTokens":10000,"totalTokens":3202238,"prefillTokensPerSec":58744.56,"generationTokensPerSec":18.76,"combinedTokensPerSec":5450.45,"totalDurationMs":587518,"timeToFirstTokenMs":54341,"rawTimestamps":{"ttftMs":54341,"allTtftMs":[54341,33453,49862,14811,16941,35825,8885],"generationDurationMs":533177,"turns":[{"turnId":"turn-0","durationMs":62649},{"turnId":"turn-1","durationMs":58961,"ttftMs":54341},{"turnId":"turn-2","durationMs":57096},{"turnId":"turn-3","durationMs":34112,"ttftMs":33453},{"turnId":"turn-4","durationMs":51245,"ttftMs":49862},{"turnId":"turn-5","durationMs":22643},{"turnId":"turn-6","durationMs":24897,"ttftMs":14811},{"turnId":"turn-7","durationMs":18192,"ttftMs":16941},{"turnId":"turn-8","durationMs":37301,"ttftMs":35825},{"turnId":"turn-9","durationMs":16410},{"turnId":"turn-10","durationMs":16345},{"turnId":"turn-11","durationMs":15448},{"turnId":"turn-12","durationMs":44179},{"turnId":"turn-13","durationMs":20599},{"turnId":"turn-14","durationMs":15530},{"turnId":"turn-15","durationMs":14248},{"turnId":"turn-16","durationMs":15685},{"turnId":"turn-17","durationMs":15853},{"turnId":"turn-18","durationMs":16149},{"turnId":"turn-19","durationMs":18121,"ttftMs":8885},{"turnId":"turn-20","durationMs":11855}]}}
{"timestamp":"2026-05-18T20:09:48.805Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":2,"inputTokens":715,"outputTokens":197,"totalTokens":912,"prefillTokensPerSec":16.15,"generationTokensPerSec":13.02,"combinedTokensPerSec":15.35,"totalDurationMs":59418,"timeToFirstTokenMs":44282,"rawTimestamps":{"ttftMs":44282,"allTtftMs":[44282],"generationDurationMs":15136,"turns":[{"turnId":"turn-0","durationMs":53061,"ttftMs":44282},{"turnId":"turn-1","durationMs":6357}]}}
{"timestamp":"2026-05-18T20:13:13.949Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":9,"inputTokens":1405406,"outputTokens":1702,"totalTokens":1407108,"prefillTokensPerSec":86603.77,"generationTokensPerSec":10.6,"combinedTokensPerSec":7958.31,"totalDurationMs":176810,"timeToFirstTokenMs":16228,"rawTimestamps":{"ttftMs":16228,"allTtftMs":[16228,7810],"generationDurationMs":160582,"turns":[{"turnId":"turn-0","durationMs":17373,"ttftMs":16228},{"turnId":"turn-1","durationMs":36359},{"turnId":"turn-2","durationMs":14630},{"turnId":"turn-3","durationMs":36693},{"turnId":"turn-4","durationMs":14968},{"turnId":"turn-5","durationMs":16571},{"turnId":"turn-6","durationMs":14690},{"turnId":"turn-7","durationMs":14596},{"turnId":"turn-8","durationMs":10930,"ttftMs":7810}]}}
{"timestamp":"2026-05-18T20:33:37.419Z","provider":"openrouter","model":"deepseek/deepseek-v4-flash","turnCount":15,"inputTokens":44601,"outputTokens":2028,"totalTokens":46629,"prefillTokensPerSec":11909.48,"generationTokensPerSec":29.39,"combinedTokensPerSec":640.91,"totalDurationMs":72754,"timeToFirstTokenMs":3745,"rawTimestamps":{"ttftMs":3745,"allTtftMs":[3745],"generationDurationMs":69009,"turns":[{"turnId":"turn-0","durationMs":4398},{"turnId":"turn-1","durationMs":3076},{"turnId":"turn-2","durationMs":3511},{"turnId":"turn-3","durationMs":2850},{"turnId":"turn-4","durationMs":3184},{"turnId":"turn-5","durationMs":4136},{"turnId":"turn-6","durationMs":3897},{"turnId":"turn-7","durationMs":2935},{"turnId":"turn-8","durationMs":5794},{"turnId":"turn-9","durationMs":4349},{"turnId":"turn-10","durationMs":4636},{"turnId":"turn-11","durationMs":2644},{"turnId":"turn-12","durationMs":12015},{"turnId":"turn-13","durationMs":9405},{"turnId":"turn-14","durationMs":5924,"ttftMs":3745}]}}
+4
View File
@@ -3,6 +3,10 @@
This project has a *zero defects policy*. This project has a *zero defects policy*.
When you are unsure about something, ALWAYS ask the user. When you are unsure about something, ALWAYS ask the user.
# Communication
When creating yaks or working on features, always ask clarifying questions to ensure understanding. Ask questions one at a time to maintain clear communication and allow for focused discussion.
You run in a `nono` sandbox. Always ask the user to run commands that require privilege, like docker, sudo or package installation. You run in a `nono` sandbox. Always ask the user to run commands that require privilege, like docker, sudo or package installation.
+4
View File
@@ -26,6 +26,10 @@ test:
format: format:
@make -C app format @make -C app format
# Serve the app
serve:
@cd app && mix phx.server
# Run Playwright browser-based tests (requires running server) # Run Playwright browser-based tests (requires running server)
# Set PLAYWRIGHT_BASE_URL to point at the running Phoenix app # Set PLAYWRIGHT_BASE_URL to point at the running Phoenix app
playwright: playwright:
+1 -1
View File
@@ -6,7 +6,7 @@ Posts live as markdown files in git and compile into the BEAM at build time: zer
The project structure is modular, I wanted to have a personal blog, and an easy way to add an 'engineering' blog to fledgling SaaS products, as well as release notes. The release notes now also serve to announce new features on my blog. The project structure is modular, I wanted to have a personal blog, and an easy way to add an 'engineering' blog to fledgling SaaS products, as well as release notes. The release notes now also serve to announce new features on my blog.
Status: Expirimental. Works on my server ;-). Status: Experimental. Works on my server ;-).
Enjoy, [Willem van den Ende](https://willemvandenende.com) Enjoy, [Willem van den Ende](https://willemvandenende.com)
+65 -1
View File
@@ -9,7 +9,41 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1" "highlight.js": "^11.11.1",
"playwright": "^1.61.1"
},
"devDependencies": {
"@playwright/test": "^1.60.0"
}
},
"node_modules/@playwright/test": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/highlight.js": { "node_modules/highlight.js": {
@@ -20,6 +54,36 @@
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }
},
"node_modules/playwright": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.61.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
} }
} }
} }
+2 -1
View File
@@ -16,6 +16,7 @@
"@playwright/test": "^1.60.0" "@playwright/test": "^1.60.0"
}, },
"dependencies": { "dependencies": {
"highlight.js": "^11.11.1" "highlight.js": "^11.11.1",
"playwright": "^1.61.1"
} }
} }
@@ -420,6 +420,30 @@ defmodule FirehoseWeb.CoreComponents do
""" """
end end
@doc """
Renders an RSS feed icon (inline SVG).
## Examples
<.rss_icon class="size-5" />
"""
attr :class, :string, default: "size-4"
attr :rest, :global
def rss_icon(assigns) do
~H"""
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class={[@class]}
{@rest}
>
<path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93v-2.83Z" />
</svg>
"""
end
## JS Commands ## JS Commands
def show(js \\ %JS{}, selector) do def show(js \\ %JS{}, selector) do
@@ -39,4 +39,30 @@
</div> </div>
</main> </main>
<footer class="border-t border-base-200 px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-3">
<h3 class="text-sm font-semibold tracking-wide text-base-content/60 uppercase">Subscribe</h3>
<div class="flex flex-wrap gap-4">
<a
href="/api/blog/engineering/feed.xml"
class="inline-flex items-center gap-2 text-sm text-base-content/70 hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-4 text-orange-500" />
Engineering Blog
</a>
<a
href="/api/blog/releases/feed.xml"
class="inline-flex items-center gap-2 text-sm text-base-content/70 hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-4 text-orange-500" />
Release Notes
</a>
</div>
</div>
</footer>
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />
@@ -41,6 +41,9 @@
<% end %> <% end %>
<meta name="twitter:card" content={meta.twitter_card} /> <meta name="twitter:card" content={meta.twitter_card} />
<% end %> <% end %>
<%= for feed <- get_content_for(assigns, :feed_links) do %>
<link rel="alternate" type="application/rss+xml" title={feed.title} href={feed.rss_url} />
<% end %>
</head> </head>
<body> <body>
{@inner_content} {@inner_content}
@@ -9,9 +9,11 @@ defmodule FirehoseWeb.BlogController do
result = blog.paginate(page) result = blog.paginate(page)
meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url()) meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url())
rss_url = rss_feed_url(blog)
conn conn
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()})
|> render(:index, |> render(:index,
page_title: blog.title(), page_title: blog.title(),
blog_title: blog.title(), blog_title: blog.title(),
@@ -19,7 +21,8 @@ defmodule FirehoseWeb.BlogController do
posts: result.entries, posts: result.entries,
base_path: blog.base_path(), base_path: blog.base_path(),
page: result.page, page: result.page,
total_pages: result.total_pages total_pages: result.total_pages,
rss_feed_url: rss_url
) )
end end
@@ -27,31 +30,40 @@ defmodule FirehoseWeb.BlogController do
blog = conn.assigns.blog blog = conn.assigns.blog
post = blog.get_post!(slug) post = blog.get_post!(slug)
visibility = Blogex.Post.visibility(post) visibility = Blogex.Post.visibility(post)
rss_url = rss_feed_url(blog)
meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog) meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog)
conn conn
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()})
|> render(:show, |> render(:show,
page_title: post.title, page_title: post.title,
post: post, post: post,
meta: meta, meta: meta,
base_path: blog.base_path(), base_path: blog.base_path(),
visibility: visibility, visibility: visibility,
authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user) authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user),
rss_feed_url: rss_url
) )
end end
def tag(conn, %{"tag" => tag}) do def tag(conn, %{"tag" => tag}) do
blog = conn.assigns.blog blog = conn.assigns.blog
posts = blog.posts_by_tag(tag) posts = blog.posts_by_tag(tag)
rss_url = rss_feed_url(blog)
render(conn, :tag, rss_link = %{rss_url: rss_url, title: blog.title()}
conn
|> FirehoseWeb.Layouts.put_content_for(:feed_links, rss_link)
|> render(:tag,
page_title: "#{blog.title()}#{tag}", page_title: "#{blog.title()}#{tag}",
blog_title: blog.title(), blog_title: blog.title(),
tag: tag, tag: tag,
posts: posts, posts: posts,
base_path: blog.base_path() base_path: blog.base_path(),
rss_feed_url: rss_url
) )
end end
@@ -77,4 +89,9 @@ defmodule FirehoseWeb.BlogController do
_ -> 1 _ -> 1
end end
end end
defp rss_feed_url(blog) do
base_id = blog.base_path() |> String.trim_leading("/blog/")
FirehoseWeb.Endpoint.url() <> "/api/blog/#{base_id}/feed.xml"
end
end end
@@ -1,6 +1,17 @@
<div class="space-y-8"> <div class="space-y-8">
<header> <header>
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1> <div class="flex items-center gap-3">
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to {@blog_title} RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<p :if={@blog_description} class="mt-2 text-base-content/70">{@blog_description}</p> <p :if={@blog_description} class="mt-2 text-base-content/70">{@blog_description}</p>
</header> </header>
@@ -1,5 +1,16 @@
<div class="space-y-8"> <div class="space-y-8">
<a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a> <div class="flex items-center justify-between">
<a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<%= if @authenticated and @visibility == :draft do %> <%= if @authenticated and @visibility == :draft do %>
<div <div
@@ -1,6 +1,17 @@
<div class="space-y-8"> <div class="space-y-8">
<header> <header>
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1> <div class="flex items-center gap-3">
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to {@blog_title} RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<p class="mt-2 text-base-content/70">Posts tagged "{@tag}"</p> <p class="mt-2 text-base-content/70">Posts tagged "{@tag}"</p>
</header> </header>
@@ -39,7 +39,7 @@ defmodule FirehoseWeb.EditorDashboardLive do
"pb-2 px-1 text-sm font-medium transition-colors", "pb-2 px-1 text-sm font-medium transition-colors",
if(@active_tab == :drafts, if(@active_tab == :drafts,
do: "border-b-2 border-zinc-900 text-zinc-900", do: "border-b-2 border-zinc-900 text-zinc-900",
else: "text-zinc-500 hover:text-zinc-700" else: "text-base-content/70 hover:text-base-content"
) )
]} ]}
> >
@@ -52,7 +52,7 @@ defmodule FirehoseWeb.EditorDashboardLive do
"pb-2 px-1 text-sm font-medium transition-colors", "pb-2 px-1 text-sm font-medium transition-colors",
if(@active_tab == :scheduled, if(@active_tab == :scheduled,
do: "border-b-2 border-zinc-900 text-zinc-900", do: "border-b-2 border-zinc-900 text-zinc-900",
else: "text-zinc-500 hover:text-zinc-700" else: "text-base-content/70 hover:text-base-content"
) )
]} ]}
> >
@@ -61,17 +61,17 @@ defmodule FirehoseWeb.EditorDashboardLive do
</div> </div>
<div id="drafts-tab" class={if(@active_tab != :drafts, do: "hidden")}> <div id="drafts-tab" class={if(@active_tab != :drafts, do: "hidden")}>
<div :if={@drafts == []} class="text-zinc-500 text-sm">No drafts</div> <div :if={@drafts == []} class="text-base-content/70 text-sm">No drafts</div>
<div :for={post <- @drafts} class="py-4 border-b border-zinc-100 last:border-0"> <div :for={post <- @drafts} class="py-4 border-b border-zinc-100 last:border-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<.link <.link
navigate={post_path(post)} navigate={post_path(post)}
class="text-base font-medium text-zinc-900 hover:underline" class="text-base font-medium text-base-content hover:underline"
> >
{post.title} {post.title}
</.link> </.link>
<div class="text-sm text-zinc-500 mt-1"> <div class="text-sm text-base-content/70 mt-1">
{post.author} · {Calendar.strftime(post.date, "%b %d, %Y")} · {post.author} · {Calendar.strftime(post.date, "%b %d, %Y")} ·
<span class="text-amber-600 font-medium">Draft</span> <span class="text-amber-600 font-medium">Draft</span>
</div> </div>
@@ -81,17 +81,17 @@ defmodule FirehoseWeb.EditorDashboardLive do
</div> </div>
<div id="scheduled-tab" class={if(@active_tab != :scheduled, do: "hidden")}> <div id="scheduled-tab" class={if(@active_tab != :scheduled, do: "hidden")}>
<div :if={@scheduled == []} class="text-zinc-500 text-sm">No scheduled posts</div> <div :if={@scheduled == []} class="text-base-content/70 text-sm">No scheduled posts</div>
<div :for={post <- @scheduled} class="py-4 border-b border-zinc-100 last:border-0"> <div :for={post <- @scheduled} class="py-4 border-b border-zinc-100 last:border-0">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<.link <.link
navigate={post_path(post)} navigate={post_path(post)}
class="text-base font-medium text-zinc-900 hover:underline" class="text-base font-medium text-base-content hover:underline"
> >
{post.title} {post.title}
</.link> </.link>
<div class="text-sm text-zinc-500 mt-1"> <div class="text-sm text-base-content/70 mt-1">
{post.author} · {Calendar.strftime(post.date, "%b %d, %Y")} · {post.author} · {Calendar.strftime(post.date, "%b %d, %Y")} ·
<span class="text-blue-600 font-medium"> <span class="text-blue-600 font-medium">
{Post.days_until_live(post)} days until live {Post.days_until_live(post)} days until live
@@ -40,9 +40,9 @@ defmodule FirehoseWeb.MicroprintsLive do
@impl true @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
<div class="max-w-6xl mx-auto"> <div class="max-w-4xl mx-auto">
<h1 class="text-2xl font-bold mb-2">Microprints</h1> <h1 class="text-2xl font-bold mb-6">Microprints</h1>
<p class="text-sm text-zinc-500 mb-6"> <p class="text-sm text-base-content/70 mb-4">
Visual fingerprints of source code files. Click a line to highlight it. Visual fingerprints of source code files. Click a line to highlight it.
Click a card to expand and view the source. Click a card to expand and view the source.
</p> </p>
+1
View File
@@ -75,6 +75,7 @@ defmodule FirehoseWeb.Router do
pipe_through [:browser, :require_authenticated_user] pipe_through [:browser, :require_authenticated_user]
live_session :authenticated_user, live_session :authenticated_user,
layout: {FirehoseWeb.Layouts, :app},
on_mount: [{FirehoseWeb.UserAuth, :ensure_authenticated}] do on_mount: [{FirehoseWeb.UserAuth, :ensure_authenticated}] do
live "/editor/dashboard", EditorDashboardLive live "/editor/dashboard", EditorDashboardLive
end end
+1 -1
View File
@@ -4,7 +4,7 @@ defmodule Firehose.MixProject do
def project do def project do
[ [
app: :firehose, app: :firehose,
version: "0.1.0", version: "0.2.0",
elixir: "~> 1.15", elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
+5 -2
View File
@@ -24,16 +24,17 @@
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"}, "lazy_html": {:hex, :lazy_html, "0.1.10", "ffe42a0b4e70859cf21a33e12a251e0c76c1dff76391609bd56702a0ef5bc429", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "50f67e5faa09d45a99c1ddf3fac004f051997877dc8974c5797bb5ccd8e27058"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"mdex_native": {:hex, :mdex_native, "0.2.4", "a74b9d799d3fc487b1f97b70ee9b04108c7af0514920d56828e173d19312c2a9", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d71185c43c5209c6939f4b65f903f71c892512c93ea6843171227855d2d976f5"},
"microprints": {:git, "https://gitea.apps.sustainabledelivery.com/QWAN/microprints-phoenix.git", "29ef59ff6eb41853b6f91872d8fffdfba4d85a62", [branch: "main"]}, "microprints": {:git, "https://gitea.apps.sustainabledelivery.com/QWAN/microprints-phoenix.git", "29ef59ff6eb41853b6f91872d8fffdfba4d85a62", [branch: "main"]},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"nimble_publisher": {:hex, :nimble_publisher, "1.1.1", "3ea4d4cfca45b11a5377bce7608367a9ddd7e717a9098161d8439eca23e239aa", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d67e15bddf07e8c60f75849008b78ea8c6b2b4ae8e3f882ccf0a22d57bd42ed0"}, "nimble_publisher": {:hex, :nimble_publisher, "2.0.0", "f4fc100949ccdf1831094ff303b79f27c6f04b2ead829409426ee0d9ed3aa55b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:mdex_native, "~> 0.1", [hex: :mdex_native, repo: "hexpm", optional: false]}], "hexpm", "88997f74b763c24a15d20a90d7cd4ef1fd01172a2b4651e54d8021a44659f2c8"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
@@ -47,6 +48,8 @@
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"rustler": {:hex, :rustler, "0.38.0", "7a8906998ff0d28e3021c0a73264abcda719bda344b2e58307c6805b0f87c9b4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "704c03c1bf66be12b031c5a389347b91c81c5cb819a24b068b0de36fe4a5652a"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"swoosh": {:hex, :swoosh, "1.23.0", "a1b7f41705357ffb06457d177e734bf378022901ce53889a68bcc59d10a23c27", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "97aaf04481ce8a351e2d15a3907778bdf3b1ea071cfff3eb8728b65943c77f6d"}, "swoosh": {:hex, :swoosh, "1.23.0", "a1b7f41705357ffb06457d177e734bf378022901ce53889a68bcc59d10a23c27", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "97aaf04481ce8a351e2d15a3907778bdf3b1ea071cfff3eb8728b65943c77f6d"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
-58
View File
@@ -1,58 +0,0 @@
import { defineConfig, devices } from "@playwright/test";
// Default Playwright e2e configuration.
//
// Uses Firefox by default (Playwright Chromium crashes on macOS 15
// due to Crashpad permission restrictions).
//
// ## Usage
//
// make playwright # run with default browser (Firefox)
// PLAYWRIGHT_BROWSER=chromium make playwright
// PLAYWRIGHT_BROWSER=webkit make playwright
//
// # Run a single test file:
// PLAYWRIGHT_BROWSERS_PATH=assets/node_modules/playwright-core/.local-browsers \
// npx playwright test --config=playwright.config.ts test/e2e/smoke.spec.ts
//
// ## Docker Chrome (alternative)
//
// Use playwright.docker.config.ts instead of this config:
//
// docker compose -f docker-compose.chrome.yml up -d
// npx playwright test --config=playwright.docker.config.ts
const browserEnv = process.env.PLAYWRIGHT_BROWSER || "firefox";
const deviceFor = (name: string) => {
switch (name) {
case "chromium": return devices["Desktop Chrome"];
case "firefox": return devices["Desktop Firefox"];
case "webkit": return devices["Desktop Safari"];
default:
console.warn(`Unknown browser "${name}", falling back to Firefox`);
return devices["Desktop Firefox"];
}
};
export default defineConfig({
testDir: "./test/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "list",
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:8056",
trace: "on-first-retry",
headless: process.env.PLAYWRIGHT_HEADLESS !== "false",
},
projects: [
{
name: browserEnv,
use: { ...deviceFor(browserEnv) },
},
],
});
@@ -9,4 +9,10 @@
I had started on a "Write your own coding agent" exercise. Four iterations in, actually. And then I found [Tiny Agents]( https://tinyagents.dev/lesson/agent-loop), a set of interactive exercises that let you experience how agents work, from a simple chat request, through a tool, more tools etc. It has a live graph, that visualises of the flow of data and actions. I had started on a "Write your own coding agent" exercise. Four iterations in, actually. And then I found [Tiny Agents]( https://tinyagents.dev/lesson/agent-loop), a set of interactive exercises that let you experience how agents work, from a simple chat request, through a tool, more tools etc. It has a live graph, that visualises of the flow of data and actions.
It is good fun to play with, it starts simple and builds up. It lets you inspect the messages between the 'agent' loop code and the large language model server (which is just HTTP and some JSON). It is good fun to play with, it starts simple and builds up. It lets you inspect the messages between the 'agent' loop code and the large language model server (which is just HTTP and som![](app/priv/static/images/blog/2026/agent-nearby_20260701_220759.png)e JSON).
![]/images/blog/2026/Screen![](app/priv/static/images/blog/2026/agent-nearby_20260701_220849.png)shot 2026-06-30 at 22.42.49.png)
![agent nearby](/images/blog/2026/agent-nearby.png)
![](app/priv/static/images/blog/2026/agent-nearby_20260701_220606.png)
@@ -1,8 +1,8 @@
%{ %{
title: "Replace Moralistic Programming with Situational Awareness", title: "Replace Moralistic Programming with Situational Awareness",
author: "Willem van den Ende", author: "Willem van den Ende",
tags: ~w(craft, situational-awareness), tags: ~w(craft situational-awareness),
description: "", description: "I don't like what I now call Moralistic Programming. 'You are not a good developer if <you don't use my pet favourite practice>'. I see some of that resurfacing in discussions around coding agents. I value Situational Awareness over best practices and fingerpointing..",
published: true published: true
} }
--- ---
@@ -0,0 +1,16 @@
%{
title: "Synthetic TDD",
author: "Willem van den Ende",
tags: ~w(Vocabulary AI TDD),
description: "Synthetic Test Driven Development is when you have a coding agent generate the tests and the code, and maybe also do the refactor step with an agent.",
published: true
}
---
I'm starting small posts under the vocabulary tag, to tease out concepts that fall out of other posts, but would be too much of an aside there. This one line could go under "Jargon", to be fair. These may evolve to larger post. I am going to try and at least leave you with a question in each :-).
*Synthetic Test Driven Development* is when you have a coding agent generate the tests and the code, and also do the refactor step(s) with an agent.".
When considering _Synthetic TDD_ it is worth considering what benefits you get from writing tests first as an activity, in a very short cycle, and what value you get from having tests as an artifact.
@@ -0,0 +1,253 @@
%{
title: "At coding agent skill to guide editing in-place",
author: "Willem van den Ende",
tags: ~w(AI coding-agent baby-steps),
description: "With an @<agent> instruction in a file, you can use your regular coding agent on small parts of your code, without modifiying your editor / IDE.",
published: false
}
---
TLDR
===
I made an agent skill that let's me one of Aider's favourite tricks: naming the agent in the code with `@aider extract the code below into a method <foo>`, so you don't have to go back and forth between a chat window and the code as much.
Aider is a coding agent that precedes claude code. It didn't have tool calls, but did have very well defined mini-flows like this.
I remember Aider picking this up on save, and executing immediately. This skill is a bit cruder, I run the command `skill:at-pi` and then it searches in recently changed files for lines annotated with `@pi` or `@Pi`.
The main benefit of an @<agent> instruction in a file, is that you can use any coding agent and any editor / IDE. I am not a fan of 'spicy autocomplete' that just starts generating text while I am trying to do some wishful thinking.
A skill like this isn't that hard to make. One prompt, and some iteration to make it work for me. Full skill is included at the bottom of this post. Read through it, and make your own. Curisous what you'll come up with.
Backstory
===
I am slowly making WeReview 2.0 production ready. It was mostly done with [Synthetic TDD](/blog/engineering/synthetic-tdd) so far. I was blown away over a year ago when I managed to do a rewrite of 80% of WeReview in 5 days calendar time, during a school holiday. WeReview is an opinionated session review system that helps focused conferences create programs fast with little fuss. This was one of those 'blown away' moments.
I have in places explicitly driven the design, like making flexible forms with the help of an Allium Spec as a separate system. Answering questions about "where are we" and "how did that flow work again?", "what do we still need to do" is sometimes more work than I like. So I have decided to generate documents showing the flow from one screen to the next as a side effect of running integration tests. This was inspired by a Matteo Vaccari's talk on TDD and AI, in which he demonstrated his customer focused tests that are not necessarily end to end, inspired by Ward Cunningham's Fit. That reminded my of Ward Cunningham's Swim system, that he made for the Eclipse Foundation. There is a paper about it that I will include in a follow up post.
My users use the application through screens, I can model the domain as much as I want, but that is what they interact with. I have cheaply made end to end tests, but they are slow and fiddly, and give me a 'moment in time'. I can render videos, but those are also not easy to follow for the key stakeholders. So a document describing steps in the flow, maybe with side steps as in Ward Cunninghams' work felt like a good idea, and reading his paper backwards it can be done in small steps.
Doing that took extending tests to output HTML in the right places. For this Pi + Qwen worked quite well, in the get it Run sense. Get it right, less so. I had some ideas on extracting a DSL, broke them down into tasks. When I do that, I tend to over-engineer. My coding agent less so, but once it worked, I had 400 lines of code for a test that was just spanning three screens.
Refactoring by hand and by Agent
----
This took 1.6 minutes, which is not particularly fast. But I was doing other things. There is refactoring tooling for Elixir, but it is not that easy to setup, I am in the process of doing it. I have paid attention to the extract setup refactoring more closely and will do the next one by hand, probably. I have done some extract functions by hand to keep a feel for Elixir, and also because it sometimes takes less time to just do it than to explain it.
Aside: when the coding agent reports the number of tests
----
Also note, that as Pi / Qwen reported success, that not all the tests have been run (there are 998 tests in total, this is an umbrella project with `make test` at the root)
> - ✅ All 739 tests pass
> - ✅ Clean compilation (no warnings)
> - ✅ Committed as 2e2086d
The rubber duck in the blog post is strong. All the tests are run, but in three rounds, as this is a small mono-repo. I am attempting to keep some things that are not specific for the domain, like flexible forms, separate, without spinning them off in to separate repos. The coding agent picked up on the last run, which has 739 tests without mentioning the other ones.
I was tempted to blame the agent for not running all tests, but they are part of a post-commit hook, so they get run deterministically, no matter what.
About the skill
====
It is probably overkill to use `git status --porcelain` a search through all files might work as well, or better. `rg` is not working in my agents' sandbox for some reason, I need to figure this out still. It is also specific to elixir in places. Also not sure if prioritization is necesary. I put it in, because when I see multiple things that need work in a piece of code, I want to annotate before I forget (this is analogous to putting test lists in code, but for refactorings too).
I had expected to get this to work in one prompt, but maybe my perfectionism got the better of me, once again. I thought do do this with sub-agents in one go (looking at the skill) and getting the context picked up besides my one line instruction, was interesting. See the *Context Resolution* section below.
Note that none of the `@pi` instructions will make it into the commit history, if you work one instruction at a time. Luckily the skill has an example:
```elixir
# @Pi: move to Swimex.Report
defp extract_fragment(full_html, selector) do
```
That already needed a re-prompt, because I had specified 'pi' in the instruction to create the skill, and used 'Pi' when trying it out. Moving a function to another module is simple, but annoying:
- Copy function over to new module.
- Import function in old module
- Make sure it is referenced correctly by all call sites
- Delete original function
The At Pi skill in full
=====
```
---
name: at-pi
description: Finds @pi comments in staged/unstaged changed files, presents them for prioritization, then executes each via a sub-agent (pi -p) with context from relevant imports. Use when you want to batch-execute developer instructions left as @pi annotations across changed files.
---
```
# @pi Skill
Scans changed git files for `@pi` annotated instructions, prioritizes them with the user, then executes each sequentially via a sub-agent with contextual file information.
## Step 1: Discover Changed Files
Run:
```bash
git status --porcelain | awk 'NR>0 {print $2}'
```
This returns staged + unstaged + untracked files (one path per line).
Filter to code files you care about: `.ex`, `.exs`, `.heex`, `.js`, `.ts`, `.py`, `.rb`, etc.
## Step 2: Scan for @pi Comments
For each changed file, scan for `@pi` annotations. Two formats are supported:
### Single-line
```elixir
# @pi fix the email validation to also reject nil
def validate_email(email), do: ...
```
Instruction = everything after `@pi ` on that same line.
### Multi-line (block comment)
```elixir
# @pi
# Fix the email validation logic here.
# It should handle: nil, empty string, and invalid format.
# See WereviewWeb.EmailValidator for reference patterns.
def validate_email(email), do: ...
```
Instruction = everything after `@pi` on the tag line (may be empty), plus all continuation lines that start with `# ` (or `// ` for JS/TS) until a blank line, end of file, or a non-annotation comment line.
Strip the leading `# `, `// `, or `; ` markers and join into one instruction string.
### Parsing approach
Use `grep -ni '@pi' <file>` to find lines with `@pi` (case-insensitive — catches `@Pi`, `@PI`, `@pi`, etc.), then read surrounding context to capture multi-line blocks.
### Context resolution (important)
When a `@pi` comment appears immediately **above** a function definition, struct, module, or code block, the instruction likely refers to **that code**. Read that code to identify its name, arity, and purpose. Infer this relationship and carry it into the sub-agent prompt.
For example:
```elixir
# @Pi: move to Swimex.Report
defp extract_fragment(full_html, selector) do
```
The intent is: move `extract_fragment/2` into the Swimex.Report module. The sub-agent must understand **what** is being referred to, not just the bare instruction text.
## Step 3: Build Instruction List
For each `@pi` comment found, build a structured entry:
```
[N] FILE:PATH:LINE "instruction text"
```
Where `[N]` is the discovery order index (1-based).
Example output:
```
Found 3 @pi annotations in changed files:
[1] wereview/apps/wereview/test/support/swimex_report.ex:82
"move the output_dir logic here to use the WEREREVIEW_TEST_REPORT_DIR env var"
[2] wereview/lib/wereview_web/email_validator.ex:15
"add regex validation for international domain names"
[3] wereview/apps/wereview_core/lib/wereview/tenants.ex:44
"handle the case where tenant slug already exists — return {:error, :duplicate_slug}"
```
## Step 4: Prioritize with User
For each instruction, **resolve its intent** before presenting:
1. If the `@pi` comment is immediately above a code block (function/struct/module), identify that code and describe what will happen.
2. Present a clear action statement, not just the raw instruction text.
Example output:
```
Found 3 @pi annotations in changed files:
[1] wereview/apps/wereview/test/support/swimex_report.ex:82
"move the output_dir logic here to use the WEREREVIEW_TEST_REPORT_DIR env var"
[2] wereview/lib/wereview_web/email_validator.ex:15
"add regex validation for international domain names"
[3] wereview/apps/wereview/test/wereview_web/live/session_submit_live_test.exs:7
→ The function extract_fragment/2 will be moved to Swimex.Report
(raw instruction: "todo move to Swimex.Report")
```
Ask the user to reorder by specifying a sequence. For example:
> Execute in this order: 3, 1, 2 (skip 2)
Or just press Enter to execute in discovery order. The user can skip any by omitting it.
## Step 5: Discover Relevant Context Files
For each instruction you are about to execute:
1. Look at the file containing the `@pi` comment
2. Extract import/require/use/include statements from that file to find relevant neighbors
3. For Elixir (`.ex` / `.exs`):
- `import X` — module being imported
- `require X` — module being required
- `use X` — module being used with callbacks
4. Resolve module names to actual file paths in the project
5. Read those files so you know what's available
Example for Elixir:
```elixir
# From wereview/lib/wereview_web/email_validator.ex
import WereviewWeb.EmailHelpers wereview/lib/wereview_web/email_helpers.ex
use WereviewCore.Validation wereview/apps/wereview_core/lib/validation.ex
```
## Step 6: Execute via Sub-Agent (Sequential)
For each instruction in the prioritized order:
1. **Build the sub-agent prompt**: Combine the original `@pi` instruction with context about nearby relevant files. Include:
- The **resolved intent** (what code/block the annotation refers to, e.g., "move function `extract_fragment/2`")
- The raw instruction text
- The file path and line number of the annotation
- A brief summary of what the relevant imported/required files provide (if you read them)
- Any other contextual info you already know about the codebase
2. **Spawn the sub-agent**:
```bash
pi -p "<enriched prompt>"
```
3. The sub-agent will modify files in place. You do NOT re-read or re-apply changes yourself — trust the sub-agent to make its changes.
## Step 7: Show Changes and Ask Confirmation
After all instructions are executed:
1. Run `git diff` to show all uncommitted changes
2. Present the diff summary to the user:
```
Changes after @pi execution:
modified: wereview/apps/wereview/test/support/swimex_report.ex
- removed unused @output_dir (already handled internally)
+ added email validation logic
modified: wereview/lib/wereview_web/email_validator.ex
+ international domain regex support
Commit these changes? (y/n)
```
3. If user says `y`: run `git add -A && git commit -m "Execute @pi annotations"`
4. If user says `n`: leave changes uncommitted for manual review
## Error Handling
- If a file cannot be read, skip it and note the error
- If no `@pi` comments are found in changed files, report: "No @pi annotations found in changed files."
- If `git status` fails (not in a git repo), stop and inform the user
- If the user cancels mid-execution, show partial results so far and exit
@@ -0,0 +1,105 @@
%{
title: "Does AI assistance support tiny open source projects?",
author: "Willem van den Ende",
tags: ~w(pi.dev OpenSource deterministic),
description: "I am making small contributions to tiny open source projects I have just started using, where I would let it be before coding agents.",
published: true
}
---
Many larger open source projects are struggling with floods of AI assisted contributions. The FT has a great piece about this by Sam Learner [Who cleans up after the vibe coding party?](https://www.ft.com/content/cec8df9e-b43b-4cd1-8feb-c07e804e8d33). But miniscule open source projects may fare better. I am now contributing to tiny open source projects while just trying them out.
In the before times, I would often move on or maybe ask a question when I had trouble installing a tool I was trying out. Say it had security issues, or something didn&rsquo;t work quite right on first try. I might fix it locally and have a play. But by the time I had finished doing the work, I would have no more energy to be social and make pull requests.
Especially if I need to fix more than one thing, and the author might not want to accept both. I am very much a trunk based development person, so I prefer to commit a stream of commits to main. But working asynchronously with people I don&rsquo;t know, more communication is required to make my changes make sense to the other party.
Example - DeepClause today
----
I am playing with [the DeepClause command line interface](https://github.com/deepclause/deepclause-sdk) , and had to run `npm audit fix` after installing. After fixing that, building it failed. So I opened [Pi](/blog/engineering/tag/pi.dev) and fixed it. 10 minutes work, two candidate Pull Requests (PR). One `main` branch with two commits.
As mentioned above, it is not something I do often. When working in client projects, the teams I work in can usually choose to stay out of the PR review circus and either just push to main, or pay lip service to the PR process.
Making two pull requests in one go
---
So how do I make two pull requests out of it? I ask [my assistant](/blog/engineering/tag/Pi.dev), learning something in the process:
> Now I have two commits. what if I wanted to make a Pull Request on github for each commit separately?
It then explained how to find the changed commits (this I knew, but saved me typing), and made the branches. If you look at the last `$`, cutting two branches is a one liner for [the LLM](/blog/engineering/tag/pi.dev)
```
1. Find your base commit (the one before both new commits):
$ cd /Users/willem/dev/spikes/llm/deepclause-sdk && git log --oneline -5
a305d5b fix: add missing FS type declaration for Emscripten filesystem interface
8edcaad fix esbuild and other vulnerabilities
8aa9234 v0.86
3f51edf v0.85
ecd708c v0.83
Took 0.0s
Thinking...
You have two commits on main:
a305d5b fix: add missing FS type declaration
8edcaad fix esbuild and other vulnerabilities
8aa9234 v0.86 ← base
To make separate PRs, create a branch from main before each commit, push to GitHub, and open PRs targeting main.
$ cd /Users/willem/dev/spikes/llm/deepclause-sdk && git branch pr/vulnerability-fix 8edcaad && git branch pr/fs-type-fix a305d5b
```
It then provided instructions, conveniently not knowing that I can not push to &rsquo;origin&rsquo; but have to fork it on github first. But that is ok.
>
>
> Here are the steps:
>
> 1. Push the branches to GitHub
>
> \`\`\`bash
> git push origin pr/vulnerability-fix
> git push origin pr/fs-type-fix
> \`\`\`
>
> 1. Open the PRs in your browser
>
> \`\`\`bash
>
> gh pr create &ndash;base main &ndash;head pr/vulnerability-fix &ndash;title &ldquo;fix: esbuild and other vulnerabilities&rdquo; &ndash;body &ldquo;Updates esbuild and other dependencies to fix vulnerabilities.&rdquo;
>
> gh pr create &ndash;base main &ndash;head pr/fs-type-fix &ndash;title "fix: add missing FS type declaration for Emscripten filesystem interface" &ndash;body "Defines FS type alias with methods used by swipl-wasm. Fixes TS2304: Cannot find name
> 'FS'."
> \`\`\`
>
> If you don&rsquo;t have the gh CLI installed, open these URLs in your browser instead:
>
> - PR 1: <https://github.com/deepclause/deepclause-sdk/compare/main...pr/vulnerability-fix>
> - PR 2: <https://github.com/deepclause/deepclause-sdk/compare/main...pr/fs-type-fix>
Now I can get my agent to solve the actual problem I had after just ~npm install~ing it.
Not making the same mistake twice
----
It did take me two projects to know to ask the question. I had a similar thing with [pi-browser](https://github.com/larsderidder/pi-browser), where the dependencies were out of date, and the extension was showing as `src` on loading, which confused me no end - I thought the extension had not loaded.
So I pushed the dependencies update, waiting for it to be accepted, which happened within a day or so, and then forgot to push the next commit. So this time I paused, asked my agent and pushed to Pull Requests. Although it is mighty tempting to &rsquo;just prompt my own fork&rsquo; into existing, the &rsquo;just&rsquo; is doing a lot of heavy lifting there. And from the looks of it, a lot of thought and tinkering has gone into this project over the last year.
So while I do enjoy prompting, I value collaboration more.
Do you contribute more now?
Further reading
---
I wrote about extracting knowledge and tools from repeating patterns in [Stochastig to Deterministic](https://www.qwan.eu/2025/12/11/stochastic-to-deterministic.html). I find DeepClause interesting, because it uses Symbolic AI (Prolog) to extract e.g. agentic loops into deterministic code. So instead of having an LLM control repetition, we can have code do the looping, e.g. over a number of tasks. Prolog supports _backtracking_, trying a number of times and falling back out of the box, and [DeepClause](https://github.com/deepclause/deepclause-sdk) uses that to reset the context for the LLM and try again.
@@ -0,0 +1,116 @@
%{
title: "Swim the Test",
author: "Willem van den Ende",
tags: ~w(ATDD ComprehensionMaxxing CollaborationMaxxing),
description: "Every application has some kind of workflow. Understanding larger workflows, making them cohesive and understandable to users and developers is non-trivial. I re-discovered Ward Cunningham's Swim the Test approach, this is the Further Reading section of future posts.",
published: true
}
---
TLDR;
----
Every application has some kind of workflow. Understanding larger workflows, making them cohesive and understandable to users and developers is non-trivial. I re-discovered Ward Cunningham's Swim the Test approach, this is the Further Reading section of future posts.
[Jon Uddell's 2008 post about Ward Cunninghams' implementation of Brian Marick's 'Visible Workings'] https://blog.jonudell.net/2008/03/04/ward-cunninghams-visible-workings/ is a succinct introduction . connecting the Visible Workings pattern to the implementation made for a workflow system on behalf of the Eclipse Foundation.
[The "Swim" System for User-Oriented Presentation for Test-Case results](https://wiki.eclipse.org/images/2/20/Swim_System_PNSQC_2007_Paper.pdf). Plain spoken Paper by Ward Cunningham, Bjorn Freeman-Benson and Karl Matthias.
> Perhaps the highest compliment we received was when one of our customers (SC) said that she
> easily understood how everything worked because it "uses her words" in the interface panels and
> documentation.
Why this post?
====
Refactoring integration tests towards *Swim the Test* turns out to be easier than to write about it. And was not as hard as I thought - it can be done in baby steps. I keep both losing the literature references, and at the same time finding more or them. So getting them out of the way should help me post a 'how' post more easily.
Swim the test
====
> Finally you can *swim the test*. Here the steps and results are laid out in a
> table. Time advances as you move down the rows, and theres a column for every
actor in the workflow.
!["Change personal address" Sequence diagram. 'Start' at the left, 'big2' and 'emo' column. big2 did some actions, and this results in 'User address changed'. Followed by some 'variations' (the swim paper in the linked post explains this). The 'emo' column has one 'emo address changed' box. Screenshot by Jon Udell](https://i0.wp.com/jonudell.net/img/visibleworking3.png?w=656)
Screenshot by Jon Udell.
[Jon Uddell's post]( https://blog.jonudell.net/2008/03/04/ward-cunninghams-visible-workings/) is a succinct introduction . connecting the Visible Workings pattern to the implementation made for a workflow system on behalf of the Eclipse Foundation.
The "Swim" paper
====
I learnt about the Swim system when Ward Cunningham ran a session about it at the first Agile Open Northwest (2006 or 2007 I think - this is a blog post, I may do the research later or it doesn't ship..)
[The "Swim" System for User-Oriented Presentation for Test-Case results](https://wiki.eclipse.org/images/2/20/Swim_System_PNSQC_2007_Paper.pdf) by Ward Cunningham
Bjorn Freeman-Benson and Karl Matthias
I only found this paper existed now that I went looking for it. It also explained (when you read it back to front) on how to get started. The workflow with swimlanes looks very sophisticated. It mentiones `run.php` further down, which is 'just' one flow with some screenshots.
The reason I was attracted to domain modelling (long before Eric Evans' 2003 DDD book) was this:
> Perhaps the highest compliment we received was when one of our customers (SC) said that she
> easily understood how everything worked because it "uses her words" in the interface panels and
> documentation.
Simplicity is achieved when there is nothing left to take away. Compare this to DDD's "Ubiquitous Language". The scare quotes are intentional. As a non-native speaker I had to look up Ubiquitous. Compare this with the paragraph above. I appreciate many of the user-focused things that come out of the DDD community, like Event Storming, but ask yourself, if a patterns introduce barriers to outsiders, or if it removes them (and when introducing barriers, is the trade-off worth it?).
>At the same time,
> our abstractions are detailed enough to be believed because we are running the real code in our
> simulations instead of bubbles and arrows diagrams. We respect the user's expertise without
> dumbing down our system or forcing them to use our terminology.
A large application is built in many small steps
=====
This looks like a lot of work, and I haven't integrated this in an application. Yet. Reworking existing integration tests to output dialogs as HTML for each step was not _that_ much work, and the rewards are instantaneous: multi-step workflows now become glanceable. And as they get longer, I can make small improvements to improve glanceability.
I am trying to draw in the users as well, let's see how this goes.
Further, further, Reading
====
This is meta - a Further Reading section in a further reading post ;-).
A harness for behaviour
----
I was reminded of the swim system by this talk of Matteo Vaccari on youtube which focuses on tests that are understandable by the stakeholders.
[A Harness for Behaviour: Ensuring Ai-generated Code Does What We Want - TDD in the Age of AI ](https://www.youtube.com/watch?v=3tdmoj35HG0)
He referenced Fit and focuses on writing tests that work as a communication medium between users, stakeholders and developers. He uses an external DSL in YAML. I am not against YAML, but someoone's remark last year that all new programming languages appear to be YAML gives me pause, whenever I see it. The main thing is when it works it works. Context is key. (not further reading: I ended up with an internal DSL and screenshots in small steps as a medium - the users don't need to be able to write tests, yet, communication about what we are building and how it works is key. In small steps - I don't believe good UX comes from large batches). [LinkedIn post on Matteo's account](https://www.linkedin.com/posts/matteovaccari_the-video-of-my-presentation-at-platmosphere-share-7478330775579586560-vSoX) in case you want to comment.
Matteo blogged about this earlier:
[Acceptance tests for AI Assisted development](https://matteo.vaccari.name/posts/acceptance-tests-for-ai-assisted-development/) on "grey box testing". Refers to [Approved Fixtures Pattern](https://lexler.github.io/augmented-coding-patterns/patterns/approved-scenarios/) and is also worth reading on its' own.
Visible Workings
---
[Visible Workings]( https://web.archive.org/web/20110202132102/http://visibleworkings.com) on archive.org, as the original site is no longer active. I like the notion of "Tinkerable Software".
>Explanatory software goes hand in hand with tinkerable software because the dedicated amateur will need explanations to do the tinkering well.
"View Source"
----
A comment in one of Jon Udell's posts about 'View Source' reminded me of the work of [Sue Smith](https://www.sue.codes/) My timebox is up, she has a great blog in which is she writes about building tools in an IDE to make software development accessible to newcomers. I remember her writing about how to replicate the 'view source' experience of early web 2.0, where you could find out how a web front-end worked by 'just' viewing the source.
[What comes after view source?](https://www.sue.codes/blog/afterviewsource/)
[Being intentional about friction](https://www.sue.codes/blog/whatfriction/) is more tangential, and inspiring. Some comment about the link to the tests in an application on one of Jon Udell's 'swim' posts may be related. That link to the tests inside an application dialog may be seen as friction, but also, as Sue puts it:
> Removing friction does not necessarily empower people
and
> Being able to choose which details are worth paying attention to is the trick.
Happy reading and watching
====
Thank you for making it to the end. I hope this inspires you to do more reading, watching, and more importantly, building.
As [Steve Freeman put it just now](https://mastodonapp.uk/@sf105/116991301858959311):
> ward and Bjorns “swimlanes” work for the Eclipse foundation was an inspiration. Its a crime against software that it was left derelict.
@@ -0,0 +1,149 @@
%{
title: "Learning to Swim test",
author: "Willem van den Ende",
tags: ~w(ATDD ComprehensionMaxxing CollaborationMaxxing SyntheticTDD Refactoring),
description: "The faster you can build, the more pressing communication with others and understanding together becomes. You can build software to promote understanding and collaboration, but where do you start? I found a foothold in the 'swim tests' paper and existing integration tests.",
published: false
}
---
I often find it easier to do and build stuff than to write about it. Getting over the 'further reading' section in [swim the test](https://willemvandenende.com/blog/engineering/swim-the-test) appears to have freed me up to write more.
I wrote with the LinkedIn comment announcing that post:
> The faster you can build, the more pressing communication with others and understanding together becomes.
A view of a foothold
====
Reading the "Swim System" paper, it is clear that you can build software to promote understanding and collaboration, but where do you start? I found a foothold in the existing integration tests, and a discussion on page 14 and 15 of the [Swim Systems paper](https://wiki.eclipse.org/images/2/20/Swim_System_PNSQC_2007_Paper.pdf).
![Screenshots. A dialog with test annotations on the left, a sequence diagram with test results on the right](/images/blog/2026/swim-asterisk-dialog.png)
The left part of the screenshot, a dialog with some steps, was something I could build. There is a description of how they abstracted the report creation so one source produces multiple targets, e.g. the dialog on the left and the swimlane view on the right. That is also something that can be factored out later, starting with a point solution.
I eventually read the whole paper front to back after skimming it and pondering it over. I also remember what Ward Cunningham mentioned when he presented this work: that tools like Fit(Nesse) and this are contextual, and these are something to be made in context, not off-the-shelf tools. So I read this paper, contrasted it with Matteo Vaccari's work, and my own experience in stakeholder and user-focused tests, and what we needed for the application I am working on.
What do we want?
====
From the swim systems paper
---
I liked:
- application dialogs as a way to explain the system
- backlinks to the tests from the dialogs in the actual application
- display of errors inline
- The swimlanes, and the way events are used
- Standard flows and variations on them, implemented and inspectable simply
I think the paper predates the Event Sourcing hype, seeing the past-tense framed things in the swimlanes
What I missed from the paper, and can now see after starting to build:
- Failing tests also displayed in the report (see the red "Trouble can't find <x>" sections on the right)
![Screenshots. Passed example on the left - title is a green bar. Failed example on the right, red heading and two 'trouble' bars. A clock and timestamp indicate when the tests were run.](/images/blog/2026/swim-asterisk-example.png)
What I wanted to do differently:
Some actions are abstract enough, eg. _login 'developer1'_ others are too UI focused for what I need. The order in which fields are filled in is often not important. Or so I thought, when writing this I have written some tests where one action makes another one (im)possible. So most of the time a whole dialog can be worked through by data, using the Test Data Builder pattern to allow for differences.
I have a test setup out of the box (Phoenix Liveview Test) that works well enough, and I guessed (correctly) that it would have primitives to export HTML for the reports as well.
So to start with, I decided to produce reports, and leave reporting on unmet expectations to regular assertions in the existing test framework.
From Matteo Vaccari's ATDD and AI talk
----
Matteo presented an external DSL. I liked the idea of a DSL, and decided to start with an internal DSL first. Elixir has good support for this, and I could drive this by extracting helpers from existing tests and making the scenarios longer so there are more steps in the workflow.
Some of the lines in Matteo's DSL I found a bit HTTP specific on seeing it, but then found that my tests made by [Synthetic TDD](/blog/engineering/synthetic-tdd) also suffered from this. At one point a response is parsed for an identifier that is used in subsequent tests.
See below in working in small steps on how I am dealing with that.
From my own failures
----
- being able to see the whole and have confidence it fits together coherently
- testing as a feedback mechanism was secondary, strangely enough
Having the screens, step by step, gave me confidence where I was, and made it easy to identify the next step. It did turn out to have value as a technical feedback mechanism earlier than expected. I had worked bottom up for part of a feature, and added the next step in a flow once the parts were done. Some bits to integrate them were missing, as evidenced by the new step. Good encourgement to work acceptance test driven the next time.
On being able to have confidence in coherence
---
The swim systems paper appealed to me, because I have trouble keeping an overview of workflows with multiple steps. This was always a challenge working in the XP way, as we very quickly can create a well-tested app with complicated flows. Keeping the whole coherent and "ready to mind" is hard. "Ready to hand" with good unit tests became very cheap, but seeing the forest and the trees was sometimes difficult.
This became worse with LLM generated systems. When I can create a prototype out of thin air in a couple of hours, with hundreds of tests, and several screens and can let a coding agent produce the demos (see previous posts) it feels great. But when someone else is interested and asks me how it fits together, that becomes a lot harder.
I was hesitant to take some of my visual collaboration tools to market, and I did not understand why. After building the first steps of workflow tests I do understand. It may look like a collaboration tool is about the sticky notes, but the magic is in the collaboration rules that are encoded in things like how groups are structured, how invitations work, what the unit of a 'tenant' is in a SaaS applications, who gets to (dis)invite who, how, when? Or in moderation: who invited this _Person_ and why?
It is great brainstorm-building a mobile first product outcomes focused board, but then adding teams and seeing bits of flow missing is annoying. These flows are necessary for any collaborative application, visual or not, and they are different, because the combination of peoples' roles and assumptions are unique for each application.
So I wanted, just for myself, to see as much of the flow as possible. End-to-End tests through browsers are now very cheap to make (I can make dozens in a day), but maintaining and running them is not. Especially when I am working on my laptop with a coding agent. Having sandboxed the agent, accessing a browser is not trivial.
The unit and integration tests I have so far do give me confidence that most things work, but not that the whole application flows hang together as I want. This was triggered by a stakeholder asking: "Where are we with <tool> 2.0?" . (more about <tool> later. previous blog posts drafts failed on digressions).
Every application has workflows. They may not appear to be core, but they shape collaboration, so they are.
Working in small steps, AI augmented where useful
=====
I had an integration test that could serve as a starting point. But the workflow only had two steps. Several things are taken care of beforehand, analogous to the `login(''developer1')` in the Swim Systems paper. At least it was a good place to start, so the process was roughly:
1. Find out how to write pages and dialogs (a part of the page) as html
2. Decide how to collect these as the test progresses
3. Turn the collected parts into a report
4. Refactor the existing test towards a DSL
5. Add more steps
6. Go to 4.
For 1. I found a forum post, and I already had some [Synthetic](/blog/engineering/2026/07-13-synthetic-tdd.md) tests. This was also a good opportunity to re-read [the documentation](https://phoenix-live-view.hexdocs.pm/Phoenix.LiveViewTest.html). Rendering pages, components, selecting elements and getting the text back is all built in, so all we need to do is wrap it in a page and save the parts. I did consider writing the reports out as markdown, with html snippets at some point. When rendering html to a pdf, the page breaks happen in the middle of screenshots sometimes. But the PDF already lacks the styling.
I initially iterated with [Pi](/blog/engineering/tag/pi.dev) on how to collect tests. I had a fancy idea of collecting the various dialogs in a process (well supported e.g. by Elixir GenServers), then thought of doing it the unix way (write out dialogs, than `cat` them all together), and ended up collecting step outputs in a list, and rendering the list at the end. This did require re-ordering the test a bit: the `assert` has to come at the end, after creating the report.
My assumption was that a model could generate more steps, after refactoring towards a simple flow of steps. The generated tests had some noise in parsing outputs to determine inputs for the next steps. Finding the right extractions was a bit of work, and at the same time gave me a better understanding of what I wanted.
Away from the desk, away from the screen?
----
I was mostly away from my desk last week. After getting three steps working, in small steps, refactoring by hand (my local model had some trouble moving things around in to files, and I needed to learn more about how to organise tests and support code in a Phoenix Liveview project ), the basic flow is now at five steps. Pi with Deepseek v4 flash did most of that, with me doing 'yes' prompts from my phone.
![Screenshot of Pi session - featuring 'insufficient credits'](/static/images/blog/2026/pidev-step5-generated.png)
This insufficient credits message hasn't happened to me in a while. I deposited 25 USD in may last year on OpenRouter. Since then I mostly used Claude Code or local models. Being away from my desk means running Pi on a server, with deepseek. I spent about 10$ in the last month, preparing a keynote and TDD with AI workshops for Philips (hit me up if you are interested too, at QWAN we are responsibly extending our offerings, now that we have a better understanding of how to augment teams, and improve the outcomes and quality using coding agents). So probably about 1 or 2 $ on workshop preparation and 8 for the testing support and bringing a vibe TDD'ed app to production.
It is nice to be able to make progress while having friends over. I have learned to not multi-task too much, and when a question is too big to answer on my phone, I will just percolate on it. Good designs come from working on that - you have to be seen working for creativity to happen, and from stepping away from the screen and coming back with a fresh perspective.
Show me the reports
====
TODO add validation failure report, and full flow so far.
Reworking is easier than writing
====
Reworking and refactoring existing user-focusing integration tests towards this turns out to be easier than writing about it. A number of ideas collide, and I had trouble keeping track of the references. So the first blogpost about this was the "further reading" section.
And here as well, Programming as theory building works. I built towards the principles above, learnt a few things, got some value out early, and now have an idea of the next steps.
Further Reading
----
[Swim the test](https://willemvandenende.com/blog/engineering/swim-the-test) has more of the literature background, including the Sim Systems paper and what I mentioned about Matteo Vaccari.
The Swim Systems paper is well worth reading, it is as good as any of my favourite software engineering papers from the 1970s (but 30 years more recent).
Felienne on Programming as Theory Building
Refactoring to Patterns
John Nolan on explaining what happened after the fact
Machiel Groeneveld on wanting the reasoning
@@ -0,0 +1,25 @@
%{
title: "Retort",
author: "Willem van den Ende",
tags: ~w(Synthetic TDD pi.dev coding-agents evals ),
description: "At first I was bemused by Retort, Adrian Cockroft's whole stack coding agent evaluation tool. It is growing on me, and might be useful as a starting point for custom evals.",
published: true
}
---
I was bemused by Adrian Cockrofts' [Retort](https://github.com/adrianco/retort) - it looks like Claude Code mostly independently running evals on coding agent x model x target language combinations. The most recent report however, matches my anecdata. I have started on something like Retort earlier this year, but much smaller - custom tiny agent, a couple of small language models and about 10 programming languages with a number of small tasks (extract method, find code smells in a file etc). I parked it, because it looked like that would be significant work to turn into something explainable to more people. So it is great to see Retort blossoming.
The main takeaway for me from the latest one is that the number of turns taken by the agent seems to be a good predictor for speed of task completion, and Qwen 35B, that I use a lot, does as well as the better frontier models on this task (and better than some of the worse frontier models). My hypothesis (hope?) that a decent harness with a smaller model would perform well on incremental tasks seems to pan out.
Reading the readme of retort, the factors are free to label and create. It produces a number of synthetic 'blogs'. The [optimal blog](https://github.com/adrianco/retort/blob/main/optimal-blog.md) I find interesting, bedcause it compares anthropic frontier models with one model I use regularly (Qwen3.6-35B-A3B) and one that I have used successfully for legacy code analysis (Qwen3-Coder-Next 80B ).
Not all outcomes match my experience, Qwen3.6-35B-A3B does well enough with Elixir production code, although it struggles with macros and moving functions across modules. That, admittedly, is something more LLMs struggle with. Explaining that often takes more time than doing it by hand.
It would be interesting to see the same benchmarks with Deepseek V4 Flash, widely used on somewhat larger machines (128GB VRAM, or less VRAM + more RAM combinations) and cheap on Openrouter (I haven't gone over a dollar yet for a day of intensive use), and Qwen 3.6 27B (much slower in tokens per second on my Macbook pro M3 Max, but seems to get more complex tasks done than 35B. I tried it yesterday on a Docker deploy that 35B struggled with, and it fixed the two issues I had independently. Feedback loops do work, as long as the model is up to the task ).
Reading through the _optimal_ blog I found the source of my bemusement. Retort also does some BDD / TDD / ATDD and 'neutral' (no tests?) comparisons. I believe these are all [Synthetic TDD](https://willemvandenende.com/blog/engineering/synthetic-tdd). I have used coding agents (mainly Claude Code up to april) extensively to generate tests, recently with the help of Allium. Without tests coding agents go off the rails quickly. And I am lazy, as a good programmer should be, so I like having tests generated and providing feedback loops. But synthetic tests do have limitations. They carry an LLMs interpretation of your intent, and may not drive the design.
It may be that design doesn't matter everywhere equally. I am now working on some code where it does, and explaining myself in code (with some assistance) is faster and more precise than talking to an LLM.
I am not sure how that kind of intent could fit in a benchmark like Retort. What do you believe? Does intent and design in tests matter? If so, where and when?
@@ -0,0 +1,38 @@
%{
title: "Developing with open weights models only, three months in",
author: "Willem van den Ende",
tags: ~w(pi.dev open-weights ),
description: "It is a bit over three months ago that my linkedin post about cancelling my Claude Max subscription went viral. Time for an update. The first six weeks were uneventful, then I started missing some things from before, and yesterday I closed the loop.",
published: true
}
---
I blogged about [My local agentic development setup](/blog/engineering/my-local-agentic-dev-setup-today) the day after my post about cancelling my Claude Max subscription and going all in on local models went viral.
I am still using the same flexible harness - [Pi.dev](/blog/engineering/tag/pi.dev), with surprisingly few extensions (apart from yesterday, see below for prime-agent). I had expected to add web-search, but even my local models have a lot of knowledge, know how to use tools and look up urls from documentation. Enough to start building [Swim the test](/blog/engineering/swim-the-test), which is quite niche, into an existing system. I occasionally paste a hand-searched link into the prompt and that is enough.
The first six weeks were uneventful. I didn't need to create large prototypes in a day like before, that helped. I was busy preparing larger sessions (a coding dojo with 70 participants and 10 facilitators for a UK client, and a keynote about AI and TDD and a workshops on AI with TDD for Philips Image Guided Therapy). So mostly smaller things, and sometimes making small in-the-loop changes to existing code.
Then in a conversation with Chris Matts he mentioned he sketches UI on a piece of paper and then lets one of the hosted solutions (Figma or ChatGPT) create a screen out of it. I wasn't set up to do that. After that I have set up [image recognition in Pi.dev](/blog/engineering/enabling-a-local-model-to-explain-images-in-pidev). Surprisingly good. I haven't used it to generate screens, but could make that with some extensions.
In July I went back to working on a larger piece of software, and improvements to local inference engines were coming in thick and fast. I can now run Qwen3.6 27B, their dense model, at conversational speed, and my previous go to, their 35B mixture of experts model, runs a lot faster as well. For smaller, me-in-the-loop changes with tests, the 35B model works quite well. For trickier things, or larger independent runs, where the coding agent runs through a list of tasks and executes them in sub-agents one by one, the 27B model performs better. Slower per token, faster to outcome.
With [mtplx](https://github.com/youssofal/mtplx) instead of LLamaCPP I can now run the dense Qwen3.6 27B at conversational speed (was below 20 tokens per second, now often 30 at the start of a conversation). This means I can run more complex development tasks locally. If it slows down later, I'll go do something else, as long as it can finish the job independently.
As an example, it managed to find something properly obscure that Deepseek v4 flash also could find, but the 35B model could not. I am keeping that private as a benchmark. Before this, none of the MLX (mac accellerated) inference engines could handle long context (for me). Mtplx does that - speed does not matter if you can't get a good outcome.
MTPLX is also a proper consumer solution. It comes with a GUI to select models and see how it is running. It will figure out the best parameters to run a model with by running a benchmark on your machine after downloading a model, and comes with pre-selected models and quantizations. The screenshot below was after installing a smaller variation of the qwen27B model. Before this I had never seen any of them do more than 20 tokens per second. Around 30 is good enough for a conversation for me.
The screenshot below was taken when I installed a smaller version of the Qwen 27B model. It shows a few sessions tested on my laptop, and which one was found to be fastest, for me.
![Mtplx installation screenshot](/images/blog/2026/mtplx-install.png)
I still have LLamaCPP, but mtplx does well enough at the moment. I would us LLamaCPP when I want to run multiple smaller models on my mac simultaneously - mtplx so far only runs one model at a time. No problem for 35B and 27B as more than one does not really fit in 64GB (V)RaM.
Since the school holidays were starting and I am away from my desk more often, I revived my [remote environment in a virtual machine](/blog/engineering/how-to-get-started-with-the-pi-coding-agent-on-a-vps), so I could develop on my phone. I had used DeepSeek V4 Flash in part when preparing exercises. This model can be locally on slightly more VRAM than I have.I use openrouter for it. This has worked very well for adding features to existing code, doing longer running implementation sessions etc. It is cheaper to host than Qwen 3.6 27B apparently.
On Friday I spent a day with prime-agent. This is a ready-to-use Recursive Language Model adaptation of Pi. This runs instances of Pi on subtasks, and creates memories of things that could have been done better. It appears to do long running tasks without a ticket system like yaks, beads or a markdown file well enough. It is an example of not attaching new ways of working to old flows (ticket systems, version control). There may be better ways to discover. RLM appears to be one of them. I am doing `new session` less often, longer contexts seem to work well enough (although slower). As the memory built up, starting 'fresh' also is fast enough also is fast enough
[Prime-agent](https://github.com/PrimeIntellect-ai/prime-agent) means I can do what I did with claude code before, developing larger prototypes with tests in a conversation. But now with smaller open weights models. The 0731 release of deepseek-v4-flash appears to be strong. Prime-agent with feels more coherent than claude code in april. After three months of not doing larger prototypes, and developing all the things I had thought of before, I now have some new ideas. The harness is at least important as the model, and experiments in harness development are ongoing by many people.
@@ -0,0 +1,16 @@
%{
title: "v0.2.0 — RSS Subscribe Links",
author: "Willem van den Ende",
tags: ~w(release),
description: "RSS feed subscribe links with classic waveguide icons across blog pages, post pages, and site footer."
}
---
RSS feeds have always been available at `/api/blog/engineering/feed.xml` and `/api/blog/releases/feed.xml`, but they were never advertised. This release adds subscribe links throughout the site.
## What's new
- RSS `<link>` tags in `<head>` on all blog pages for browser/feed reader auto-discovery
- Orange RSS icon next to blog title on index pages
- RSS icon on each post page
- Subscribe section in the footer with links to both feeds
+20 -2
View File
@@ -11,11 +11,29 @@
# and so on) as they will fail if something goes wrong. # and so on) as they will fail if something goes wrong.
if Mix.env() == :dev do if Mix.env() == :dev do
import Ecto.Query
alias Firehose.Accounts alias Firehose.Accounts
# Create demo user if not already present # Get or create demo user
unless Accounts.get_user_by_email("demo@example.com") do user = Accounts.get_user_by_email("demo@example.com")
unless user do
{:ok, user} = Accounts.register_user(%{email: "demo@example.com"}) {:ok, user} = Accounts.register_user(%{email: "demo@example.com"})
end
# Ensure user is confirmed to allow password login and avoid magic link conflicts
if is_nil(user.confirmed_at) do
Firehose.Repo.update_all(
from(u in Accounts.User, where: u.email == ^"demo@example.com"),
set: [confirmed_at: DateTime.utc_now()]
)
# Reload user after confirmation
user = Accounts.get_user_by_email("demo@example.com")
end
# Set password if not already set
unless user.hashed_password do
{:ok, {_user, _tokens}} = Accounts.update_user_password(user, %{password: "password123!"}) {:ok, {_user, _tokens}} = Accounts.update_user_password(user, %{password: "password123!"})
end end
end end
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 KiB

-41
View File
@@ -1,41 +0,0 @@
/**
* Auth helpers for Playwright e2e tests.
*
* Provides utilities for logging in as a test user.
* The app uses email-only registration (magic-link flow for initial confirmation)
* and email+password for subsequent logins.
*/
import { Page } from "@playwright/test";
export const TEST_USER = {
email: process.env.PLAYWRIGHT_EMAIL || "test@firehose.test",
password: process.env.PLAYWRIGHT_PASSWORD || "testpassword123!",
};
/**
* Navigate to the login page, fill in credentials, and submit.
* Returns the page after successful login (redirected to home).
*/
export async function loginAsTestUser(page: Page) {
await page.goto("/users/log-in");
// Use the password-based login form
await page.getByLabel("Email").fill(TEST_USER.email);
await page.getByLabel("Password").fill(TEST_USER.password);
// Click "Log in and stay logged in"
await page.getByRole("button", { name: /log in and stay logged in/i }).click();
await page.waitForURL("/");
}
/**
* Navigate to the registration page, fill in email, and submit.
* Note: In dev mode, registration is invite-only by default. Set
* `PLAYWRIGHT_ALLOW_REGISTRATION=true` to run tests against a server
* with open registration enabled.
*/
export async function registerTestUser(page: Page) {
await page.goto("/users/register");
await page.getByLabel("Email").fill(TEST_USER.email);
await page.getByRole("button", { name: /create an account/i }).click();
await page.waitForURL("/users/log-in");
}
-33
View File
@@ -1,33 +0,0 @@
import { test, expect } from "./fixtures";
import { loginAsTestUser } from "./auth-helpers";
test.describe("Authenticated pages", () => {
test("redirects unauthenticated users to login page", async ({ page }) => {
const response = await page.goto("/editor/dashboard");
// Should redirect to login
expect(response?.url()).toContain("/users/log-in");
});
test("can log in and access the editor dashboard", async ({ page }) => {
await loginAsTestUser(page);
// After login, we should be on the home page
await expect(page).toHaveURL("/");
await expect(page.locator("body")).not.toBeEmpty();
});
test("can log out", async ({ page }) => {
await loginAsTestUser(page);
// Now log out
await page.goto("/users/log-out");
// Should redirect to home
await expect(page).toHaveURL("/");
// Try accessing an authenticated page
await page.goto("/editor/dashboard");
// Should redirect to login
await expect(page).toHaveURL(/\/users\/log-in/);
});
});
-28
View File
@@ -1,28 +0,0 @@
/**
* Custom Playwright test fixtures.
*
* Provides a `test` function that connects to a remote Chrome via CDP
* when PLAYWRIGHT_CDP_URL is set (Docker Chrome mode).
* Falls back to the default local browser otherwise.
*/
import { test as base, chromium } from "@playwright/test";
type Fixtures = {
// Browser is provided by Playwright Test by default.
// We only override it when connecting to remote Docker Chrome.
};
const cdpUrl = process.env.PLAYWRIGHT_CDP_URL;
export const test = cdpUrl
? base.extend<Fixtures>({
browser: async ({}, use) => {
const browser = await chromium.connectOverCDP(cdpUrl);
await use(browser);
await browser.close();
},
})
: base;
export { expect } from "@playwright/test";
-29
View File
@@ -1,29 +0,0 @@
import { test, expect } from "./fixtures";
test.describe("Connectivity smoke test", () => {
test("visits the homepage and verifies it loads", async ({ page }) => {
const response = await page.goto("/");
expect(response?.ok()).toBeTruthy();
// Verify the page has content
await expect(page).toHaveTitle(/Firehose/);
});
test("visits the contact page", async ({ page }) => {
const response = await page.goto("/contact");
expect(response?.ok()).toBeTruthy();
await expect(page.locator("body")).not.toBeEmpty();
});
test("visits the blog engineering index", async ({ page }) => {
const response = await page.goto("/blog/engineering");
expect(response?.ok()).toBeTruthy();
await expect(page.locator("body")).not.toBeEmpty();
});
test("visits the blog releases index", async ({ page }) => {
const response = await page.goto("/blog/releases");
expect(response?.ok()).toBeTruthy();
await expect(page.locator("body")).not.toBeEmpty();
});
});
@@ -86,5 +86,15 @@ defmodule FirehoseWeb.EditorDashboardLiveTest do
{:ok, _view, html} = live(conn, ~p"/editor/dashboard") {:ok, _view, html} = live(conn, ~p"/editor/dashboard")
refute html =~ "Live Post" refute html =~ "Live Post"
end end
test "renders with app layout including menu", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/editor/dashboard")
# Verify the app layout is applied by checking for menu elements
assert html =~ "Willem&#39;s firehose"
assert html =~ "Blog"
assert html =~ "Releases"
assert html =~ "Contact"
assert html =~ "Dashboard" # This should be in the menu
end
end end
end end
@@ -90,10 +90,9 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
assert html =~ ~s(phx-value-path="#{file_a}") assert html =~ ~s(phx-value-path="#{file_a}")
assert html =~ "Collapse" assert html =~ "Collapse"
# Highlight a line in file B (rect elements inside SVG) # Highlight a line in file B by dispatching the highlight_line event
view # (microprint renders multiple rect segments per line, element() can't pick one)
|> element(~s(svg rect[phx-value-line="1"][phx-value-path="#{file_b}"])) render_click(view, "highlight_line", %{"line" => "1", "path" => file_b})
|> render_click()
# file A should be collapsed when highlighting a different file # file A should be collapsed when highlighting a different file
html = render(view) html = render(view)
@@ -145,6 +144,22 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
end end
end end
describe "app layout with menu" do
test "renders the microprints page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/microprints")
assert html =~ "Microprints"
end
test "renders with page content and legend", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/microprints")
# Verify the page renders expected content (render/1 returns LV inner HTML only)
assert html =~ "Visual fingerprints of source code files"
assert html =~ "Function"
assert html =~ "Module"
assert html =~ "Keyword"
end
end
describe "URL state persistence" do describe "URL state persistence" do
test "expanded path is restored from query param on mount", %{conn: conn} do test "expanded path is restored from query param on mount", %{conn: conn} do
files = MicroprintsLive.scan_source_files() files = MicroprintsLive.scan_source_files()
+3 -1
View File
@@ -153,7 +153,9 @@ defmodule Blogex.Blog do
def _validate_links(posts, blog_id) do def _validate_links(posts, blog_id) do
Enum.each(posts, fn post -> Enum.each(posts, fn post ->
case Blogex.LinkValidator.validate_body(post.body, blog_id, post_id: post.id) do case Blogex.LinkValidator.validate_body(post.body, blog_id, post_id: post.id) do
:ok -> :ok :ok ->
:ok
{:error, errors} -> {:error, errors} ->
raise Blogex.LinkError, raise Blogex.LinkError,
blog: blog_id, blog: blog_id,
-1
View File
@@ -174,7 +174,6 @@ defmodule Blogex.LinkValidator do
end end
end end
# --- Private helpers --- # --- Private helpers ---
@doc false @doc false
+3 -2
View File
@@ -29,7 +29,7 @@ defmodule Blogex.MixProject do
defp deps do defp deps do
[ [
{:nimble_publisher, "~> 1.1"}, {:nimble_publisher, "~> 2.0"},
{:makeup_elixir, ">= 0.0.0"}, {:makeup_elixir, ">= 0.0.0"},
{:makeup_erlang, ">= 0.0.0"}, {:makeup_erlang, ">= 0.0.0"},
{:phoenix, "~> 1.7"}, {:phoenix, "~> 1.7"},
@@ -37,7 +37,8 @@ defmodule Blogex.MixProject do
{:phoenix_live_view, "~> 1.0"}, {:phoenix_live_view, "~> 1.0"},
{:jason, "~> 1.4"}, {:jason, "~> 1.4"},
{:plug, "~> 1.15"}, {:plug, "~> 1.15"},
{:ex_doc, "~> 0.34", only: :dev, runtime: false} {:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:rustler, "~> 0.38.0", optional: true}
] ]
end end
+5 -2
View File
@@ -3,12 +3,13 @@
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"mdex_native": {:hex, :mdex_native, "0.2.4", "a74b9d799d3fc487b1f97b70ee9b04108c7af0514920d56828e173d19312c2a9", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "d71185c43c5209c6939f4b65f903f71c892512c93ea6843171227855d2d976f5"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_publisher": {:hex, :nimble_publisher, "1.1.1", "3ea4d4cfca45b11a5377bce7608367a9ddd7e717a9098161d8439eca23e239aa", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d67e15bddf07e8c60f75849008b78ea8c6b2b4ae8e3f882ccf0a22d57bd42ed0"}, "nimble_publisher": {:hex, :nimble_publisher, "2.0.0", "f4fc100949ccdf1831094ff303b79f27c6f04b2ead829409426ee0d9ed3aa55b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: true]}, {:mdex_native, "~> 0.1", [hex: :mdex_native, repo: "hexpm", optional: false]}], "hexpm", "88997f74b763c24a15d20a90d7cd4ef1fd01172a2b4651e54d8021a44659f2c8"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.27", "9afcab28b0c82afdc51044e661bcd5b8de53d242593d34c964a37710b40a42af", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "415735d0b2c612c9104108b35654e977626a0cb346711e1e4f1ed16e3c827ede"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.27", "9afcab28b0c82afdc51044e661bcd5b8de53d242593d34c964a37710b40a42af", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "415735d0b2c612c9104108b35654e977626a0cb346711e1e4f1ed16e3c827ede"},
@@ -16,6 +17,8 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"rustler": {:hex, :rustler, "0.38.0", "7a8906998ff0d28e3021c0a73264abcda719bda344b2e58307c6805b0f87c9b4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "704c03c1bf66be12b031c5a389347b91c81c5cb819a24b068b0de36fe4a5652a"},
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},