Compare commits
3 Commits
d41faddfca
...
9f37dbbff0
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f37dbbff0 | |||
| df37f15f73 | |||
| 9c43a0a22b |
116
.cursor/debug.log
Normal file
116
.cursor/debug.log
Normal file
@@ -0,0 +1,116 @@
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434706543}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434706552}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434707852}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434707860}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434708466}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434708474}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768434709765}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768434709787}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768434717888}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":201,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768434717903}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434717904}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434717913}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434738084}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434738093}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434739031}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434739040}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434746453}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434746496}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768434748859}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768434748866}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768434773921}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":201,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768434773931}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434773931}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434773940}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768434810105}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768434810119}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768434816383}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":201,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768434816399}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434816400}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434816414}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434944656}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434944756}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434945369}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434945427}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434946108}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434946162}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434946680}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434946736}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768434947640}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768434947656}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768434953064}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":201,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768434953079}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434953080}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768434953093}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435103720}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435103776}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435104593}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435104647}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435105158}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435105253}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435275247}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435275315}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435276178}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435276278}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435276945}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435276998}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768435278150}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768435278162}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768435281966}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":400,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768435281988}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435387623}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435387680}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435388399}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435388454}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/create-form-fragment","targetId":""},"timestamp":1768435389910}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/create-form-fragment","targetId":""},"timestamp":1768435389922}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs","targetId":"tabs-list"},"timestamp":1768435393213}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs","targetId":"tabs-list"},"timestamp":1768435393231}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435393233}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435393245}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/12/content-fragment","targetId":"tab-content"},"timestamp":1768435395729}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/12/content-fragment","targetId":"tab-content"},"timestamp":1768435395748}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/13/content-fragment","targetId":"tab-content"},"timestamp":1768435396771}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/13/content-fragment","targetId":"tab-content"},"timestamp":1768435396788}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/12/content-fragment","targetId":"tab-content"},"timestamp":1768435398656}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/12/content-fragment","targetId":"tab-content"},"timestamp":1768435398674}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/13/content-fragment","targetId":"tab-content"},"timestamp":1768435399748}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/13/content-fragment","targetId":"tab-content"},"timestamp":1768435399774}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435668310}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":false,"hasPaletteContainer":false,"hasLightingController":false},"timestamp":1768435668311}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435668355}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435669841}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":false,"hasPaletteContainer":false,"hasLightingController":false},"timestamp":1768435669842}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435669852}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435672686}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435673713}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435674316}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435674560}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435680419}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":false,"modalActive":null},"timestamp":1768435680897}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435814285}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":true,"hasPaletteContainer":false,"hasLightingController":false},"timestamp":1768435814287}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H3","location":"src/static/color_palette.js:DOMContentLoaded","message":"palette script loaded","data":{"hasButton":true,"hasModal":true,"hasClose":true},"timestamp":1768435814287}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435814350}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435815080}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H3","location":"src/static/color_palette.js:DOMContentLoaded","message":"palette script loaded","data":{"hasButton":true,"hasModal":true,"hasClose":true},"timestamp":1768435815081}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":true,"hasPaletteContainer":false,"hasLightingController":false},"timestamp":1768435815082}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435815135}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435815724}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H3","location":"src/static/color_palette.js:DOMContentLoaded","message":"palette script loaded","data":{"hasButton":true,"hasModal":true,"hasClose":true},"timestamp":1768435815725}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":true,"hasPaletteContainer":false,"hasLightingController":false},"timestamp":1768435815725}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435815778}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H4","location":"src/static/color_palette.js:openModal","message":"palette modal opened","data":{"active":true},"timestamp":1768435817104}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":true,"modalActive":true},"timestamp":1768435817105}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H4","location":"src/static/color_palette.js:closeModal","message":"palette modal closed","data":{"active":false},"timestamp":1768435820180}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435931118}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":true,"hasPaletteContainer":true,"hasLightingController":false},"timestamp":1768435931120}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H3","location":"src/static/color_palette.js:DOMContentLoaded","message":"palette script loaded","data":{"hasButton":true,"hasModal":true,"hasClose":true,"hasContainer":true,"hasAddButton":true},"timestamp":1768435931119}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435931173}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:htmx:beforeRequest","message":"htmx request","data":{"path":"/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435931791}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H1","location":"src/templates/index.html:DOMContentLoaded","message":"palette elements presence","data":{"hasPaletteButton":true,"hasPaletteModal":true,"hasPaletteContainer":true,"hasLightingController":false},"timestamp":1768435931793}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H3","location":"src/static/color_palette.js:DOMContentLoaded","message":"palette script loaded","data":{"hasButton":true,"hasModal":true,"hasClose":true,"hasContainer":true,"hasAddButton":true},"timestamp":1768435931793}
|
||||
{"sessionId":"debug-session","runId":"tabs-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:htmx:afterRequest","message":"htmx response","data":{"status":200,"responseURL":"http://localhost:5000/tabs/list-fragment","targetId":"tabs-list"},"timestamp":1768435931895}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H2","location":"src/templates/index.html:color-palette-btn","message":"color palette button clicked","data":{"hasPaletteModal":true,"modalActive":true},"timestamp":1768435933111}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H4","location":"src/static/color_palette.js:openModal","message":"palette modal opened","data":{"active":true},"timestamp":1768435933110}
|
||||
{"sessionId":"debug-session","runId":"palette-pre-fix","hypothesisId":"H4","location":"src/static/color_palette.js:closeModal","message":"palette modal closed","data":{"active":false},"timestamp":1768435943332}
|
||||
6
Pipfile
6
Pipfile
@@ -7,8 +7,14 @@ name = "pypi"
|
||||
mpremote = "*"
|
||||
pyserial = "*"
|
||||
esptool = "*"
|
||||
pyjwt = "*"
|
||||
watchfiles = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
|
||||
[scripts]
|
||||
web = "python /home/pi/led-controller/tests/web.py"
|
||||
watch = "python -m watchfiles 'python /home/pi/led-controller/tests/web.py' /home/pi/led-controller/src /home/pi/led-controller/tests"
|
||||
|
||||
853
Pipfile.lock
generated
853
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "8b14bb293b7e7117ffc89c2bc92d7aa2290e8f68be7fc0f073f2b3f7f959ef71"
|
||||
"sha256": "24a0e63d49a769fb2bbc35d7d361aeb0c8563f2d65cbeb24acfae9e183d1c0ca"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,152 +16,122 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"argcomplete": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591",
|
||||
"sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf"
|
||||
"sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703",
|
||||
"sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"
|
||||
],
|
||||
"markers": "sys_platform != 'win32'",
|
||||
"version": "==3.6.2"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.12.1"
|
||||
},
|
||||
"bitarray": {
|
||||
"hashes": [
|
||||
"sha256:013ba795deb6c54fdb0e70103fc142f97746074d2f67b4b6a8f67a17f2d03f06",
|
||||
"sha256:01df531279959c95c0eb1eccd3e6121cb241ddcb821594f3eb07a94b086f71a0",
|
||||
"sha256:0330f470bdb76825d760215e01f8d60ce09d4ac84434b364e27236db5657d323",
|
||||
"sha256:05db62a7867702ddc7f4c58ed3804d5aa9cc0cf5ce652f98b30281a2d1174bda",
|
||||
"sha256:0626cfd86070cc71bf089e9c62c27c03ced24d3ebc44ff9b1c6a590991ace74f",
|
||||
"sha256:07d9fa226a06971ca35c720c99666cb8542f0e4d5cf234583e0822b45e68755b",
|
||||
"sha256:0a4bb5dd53250e3c70924fd473034cb2e741027938702d9cc319646e53091dc1",
|
||||
"sha256:0bedc6531388e719d8fa1eb80b1bcf97ccdccedf4a0daa02bc4f81d34a50d309",
|
||||
"sha256:0dd3b351628fe0edf812d8a7e29d2b44ca8a5599d871fadf5cfa5362dbd10689",
|
||||
"sha256:0e942e5ac197e31ce6108ab783cbb177f6372289a5a0c9a84dcd8e3ea1129748",
|
||||
"sha256:0efde6c15876a159733d6d57512fc565581e3bba877ad84508b224758c4bd50f",
|
||||
"sha256:1037ac94bf04f59e0085b24dc8252d663e6e5024af4de1372bc026efa8d4bd01",
|
||||
"sha256:10c1dc463de03521b7a350426449daa1606177e2e15f6874a27b1a7330f42a4f",
|
||||
"sha256:11fcc8e92699a2463055ceab63071ff2179a1f53d1284f4b7b9a405365065efa",
|
||||
"sha256:122d390230075671420845b6d59d01d9e3e5a16f9ad8791a78ef06396a6ca2d7",
|
||||
"sha256:12cc15dba08edb6e80c3a7f43cfaebba98dcbb89b120d534e32a42cd57c5f15f",
|
||||
"sha256:12fb9cdd96d1d1646d4fe67aa21e28d2fcedb431c703d1f37b2221fcb0cfe0c9",
|
||||
"sha256:15197c8a3ec258401f80bbcc64b942d82dfcf3d9549320147aef900c80bdf77b",
|
||||
"sha256:15714d2dc4fe6bb3c93ffa88cab026da993c6bc131c191fb3c59f697847a7621",
|
||||
"sha256:16651c323570bf9ddf43b155aa47d58c0046bd0f98cec3fdd8cfac9a9468da5c",
|
||||
"sha256:171f2fd9f0d3e3e9f08addcd8393677df7e5e55a294c13b5ac16a961870ee647",
|
||||
"sha256:1babc8dba17fad7409ca1cfe6ec4b89d175070f20d2c6f97f87d1c257be4aea9",
|
||||
"sha256:1ed6b53f947ae738258175ecf8f249172a416204f18ce67ba67a3f02dc910703",
|
||||
"sha256:1feb9bf948d075d7599632c14d3a499e31718502355f2ec96690d09ff7f71b8d",
|
||||
"sha256:1fec333d4c744b32396a46378ed42b05ffa90aa62ae99ed799c851e6a2134327",
|
||||
"sha256:20408d8d6eb3ff5ef3c62ece8a785b7cfb1a4be6979ee614eac63d578fe9b303",
|
||||
"sha256:2232724b1b0822ca56a6649769147104306849d2841bba5cdee746c4748ce34b",
|
||||
"sha256:268ec3d5744ced25edfcf65e01ce4b72592b0b587d8919bc409288e97e2831a1",
|
||||
"sha256:27bb390521ba1032b95e31683fa9aed042222fca653760d5101435c2dbf28ede",
|
||||
"sha256:2bcc21447e4a7f132485905b471164d51030da829563512789a7085817545f3b",
|
||||
"sha256:2d0b70cf75f82c919fe486af185895a77644ac3621ea8bd5b5a82fd21c03c843",
|
||||
"sha256:30f8925e9a101843a89e55f527f581b9da34bd97a697c063754be0b681c49694",
|
||||
"sha256:332a373fe20fdba78968bbeeb1aa01f2d861a30d938bb986e7101246cf371500",
|
||||
"sha256:336369abb755401278221577f83963d4522a0454de4bbf3cc913d24855d8a47e",
|
||||
"sha256:33eee090eade2c8303bfc01a9e104fea306d330035b18b5c50a04cb0cb76f08d",
|
||||
"sha256:340dd788dad07ad004b591925e4b906786aaefb6632ea9d9ac616913f3cafa4e",
|
||||
"sha256:341104cb87536114dc30728231427a335db4f90ea7e9ab94d8b1a94ff253624f",
|
||||
"sha256:35c4d724b79a3a0878999dff799d477c7eb771fd96695cf9bc5aec8aa4d956a2",
|
||||
"sha256:36efcce5a4b18c0920d623c99fa16d2fd1bd2315e666f829d50c1a6fa1d6891a",
|
||||
"sha256:3b6dede8d214301d2e0133c76cb59c713ca9dfbadd039165dfde181f4aa8f6b7",
|
||||
"sha256:3d089a0570e2acfabac9dd40ee7bfbc36ec48ff73c9312f3e61ebf31b315d05d",
|
||||
"sha256:3edbc614128b1f054584924e330f04f083f2e2bbb1bb6df1559a85cca490d408",
|
||||
"sha256:4292ef2a67ff6a3811e018c7e32c3ce4fb74c2f5c85257c06222895138df86f4",
|
||||
"sha256:437d983fb4b34874faa2c6a0247be770ec3935b4cedc16f65f8a4cbf8c970f03",
|
||||
"sha256:4616a3318ce687b0bda0b3f09f9b074f521ebf6f6fd527c1ff96619390a3f3e2",
|
||||
"sha256:49a41a724693b9f15ac965f548c2f68f6ff7b0ab36a29009d82e99f7d402888b",
|
||||
"sha256:4aa615307dbe11f8776af6a01a056ac6851ab200ef7a4ab49140bfac2fada5e4",
|
||||
"sha256:4db7cd3926ba52c5f4d4e76f9813ccc89b8c2cb077fccac0b86289f5db9b3710",
|
||||
"sha256:4fddc0366431da5b6c0f5527e8e7093680f76706420acdfb460be4a6cfb03197",
|
||||
"sha256:51126ccfb42d32da59f8eccaba2952e6091be534d45f594ea2b60a9c621734f5",
|
||||
"sha256:51e27ac27a6c85eb4f970e71134c0dc60b753ec9d18e2aeac5b3a5b31ea0847d",
|
||||
"sha256:5776328e0630af51e11c6dcf44490ef8c4b4f862e88ca48cb619ef65d20e6b67",
|
||||
"sha256:5811b9aeacc8c2b62ed0732649600405a7df5bf28eb7b7475f56822f702fc718",
|
||||
"sha256:5af9d61f383335a52c28cac82b2a06ecf7ad72bb6a7e90711cb7534ce8c5fa07",
|
||||
"sha256:5c62c2ae324c486f8e8f0482d5a8635e255da5302c44e7a5df83eee7d87e28ec",
|
||||
"sha256:5cfd6bbd0bdd47335828a8269ce66729dff6125b4f92642f465f9924f22f4fba",
|
||||
"sha256:5e5fac8a9d1140ae55858f13914574ca63b48f968e424a02d918e46602569c02",
|
||||
"sha256:64fb1eef44861fa301f393f8b4a6606c6b030db152b8aeaa6e5370c75887c1b6",
|
||||
"sha256:6799c9002876eae5e88a48220638144d80cc3e754dfae7333482b2b185f9bc31",
|
||||
"sha256:6d39ea865f22c14f7adf44e645ff71d459b3e9588c58c71ef7b8ce488b90b29c",
|
||||
"sha256:6d4564cc36b12e8ba5d40c8fde9978012dfe912d038343c12a01b88df8ca90a1",
|
||||
"sha256:6e7274cdfe405c4e70a585b997d3a8c001425c03fa37d09a8e5460828a3d8bd6",
|
||||
"sha256:6e812aabec2438a50c58c15755db8a9f7679b604a9d3992903f9a29a9da37356",
|
||||
"sha256:70a01907ceebadbd6082b971d57d80e5af97667a8a45938a46ae23df42589c9b",
|
||||
"sha256:7201d4574f70a0f563a27e12028e39231bde277f8a2768a2c530c4f82f95b3b8",
|
||||
"sha256:722c105dd4229b91d17804a0855e8f27519ceee99d8fd4db80bf09b507d7fb60",
|
||||
"sha256:75860edbfca1550b66105e59ec06a6437935283ee9849d0b2a302f73e1260dad",
|
||||
"sha256:75df7335ed7324a1ee9002d747c36a37de42b6469601ac39fef00c6bd80a4cb4",
|
||||
"sha256:77fd3e9c576f3952870b527c3a42795108946862ec11a3b17a723939dae76a12",
|
||||
"sha256:788947fd11fad912ad17b1dee810e142c63b509a4a7b0211a44549a94baba593",
|
||||
"sha256:78a8dd296c1ee30f9e3d0fe48d0168d89f99133d797f7fc2b1993bab1b23c21f",
|
||||
"sha256:7d22ed65d138ddaf63c743a68869e78eac6a7b7632ab97ccfbf0fd96ebc43001",
|
||||
"sha256:823decea26d8be2ec46000583114d050d02033f99e54e3285c0a80f31e3d7784",
|
||||
"sha256:83e008a8d7c115926717d826cbd3bc5e35816c63feff43da9456d09df9842743",
|
||||
"sha256:8733f59c5f07f043d7e9936201fd0674e591244520c8725f0061a8f7dcd71cc1",
|
||||
"sha256:87e7a438f6a806a3f508f7d9165e3b6162bfbe0351ce1b4a954f9fb76155ea9d",
|
||||
"sha256:8ab8dc6c2e55ad9bc918672f258ac3a1251fc6621dbe4e3edfc4617b91ad6eb9",
|
||||
"sha256:8f02850724d3e6c57265246329eeb71893a4a6884521b7f18fc5d9ea467300fe",
|
||||
"sha256:8f2c1c3d1d0109b993791755f18d4b495f02744118f8f683eed982b9c8ed8687",
|
||||
"sha256:8f9a4d5595c69b65d4198d952c820e48563d66d3e17aaa46645de1196f5ff12b",
|
||||
"sha256:9007e6b0a9580eaf2826b2019b7d799ea94249fd167ddd3fde1b6b39f5bff390",
|
||||
"sha256:93b88fa7bd0f958d7172862dcbebbe7c96eff90f989c6ddd28ec6e28bbe3f768",
|
||||
"sha256:957524b4f2185bde3e1e0408320ded62de8fb2c4646d3bc74b4c8365cbf9eb50",
|
||||
"sha256:95e3cb8e64672a977b3b0cc9c0f92b6d398b4aa89c96e84a92688efd312bef2e",
|
||||
"sha256:95efab4a2fc0f9329332e62e22c505ae8d355991c1c58d4d37d05ae4a6e65b01",
|
||||
"sha256:99b35f36d70d033d6548a3e90c57fccde197190606de9466869706454993bbc5",
|
||||
"sha256:9a57105bd2100c2a98cee8fca7cab26a1c0c1f0926b0ae78bc9cc9715c2d83e9",
|
||||
"sha256:9be0b089fd1cb7d5c88ff0277585df141471ff7ee8ed0bf9b386f23d5fde5573",
|
||||
"sha256:a2115dce1756537acd870ecdb9ec96ff3ace1f4de470ffc9112c0214fae6aef4",
|
||||
"sha256:a27456e66fae5726b2b1b9bc3ee0e2f1235bf8a353dc216d2651ad0652596657",
|
||||
"sha256:a2be3bf9a5230fe6c3c99eab4a7a015f137da4b795e8d7e9f641171ba65398fc",
|
||||
"sha256:a7eaed4731bd84504176ba5e0af3eba7a6e66afe208d5efb6a8779b66ecd51aa",
|
||||
"sha256:ae10e24915c7d84f5edb39d5385455b961c66e90a40b786cfcfba59f8399999e",
|
||||
"sha256:b0f896a433075ab422266ace595f12c49c417ec8bacfa85ae453b45a8694de14",
|
||||
"sha256:b1f0fa69cb879924391bd7751cc8135602d03b6cdac6dbc830ab60164d70e5a0",
|
||||
"sha256:b238e48844645ac397cfc67f5c8df86d640a9b33063c82ca2393a39e48b01c15",
|
||||
"sha256:b2c3541aab6ac40d9090cb52788541bcdc06524d2cdba18f9cc9cbd1edafd093",
|
||||
"sha256:b38516170daa962e342d54b1677d81c32826f9e94c21856e879b46b6e2008293",
|
||||
"sha256:bb04ec25c55614d60bbe1591a59ff29149030010ee3e1262c16a43460b193cfd",
|
||||
"sha256:bb1dcea97241397517cb52dc2646648d33a291534d8b1a8f7039d5583ef6e5f5",
|
||||
"sha256:bbe0786261a3d7c9214a8c348edc64a75c70ca4eab3164b1ec97aa10c0f0855d",
|
||||
"sha256:bcfd19c1b3c56dfde1b524e54b0f1d73a439c488dbeb40ff677b9e0a339e2356",
|
||||
"sha256:c217dc3559b21bcfc58ea37af11f7a25e8b1f71d855992bf3453b9c1ae6c02a0",
|
||||
"sha256:c233e308fd055fff62679e6a72744cf3b51621a6c18930bbcbe78649bc97f481",
|
||||
"sha256:c2984abfc4e6281e703675280edbcf7618fa6983367d1fb4822b41917e2c3490",
|
||||
"sha256:c7576226ee79957e8a4ff64addf2929cbbf2bf749ec622f325adc615d8470b14",
|
||||
"sha256:c85f235dc643857e2c8e0a93e91f1099dc56db2b4bebd160c08cae8d5ddaec21",
|
||||
"sha256:c991ba157475c196bdf9c2368d5159208dbdf23d715de8e8d94b8c1cb739bddb",
|
||||
"sha256:cf924be7b97cc5bec88bf63c09732aa5c90bd00f3152cffebed259a49df351b0",
|
||||
"sha256:d2c567ce44f9d821776682ece59237b5761443121137afa656b9f586157176af",
|
||||
"sha256:d3c0db664bffeb4bb80b228ed31773ccb701da11f266f9d8a56732e083e2cab0",
|
||||
"sha256:d4755823c68ff36b3defb2079258279fb0fb3a2d19dbd1401a28672a10f26ae0",
|
||||
"sha256:d47810266e7c5b74fe61e983cf7e6e3937120473cba6f6e1ecdc411e6f27b639",
|
||||
"sha256:d5349869fa33bd28f362f8702cc1bb1a4ec1c7cde7183cdc41933eb794c3d651",
|
||||
"sha256:d5c10255889045479b86405dd040c58e77ccf4f63a0e6e686d341b5fd8fa32c2",
|
||||
"sha256:d62db2fbf0a923ecbf5b71babc9deabd5ccea74d275bf74a5e37c050238d8f6a",
|
||||
"sha256:d6895389eeebf6836cfad1b301bae9e5386e3b94a21076aaf0c2dab0524af6d1",
|
||||
"sha256:d9ce8cb9161288288859032227039197f4ce6cbf1ff5f022b060216e49f8b591",
|
||||
"sha256:de59a5a4a54fbdb00c716a8a54935bbe19248c99647c64964b11f2f787a67cec",
|
||||
"sha256:e11ea9eabf6c886811bdc0bf4ea3350a07c7354c8abc6b6a8bd707eef687bcf5",
|
||||
"sha256:e1652bf956c8874c790fe78f0dcdc0de04d82ded81373759bfc05f427afd1ff3",
|
||||
"sha256:e264ad4a850bd1488a754b5e812d09d74fede57bc5ae679a4316ff08aa8edcaa",
|
||||
"sha256:e4e1437bda368e46871d7fbc8e1de4260c302ff22cfb734e1907f164933c7fdf",
|
||||
"sha256:e66f6fd558abf563eb68580a3961e47f7c843c2fd80be931027bc484f933b4b7",
|
||||
"sha256:e8fba33d97fd6b682c9b9107ea1f652485f3149af5aa3b6c8698034007f4adf5",
|
||||
"sha256:ef3f2dc1a95bec2af77c8685c847d41fc0c64d7329c994b6054c54462f835401",
|
||||
"sha256:f0e9aa0722b339f971e0f55b3c418d825d1ab7ecd71c2b10115897a3a39352d3",
|
||||
"sha256:f455c100df47295ca19eb36527462fecbb2710140d92a61228df4cfdd2d7dd81",
|
||||
"sha256:f4c445ed8e059a3cb6c72e4ff68b947d050791f4007a2b6e68afe12b1b2852ab",
|
||||
"sha256:f55b48bccd4f5c97401e18e9ad4483ccc4fea2d8feded13eee4a05d6850f90cf",
|
||||
"sha256:f661ed4bda9a82874cda44829dab0ef604090b8b2f8e9d1759766ffa51f1d6fe",
|
||||
"sha256:fbbc606b8bf3578356d93db02d071824f66bb7f18e5aa57aa4d74fcd6898d87c",
|
||||
"sha256:fbcf4b12fa21df99d9a5855aa52e1ec9ab0e42735d9d0f003d0b737c62522e69",
|
||||
"sha256:fc534fc485fae64ab6a5b21b26815a3dec4e57f0dc373dd3c44242e89437f3ed"
|
||||
"sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199",
|
||||
"sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a",
|
||||
"sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e",
|
||||
"sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3",
|
||||
"sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e",
|
||||
"sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4",
|
||||
"sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861",
|
||||
"sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5",
|
||||
"sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521",
|
||||
"sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d",
|
||||
"sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55",
|
||||
"sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9",
|
||||
"sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce",
|
||||
"sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a",
|
||||
"sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9",
|
||||
"sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e",
|
||||
"sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b",
|
||||
"sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30",
|
||||
"sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6",
|
||||
"sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80",
|
||||
"sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11",
|
||||
"sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f",
|
||||
"sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25",
|
||||
"sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77",
|
||||
"sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0",
|
||||
"sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125",
|
||||
"sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671",
|
||||
"sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de",
|
||||
"sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860",
|
||||
"sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe",
|
||||
"sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d",
|
||||
"sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc",
|
||||
"sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df",
|
||||
"sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a",
|
||||
"sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8",
|
||||
"sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9",
|
||||
"sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe",
|
||||
"sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607",
|
||||
"sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf",
|
||||
"sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee",
|
||||
"sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa",
|
||||
"sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954",
|
||||
"sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a",
|
||||
"sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d",
|
||||
"sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428",
|
||||
"sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e",
|
||||
"sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54",
|
||||
"sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5",
|
||||
"sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24",
|
||||
"sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f",
|
||||
"sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a",
|
||||
"sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b",
|
||||
"sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70",
|
||||
"sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7",
|
||||
"sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a",
|
||||
"sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada",
|
||||
"sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541",
|
||||
"sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b",
|
||||
"sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4",
|
||||
"sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2",
|
||||
"sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd",
|
||||
"sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646",
|
||||
"sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89",
|
||||
"sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa",
|
||||
"sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd",
|
||||
"sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1",
|
||||
"sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb",
|
||||
"sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220",
|
||||
"sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c",
|
||||
"sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310",
|
||||
"sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2",
|
||||
"sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e",
|
||||
"sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c",
|
||||
"sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a",
|
||||
"sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a",
|
||||
"sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594",
|
||||
"sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8",
|
||||
"sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52",
|
||||
"sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20",
|
||||
"sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8",
|
||||
"sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6",
|
||||
"sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9",
|
||||
"sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02",
|
||||
"sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425",
|
||||
"sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d",
|
||||
"sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2",
|
||||
"sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7",
|
||||
"sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4",
|
||||
"sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096",
|
||||
"sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d",
|
||||
"sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149",
|
||||
"sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b",
|
||||
"sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35",
|
||||
"sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773",
|
||||
"sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d",
|
||||
"sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6",
|
||||
"sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741",
|
||||
"sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f",
|
||||
"sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8",
|
||||
"sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f",
|
||||
"sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148",
|
||||
"sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8",
|
||||
"sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97",
|
||||
"sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"
|
||||
],
|
||||
"version": "==3.4.0"
|
||||
"version": "==3.8.0"
|
||||
},
|
||||
"bitstring": {
|
||||
"hashes": [
|
||||
@@ -173,135 +143,176 @@
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8",
|
||||
"sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2",
|
||||
"sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1",
|
||||
"sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15",
|
||||
"sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36",
|
||||
"sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824",
|
||||
"sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8",
|
||||
"sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36",
|
||||
"sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17",
|
||||
"sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf",
|
||||
"sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc",
|
||||
"sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3",
|
||||
"sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed",
|
||||
"sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702",
|
||||
"sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1",
|
||||
"sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8",
|
||||
"sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903",
|
||||
"sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6",
|
||||
"sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d",
|
||||
"sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b",
|
||||
"sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e",
|
||||
"sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be",
|
||||
"sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c",
|
||||
"sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683",
|
||||
"sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9",
|
||||
"sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c",
|
||||
"sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8",
|
||||
"sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1",
|
||||
"sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4",
|
||||
"sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655",
|
||||
"sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67",
|
||||
"sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595",
|
||||
"sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0",
|
||||
"sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65",
|
||||
"sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41",
|
||||
"sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6",
|
||||
"sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401",
|
||||
"sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6",
|
||||
"sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3",
|
||||
"sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16",
|
||||
"sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93",
|
||||
"sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e",
|
||||
"sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4",
|
||||
"sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964",
|
||||
"sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c",
|
||||
"sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576",
|
||||
"sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0",
|
||||
"sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3",
|
||||
"sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662",
|
||||
"sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3",
|
||||
"sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff",
|
||||
"sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5",
|
||||
"sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd",
|
||||
"sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f",
|
||||
"sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5",
|
||||
"sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14",
|
||||
"sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d",
|
||||
"sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9",
|
||||
"sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7",
|
||||
"sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382",
|
||||
"sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a",
|
||||
"sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e",
|
||||
"sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a",
|
||||
"sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4",
|
||||
"sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99",
|
||||
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
|
||||
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
|
||||
"sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb",
|
||||
"sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b",
|
||||
"sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f",
|
||||
"sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9",
|
||||
"sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44",
|
||||
"sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2",
|
||||
"sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c",
|
||||
"sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75",
|
||||
"sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65",
|
||||
"sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e",
|
||||
"sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a",
|
||||
"sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e",
|
||||
"sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25",
|
||||
"sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a",
|
||||
"sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe",
|
||||
"sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b",
|
||||
"sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91",
|
||||
"sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592",
|
||||
"sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187",
|
||||
"sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c",
|
||||
"sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1",
|
||||
"sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94",
|
||||
"sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba",
|
||||
"sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb",
|
||||
"sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165",
|
||||
"sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529",
|
||||
"sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca",
|
||||
"sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c",
|
||||
"sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6",
|
||||
"sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c",
|
||||
"sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0",
|
||||
"sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743",
|
||||
"sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63",
|
||||
"sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5",
|
||||
"sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5",
|
||||
"sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4",
|
||||
"sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d",
|
||||
"sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b",
|
||||
"sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93",
|
||||
"sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205",
|
||||
"sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27",
|
||||
"sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512",
|
||||
"sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d",
|
||||
"sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c",
|
||||
"sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037",
|
||||
"sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26",
|
||||
"sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322",
|
||||
"sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb",
|
||||
"sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c",
|
||||
"sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8",
|
||||
"sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4",
|
||||
"sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414",
|
||||
"sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9",
|
||||
"sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664",
|
||||
"sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9",
|
||||
"sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775",
|
||||
"sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739",
|
||||
"sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc",
|
||||
"sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062",
|
||||
"sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe",
|
||||
"sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9",
|
||||
"sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92",
|
||||
"sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5",
|
||||
"sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13",
|
||||
"sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d",
|
||||
"sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26",
|
||||
"sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f",
|
||||
"sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495",
|
||||
"sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b",
|
||||
"sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6",
|
||||
"sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c",
|
||||
"sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef",
|
||||
"sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5",
|
||||
"sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18",
|
||||
"sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad",
|
||||
"sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3",
|
||||
"sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7",
|
||||
"sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5",
|
||||
"sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534",
|
||||
"sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49",
|
||||
"sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2",
|
||||
"sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5",
|
||||
"sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453",
|
||||
"sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"
|
||||
],
|
||||
"markers": "platform_python_implementation != 'PyPy'",
|
||||
"version": "==1.17.1"
|
||||
"markers": "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a",
|
||||
"sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==8.3.1"
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259",
|
||||
"sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43",
|
||||
"sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645",
|
||||
"sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8",
|
||||
"sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44",
|
||||
"sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d",
|
||||
"sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f",
|
||||
"sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d",
|
||||
"sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54",
|
||||
"sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9",
|
||||
"sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137",
|
||||
"sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f",
|
||||
"sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c",
|
||||
"sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334",
|
||||
"sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c",
|
||||
"sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b",
|
||||
"sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2",
|
||||
"sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375",
|
||||
"sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88",
|
||||
"sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5",
|
||||
"sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647",
|
||||
"sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c",
|
||||
"sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359",
|
||||
"sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5",
|
||||
"sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d",
|
||||
"sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028",
|
||||
"sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01",
|
||||
"sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904",
|
||||
"sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d",
|
||||
"sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93",
|
||||
"sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06",
|
||||
"sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff",
|
||||
"sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76",
|
||||
"sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff",
|
||||
"sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759",
|
||||
"sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4",
|
||||
"sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053"
|
||||
"sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217",
|
||||
"sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d",
|
||||
"sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc",
|
||||
"sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71",
|
||||
"sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971",
|
||||
"sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a",
|
||||
"sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926",
|
||||
"sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc",
|
||||
"sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d",
|
||||
"sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b",
|
||||
"sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20",
|
||||
"sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044",
|
||||
"sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3",
|
||||
"sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715",
|
||||
"sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4",
|
||||
"sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506",
|
||||
"sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f",
|
||||
"sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0",
|
||||
"sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683",
|
||||
"sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3",
|
||||
"sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21",
|
||||
"sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91",
|
||||
"sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c",
|
||||
"sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8",
|
||||
"sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df",
|
||||
"sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c",
|
||||
"sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb",
|
||||
"sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7",
|
||||
"sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04",
|
||||
"sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db",
|
||||
"sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459",
|
||||
"sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea",
|
||||
"sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914",
|
||||
"sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717",
|
||||
"sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9",
|
||||
"sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac",
|
||||
"sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32",
|
||||
"sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec",
|
||||
"sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1",
|
||||
"sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb",
|
||||
"sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac",
|
||||
"sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665",
|
||||
"sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e",
|
||||
"sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb",
|
||||
"sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5",
|
||||
"sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936",
|
||||
"sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de",
|
||||
"sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372",
|
||||
"sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54",
|
||||
"sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422",
|
||||
"sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849",
|
||||
"sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c",
|
||||
"sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963",
|
||||
"sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
|
||||
"version": "==44.0.3"
|
||||
},
|
||||
"ecdsa": {
|
||||
"hashes": [
|
||||
"sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3",
|
||||
"sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==0.19.1"
|
||||
"markers": "python_version >= '3.8' and python_full_version not in '3.9.0, 3.9.1'",
|
||||
"version": "==46.0.3"
|
||||
},
|
||||
"esptool": {
|
||||
"hashes": [
|
||||
"sha256:dc4ef26b659e1a8dcb019147c0ea6d94980b34de99fbe09121c7941c8b254531"
|
||||
"sha256:2ea9bcd7eb263d380a4fe0170856a10e4c65e3f38c757ebdc73584c8dd8322da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.8.1"
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.11"
|
||||
},
|
||||
"intelhex": {
|
||||
"hashes": [
|
||||
@@ -310,22 +321,61 @@
|
||||
],
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"markdown-it-py": {
|
||||
"hashes": [
|
||||
"sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147",
|
||||
"sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"mdurl": {
|
||||
"hashes": [
|
||||
"sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8",
|
||||
"sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.1.2"
|
||||
},
|
||||
"mpremote": {
|
||||
"hashes": [
|
||||
"sha256:42691ff8f7ea4b5f2fc1b51de99609995d383671a4b4d4daad8cbd486d26aa23",
|
||||
"sha256:d0dcd8ab364d87270e1766308882e536e541052efd64aadaac83bc7ebbea2815"
|
||||
"sha256:11d134c69b21b487dae3d03eed54c8ccbf84c916c8732a3e069a97cae47be3d4",
|
||||
"sha256:6bb75774648091dad6833af4f86c5bf6505f8d7aec211380f9e6996c01d23cb5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==1.25.0"
|
||||
"version": "==1.27.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda",
|
||||
"sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==4.5.1"
|
||||
},
|
||||
"pycparser": {
|
||||
"hashes": [
|
||||
"sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6",
|
||||
"sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"
|
||||
"sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2",
|
||||
"sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"
|
||||
],
|
||||
"markers": "implementation_name != 'PyPy'",
|
||||
"version": "==2.23"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
|
||||
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.22"
|
||||
"version": "==2.19.2"
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953",
|
||||
"sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10.1"
|
||||
},
|
||||
"pyserial": {
|
||||
"hashes": [
|
||||
@@ -337,62 +387,82 @@
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff",
|
||||
"sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48",
|
||||
"sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086",
|
||||
"sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e",
|
||||
"sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133",
|
||||
"sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5",
|
||||
"sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484",
|
||||
"sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee",
|
||||
"sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5",
|
||||
"sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68",
|
||||
"sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a",
|
||||
"sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf",
|
||||
"sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99",
|
||||
"sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8",
|
||||
"sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85",
|
||||
"sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19",
|
||||
"sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc",
|
||||
"sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a",
|
||||
"sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1",
|
||||
"sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317",
|
||||
"sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c",
|
||||
"sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631",
|
||||
"sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d",
|
||||
"sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652",
|
||||
"sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5",
|
||||
"sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e",
|
||||
"sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b",
|
||||
"sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8",
|
||||
"sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476",
|
||||
"sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706",
|
||||
"sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563",
|
||||
"sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237",
|
||||
"sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b",
|
||||
"sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083",
|
||||
"sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180",
|
||||
"sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425",
|
||||
"sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e",
|
||||
"sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f",
|
||||
"sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725",
|
||||
"sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183",
|
||||
"sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab",
|
||||
"sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774",
|
||||
"sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725",
|
||||
"sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e",
|
||||
"sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5",
|
||||
"sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d",
|
||||
"sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290",
|
||||
"sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44",
|
||||
"sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed",
|
||||
"sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4",
|
||||
"sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba",
|
||||
"sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12",
|
||||
"sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"
|
||||
"sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c",
|
||||
"sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a",
|
||||
"sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3",
|
||||
"sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956",
|
||||
"sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6",
|
||||
"sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c",
|
||||
"sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65",
|
||||
"sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a",
|
||||
"sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0",
|
||||
"sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b",
|
||||
"sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1",
|
||||
"sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6",
|
||||
"sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7",
|
||||
"sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e",
|
||||
"sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007",
|
||||
"sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310",
|
||||
"sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4",
|
||||
"sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9",
|
||||
"sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295",
|
||||
"sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea",
|
||||
"sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0",
|
||||
"sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e",
|
||||
"sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac",
|
||||
"sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9",
|
||||
"sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7",
|
||||
"sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35",
|
||||
"sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb",
|
||||
"sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b",
|
||||
"sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69",
|
||||
"sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5",
|
||||
"sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b",
|
||||
"sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c",
|
||||
"sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369",
|
||||
"sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd",
|
||||
"sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824",
|
||||
"sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198",
|
||||
"sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065",
|
||||
"sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c",
|
||||
"sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c",
|
||||
"sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764",
|
||||
"sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196",
|
||||
"sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b",
|
||||
"sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00",
|
||||
"sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac",
|
||||
"sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8",
|
||||
"sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e",
|
||||
"sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28",
|
||||
"sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3",
|
||||
"sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5",
|
||||
"sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4",
|
||||
"sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b",
|
||||
"sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf",
|
||||
"sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5",
|
||||
"sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702",
|
||||
"sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8",
|
||||
"sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788",
|
||||
"sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da",
|
||||
"sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d",
|
||||
"sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc",
|
||||
"sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c",
|
||||
"sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba",
|
||||
"sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f",
|
||||
"sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917",
|
||||
"sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5",
|
||||
"sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26",
|
||||
"sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f",
|
||||
"sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b",
|
||||
"sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be",
|
||||
"sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c",
|
||||
"sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3",
|
||||
"sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6",
|
||||
"sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926",
|
||||
"sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==6.0.2"
|
||||
"version": "==6.0.3"
|
||||
},
|
||||
"reedsolo": {
|
||||
"hashes": [
|
||||
@@ -401,13 +471,144 @@
|
||||
],
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"six": {
|
||||
"rich": {
|
||||
"hashes": [
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
"sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4",
|
||||
"sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"version": "==1.17.0"
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==14.2.0"
|
||||
},
|
||||
"rich-click": {
|
||||
"hashes": [
|
||||
"sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6",
|
||||
"sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.9.5"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||
],
|
||||
"markers": "python_version < '3.13'",
|
||||
"version": "==4.15.0"
|
||||
},
|
||||
"watchfiles": {
|
||||
"hashes": [
|
||||
"sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c",
|
||||
"sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43",
|
||||
"sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510",
|
||||
"sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0",
|
||||
"sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2",
|
||||
"sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b",
|
||||
"sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18",
|
||||
"sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219",
|
||||
"sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3",
|
||||
"sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4",
|
||||
"sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803",
|
||||
"sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94",
|
||||
"sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6",
|
||||
"sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce",
|
||||
"sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099",
|
||||
"sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae",
|
||||
"sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4",
|
||||
"sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43",
|
||||
"sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd",
|
||||
"sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10",
|
||||
"sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374",
|
||||
"sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051",
|
||||
"sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d",
|
||||
"sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34",
|
||||
"sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49",
|
||||
"sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7",
|
||||
"sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844",
|
||||
"sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77",
|
||||
"sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b",
|
||||
"sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741",
|
||||
"sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e",
|
||||
"sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33",
|
||||
"sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42",
|
||||
"sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab",
|
||||
"sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc",
|
||||
"sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5",
|
||||
"sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da",
|
||||
"sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e",
|
||||
"sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05",
|
||||
"sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a",
|
||||
"sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d",
|
||||
"sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701",
|
||||
"sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863",
|
||||
"sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2",
|
||||
"sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101",
|
||||
"sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02",
|
||||
"sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b",
|
||||
"sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6",
|
||||
"sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb",
|
||||
"sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620",
|
||||
"sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957",
|
||||
"sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6",
|
||||
"sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d",
|
||||
"sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956",
|
||||
"sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef",
|
||||
"sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261",
|
||||
"sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02",
|
||||
"sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af",
|
||||
"sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9",
|
||||
"sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21",
|
||||
"sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336",
|
||||
"sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d",
|
||||
"sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c",
|
||||
"sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31",
|
||||
"sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81",
|
||||
"sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9",
|
||||
"sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff",
|
||||
"sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2",
|
||||
"sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e",
|
||||
"sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc",
|
||||
"sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404",
|
||||
"sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01",
|
||||
"sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18",
|
||||
"sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3",
|
||||
"sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606",
|
||||
"sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04",
|
||||
"sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3",
|
||||
"sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14",
|
||||
"sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c",
|
||||
"sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82",
|
||||
"sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610",
|
||||
"sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0",
|
||||
"sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150",
|
||||
"sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5",
|
||||
"sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c",
|
||||
"sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a",
|
||||
"sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b",
|
||||
"sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d",
|
||||
"sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70",
|
||||
"sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70",
|
||||
"sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f",
|
||||
"sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24",
|
||||
"sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e",
|
||||
"sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be",
|
||||
"sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5",
|
||||
"sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e",
|
||||
"sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f",
|
||||
"sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88",
|
||||
"sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb",
|
||||
"sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849",
|
||||
"sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d",
|
||||
"sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c",
|
||||
"sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44",
|
||||
"sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac",
|
||||
"sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428",
|
||||
"sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b",
|
||||
"sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5",
|
||||
"sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa",
|
||||
"sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
||||
2
clear-debug-log.sh
Executable file
2
clear-debug-log.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env sh
|
||||
rm -f /home/pi/led-controller/.cursor/debug.log
|
||||
17
db/group.json
Normal file
17
db/group.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"1": {
|
||||
"name": "Main Group",
|
||||
"devices": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"name": "Accent Group",
|
||||
"devices": [
|
||||
"4",
|
||||
"5"
|
||||
]
|
||||
}
|
||||
}
|
||||
39
db/palette.json
Normal file
39
db/palette.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"1": {
|
||||
"name": "Default Colors",
|
||||
"colors": [
|
||||
"#FF0000",
|
||||
"#00FF00",
|
||||
"#0000FF",
|
||||
"#FFFF00",
|
||||
"#FF00FF",
|
||||
"#00FFFF",
|
||||
"#FFFFFF",
|
||||
"#000000",
|
||||
"#FFA500",
|
||||
"#800080"
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"name": "Warm Colors",
|
||||
"colors": [
|
||||
"#FF6B6B",
|
||||
"#FF8E53",
|
||||
"#FFA07A",
|
||||
"#FFD700",
|
||||
"#FFA500",
|
||||
"#FF6347"
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"name": "Cool Colors",
|
||||
"colors": [
|
||||
"#4ECDC4",
|
||||
"#44A08D",
|
||||
"#96CEB4",
|
||||
"#A8E6CF",
|
||||
"#5F9EA0",
|
||||
"#4682B4"
|
||||
]
|
||||
}
|
||||
}
|
||||
46
db/pattern.json
Normal file
46
db/pattern.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"on": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"off": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"rainbow": {
|
||||
"Step Rate": "n1",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"transition": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"chase": {
|
||||
"Colour 1 Length": "n1",
|
||||
"Colour 2 Length": "n2",
|
||||
"Step 1": "n3",
|
||||
"Step 2": "n4",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"pulse": {
|
||||
"Attack": "n1",
|
||||
"Hold": "n2",
|
||||
"Decay": "n3",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"circle": {
|
||||
"Head Rate": "n1",
|
||||
"Max Length": "n2",
|
||||
"Tail Rate": "n3",
|
||||
"Min Length": "n4",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"blink": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
}
|
||||
}
|
||||
57
db/preset.json
Normal file
57
db/preset.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"1": {
|
||||
"name": "Warm White",
|
||||
"pattern": "on",
|
||||
"colors": [
|
||||
"#FFE5B4",
|
||||
"#FFDAB9",
|
||||
"#FFE4B5"
|
||||
],
|
||||
"brightness": 200,
|
||||
"delay": 100,
|
||||
"n1": 10,
|
||||
"n2": 10,
|
||||
"n3": 10,
|
||||
"n4": 10,
|
||||
"n5": 0,
|
||||
"n6": 0
|
||||
},
|
||||
"2": {
|
||||
"name": "Rainbow",
|
||||
"pattern": "rainbow",
|
||||
"colors": [
|
||||
"#FF0000",
|
||||
"#FF7F00",
|
||||
"#FFFF00",
|
||||
"#00FF00",
|
||||
"#0000FF",
|
||||
"#4B0082",
|
||||
"#9400D3"
|
||||
],
|
||||
"brightness": 255,
|
||||
"delay": 50,
|
||||
"n1": 20,
|
||||
"n2": 15,
|
||||
"n3": 10,
|
||||
"n4": 5,
|
||||
"n5": 0,
|
||||
"n6": 0
|
||||
},
|
||||
"3": {
|
||||
"name": "Pulse Red",
|
||||
"pattern": "pulse",
|
||||
"colors": [
|
||||
"#FF0000",
|
||||
"#CC0000",
|
||||
"#990000"
|
||||
],
|
||||
"brightness": 180,
|
||||
"delay": 200,
|
||||
"n1": 30,
|
||||
"n2": 20,
|
||||
"n3": 10,
|
||||
"n4": 5,
|
||||
"n5": 0,
|
||||
"n6": 0
|
||||
}
|
||||
}
|
||||
1
db/profile.json
Normal file
1
db/profile.json
Normal file
@@ -0,0 +1 @@
|
||||
{"1": {"name": "Default", "tabs": ["1", "2"], "scenes": ["1", "2"], "palette": ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FFFFFF", "#000000"]}, "2": {"name": "test", "type": "tabs", "tabs": ["12", "13"], "scenes": [], "palette": ["#b93c3c", "#3cb961"], "color_palette": ["#b93c3c", "#3cb961"]}}
|
||||
22
db/scene.json
Normal file
22
db/scene.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"1": {
|
||||
"name": "Default Scene",
|
||||
"sequences": [
|
||||
"1"
|
||||
],
|
||||
"groups": [
|
||||
"1"
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"name": "Party Mode",
|
||||
"sequences": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"groups": [
|
||||
"1",
|
||||
"2"
|
||||
]
|
||||
}
|
||||
}
|
||||
30
db/sequence.json
Normal file
30
db/sequence.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"1": {
|
||||
"group_name": "Main Group",
|
||||
"presets": [
|
||||
"1",
|
||||
"2"
|
||||
],
|
||||
"sequence_duration": 3000,
|
||||
"sequence_transition": 500,
|
||||
"sequence_loop": true,
|
||||
"sequence_repeat_count": 0,
|
||||
"sequence_active": false,
|
||||
"sequence_index": 0,
|
||||
"sequence_start_time": 0
|
||||
},
|
||||
"2": {
|
||||
"group_name": "Accent Group",
|
||||
"presets": [
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"sequence_duration": 2000,
|
||||
"sequence_transition": 300,
|
||||
"sequence_loop": true,
|
||||
"sequence_repeat_count": 0,
|
||||
"sequence_active": false,
|
||||
"sequence_index": 0,
|
||||
"sequence_start_time": 0
|
||||
}
|
||||
}
|
||||
1
db/tab.json
Normal file
1
db/tab.json
Normal file
@@ -0,0 +1 @@
|
||||
{"1": {"name": "Main", "names": ["1", "2", "3"], "presets": ["1", "2"]}, "2": {"name": "Accent", "names": ["4", "5"], "presets": ["2", "3"]}, "3": {"name": "", "names": [], "presets": []}, "4": {"name": "", "names": [], "presets": []}, "5": {"name": "", "names": [], "presets": []}, "6": {"name": "", "names": [], "presets": []}, "7": {"name": "", "names": [], "presets": []}, "8": {"name": "", "names": [], "presets": []}, "9": {"name": "", "names": [], "presets": []}, "10": {"name": "", "names": [], "presets": []}, "11": {"name": "", "names": [], "presets": []}, "12": {"name": "test2", "names": ["1"], "presets": [], "colors": ["#b93c3c", "#761e1e", "#ffffff"]}, "13": {"name": "test5", "names": ["1"], "presets": []}}
|
||||
50
dev.py
50
dev.py
@@ -6,28 +6,48 @@ import sys
|
||||
|
||||
print(sys.argv)
|
||||
|
||||
port = sys.argv[1]
|
||||
# Extract port (first arg if it's not a command)
|
||||
commands = ["src", "lib", "ls", "reset", "follow", "db"]
|
||||
port = None
|
||||
if len(sys.argv) > 1 and sys.argv[1] not in commands:
|
||||
port = sys.argv[1]
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
for cmd in sys.argv[1:]:
|
||||
print(cmd)
|
||||
match cmd:
|
||||
case "src":
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
|
||||
if port:
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", ".", ":" ], cwd="src")
|
||||
else:
|
||||
print("Error: Port required for 'src' command")
|
||||
case "lib":
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
|
||||
if port:
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "lib", ":" ])
|
||||
else:
|
||||
print("Error: Port required for 'lib' command")
|
||||
case "ls":
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
|
||||
if port:
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "ls", ":" ])
|
||||
else:
|
||||
print("Error: Port required for 'ls' command")
|
||||
case "reset":
|
||||
with serial.Serial(port, baudrate=115200) as ser:
|
||||
ser.write(b'\x03\x03\x04')
|
||||
if port:
|
||||
with serial.Serial(port, baudrate=115200) as ser:
|
||||
ser.write(b'\x03\x03\x04')
|
||||
else:
|
||||
print("Error: Port required for 'reset' command")
|
||||
case "follow":
|
||||
with serial.Serial(port, baudrate=115200) as ser:
|
||||
while True:
|
||||
if ser.in_waiting > 0: # Check if there is data in the buffer
|
||||
data = ser.readline().decode('utf-8').strip() # Read and decode the data
|
||||
print(data)
|
||||
|
||||
|
||||
|
||||
if port:
|
||||
with serial.Serial(port, baudrate=115200) as ser:
|
||||
while True:
|
||||
if ser.in_waiting > 0: # Check if there is data in the buffer
|
||||
data = ser.readline().decode('utf-8').strip() # Read and decode the data
|
||||
print(data)
|
||||
else:
|
||||
print("Error: Port required for 'follow' command")
|
||||
case "db":
|
||||
if port:
|
||||
subprocess.call(["mpremote", "connect", port, "fs", "cp", "-r", "db", ":" ])
|
||||
else:
|
||||
print("Error: Port required for 'db' command")
|
||||
|
||||
BIN
lib/microdot/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
lib/microdot/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
lib/microdot/__pycache__/helpers.cpython-311.pyc
Normal file
BIN
lib/microdot/__pycache__/helpers.cpython-311.pyc
Normal file
Binary file not shown.
BIN
lib/microdot/__pycache__/microdot.cpython-311.pyc
Normal file
BIN
lib/microdot/__pycache__/microdot.cpython-311.pyc
Normal file
Binary file not shown.
BIN
lib/microdot/__pycache__/websocket.cpython-311.pyc
Normal file
BIN
lib/microdot/__pycache__/websocket.cpython-311.pyc
Normal file
Binary file not shown.
159
lib/microdot/session.py
Normal file
159
lib/microdot/session.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import jwt
|
||||
from microdot.microdot import invoke_handler
|
||||
from microdot.helpers import wraps
|
||||
|
||||
|
||||
class SessionDict(dict):
|
||||
"""A session dictionary.
|
||||
|
||||
The session dictionary is a standard Python dictionary that has been
|
||||
extended with convenience ``save()`` and ``delete()`` methods.
|
||||
"""
|
||||
def __init__(self, request, session_dict):
|
||||
super().__init__(session_dict)
|
||||
self.request = request
|
||||
|
||||
def save(self):
|
||||
"""Update the session cookie."""
|
||||
self.request.app._session.update(self.request, self)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the session cookie."""
|
||||
self.request.app._session.delete(self.request)
|
||||
|
||||
|
||||
class Session:
|
||||
"""Session handling
|
||||
|
||||
:param app: The application instance.
|
||||
:param secret_key: The secret key, as a string or bytes object.
|
||||
:param cookie_options: A dictionary with cookie options to pass as
|
||||
arguments to :meth:`Response.set_cookie()
|
||||
<microdot.Response.set_cookie>`.
|
||||
"""
|
||||
secret_key = None
|
||||
|
||||
def __init__(self, app=None, secret_key=None, cookie_options=None):
|
||||
self.secret_key = secret_key
|
||||
self.cookie_options = cookie_options or {}
|
||||
if app is not None:
|
||||
self.initialize(app)
|
||||
|
||||
def initialize(self, app, secret_key=None, cookie_options=None):
|
||||
if secret_key is not None:
|
||||
self.secret_key = secret_key
|
||||
if cookie_options is not None:
|
||||
self.cookie_options = cookie_options
|
||||
if 'path' not in self.cookie_options:
|
||||
self.cookie_options['path'] = '/'
|
||||
if 'http_only' not in self.cookie_options:
|
||||
self.cookie_options['http_only'] = True
|
||||
app._session = self
|
||||
|
||||
def get(self, request):
|
||||
"""Retrieve the user session.
|
||||
|
||||
:param request: The client request.
|
||||
|
||||
The return value is a session dictionary with the data stored in the
|
||||
user's session, or ``{}`` if the session data is not available or
|
||||
invalid.
|
||||
"""
|
||||
if not self.secret_key:
|
||||
raise ValueError('The session secret key is not configured')
|
||||
if hasattr(request.g, '_session'):
|
||||
return request.g._session
|
||||
session = request.cookies.get('session')
|
||||
if session is None:
|
||||
request.g._session = SessionDict(request, {})
|
||||
return request.g._session
|
||||
request.g._session = SessionDict(request, self.decode(session))
|
||||
return request.g._session
|
||||
|
||||
def update(self, request, session):
|
||||
"""Update the user session.
|
||||
|
||||
:param request: The client request.
|
||||
:param session: A dictionary with the update session data for the user.
|
||||
|
||||
Applications would normally not call this method directly, instead they
|
||||
would use the :meth:`SessionDict.save` method on the session
|
||||
dictionary, which calls this method. For example::
|
||||
|
||||
@app.route('/')
|
||||
@with_session
|
||||
def index(request, session):
|
||||
session['foo'] = 'bar'
|
||||
session.save()
|
||||
return 'Hello, World!'
|
||||
|
||||
Calling this method adds a cookie with the updated session to the
|
||||
request currently being processed.
|
||||
"""
|
||||
if not self.secret_key:
|
||||
raise ValueError('The session secret key is not configured')
|
||||
|
||||
encoded_session = self.encode(session)
|
||||
|
||||
@request.after_request
|
||||
def _update_session(request, response):
|
||||
response.set_cookie('session', encoded_session,
|
||||
**self.cookie_options)
|
||||
return response
|
||||
|
||||
def delete(self, request):
|
||||
"""Remove the user session.
|
||||
|
||||
:param request: The client request.
|
||||
|
||||
Applications would normally not call this method directly, instead they
|
||||
would use the :meth:`SessionDict.delete` method on the session
|
||||
dictionary, which calls this method. For example::
|
||||
|
||||
@app.route('/')
|
||||
@with_session
|
||||
def index(request, session):
|
||||
session.delete()
|
||||
return 'Hello, World!'
|
||||
|
||||
Calling this method adds a cookie removal header to the request
|
||||
currently being processed.
|
||||
"""
|
||||
@request.after_request
|
||||
def _delete_session(request, response):
|
||||
response.delete_cookie('session', **self.cookie_options)
|
||||
return response
|
||||
|
||||
def encode(self, payload, secret_key=None):
|
||||
return jwt.encode(payload, secret_key or self.secret_key,
|
||||
algorithm='HS256')
|
||||
|
||||
def decode(self, session, secret_key=None):
|
||||
try:
|
||||
payload = jwt.decode(session, secret_key or self.secret_key,
|
||||
algorithms=['HS256'])
|
||||
except jwt.exceptions.PyJWTError: # pragma: no cover
|
||||
return {}
|
||||
return payload
|
||||
|
||||
|
||||
def with_session(f):
|
||||
"""Decorator that passes the user session to the route handler.
|
||||
|
||||
The session dictionary is passed to the decorated function as an argument
|
||||
after the request object. Example::
|
||||
|
||||
@app.route('/')
|
||||
@with_session
|
||||
def index(request, session):
|
||||
return 'Hello, World!'
|
||||
|
||||
Note that the decorator does not save the session. To update the session,
|
||||
call the :func:`session.save() <microdot.session.SessionDict.save>` method.
|
||||
"""
|
||||
@wraps(f)
|
||||
async def wrapper(request, *args, **kwargs):
|
||||
return await invoke_handler(
|
||||
f, request, request.app._session.get(request), *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
152
run_web.py
Normal file
152
run_web.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local development web server - imports and runs main.py with port 5000
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
# Add src and lib to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib'))
|
||||
|
||||
# Import the main module
|
||||
from src import main as main_module
|
||||
|
||||
# Override the port in the main function
|
||||
async def run_local():
|
||||
"""Run main with port 5000 for local development."""
|
||||
from settings import Settings
|
||||
import gc
|
||||
|
||||
# Mock MicroPython modules for local development
|
||||
class MockMachine:
|
||||
class WDT:
|
||||
def __init__(self, timeout):
|
||||
pass
|
||||
def feed(self):
|
||||
pass
|
||||
import sys as sys_module
|
||||
sys_module.modules['machine'] = MockMachine()
|
||||
|
||||
class MockESPNow:
|
||||
def __init__(self):
|
||||
self.active_value = False
|
||||
self.peers = []
|
||||
def active(self, value):
|
||||
self.active_value = value
|
||||
print(f"[MOCK] ESPNow active: {value}")
|
||||
def add_peer(self, peer):
|
||||
self.peers.append(peer)
|
||||
print(f"[MOCK] Added peer: {peer.hex() if hasattr(peer, 'hex') else peer}")
|
||||
async def asend(self, peer, data):
|
||||
print(f"[MOCK] Would send to {peer.hex() if hasattr(peer, 'hex') else peer}: {data}")
|
||||
|
||||
class MockAIOESPNow:
|
||||
def __init__(self):
|
||||
pass
|
||||
def active(self, value):
|
||||
return MockESPNow()
|
||||
def add_peer(self, peer):
|
||||
pass
|
||||
|
||||
class MockNetwork:
|
||||
class WLAN:
|
||||
def __init__(self, interface):
|
||||
self.interface = interface
|
||||
def active(self, value):
|
||||
print(f"[MOCK] WLAN({self.interface}) active: {value}")
|
||||
STA_IF = 0
|
||||
|
||||
# Replace MicroPython modules with mocks
|
||||
sys_module.modules['aioespnow'] = type('module', (), {'AIOESPNow': MockESPNow})()
|
||||
sys_module.modules['network'] = MockNetwork()
|
||||
|
||||
# Mock gc if needed
|
||||
if not hasattr(gc, 'collect'):
|
||||
class MockGC:
|
||||
def collect(self):
|
||||
pass
|
||||
gc = MockGC()
|
||||
|
||||
settings = Settings()
|
||||
print("Starting LED Controller Web Server (Local Development)")
|
||||
print("=" * 60)
|
||||
|
||||
# Mock network
|
||||
import network
|
||||
network.WLAN(network.STA_IF).active(True)
|
||||
|
||||
# Mock ESPNow
|
||||
import aioespnow
|
||||
e = aioespnow.AIOESPNow()
|
||||
e.active(True)
|
||||
e.add_peer(b"\xbb\xbb\xbb\xbb\xbb\xbb")
|
||||
|
||||
from microdot import Microdot, send_file
|
||||
from microdot.websocket import with_websocket
|
||||
|
||||
import controllers.preset as preset
|
||||
import controllers.profile as profile
|
||||
import controllers.group as group
|
||||
import controllers.sequence as sequence
|
||||
import controllers.tab as tab
|
||||
import controllers.palette as palette
|
||||
import controllers.scene as scene
|
||||
|
||||
app = Microdot()
|
||||
|
||||
# Mount model controllers as subroutes
|
||||
app.mount(preset.controller, '/presets')
|
||||
app.mount(profile.controller, '/profiles')
|
||||
app.mount(group.controller, '/groups')
|
||||
app.mount(sequence.controller, '/sequences')
|
||||
app.mount(tab.controller, '/tabs')
|
||||
app.mount(palette.controller, '/palettes')
|
||||
app.mount(scene.controller, '/scenes')
|
||||
|
||||
# Serve index.html at root
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
"""Serve the main web UI."""
|
||||
return send_file('src/templates/index.html')
|
||||
|
||||
# Static file route
|
||||
@app.route("/static/<path:path>")
|
||||
def static_handler(request, path):
|
||||
"""Serve static files."""
|
||||
if '..' in path:
|
||||
return 'Not found', 404
|
||||
return send_file('src/static/' + path)
|
||||
|
||||
@app.route('/ws')
|
||||
@with_websocket
|
||||
async def ws(request, ws):
|
||||
while True:
|
||||
data = await ws.receive()
|
||||
if data:
|
||||
await e.asend(b"\xbb\xbb\xbb\xbb\xbb\xbb", data)
|
||||
print(data)
|
||||
else:
|
||||
break
|
||||
|
||||
# Use port 5000 for local development
|
||||
port = 5000
|
||||
print(f"Starting server on http://0.0.0.0:{port}")
|
||||
print(f"Open http://localhost:{port} in your browser")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
await app.start_server(host="0.0.0.0", port=port, debug=True)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down server...")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Change to project root
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
# Override settings path for local development
|
||||
import settings as settings_module
|
||||
settings_module.Settings.SETTINGS_FILE = os.path.join(os.getcwd(), 'settings.json')
|
||||
|
||||
asyncio.run(run_local())
|
||||
1
settings.json
Normal file
1
settings.json
Normal file
@@ -0,0 +1 @@
|
||||
{"session_secret_key": "06a2a6ac631cc8db0658373b37f7fe922a8a3ed3a27229e1adb5448cb368f2b7"}
|
||||
BIN
src/__pycache__/main.cpython-311.pyc
Normal file
BIN
src/__pycache__/main.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/settings.cpython-311.pyc
Normal file
BIN
src/__pycache__/settings.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
import settings
|
||||
import wifi
|
||||
import util.wifi as wifi
|
||||
from settings import Settings
|
||||
|
||||
s = Settings()
|
||||
|
||||
name = s.get('name', 'led')
|
||||
name = s.get('name', 'led-controller')
|
||||
wifi.ap(name, '')
|
||||
|
||||
1
src/controllers/__init__.py
Normal file
1
src/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Controllers package
|
||||
BIN
src/controllers/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/group.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/palette.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/palette.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/preset.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/preset.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/profile.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/profile.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/scene.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/sequence.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/sequence.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controllers/__pycache__/tab.cpython-311.pyc
Normal file
BIN
src/controllers/__pycache__/tab.cpython-311.pyc
Normal file
Binary file not shown.
55
src/controllers/pattern.py
Normal file
55
src/controllers/pattern.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from microdot import Microdot
|
||||
from models.pattern import Pattern
|
||||
import json
|
||||
|
||||
controller = Microdot()
|
||||
patterns = Pattern()
|
||||
|
||||
|
||||
@controller.get('')
|
||||
async def list_patterns(request):
|
||||
"""List all patterns."""
|
||||
return json.dumps(patterns), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
@controller.get('/<id>')
|
||||
async def get_pattern(request, id):
|
||||
"""Get a specific pattern by ID."""
|
||||
pattern = patterns.read(id)
|
||||
if pattern is not None:
|
||||
return json.dumps(pattern), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Pattern not found"}), 404
|
||||
|
||||
|
||||
@controller.post('')
|
||||
async def create_pattern(request):
|
||||
"""Create a new pattern."""
|
||||
try:
|
||||
data = request.json or {}
|
||||
name = data.get("name", "")
|
||||
pattern_id = patterns.create(name, data.get("data", {}))
|
||||
if data:
|
||||
patterns.update(pattern_id, data)
|
||||
return json.dumps(patterns.read(pattern_id)), 201, {'Content-Type': 'application/json'}
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
|
||||
@controller.put('/<id>')
|
||||
async def update_pattern(request, id):
|
||||
"""Update an existing pattern."""
|
||||
try:
|
||||
data = request.json
|
||||
if patterns.update(id, data):
|
||||
return json.dumps(patterns.read(id)), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Pattern not found"}), 404
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
|
||||
@controller.delete('/<id>')
|
||||
async def delete_pattern(request, id):
|
||||
"""Delete a pattern."""
|
||||
if patterns.delete(id):
|
||||
return json.dumps({"message": "Pattern deleted successfully"}), 200
|
||||
return json.dumps({"error": "Pattern not found"}), 404
|
||||
@@ -1,4 +1,5 @@
|
||||
from microdot import Microdot
|
||||
from microdot.session import with_session
|
||||
from models.profile import Profile
|
||||
import json
|
||||
|
||||
@@ -10,6 +11,21 @@ async def list_profiles(request):
|
||||
"""List all profiles."""
|
||||
return json.dumps(profiles), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
@controller.get('/current')
|
||||
@with_session
|
||||
async def get_current_profile(request, session):
|
||||
"""Get the current profile ID from session (or fallback)."""
|
||||
profile_list = profiles.list()
|
||||
current_id = session.get('current_profile')
|
||||
if not current_id and profile_list:
|
||||
current_id = profile_list[0]
|
||||
session['current_profile'] = str(current_id)
|
||||
session.save()
|
||||
if current_id:
|
||||
profile = profiles.read(current_id)
|
||||
return json.dumps({"id": current_id, "profile": profile}), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "No profile available"}), 404
|
||||
|
||||
@controller.get('/<id>')
|
||||
async def get_profile(request, id):
|
||||
"""Get a specific profile by ID."""
|
||||
@@ -18,6 +34,16 @@ async def get_profile(request, id):
|
||||
return json.dumps(profile), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Profile not found"}), 404
|
||||
|
||||
@controller.post('/<id>/apply')
|
||||
@with_session
|
||||
async def apply_profile(request, session, id):
|
||||
"""Apply a profile by saving it to session."""
|
||||
if not profiles.read(id):
|
||||
return json.dumps({"error": "Profile not found"}), 404
|
||||
session['current_profile'] = str(id)
|
||||
session.save()
|
||||
return json.dumps({"message": "Profile applied", "id": str(id)}), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
@controller.post('')
|
||||
async def create_profile(request):
|
||||
"""Create a new profile."""
|
||||
@@ -31,6 +57,26 @@ async def create_profile(request):
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.put('/current')
|
||||
@with_session
|
||||
async def update_current_profile(request, session):
|
||||
"""Update the current profile using session (or fallback)."""
|
||||
try:
|
||||
data = request.json or {}
|
||||
profile_list = profiles.list()
|
||||
current_id = session.get('current_profile')
|
||||
if not current_id and profile_list:
|
||||
current_id = profile_list[0]
|
||||
session['current_profile'] = str(current_id)
|
||||
session.save()
|
||||
if not current_id:
|
||||
return json.dumps({"error": "No profile available"}), 404
|
||||
if profiles.update(current_id, data):
|
||||
return json.dumps(profiles.read(current_id)), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Profile not found"}), 404
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.put('/<id>')
|
||||
async def update_profile(request, id):
|
||||
"""Update an existing profile."""
|
||||
|
||||
49
src/controllers/scene.py
Normal file
49
src/controllers/scene.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from microdot import Microdot
|
||||
from models.scene import Scene
|
||||
import json
|
||||
|
||||
controller = Microdot()
|
||||
scenes = Scene()
|
||||
|
||||
@controller.get('')
|
||||
async def list_scenes(request):
|
||||
"""List all scenes."""
|
||||
return json.dumps(scenes), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
@controller.get('/<id>')
|
||||
async def get_scene(request, id):
|
||||
"""Get a specific scene by ID."""
|
||||
scene = scenes.read(id)
|
||||
if scene:
|
||||
return json.dumps(scene), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Scene not found"}), 404
|
||||
|
||||
@controller.post('')
|
||||
async def create_scene(request):
|
||||
"""Create a new scene."""
|
||||
try:
|
||||
data = request.json
|
||||
scene_id = scenes.create()
|
||||
if scenes.update(scene_id, data):
|
||||
return json.dumps(scenes.read(scene_id)), 201, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Failed to create scene"}), 400
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.put('/<id>')
|
||||
async def update_scene(request, id):
|
||||
"""Update an existing scene."""
|
||||
try:
|
||||
data = request.json
|
||||
if scenes.update(id, data):
|
||||
return json.dumps(scenes.read(id)), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Scene not found"}), 404
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.delete('/<id>')
|
||||
async def delete_scene(request, id):
|
||||
"""Delete a scene."""
|
||||
if scenes.delete(id):
|
||||
return json.dumps({"message": "Scene deleted successfully"}), 200
|
||||
return json.dumps({"error": "Scene not found"}), 404
|
||||
@@ -1,15 +1,253 @@
|
||||
from microdot import Microdot
|
||||
from microdot import Microdot, send_file
|
||||
from microdot.session import with_session
|
||||
from models.tab import Tab
|
||||
from models.profile import Profile
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
controller = Microdot()
|
||||
tabs = Tab()
|
||||
profiles = Profile()
|
||||
|
||||
def get_current_profile_id(session=None):
|
||||
"""Get the current active profile ID from session or fallback to first."""
|
||||
profile_list = profiles.list()
|
||||
session_profile = None
|
||||
if session is not None:
|
||||
session_profile = session.get('current_profile')
|
||||
if session_profile and session_profile in profile_list:
|
||||
return session_profile
|
||||
if profile_list:
|
||||
return profile_list[0]
|
||||
return None
|
||||
|
||||
def get_profile_tab_order(profile_id):
|
||||
"""Get the tab order for a profile."""
|
||||
if not profile_id:
|
||||
return []
|
||||
profile = profiles.read(profile_id)
|
||||
if profile:
|
||||
# Support both "tab_order" (old) and "tabs" (new) format
|
||||
return profile.get("tabs", profile.get("tab_order", []))
|
||||
return []
|
||||
|
||||
def get_current_tab_id(request, session=None):
|
||||
"""Get the current tab ID from session."""
|
||||
if session:
|
||||
current_tab = session.get('current_tab')
|
||||
if current_tab:
|
||||
return current_tab
|
||||
|
||||
# Fallback to first tab in current profile if no session
|
||||
profile_id = get_current_profile_id(session)
|
||||
if profile_id:
|
||||
profile = profiles.read(profile_id)
|
||||
if profile:
|
||||
# Support both "tabs" (new) and "tab_order" (old) format
|
||||
tabs_list = profile.get("tabs", profile.get("tab_order", []))
|
||||
if tabs_list:
|
||||
return tabs_list[0]
|
||||
return None
|
||||
|
||||
@controller.get('')
|
||||
async def list_tabs(request):
|
||||
"""List all tabs."""
|
||||
return json.dumps(tabs), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
# HTML Fragment endpoints for htmx - must be before /<id> route
|
||||
@controller.get('/list-fragment')
|
||||
@with_session
|
||||
async def tabs_list_fragment(request, session):
|
||||
"""Return HTML fragment for the tabs list."""
|
||||
profile_id = get_current_profile_id(session)
|
||||
# #region agent log
|
||||
try:
|
||||
os.makedirs('/home/pi/led-controller/.cursor', exist_ok=True)
|
||||
with open('/home/pi/led-controller/.cursor/debug.log', 'a') as _log:
|
||||
_log.write(json.dumps({
|
||||
"sessionId": "debug-session",
|
||||
"runId": "tabs-pre-fix",
|
||||
"hypothesisId": "H1",
|
||||
"location": "src/controllers/tab.py:tabs_list_fragment",
|
||||
"message": "tabs list fragment",
|
||||
"data": {
|
||||
"profile_id": profile_id,
|
||||
"profile_count": len(profiles.list())
|
||||
},
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
if not profile_id:
|
||||
return '<div class="tabs-list">No profile selected</div>', 200, {'Content-Type': 'text/html'}
|
||||
|
||||
tab_order = get_profile_tab_order(profile_id)
|
||||
current_tab_id = get_current_tab_id(request, session)
|
||||
|
||||
html = '<div class="tabs-list">'
|
||||
for tab_id in tab_order:
|
||||
tab_data = tabs.read(tab_id)
|
||||
if tab_data:
|
||||
active_class = 'active' if str(tab_id) == str(current_tab_id) else ''
|
||||
tab_name = tab_data.get('name', 'Tab ' + str(tab_id))
|
||||
html += (
|
||||
'<button class="tab-button ' + active_class + '" '
|
||||
'hx-get="/tabs/' + str(tab_id) + '/content-fragment" '
|
||||
'hx-target="#tab-content" '
|
||||
'hx-swap="innerHTML" '
|
||||
'hx-push-url="true" '
|
||||
'hx-trigger="click" '
|
||||
'onclick="document.querySelectorAll(\'.tab-button\').forEach(b => b.classList.remove(\'active\')); this.classList.add(\'active\');">'
|
||||
+ tab_name +
|
||||
'</button>'
|
||||
)
|
||||
html += '</div>'
|
||||
return html, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
@controller.get('/create-form-fragment')
|
||||
async def create_tab_form_fragment(request):
|
||||
"""Return the create tab form HTML fragment."""
|
||||
html = '''
|
||||
<h2>Add New Tab</h2>
|
||||
<form hx-post="/tabs"
|
||||
hx-target="#tabs-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('add-tab-modal').classList.remove('active'); document.body.dispatchEvent(new Event('tabs-updated')); }">
|
||||
<label>Tab Name:</label>
|
||||
<input type="text" name="name" placeholder="Enter tab name" required>
|
||||
<label>Device IDs (comma-separated):</label>
|
||||
<input type="text" name="ids" placeholder="1,2,3" value="1">
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('add-tab-modal').classList.remove('active')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
'''
|
||||
return html, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
@controller.get('/current')
|
||||
@with_session
|
||||
async def get_current_tab(request, session):
|
||||
"""Get the current tab from session."""
|
||||
current_tab_id = get_current_tab_id(request, session)
|
||||
if not current_tab_id:
|
||||
accept_header = request.headers.get('Accept', '')
|
||||
wants_html = 'text/html' in accept_header
|
||||
if wants_html:
|
||||
return '<div class="error">No current tab set</div>', 404, {'Content-Type': 'text/html'}
|
||||
return json.dumps({"error": "No current tab set"}), 404
|
||||
|
||||
return await tab_content_fragment.__wrapped__(request, session, current_tab_id)
|
||||
|
||||
@controller.get('/<id>/content-fragment')
|
||||
@with_session
|
||||
async def tab_content_fragment(request, session, id):
|
||||
"""Return HTML fragment for tab content."""
|
||||
# Handle 'current' as a special case
|
||||
if id == 'current':
|
||||
return await get_current_tab(request, session)
|
||||
|
||||
tab = tabs.read(id)
|
||||
if not tab:
|
||||
return '<div>Tab not found</div>', 404, {'Content-Type': 'text/html'}
|
||||
|
||||
# Set this tab as the current tab in session
|
||||
session['current_tab'] = str(id)
|
||||
session.save()
|
||||
|
||||
# If this is a direct page load (not HTMX), return full UI so CSS loads.
|
||||
if not request.headers.get('HX-Request'):
|
||||
return send_file('templates/index.html')
|
||||
|
||||
tab_name = tab.get('name', 'Tab ' + str(id))
|
||||
device_ids = ', '.join(tab.get('names', []))
|
||||
|
||||
html = (
|
||||
'<div class="left-panel">'
|
||||
'<div class="left-panel-header">'
|
||||
'<div class="ids-display">'
|
||||
'<label>IDs: </label>'
|
||||
'<span id="current-ids">' + device_ids + '</span>'
|
||||
'</div>'
|
||||
'<button id="toggle-left-panel" class="btn btn-small left-panel-toggle" title="Collapse/expand controls">◀</button>'
|
||||
'</div>'
|
||||
'<div class="left-panel-body">'
|
||||
'<div class="color-palette-section">'
|
||||
'<h3>Color Palette</h3>'
|
||||
'<div id="color-palette" class="color-palette" data-tab-id="' + str(id) + '">'
|
||||
'<!-- Colors will be loaded here -->'
|
||||
'</div>'
|
||||
'<div class="palette-actions">'
|
||||
'<input type="color" id="tab-color-input" value="#ffffff">'
|
||||
'<button class="btn btn-small" id="tab-color-add-btn">Add Color</button>'
|
||||
'<button class="btn btn-small" id="tab-color-add-from-palette-btn">Add from Palette</button>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'<div class="controls-section">'
|
||||
'<div class="control-group">'
|
||||
'<label for="brightness-slider">Brightness:</label>'
|
||||
'<input type="range" id="brightness-slider" min="0" max="255" value="127" class="slider">'
|
||||
'<span id="brightness-value" class="slider-value">127</span>'
|
||||
'</div>'
|
||||
'<div class="control-group">'
|
||||
'<label for="delay-slider">Delay:</label>'
|
||||
'<input type="range" id="delay-slider" min="0" max="1000" value="0" class="slider">'
|
||||
'<span id="delay-value" class="slider-value">100 ms</span>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'<div class="n-params-section">'
|
||||
'<h3>N Parameters</h3>'
|
||||
'<div class="n-params-grid">'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n1-input" id="n1-label">n1:</label>'
|
||||
'<input type="number" id="n1-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n2-input" id="n2-label">n2:</label>'
|
||||
'<input type="number" id="n2-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n3-input" id="n3-label">n3:</label>'
|
||||
'<input type="number" id="n3-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n4-input" id="n4-label">n4:</label>'
|
||||
'<input type="number" id="n4-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n5-input" id="n5-label">n5:</label>'
|
||||
'<input type="number" id="n5-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n6-input" id="n6-label">n6:</label>'
|
||||
'<input type="number" id="n6-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n7-input" id="n7-label">n7:</label>'
|
||||
'<input type="number" id="n7-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'<div class="n-param-group">'
|
||||
'<label for="n8-input" id="n8-label">n8:</label>'
|
||||
'<input type="number" id="n8-input" min="0" max="255" value="10" class="n-input">'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'<div class="right-panel">'
|
||||
'<div class="presets-section">'
|
||||
'<h3>Presets</h3>'
|
||||
'<div id="presets-list-tab" class="presets-list">'
|
||||
'<!-- Presets will be loaded here -->'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
)
|
||||
return html, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
@controller.get('/<id>')
|
||||
async def get_tab(request, id):
|
||||
"""Get a specific tab by ID."""
|
||||
@@ -18,21 +256,6 @@ async def get_tab(request, id):
|
||||
return json.dumps(tab), 200, {'Content-Type': 'application/json'}
|
||||
return json.dumps({"error": "Tab not found"}), 404
|
||||
|
||||
@controller.post('')
|
||||
async def create_tab(request):
|
||||
"""Create a new tab."""
|
||||
try:
|
||||
data = request.json or {}
|
||||
name = data.get("name", "")
|
||||
names = data.get("names", None)
|
||||
preset_ids = data.get("presets", None)
|
||||
tab_id = tabs.create(name, names, preset_ids)
|
||||
if data:
|
||||
tabs.update(tab_id, data)
|
||||
return json.dumps(tabs.read(tab_id)), 201, {'Content-Type': 'application/json'}
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.put('/<id>')
|
||||
async def update_tab(request, id):
|
||||
"""Update an existing tab."""
|
||||
@@ -45,8 +268,147 @@ async def update_tab(request, id):
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
@controller.delete('/<id>')
|
||||
async def delete_tab(request, id):
|
||||
@with_session
|
||||
async def delete_tab(request, id, session):
|
||||
"""Delete a tab."""
|
||||
# Check if this is an htmx request (wants HTML fragment)
|
||||
accept_header = request.headers.get('Accept', '')
|
||||
wants_html = 'text/html' in accept_header
|
||||
|
||||
# Handle 'current' tab ID
|
||||
if id == 'current':
|
||||
current_tab_id = get_current_tab_id(request, session)
|
||||
if current_tab_id:
|
||||
id = current_tab_id
|
||||
else:
|
||||
if wants_html:
|
||||
return '<div class="error">No current tab to delete</div>', 404, {'Content-Type': 'text/html'}
|
||||
return json.dumps({"error": "No current tab to delete"}), 404
|
||||
|
||||
if tabs.delete(id):
|
||||
return json.dumps({"message": "Tab deleted successfully"}), 200
|
||||
# Remove from profile's tabs
|
||||
profile_id = get_current_profile_id(session)
|
||||
if profile_id:
|
||||
profile = profiles.read(profile_id)
|
||||
if profile:
|
||||
# Support both "tabs" (new) and "tab_order" (old) format
|
||||
tabs_list = profile.get('tabs', profile.get('tab_order', []))
|
||||
if id in tabs_list:
|
||||
tabs_list.remove(id)
|
||||
profile['tabs'] = tabs_list
|
||||
# Remove old tab_order if it exists
|
||||
if 'tab_order' in profile:
|
||||
del profile['tab_order']
|
||||
profiles.update(profile_id, profile)
|
||||
|
||||
# Clear session if the deleted tab was the current tab
|
||||
current_tab_id = get_current_tab_id(request, session)
|
||||
if current_tab_id == id:
|
||||
if 'current_tab' in session:
|
||||
session.pop('current_tab', None)
|
||||
session.save()
|
||||
|
||||
if wants_html:
|
||||
return await tabs_list_fragment.__wrapped__(request, session)
|
||||
else:
|
||||
return json.dumps({"message": "Tab deleted successfully"}), 200, {'Content-Type': 'application/json'}
|
||||
|
||||
if wants_html:
|
||||
return '<div class="error">Tab not found</div>', 404, {'Content-Type': 'text/html'}
|
||||
return json.dumps({"error": "Tab not found"}), 404
|
||||
|
||||
@controller.post('')
|
||||
@with_session
|
||||
async def create_tab(request, session):
|
||||
"""Create a new tab."""
|
||||
# Check if this is an htmx request (wants HTML fragment)
|
||||
accept_header = request.headers.get('Accept', '')
|
||||
wants_html = 'text/html' in accept_header
|
||||
# #region agent log
|
||||
try:
|
||||
os.makedirs('/home/pi/led-controller/.cursor', exist_ok=True)
|
||||
with open('/home/pi/led-controller/.cursor/debug.log', 'a') as _log:
|
||||
_log.write(json.dumps({
|
||||
"sessionId": "debug-session",
|
||||
"runId": "tabs-pre-fix",
|
||||
"hypothesisId": "H3",
|
||||
"location": "src/controllers/tab.py:create_tab_htmx",
|
||||
"message": "create tab with session",
|
||||
"data": {
|
||||
"wants_html": wants_html,
|
||||
"has_form": bool(request.form),
|
||||
"accept": accept_header
|
||||
},
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
|
||||
try:
|
||||
# Handle form data (htmx) or JSON
|
||||
if request.form:
|
||||
name = request.form.get('name', '').strip()
|
||||
ids_str = request.form.get('ids', '1').strip()
|
||||
names = [id.strip() for id in ids_str.split(',') if id.strip()]
|
||||
preset_ids = None
|
||||
else:
|
||||
data = request.json or {}
|
||||
name = data.get("name", "")
|
||||
names = data.get("names", None)
|
||||
preset_ids = data.get("presets", None)
|
||||
|
||||
if not name:
|
||||
if wants_html:
|
||||
return '<div class="error">Tab name cannot be empty</div>', 400, {'Content-Type': 'text/html'}
|
||||
return json.dumps({"error": "Tab name cannot be empty"}), 400
|
||||
|
||||
tab_id = tabs.create(name, names, preset_ids)
|
||||
|
||||
# Add to current profile's tabs
|
||||
profile_id = get_current_profile_id(session)
|
||||
if profile_id:
|
||||
profile = profiles.read(profile_id)
|
||||
if profile:
|
||||
# Support both "tabs" (new) and "tab_order" (old) format
|
||||
tabs_list = profile.get('tabs', profile.get('tab_order', []))
|
||||
if tab_id not in tabs_list:
|
||||
tabs_list.append(tab_id)
|
||||
profile['tabs'] = tabs_list
|
||||
# Remove old tab_order if it exists
|
||||
if 'tab_order' in profile:
|
||||
del profile['tab_order']
|
||||
profiles.update(profile_id, profile)
|
||||
# #region agent log
|
||||
try:
|
||||
os.makedirs('/home/pi/led-controller/.cursor', exist_ok=True)
|
||||
with open('/home/pi/led-controller/.cursor/debug.log', 'a') as _log:
|
||||
_log.write(json.dumps({
|
||||
"sessionId": "debug-session",
|
||||
"runId": "tabs-pre-fix",
|
||||
"hypothesisId": "H4",
|
||||
"location": "src/controllers/tab.py:create_tab_htmx",
|
||||
"message": "tab created and profile updated",
|
||||
"data": {
|
||||
"tab_id": tab_id,
|
||||
"profile_id": profile_id,
|
||||
"profile_tabs": tabs_list if profile_id and profile else None
|
||||
},
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
|
||||
if wants_html:
|
||||
# Return HTML fragment for tabs list
|
||||
return await tabs_list_fragment.__wrapped__(request, session)
|
||||
else:
|
||||
# Return JSON response
|
||||
return json.dumps(tabs.read(tab_id)), 201, {'Content-Type': 'application/json'}
|
||||
except Exception as e:
|
||||
import sys
|
||||
sys.print_exception(e)
|
||||
if wants_html:
|
||||
return f'<div class="error">Error: {str(e)}</div>', 400, {'Content-Type': 'text/html'}
|
||||
return json.dumps({"error": str(e)}), 400
|
||||
|
||||
49
src/main.py
49
src/main.py
@@ -4,19 +4,21 @@ import gc
|
||||
import machine
|
||||
from microdot import Microdot, send_file
|
||||
from microdot.websocket import with_websocket
|
||||
from microdot.session import Session
|
||||
|
||||
import aioespnow
|
||||
import network
|
||||
from controllers.preset import preset
|
||||
import controllers.preset as preset
|
||||
import controllers.profile as profile
|
||||
import controllers.group as group
|
||||
import controllers.sequence as sequence
|
||||
import controllers.tab as tab
|
||||
import controllers.palette as palette
|
||||
import controllers.scene as scene
|
||||
import controllers.pattern as pattern
|
||||
|
||||
|
||||
|
||||
async def main():
|
||||
async def main(port=80):
|
||||
settings = Settings()
|
||||
print("Starting")
|
||||
|
||||
@@ -29,13 +31,37 @@ async def main():
|
||||
|
||||
app = Microdot()
|
||||
|
||||
# Initialize sessions with a secret key from settings
|
||||
secret_key = settings.get('session_secret_key', 'led-controller-secret-key-change-in-production')
|
||||
Session(app, secret_key=secret_key)
|
||||
|
||||
# Mount model controllers as subroutes
|
||||
app.mount('/presets', preset.controller)
|
||||
app.mount('/profiles', profile.controller)
|
||||
app.mount('/groups', group.controller)
|
||||
app.mount('/sequences', sequence.controller)
|
||||
app.mount('/tabs', tab.controller)
|
||||
app.mount('/palettes', palette.controller)
|
||||
# Verify controllers are Microdot instances before mounting
|
||||
controllers_to_mount = [
|
||||
('/presets', preset, 'preset'),
|
||||
('/profiles', profile, 'profile'),
|
||||
('/groups', group, 'group'),
|
||||
('/sequences', sequence, 'sequence'),
|
||||
('/tabs', tab, 'tab'),
|
||||
('/palettes', palette, 'palette'),
|
||||
('/scenes', scene, 'scene'),
|
||||
]
|
||||
|
||||
# Mount model controllers as subroutes
|
||||
app.mount(preset.controller, '/presets')
|
||||
app.mount(profile.controller, '/profiles')
|
||||
app.mount(group.controller, '/groups')
|
||||
app.mount(sequence.controller, '/sequences')
|
||||
app.mount(tab.controller, '/tabs')
|
||||
app.mount(palette.controller, '/palettes')
|
||||
app.mount(scene.controller, '/scenes')
|
||||
app.mount(pattern.controller, '/patterns')
|
||||
|
||||
# Serve index.html at root
|
||||
@app.route('/')
|
||||
def index(request):
|
||||
"""Serve the main web UI."""
|
||||
return send_file('templates/index.html')
|
||||
|
||||
# Static file route
|
||||
@app.route("/static/<path:path>")
|
||||
@@ -59,7 +85,7 @@ async def main():
|
||||
|
||||
|
||||
|
||||
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=80))
|
||||
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port))
|
||||
|
||||
wdt = machine.WDT(timeout=10000)
|
||||
wdt.feed()
|
||||
@@ -71,4 +97,5 @@ async def main():
|
||||
await asyncio.sleep_ms(500)
|
||||
# cleanup before ending the application
|
||||
|
||||
asyncio.run(main())
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
||||
1
src/models/__init__.py
Normal file
1
src/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
BIN
src/models/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/group.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/group.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/model.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/model.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/pallet.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/pallet.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/preset.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/preset.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/profile.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/profile.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/scene.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/squence.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/squence.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/models/__pycache__/tab.cpython-311.pyc
Normal file
BIN
src/models/__pycache__/tab.cpython-311.pyc
Normal file
Binary file not shown.
@@ -1,18 +1,32 @@
|
||||
import json
|
||||
import wifi
|
||||
import ubinascii
|
||||
import machine
|
||||
import os
|
||||
|
||||
class Model(dict):
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# Singleton pattern: return existing instance if it exists
|
||||
if not hasattr(cls, '_instance'):
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self.file = self.__class__.__name__ + ".json"
|
||||
# Only initialize once (check if already initialized)
|
||||
if hasattr(self, '_initialized'):
|
||||
return
|
||||
|
||||
# Create /db directory if it doesn't exist (MicroPython compatible)
|
||||
try:
|
||||
os.mkdir("/db")
|
||||
except OSError:
|
||||
pass # Directory already exists, which is fine
|
||||
self.class_name = self.__class__.__name__
|
||||
self.file = f"/db/{self.class_name.lower()}.json"
|
||||
super().__init__()
|
||||
|
||||
self.load() # Load settings from file during initialization
|
||||
self._initialized = True
|
||||
|
||||
def set_defaults(self):
|
||||
self = {}
|
||||
self.clear()
|
||||
|
||||
def get_next_id(self):
|
||||
"""Get the next available ID for creating a new record."""
|
||||
@@ -23,20 +37,27 @@ class Model(dict):
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
# Ensure directory exists
|
||||
try:
|
||||
os.mkdir("/db")
|
||||
except OSError:
|
||||
pass # Directory already exists
|
||||
j = json.dumps(self)
|
||||
with open(self.file, 'w') as file:
|
||||
file.write(j)
|
||||
print("Settings saved successfully.")
|
||||
print(f"{self.class_name} saved successfully to {self.file}")
|
||||
except Exception as e:
|
||||
print(f"Error saving settings: {e}")
|
||||
print(f"Error saving {self.class_name} to {self.file}: {e}")
|
||||
import sys
|
||||
sys.print_exception(e)
|
||||
|
||||
def load(self):
|
||||
try:
|
||||
with open(self.file, 'r') as file:
|
||||
loaded_settings = json.load(file)
|
||||
self.update(loaded_settings)
|
||||
print("Settings loaded successfully.")
|
||||
print(f"{self.class_name} loaded successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error loading settings")
|
||||
print(f"Error loading {self.class_name}")
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
|
||||
38
src/models/pattern.py
Normal file
38
src/models/pattern.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from models.model import Model
|
||||
|
||||
|
||||
class Pattern(Model):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def create(self, name="", data=None):
|
||||
pattern_name = str(name).strip()
|
||||
if not pattern_name:
|
||||
pattern_name = self.get_next_id()
|
||||
self[pattern_name] = data if isinstance(data, dict) else {}
|
||||
self.save()
|
||||
return pattern_name
|
||||
|
||||
def read(self, id):
|
||||
id_str = str(id)
|
||||
return self.get(id_str, None)
|
||||
|
||||
def update(self, id, data):
|
||||
id_str = str(id)
|
||||
if id_str not in self:
|
||||
return False
|
||||
if isinstance(data, dict):
|
||||
self[id_str].update(data)
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def delete(self, id):
|
||||
id_str = str(id)
|
||||
if id_str not in self:
|
||||
return False
|
||||
self.pop(id_str)
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def list(self):
|
||||
return list(self.keys())
|
||||
@@ -18,6 +18,8 @@ class Preset(Model):
|
||||
"n4": 0,
|
||||
"n5": 0,
|
||||
"n6": 0,
|
||||
"n7": 0,
|
||||
"n8": 0,
|
||||
}
|
||||
self.save()
|
||||
return next_id
|
||||
|
||||
@@ -4,13 +4,18 @@ class Profile(Model):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def create(self, name=""):
|
||||
def create(self, name="", profile_type="tabs"):
|
||||
"""
|
||||
Create a new profile.
|
||||
profile_type: "tabs" or "scenes" (ignoring scenes for now)
|
||||
"""
|
||||
next_id = self.get_next_id()
|
||||
self[next_id] = {
|
||||
"name": name,
|
||||
"tabs": {},
|
||||
"palette": [],
|
||||
"tab_order": []
|
||||
"type": profile_type, # "tabs" or "scenes"
|
||||
"tabs": [], # Array of tab IDs
|
||||
"scenes": [], # Array of scene IDs (for future use)
|
||||
"palette": []
|
||||
}
|
||||
self.save()
|
||||
return next_id
|
||||
|
||||
38
src/models/scene.py
Normal file
38
src/models/scene.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from models.model import Model
|
||||
|
||||
class Scene(Model):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def create(self, name="", sequences=None, groups=None):
|
||||
next_id = self.get_next_id()
|
||||
self[next_id] = {
|
||||
"name": name,
|
||||
"sequences": sequences if sequences else [],
|
||||
"groups": groups if groups else []
|
||||
}
|
||||
self.save()
|
||||
return next_id
|
||||
|
||||
def read(self, id):
|
||||
id_str = str(id)
|
||||
return self.get(id_str, None)
|
||||
|
||||
def update(self, id, data):
|
||||
id_str = str(id)
|
||||
if id_str not in self:
|
||||
return False
|
||||
self[id_str].update(data)
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def delete(self, id):
|
||||
id_str = str(id)
|
||||
if id_str not in self:
|
||||
return False
|
||||
self.pop(id_str)
|
||||
self.save()
|
||||
return True
|
||||
|
||||
def list(self):
|
||||
return list(self.keys())
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
import wifi
|
||||
import ubinascii
|
||||
import machine
|
||||
import os
|
||||
import binascii
|
||||
|
||||
class Settings(dict):
|
||||
SETTINGS_FILE = "/settings.json"
|
||||
@@ -10,8 +9,30 @@ class Settings(dict):
|
||||
super().__init__()
|
||||
self.load() # Load settings from file during initialization
|
||||
|
||||
def generate_secret_key(self):
|
||||
"""Generate a random secret key for session signing."""
|
||||
try:
|
||||
# Try to use os.urandom for secure random bytes
|
||||
random_bytes = os.urandom(32)
|
||||
return binascii.hexlify(random_bytes).decode('utf-8')
|
||||
except (AttributeError, NotImplementedError):
|
||||
# Fallback for MicroPython or systems without os.urandom
|
||||
try:
|
||||
import secrets
|
||||
return secrets.token_hex(32)
|
||||
except ImportError:
|
||||
# Last resort: use a combination of time and random
|
||||
import time
|
||||
import random
|
||||
random.seed(time.time())
|
||||
return binascii.hexlify(bytes([random.randint(0, 255) for _ in range(32)])).decode('utf-8')
|
||||
|
||||
def set_defaults(self):
|
||||
self = {}
|
||||
"""Set default settings if they don't exist."""
|
||||
if 'session_secret_key' not in self:
|
||||
self['session_secret_key'] = self.generate_secret_key()
|
||||
# Save immediately when generating a new key
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
try:
|
||||
@@ -23,12 +44,19 @@ class Settings(dict):
|
||||
print(f"Error saving settings: {e}")
|
||||
|
||||
def load(self):
|
||||
loaded_from_file = False
|
||||
try:
|
||||
with open(self.SETTINGS_FILE, 'r') as file:
|
||||
loaded_settings = json.load(file)
|
||||
self.update(loaded_settings)
|
||||
loaded_from_file = True
|
||||
print("Settings loaded successfully.")
|
||||
except Exception as e:
|
||||
print(f"Error loading settings")
|
||||
self.clear()
|
||||
finally:
|
||||
# Ensure defaults are set even if file exists but is missing keys
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
# Only save if file didn't exist or was invalid
|
||||
if not loaded_from_file:
|
||||
self.save()
|
||||
|
||||
1750
src/static/app.js
Normal file
1750
src/static/app.js
Normal file
File diff suppressed because it is too large
Load Diff
144
src/static/color_palette.js
Normal file
144
src/static/color_palette.js
Normal file
@@ -0,0 +1,144 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const paletteButton = document.getElementById('color-palette-btn');
|
||||
const paletteModal = document.getElementById('color-palette-modal');
|
||||
const closeButton = document.getElementById('color-palette-close-btn');
|
||||
const paletteContainer = document.getElementById('palette-container');
|
||||
const paletteNewColor = document.getElementById('palette-new-color');
|
||||
const paletteAddButton = document.getElementById('palette-add-color-btn');
|
||||
const profileNameDisplay = document.getElementById('palette-current-profile-name');
|
||||
|
||||
if (!paletteButton || !paletteModal || !paletteContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentProfileId = null;
|
||||
let currentPalette = [];
|
||||
let currentProfileName = null;
|
||||
|
||||
const renderPalette = () => {
|
||||
paletteContainer.innerHTML = '';
|
||||
if (!currentPalette.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = 'No colors in palette.';
|
||||
paletteContainer.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
currentPalette.forEach((color, index) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
row.dataset.color = color;
|
||||
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.width = '28px';
|
||||
swatch.style.height = '28px';
|
||||
swatch.style.borderRadius = '4px';
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.style.border = '1px solid #4a4a4a';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = color;
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.className = 'btn btn-danger btn-small';
|
||||
removeButton.textContent = 'Remove';
|
||||
removeButton.addEventListener('click', async () => {
|
||||
const updated = currentPalette.filter((_, i) => i !== index);
|
||||
await savePalette(updated);
|
||||
});
|
||||
|
||||
row.appendChild(swatch);
|
||||
row.appendChild(label);
|
||||
row.appendChild(removeButton);
|
||||
paletteContainer.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const loadPalette = async () => {
|
||||
try {
|
||||
const currentResponse = await fetch('/profiles/current', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!currentResponse.ok) {
|
||||
throw new Error('Failed to load current profile');
|
||||
}
|
||||
const currentData = await currentResponse.json();
|
||||
currentProfileId = currentData.id || null;
|
||||
const profile = currentData.profile || null;
|
||||
currentProfileName = profile ? profile.name : null;
|
||||
if (profileNameDisplay) {
|
||||
profileNameDisplay.textContent = currentProfileName || currentProfileId || 'None';
|
||||
}
|
||||
|
||||
if (!currentProfileId || !profile) {
|
||||
currentPalette = [];
|
||||
renderPalette();
|
||||
return;
|
||||
}
|
||||
|
||||
currentPalette = profile.palette || profile.color_palette || [];
|
||||
renderPalette();
|
||||
} catch (error) {
|
||||
console.error('Failed to load palette:', error);
|
||||
currentPalette = [];
|
||||
renderPalette();
|
||||
}
|
||||
};
|
||||
|
||||
const savePalette = async (newPalette) => {
|
||||
if (!currentProfileId) {
|
||||
alert('No profile selected.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/profiles/current', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
palette: newPalette,
|
||||
color_palette: newPalette,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save palette');
|
||||
}
|
||||
currentPalette = newPalette;
|
||||
renderPalette();
|
||||
} catch (error) {
|
||||
console.error('Failed to save palette:', error);
|
||||
alert('Failed to save palette.');
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
paletteModal.classList.add('active');
|
||||
loadPalette();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
paletteModal.classList.remove('active');
|
||||
};
|
||||
|
||||
paletteButton.addEventListener('click', openModal);
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', closeModal);
|
||||
}
|
||||
if (paletteAddButton && paletteNewColor) {
|
||||
paletteAddButton.addEventListener('click', async () => {
|
||||
const color = paletteNewColor.value;
|
||||
if (!color) {
|
||||
return;
|
||||
}
|
||||
if (currentPalette.includes(color)) {
|
||||
alert('Color already in palette.');
|
||||
return;
|
||||
}
|
||||
await savePalette([...currentPalette, color]);
|
||||
});
|
||||
}
|
||||
paletteModal.addEventListener('click', (event) => {
|
||||
if (event.target === paletteModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
86
src/static/patterns.js
Normal file
86
src/static/patterns.js
Normal file
@@ -0,0 +1,86 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const patternsButton = document.getElementById('patterns-btn');
|
||||
const patternsModal = document.getElementById('patterns-modal');
|
||||
const patternsCloseButton = document.getElementById('patterns-close-btn');
|
||||
const patternsList = document.getElementById('patterns-list');
|
||||
|
||||
if (!patternsButton || !patternsModal || !patternsList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderPatterns = (patterns) => {
|
||||
patternsList.innerHTML = '';
|
||||
const entries = Object.entries(patterns || {});
|
||||
if (!entries.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = 'No patterns found.';
|
||||
patternsList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
entries.forEach(([patternName, data]) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = patternName;
|
||||
|
||||
const details = document.createElement('span');
|
||||
const minDelay = data && data.min_delay !== undefined ? data.min_delay : '-';
|
||||
const maxDelay = data && data.max_delay !== undefined ? data.max_delay : '-';
|
||||
details.textContent = `${minDelay}–${maxDelay} ms`;
|
||||
details.style.color = '#aaa';
|
||||
details.style.fontSize = '0.85em';
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(details);
|
||||
patternsList.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const loadPatterns = async () => {
|
||||
patternsList.innerHTML = '';
|
||||
const loading = document.createElement('p');
|
||||
loading.className = 'muted-text';
|
||||
loading.textContent = 'Loading patterns...';
|
||||
patternsList.appendChild(loading);
|
||||
|
||||
try {
|
||||
const response = await fetch('/patterns', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load patterns');
|
||||
}
|
||||
const patterns = await response.json();
|
||||
renderPatterns(patterns);
|
||||
} catch (error) {
|
||||
console.error('Load patterns failed:', error);
|
||||
patternsList.innerHTML = '';
|
||||
const errorMessage = document.createElement('p');
|
||||
errorMessage.className = 'muted-text';
|
||||
errorMessage.textContent = 'Failed to load patterns.';
|
||||
patternsList.appendChild(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
patternsModal.classList.add('active');
|
||||
loadPatterns();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
patternsModal.classList.remove('active');
|
||||
};
|
||||
|
||||
patternsButton.addEventListener('click', openModal);
|
||||
if (patternsCloseButton) {
|
||||
patternsCloseButton.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
patternsModal.addEventListener('click', (event) => {
|
||||
if (event.target === patternsModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
381
src/static/presets.js
Normal file
381
src/static/presets.js
Normal file
@@ -0,0 +1,381 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetsButton = document.getElementById('presets-btn');
|
||||
const presetsModal = document.getElementById('presets-modal');
|
||||
const presetsCloseButton = document.getElementById('presets-close-btn');
|
||||
const presetsList = document.getElementById('presets-list');
|
||||
const presetsAddButton = document.getElementById('preset-add-btn');
|
||||
const presetEditorModal = document.getElementById('preset-editor-modal');
|
||||
const presetEditorCloseButton = document.getElementById('preset-editor-close-btn');
|
||||
const presetNameInput = document.getElementById('preset-name-input');
|
||||
const presetPatternInput = document.getElementById('preset-pattern-input');
|
||||
const presetColorsInput = document.getElementById('preset-colors-input');
|
||||
const presetBrightnessInput = document.getElementById('preset-brightness-input');
|
||||
const presetDelayInput = document.getElementById('preset-delay-input');
|
||||
const presetSaveButton = document.getElementById('preset-save-btn');
|
||||
const presetClearButton = document.getElementById('preset-clear-btn');
|
||||
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
|
||||
|
||||
if (!presetsButton || !presetsModal || !presetsList || !presetSaveButton || !presetClearButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentEditId = null;
|
||||
let cachedPresets = {};
|
||||
let cachedPatterns = {};
|
||||
|
||||
const getNumberInput = (id) => {
|
||||
const input = document.getElementById(id);
|
||||
if (!input) {
|
||||
return 0;
|
||||
}
|
||||
return parseInt(input.value, 10) || 0;
|
||||
};
|
||||
|
||||
const parseColors = (value) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.split(',')
|
||||
.map((color) => color.trim())
|
||||
.filter((color) => color.length > 0)
|
||||
.map((color) => (color.startsWith('#') ? color : `#${color}`));
|
||||
};
|
||||
|
||||
const setFormValues = (preset) => {
|
||||
if (!presetNameInput || !presetPatternInput || !presetColorsInput || !presetBrightnessInput || !presetDelayInput) {
|
||||
return;
|
||||
}
|
||||
presetNameInput.value = preset.name || '';
|
||||
presetPatternInput.value = preset.pattern || '';
|
||||
presetColorsInput.value = Array.isArray(preset.colors) ? preset.colors.join(',') : '';
|
||||
presetBrightnessInput.value = preset.brightness || 0;
|
||||
presetDelayInput.value = preset.delay || 0;
|
||||
document.getElementById('preset-n1-input').value = preset.n1 || 0;
|
||||
document.getElementById('preset-n2-input').value = preset.n2 || 0;
|
||||
document.getElementById('preset-n3-input').value = preset.n3 || 0;
|
||||
document.getElementById('preset-n4-input').value = preset.n4 || 0;
|
||||
document.getElementById('preset-n5-input').value = preset.n5 || 0;
|
||||
document.getElementById('preset-n6-input').value = preset.n6 || 0;
|
||||
document.getElementById('preset-n7-input').value = preset.n7 || 0;
|
||||
document.getElementById('preset-n8-input').value = preset.n8 || 0;
|
||||
};
|
||||
|
||||
const clearForm = () => {
|
||||
currentEditId = null;
|
||||
setFormValues({
|
||||
name: '',
|
||||
pattern: '',
|
||||
colors: [],
|
||||
brightness: 0,
|
||||
delay: 0,
|
||||
n1: 0,
|
||||
n2: 0,
|
||||
n3: 0,
|
||||
n4: 0,
|
||||
n5: 0,
|
||||
n6: 0,
|
||||
n7: 0,
|
||||
n8: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const openEditor = () => {
|
||||
if (presetEditorModal) {
|
||||
presetEditorModal.classList.add('active');
|
||||
}
|
||||
loadPatterns().then(() => {
|
||||
updatePresetNLabels(presetPatternInput ? presetPatternInput.value : '');
|
||||
});
|
||||
};
|
||||
|
||||
const closeEditor = () => {
|
||||
if (presetEditorModal) {
|
||||
presetEditorModal.classList.remove('active');
|
||||
}
|
||||
};
|
||||
|
||||
const buildPresetPayload = () => {
|
||||
return {
|
||||
name: presetNameInput ? presetNameInput.value.trim() : '',
|
||||
pattern: presetPatternInput ? presetPatternInput.value.trim() : '',
|
||||
colors: parseColors(presetColorsInput ? presetColorsInput.value : ''),
|
||||
brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
|
||||
delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
|
||||
n1: getNumberInput('preset-n1-input'),
|
||||
n2: getNumberInput('preset-n2-input'),
|
||||
n3: getNumberInput('preset-n3-input'),
|
||||
n4: getNumberInput('preset-n4-input'),
|
||||
n5: getNumberInput('preset-n5-input'),
|
||||
n6: getNumberInput('preset-n6-input'),
|
||||
n7: getNumberInput('preset-n7-input'),
|
||||
n8: getNumberInput('preset-n8-input'),
|
||||
};
|
||||
};
|
||||
|
||||
const loadPatterns = async () => {
|
||||
if (!presetPatternInput) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/patterns', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const patterns = await response.json();
|
||||
cachedPatterns = patterns || {};
|
||||
const entries = Object.keys(cachedPatterns);
|
||||
const desiredPattern = presetPatternInput.value;
|
||||
presetPatternInput.innerHTML = '<option value="">Pattern</option>';
|
||||
entries.forEach((patternName) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = patternName;
|
||||
option.textContent = patternName;
|
||||
presetPatternInput.appendChild(option);
|
||||
});
|
||||
if (desiredPattern && cachedPatterns[desiredPattern]) {
|
||||
presetPatternInput.value = desiredPattern;
|
||||
} else if (entries.length > 0) {
|
||||
let defaultPattern = entries[0];
|
||||
for (const patternName of entries) {
|
||||
const config = cachedPatterns[patternName];
|
||||
const hasMapping = config && Object.values(config).some((value) => {
|
||||
return typeof value === 'string' && value.startsWith('n');
|
||||
});
|
||||
if (hasMapping) {
|
||||
defaultPattern = patternName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
presetPatternInput.value = defaultPattern;
|
||||
}
|
||||
updatePresetNLabels(presetPatternInput.value);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load patterns:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePresetNLabels = (patternName) => {
|
||||
const labels = {};
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
labels[`n${i}`] = `n${i}:`;
|
||||
}
|
||||
const patternConfig = cachedPatterns && cachedPatterns[patternName];
|
||||
if (patternConfig && typeof patternConfig === 'object') {
|
||||
Object.entries(patternConfig).forEach(([label, key]) => {
|
||||
if (typeof key === 'string' && key.startsWith('n')) {
|
||||
labels[key] = `${label}:`;
|
||||
}
|
||||
});
|
||||
}
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const labelEl = document.getElementById(`preset-n${i}-label`);
|
||||
if (labelEl) {
|
||||
labelEl.textContent = labels[`n${i}`];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderPresets = (presets) => {
|
||||
presetsList.innerHTML = '';
|
||||
cachedPresets = presets || {};
|
||||
const entries = Object.entries(cachedPresets);
|
||||
if (!entries.length) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = 'No presets found.';
|
||||
presetsList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
entries.forEach(([presetId, preset]) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = (preset && preset.name) || presetId;
|
||||
|
||||
const details = document.createElement('span');
|
||||
const pattern = preset && preset.pattern ? preset.pattern : '-';
|
||||
details.textContent = pattern;
|
||||
details.style.color = '#aaa';
|
||||
details.style.fontSize = '0.85em';
|
||||
|
||||
const editButton = document.createElement('button');
|
||||
editButton.className = 'btn btn-secondary btn-small';
|
||||
editButton.textContent = 'Edit';
|
||||
editButton.addEventListener('click', () => {
|
||||
currentEditId = presetId;
|
||||
setFormValues(preset || {});
|
||||
openEditor();
|
||||
});
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'btn btn-danger btn-small';
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
const confirmed = confirm(`Delete preset "${label.textContent}"?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/presets/${presetId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete preset');
|
||||
}
|
||||
await loadPresets();
|
||||
if (currentEditId === presetId) {
|
||||
clearForm();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete preset failed:', error);
|
||||
alert('Failed to delete preset.');
|
||||
}
|
||||
});
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(details);
|
||||
row.appendChild(editButton);
|
||||
row.appendChild(deleteButton);
|
||||
presetsList.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const loadPresets = async () => {
|
||||
presetsList.innerHTML = '';
|
||||
const loading = document.createElement('p');
|
||||
loading.className = 'muted-text';
|
||||
loading.textContent = 'Loading presets...';
|
||||
presetsList.appendChild(loading);
|
||||
|
||||
try {
|
||||
const response = await fetch('/presets', {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load presets');
|
||||
}
|
||||
const presets = await response.json();
|
||||
renderPresets(presets);
|
||||
} catch (error) {
|
||||
console.error('Load presets failed:', error);
|
||||
presetsList.innerHTML = '';
|
||||
const errorMessage = document.createElement('p');
|
||||
errorMessage.className = 'muted-text';
|
||||
errorMessage.textContent = 'Failed to load presets.';
|
||||
presetsList.appendChild(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
presetsModal.classList.add('active');
|
||||
loadPresets();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
presetsModal.classList.remove('active');
|
||||
};
|
||||
|
||||
presetsButton.addEventListener('click', openModal);
|
||||
if (presetsCloseButton) {
|
||||
presetsCloseButton.addEventListener('click', closeModal);
|
||||
}
|
||||
if (presetsAddButton) {
|
||||
presetsAddButton.addEventListener('click', () => {
|
||||
clearForm();
|
||||
openEditor();
|
||||
});
|
||||
}
|
||||
if (presetEditorCloseButton) {
|
||||
presetEditorCloseButton.addEventListener('click', closeEditor);
|
||||
}
|
||||
presetClearButton.addEventListener('click', clearForm);
|
||||
if (presetPatternInput) {
|
||||
presetPatternInput.addEventListener('change', () => {
|
||||
updatePresetNLabels(presetPatternInput.value);
|
||||
});
|
||||
}
|
||||
if (presetAddFromPaletteButton) {
|
||||
presetAddFromPaletteButton.addEventListener('click', () => {
|
||||
const openButton = document.getElementById('color-palette-btn');
|
||||
if (openButton) {
|
||||
openButton.click();
|
||||
}
|
||||
const modal = document.getElementById('color-palette-modal');
|
||||
const modalList = document.getElementById('palette-container');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
if (!modalList || !presetColorsInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePick = (event) => {
|
||||
const row = event.target.closest('[data-color]');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const picked = row.dataset.color;
|
||||
if (!picked) {
|
||||
return;
|
||||
}
|
||||
const currentColors = parseColors(presetColorsInput.value);
|
||||
if (!currentColors.includes(picked)) {
|
||||
currentColors.push(picked);
|
||||
presetColorsInput.value = currentColors.join(',');
|
||||
}
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
modalList.removeEventListener('click', handlePick);
|
||||
};
|
||||
|
||||
modalList.addEventListener('click', handlePick);
|
||||
});
|
||||
}
|
||||
presetSaveButton.addEventListener('click', async () => {
|
||||
const payload = buildPresetPayload();
|
||||
if (!payload.name) {
|
||||
alert('Preset name is required.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = currentEditId ? `/presets/${currentEditId}` : '/presets';
|
||||
const method = currentEditId ? 'PUT' : 'POST';
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save preset');
|
||||
}
|
||||
await loadPresets();
|
||||
clearForm();
|
||||
closeEditor();
|
||||
} catch (error) {
|
||||
console.error('Save preset failed:', error);
|
||||
alert('Failed to save preset.');
|
||||
}
|
||||
});
|
||||
|
||||
presetsModal.addEventListener('click', (event) => {
|
||||
if (event.target === presetsModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
if (presetEditorModal) {
|
||||
presetEditorModal.addEventListener('click', (event) => {
|
||||
if (event.target === presetEditorModal) {
|
||||
closeEditor();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearForm();
|
||||
});
|
||||
186
src/static/profiles.js
Normal file
186
src/static/profiles.js
Normal file
@@ -0,0 +1,186 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const profilesButton = document.getElementById("profiles-btn");
|
||||
const profilesModal = document.getElementById("profiles-modal");
|
||||
const profilesCloseButton = document.getElementById("profiles-close-btn");
|
||||
const profilesList = document.getElementById("profiles-list");
|
||||
const newProfileInput = document.getElementById("new-profile-name");
|
||||
const createProfileButton = document.getElementById("create-profile-btn");
|
||||
|
||||
if (!profilesButton || !profilesModal || !profilesList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openModal = () => {
|
||||
profilesModal.classList.add("active");
|
||||
loadProfiles();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
profilesModal.classList.remove("active");
|
||||
};
|
||||
|
||||
const renderProfiles = (profiles, currentProfileId) => {
|
||||
profilesList.innerHTML = "";
|
||||
let entries = [];
|
||||
|
||||
if (Array.isArray(profiles)) {
|
||||
entries = profiles.map((profileId) => [profileId, {}]);
|
||||
} else if (profiles && typeof profiles === "object") {
|
||||
entries = Object.entries(profiles);
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "muted-text";
|
||||
empty.textContent = "No profiles found.";
|
||||
profilesList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach(([profileId, profile]) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "profiles-row";
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = (profile && profile.name) || profileId;
|
||||
if (String(profileId) === String(currentProfileId)) {
|
||||
label.textContent = `✓ ${label.textContent}`;
|
||||
label.style.fontWeight = "bold";
|
||||
label.style.color = "#FFD700";
|
||||
}
|
||||
|
||||
const applyButton = document.createElement("button");
|
||||
applyButton.className = "btn btn-secondary btn-small profiles-apply-btn";
|
||||
applyButton.textContent = "Apply";
|
||||
applyButton.addEventListener("click", async () => {
|
||||
try {
|
||||
const response = await fetch(`/profiles/${profileId}/apply`, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to apply profile");
|
||||
}
|
||||
await loadProfiles();
|
||||
document.body.dispatchEvent(new Event("tabs-updated"));
|
||||
} catch (error) {
|
||||
console.error("Apply profile failed:", error);
|
||||
alert("Failed to apply profile.");
|
||||
}
|
||||
});
|
||||
|
||||
const deleteButton = document.createElement("button");
|
||||
deleteButton.className = "btn btn-danger btn-small";
|
||||
deleteButton.textContent = "Delete";
|
||||
deleteButton.addEventListener("click", async () => {
|
||||
const confirmed = confirm(`Delete profile "${label.textContent}"?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/profiles/${profileId}`, {
|
||||
method: "DELETE",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to delete profile");
|
||||
}
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
console.error("Delete profile failed:", error);
|
||||
alert("Failed to delete profile.");
|
||||
}
|
||||
});
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(applyButton);
|
||||
row.appendChild(deleteButton);
|
||||
profilesList.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const loadProfiles = async () => {
|
||||
profilesList.innerHTML = "";
|
||||
const loading = document.createElement("p");
|
||||
loading.className = "muted-text";
|
||||
loading.textContent = "Loading profiles...";
|
||||
profilesList.appendChild(loading);
|
||||
|
||||
try {
|
||||
const response = await fetch("/profiles", {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load profiles");
|
||||
}
|
||||
const profiles = await response.json();
|
||||
let currentProfileId = null;
|
||||
try {
|
||||
const currentResponse = await fetch("/profiles/current", {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (currentResponse.ok) {
|
||||
const currentData = await currentResponse.json();
|
||||
currentProfileId = currentData.id || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to load current profile:", error);
|
||||
}
|
||||
renderProfiles(profiles, currentProfileId);
|
||||
} catch (error) {
|
||||
console.error("Load profiles failed:", error);
|
||||
profilesList.innerHTML = "";
|
||||
const errorMessage = document.createElement("p");
|
||||
errorMessage.className = "muted-text";
|
||||
errorMessage.textContent = "Failed to load profiles.";
|
||||
profilesList.appendChild(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const createProfile = async () => {
|
||||
if (!newProfileInput) {
|
||||
return;
|
||||
}
|
||||
const name = newProfileInput.value.trim();
|
||||
if (!name) {
|
||||
alert("Profile name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/profiles", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create profile");
|
||||
}
|
||||
newProfileInput.value = "";
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
console.error("Create profile failed:", error);
|
||||
alert("Failed to create profile.");
|
||||
}
|
||||
};
|
||||
|
||||
profilesButton.addEventListener("click", openModal);
|
||||
if (profilesCloseButton) {
|
||||
profilesCloseButton.addEventListener("click", closeModal);
|
||||
}
|
||||
if (createProfileButton) {
|
||||
createProfileButton.addEventListener("click", createProfile);
|
||||
}
|
||||
if (newProfileInput) {
|
||||
newProfileInput.addEventListener("keypress", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
createProfile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
profilesModal.addEventListener("click", (event) => {
|
||||
if (event.target === profilesModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
548
src/static/style.css
Normal file
548
src/static/style.css
Normal file
@@ -0,0 +1,548 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: #2e2e2e;
|
||||
color: white;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid #4a4a4a;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4a4a4a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #5a5a5a;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d32f2f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c62828;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
background-color: #1a1a1a;
|
||||
border-bottom: 2px solid #4a4a4a;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px 4px 0 0;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: #6a5acd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
flex: 0 0 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
border-right: 2px solid #4a4a4a;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.ids-display {
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.left-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.left-panel-toggle {
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.left-panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.left-panel.collapsed {
|
||||
flex: 0 0 48px;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.left-panel.collapsed .left-panel-body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.left-panel.collapsed .left-panel-toggle {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.controls-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
min-width: 100px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background-color: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #6a5acd;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
background-color: #7a6add;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: #6a5acd;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
background-color: #7a6add;
|
||||
}
|
||||
|
||||
/* Red slider */
|
||||
#red-slider {
|
||||
accent-color: #ff0000;
|
||||
}
|
||||
|
||||
#red-slider::-webkit-slider-thumb {
|
||||
background-color: #ff0000;
|
||||
}
|
||||
|
||||
#red-slider::-moz-range-thumb {
|
||||
background-color: #ff0000;
|
||||
}
|
||||
|
||||
/* Green slider */
|
||||
#green-slider {
|
||||
accent-color: #00ff00;
|
||||
}
|
||||
|
||||
#green-slider::-webkit-slider-thumb {
|
||||
background-color: #00ff00;
|
||||
}
|
||||
|
||||
#green-slider::-moz-range-thumb {
|
||||
background-color: #00ff00;
|
||||
}
|
||||
|
||||
/* Blue slider */
|
||||
#blue-slider {
|
||||
accent-color: #0000ff;
|
||||
}
|
||||
|
||||
#blue-slider::-webkit-slider-thumb {
|
||||
background-color: #0000ff;
|
||||
}
|
||||
|
||||
#blue-slider::-moz-range-thumb {
|
||||
background-color: #0000ff;
|
||||
}
|
||||
|
||||
/* Brightness slider */
|
||||
#brightness-slider {
|
||||
accent-color: #ffff00;
|
||||
}
|
||||
|
||||
#brightness-slider::-webkit-slider-thumb {
|
||||
background-color: #ffff00;
|
||||
}
|
||||
|
||||
#brightness-slider::-moz-range-thumb {
|
||||
background-color: #ffff00;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.n-params-section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.n-params-section h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.n-params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.n-param-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.n-param-group label {
|
||||
min-width: 40px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.n-input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.n-input:focus {
|
||||
outline: none;
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
|
||||
.patterns-section,
|
||||
.presets-section,
|
||||
.color-palette-section {
|
||||
background-color: #1a1a1a;
|
||||
border: 2px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.patterns-section h3,
|
||||
.presets-section h3,
|
||||
.color-palette-section h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.patterns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.presets-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.pattern-button {
|
||||
padding: 0.75rem;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.pattern-button:hover {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.pattern-button.active {
|
||||
background-color: #6a5acd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pattern-button.default-preset {
|
||||
border: 2px solid #6a5acd;
|
||||
}
|
||||
|
||||
.color-palette {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.color-swatch:hover {
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
|
||||
.color-swatch.selected {
|
||||
border-color: #FFD700;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.color-swatch-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #4a4a4a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-swatch-label {
|
||||
flex: 1;
|
||||
font-size: 0.9rem;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.color-picker-input {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.color-picker-input::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker-input::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.color-picker-input::-moz-color-swatch {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.palette-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #2e2e2e;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.modal-content label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-content input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modal-content input:focus {
|
||||
outline: none;
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
|
||||
262
src/static/tab_palette.js
Normal file
262
src/static/tab_palette.js
Normal file
@@ -0,0 +1,262 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let selectedIndex = null;
|
||||
|
||||
const getTab = async (tabId) => {
|
||||
const response = await fetch(`/tabs/${tabId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('No tab found');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const saveTabColors = async (tabId, colors) => {
|
||||
const response = await fetch(`/tabs/${tabId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ colors }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save tab colors');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const renderPalette = (paletteContainer, colors, onColorChange, onRemoveColor, onReorder) => {
|
||||
paletteContainer.innerHTML = '';
|
||||
if (!colors.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = 'No colors in palette.';
|
||||
paletteContainer.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
colors.forEach((color, index) => {
|
||||
const swatch = document.createElement('div');
|
||||
swatch.className = 'color-swatch';
|
||||
swatch.draggable = true;
|
||||
swatch.dataset.index = String(index);
|
||||
if (index === selectedIndex) {
|
||||
swatch.classList.add('selected');
|
||||
}
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'color-swatch-preview';
|
||||
preview.style.backgroundColor = color;
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.className = 'color-swatch-label';
|
||||
label.textContent = color;
|
||||
|
||||
const colorPicker = document.createElement('input');
|
||||
colorPicker.type = 'color';
|
||||
colorPicker.className = 'color-picker-input';
|
||||
colorPicker.value = color;
|
||||
colorPicker.addEventListener('change', async (event) => {
|
||||
const newColor = event.target.value;
|
||||
await onColorChange(index, newColor);
|
||||
});
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.className = 'btn btn-danger btn-small';
|
||||
removeButton.textContent = 'Remove';
|
||||
removeButton.addEventListener('click', async (event) => {
|
||||
event.stopPropagation();
|
||||
await onRemoveColor(index);
|
||||
});
|
||||
|
||||
swatch.addEventListener('dragstart', (event) => {
|
||||
event.dataTransfer.setData('text/plain', String(index));
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
});
|
||||
swatch.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
});
|
||||
swatch.addEventListener('drop', async (event) => {
|
||||
event.preventDefault();
|
||||
const fromIndex = parseInt(event.dataTransfer.getData('text/plain'), 10);
|
||||
const toIndex = parseInt(swatch.dataset.index || '-1', 10);
|
||||
if (Number.isNaN(fromIndex) || Number.isNaN(toIndex) || fromIndex === toIndex) {
|
||||
return;
|
||||
}
|
||||
await onReorder(fromIndex, toIndex);
|
||||
});
|
||||
|
||||
swatch.appendChild(preview);
|
||||
swatch.appendChild(label);
|
||||
swatch.appendChild(colorPicker);
|
||||
swatch.appendChild(removeButton);
|
||||
swatch.addEventListener('click', () => {
|
||||
selectedIndex = index;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
colorPicker.click();
|
||||
});
|
||||
|
||||
paletteContainer.appendChild(swatch);
|
||||
});
|
||||
};
|
||||
|
||||
const initTabPalette = async () => {
|
||||
const paletteContainer = document.getElementById('color-palette');
|
||||
const addButton = document.getElementById('tab-color-add-btn');
|
||||
const addFromPaletteButton = document.getElementById('tab-color-add-from-palette-btn');
|
||||
const colorInput = document.getElementById('tab-color-input');
|
||||
|
||||
if (!paletteContainer || !addButton || !colorInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = paletteContainer.dataset.tabId;
|
||||
if (!tabId) {
|
||||
renderPalette(paletteContainer, []);
|
||||
return;
|
||||
}
|
||||
|
||||
let tabData;
|
||||
try {
|
||||
tabData = await getTab(tabId);
|
||||
} catch (error) {
|
||||
renderPalette(paletteContainer, []);
|
||||
return;
|
||||
}
|
||||
|
||||
let colors = tabData.colors || [];
|
||||
if (!Array.isArray(colors)) {
|
||||
colors = [];
|
||||
}
|
||||
const onRemoveColor = async (index) => {
|
||||
if (index === null || index < 0 || index >= colors.length) {
|
||||
alert('Select a color to remove.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = colors.filter((_, i) => i !== index);
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = null;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
} catch (error) {
|
||||
console.error('Failed to remove color:', error);
|
||||
alert('Failed to remove color.');
|
||||
}
|
||||
};
|
||||
|
||||
const onReorder = async (fromIndex, toIndex) => {
|
||||
if (fromIndex < 0 || fromIndex >= colors.length || toIndex < 0 || toIndex >= colors.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = [...colors];
|
||||
const [moved] = updated.splice(fromIndex, 1);
|
||||
updated.splice(toIndex, 0, moved);
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = toIndex;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
} catch (error) {
|
||||
console.error('Failed to reorder colors:', error);
|
||||
alert('Failed to reorder colors.');
|
||||
}
|
||||
};
|
||||
|
||||
const onColorChange = async (index, newColor) => {
|
||||
if (!newColor || index < 0 || index >= colors.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = [...colors];
|
||||
updated[index] = newColor;
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = index;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
} catch (error) {
|
||||
console.error('Failed to update color:', error);
|
||||
alert('Failed to update color.');
|
||||
}
|
||||
};
|
||||
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
|
||||
addButton.onclick = async () => {
|
||||
const newColor = colorInput.value;
|
||||
if (!newColor) {
|
||||
return;
|
||||
}
|
||||
if (colors.includes(newColor)) {
|
||||
alert('Color already in palette.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = [...colors, newColor];
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = colors.length - 1;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
} catch (error) {
|
||||
console.error('Failed to add color:', error);
|
||||
alert('Failed to add color.');
|
||||
}
|
||||
};
|
||||
|
||||
if (addFromPaletteButton) {
|
||||
addFromPaletteButton.onclick = () => {
|
||||
const openButton = document.getElementById('color-palette-btn');
|
||||
if (openButton) {
|
||||
openButton.click();
|
||||
}
|
||||
const modal = document.getElementById('color-palette-modal');
|
||||
const modalList = document.getElementById('palette-container');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
if (!modalList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePick = async (event) => {
|
||||
const row = event.target.closest('[data-color]');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const picked = row.dataset.color;
|
||||
if (!picked) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!colors.includes(picked)) {
|
||||
const updated = [...colors, picked];
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = colors.indexOf(picked);
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
}
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add palette color:', error);
|
||||
alert('Failed to add palette color.');
|
||||
} finally {
|
||||
modalList.removeEventListener('click', handlePick);
|
||||
}
|
||||
};
|
||||
|
||||
modalList.addEventListener('click', handlePick);
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.target && event.target.id === 'tab-content') {
|
||||
selectedIndex = null;
|
||||
initTabPalette();
|
||||
}
|
||||
});
|
||||
|
||||
initTabPalette();
|
||||
});
|
||||
@@ -1,14 +1,295 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>RGB Slider Tabs</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="tabs"></div>
|
||||
<div class="tab-content"></div>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LED Controller - Tab Mode</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<header>
|
||||
<h1>LED Controller - Tab Mode</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-primary"
|
||||
hx-get="/tabs/create-form-fragment"
|
||||
hx-target="#add-tab-modal .modal-content"
|
||||
hx-swap="innerHTML"
|
||||
onclick="document.getElementById('add-tab-modal').classList.add('active')">
|
||||
+ Add Tab
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="edit-tab-btn">Edit Tab</button>
|
||||
<button class="btn btn-danger"
|
||||
hx-delete="/tabs/current"
|
||||
hx-target="#tabs-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-confirm="Are you sure you want to delete this tab?">
|
||||
Delete Tab
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="color-palette-btn">Color Palette</button>
|
||||
<button class="btn btn-secondary" id="presets-btn">Presets</button>
|
||||
<button class="btn btn-secondary" id="patterns-btn">Patterns</button>
|
||||
<button class="btn btn-secondary" id="profiles-btn">Profiles</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
<div class="main-content">
|
||||
<div class="tabs-container">
|
||||
<div id="tabs-list"
|
||||
hx-get="/tabs/list-fragment"
|
||||
hx-trigger="load, tabs-updated from:body"
|
||||
hx-swap="innerHTML">
|
||||
Loading tabs...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-content"
|
||||
class="tab-content"
|
||||
hx-get="/tabs/current"
|
||||
hx-trigger="load, tabs-updated from:body"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"Accept": "text/html"}'>
|
||||
<div style="padding: 2rem; text-align: center; color: #aaa;">
|
||||
Select a tab to get started
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Tab Modal -->
|
||||
<div id="add-tab-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Tab</h2>
|
||||
<form hx-post="/tabs"
|
||||
hx-target="#tabs-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-on::after-request="if(event.detail.successful) { document.getElementById('add-tab-modal').classList.remove('active'); document.body.dispatchEvent(new Event('tabs-updated')); }">
|
||||
<label>Tab Name:</label>
|
||||
<input type="text" name="name" placeholder="Enter tab name" required>
|
||||
<label>Device IDs (comma-separated):</label>
|
||||
<input type="text" name="ids" placeholder="1,2,3" value="1">
|
||||
<div class="modal-actions">
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="document.getElementById('add-tab-modal').classList.remove('active')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Tab Modal (placeholder for now) -->
|
||||
<div id="edit-tab-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Edit Tab</h2>
|
||||
<p>Edit functionality coming soon...</p>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" onclick="document.getElementById('edit-tab-modal').classList.remove('active')">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profiles Modal -->
|
||||
<div id="profiles-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Profiles</h2>
|
||||
<div class="profiles-actions">
|
||||
<input type="text" id="new-profile-name" placeholder="Profile name">
|
||||
<button class="btn btn-primary" id="create-profile-btn">Create</button>
|
||||
</div>
|
||||
<div id="profiles-list" class="profiles-list"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="profiles-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Presets Modal -->
|
||||
<div id="presets-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Presets</h2>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" id="preset-add-btn">Add</button>
|
||||
</div>
|
||||
<div id="presets-list" class="profiles-list"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="presets-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preset Editor Modal -->
|
||||
<div id="preset-editor-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Preset</h2>
|
||||
<div class="profiles-actions">
|
||||
<input type="text" id="preset-name-input" placeholder="Preset name">
|
||||
<select id="preset-pattern-input">
|
||||
<option value="">Pattern</option>
|
||||
</select>
|
||||
</div>
|
||||
<label>Colors (comma-separated hex)</label>
|
||||
<div class="profiles-actions">
|
||||
<input type="text" id="preset-colors-input" placeholder="#FF0000,#00FF00,#0000FF">
|
||||
<button class="btn btn-secondary" id="preset-add-from-palette-btn">Add from Palette</button>
|
||||
</div>
|
||||
<div class="profiles-actions">
|
||||
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
|
||||
<input type="number" id="preset-delay-input" placeholder="Delay" min="0" max="10000" value="0">
|
||||
</div>
|
||||
<div class="n-params-grid">
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n1-input" id="preset-n1-label">n1:</label>
|
||||
<input type="number" id="preset-n1-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n2-input" id="preset-n2-label">n2:</label>
|
||||
<input type="number" id="preset-n2-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n3-input" id="preset-n3-label">n3:</label>
|
||||
<input type="number" id="preset-n3-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n4-input" id="preset-n4-label">n4:</label>
|
||||
<input type="number" id="preset-n4-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n5-input" id="preset-n5-label">n5:</label>
|
||||
<input type="number" id="preset-n5-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n6-input" id="preset-n6-label">n6:</label>
|
||||
<input type="number" id="preset-n6-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n7-input" id="preset-n7-label">n7:</label>
|
||||
<input type="number" id="preset-n7-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
<div class="n-param-group">
|
||||
<label for="preset-n8-input" id="preset-n8-label">n8:</label>
|
||||
<input type="number" id="preset-n8-input" min="0" max="255" value="0" class="n-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" id="preset-save-btn">Save</button>
|
||||
<button class="btn btn-secondary" id="preset-clear-btn">Clear</button>
|
||||
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patterns Modal -->
|
||||
<div id="patterns-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Patterns</h2>
|
||||
<div id="patterns-list" class="profiles-list"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="patterns-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Color Palette Modal -->
|
||||
<div id="color-palette-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Color Palette</h2>
|
||||
<p class="muted-text">Profile: <span id="palette-current-profile-name">None</span></p>
|
||||
<div id="palette-container" class="profiles-list"></div>
|
||||
<div class="profiles-actions">
|
||||
<input type="color" id="palette-new-color" value="#ffffff">
|
||||
<button class="btn btn-primary" id="palette-add-color-btn">Add Color</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
}
|
||||
.modal.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #2e2e2e;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
}
|
||||
.modal-content label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.modal-content input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
}
|
||||
.profiles-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.profiles-actions input[type="text"] {
|
||||
flex: 1;
|
||||
}
|
||||
.profiles-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.profiles-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.muted-text {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.error {
|
||||
color: #d32f2f;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a1a1a;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<script src="/static/color_palette.js"></script>
|
||||
<script src="/static/profiles.js"></script>
|
||||
<script src="/static/tab_palette.js"></script>
|
||||
<script src="/static/patterns.js"></script>
|
||||
<script src="/static/presets.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
tests/models/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
tests/models/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
342
tests/web.py
Executable file
342
tests/web.py
Executable file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Local development web server - imports and runs src.main with port 5000
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
import signal
|
||||
|
||||
# Add project root, src, and lib to path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
src_path = os.path.join(project_root, 'src')
|
||||
lib_path = os.path.join(project_root, 'lib')
|
||||
|
||||
# Add to path in the right order - src must be first so 'models' and 'controllers' can be imported
|
||||
# This ensures imports like 'from models.preset import Preset' work
|
||||
sys.path.insert(0, src_path)
|
||||
sys.path.insert(0, lib_path)
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# Mock MicroPython modules before importing main
|
||||
class MockMachine:
|
||||
class WDT:
|
||||
def __init__(self, timeout):
|
||||
pass
|
||||
def feed(self):
|
||||
pass
|
||||
|
||||
class MockESPNow:
|
||||
def __init__(self):
|
||||
self.active_value = False
|
||||
self.peers = []
|
||||
self.websocket_client = None # Store single WebSocket connection
|
||||
def active(self, value):
|
||||
self.active_value = value
|
||||
print(f"[MOCK] ESPNow active: {value}")
|
||||
def add_peer(self, peer):
|
||||
self.peers.append(peer)
|
||||
if hasattr(peer, 'hex'):
|
||||
print(f"[MOCK] Added peer: {peer.hex()}")
|
||||
else:
|
||||
print(f"[MOCK] Added peer: {peer}")
|
||||
def register_websocket(self, ws):
|
||||
"""Register a WebSocket connection to forward ESPNow data to."""
|
||||
self.websocket_client = ws
|
||||
print(f"[MOCK] Registered WebSocket client")
|
||||
def unregister_websocket(self, ws):
|
||||
"""Unregister a WebSocket connection."""
|
||||
if self.websocket_client == ws:
|
||||
self.websocket_client = None
|
||||
print(f"[MOCK] Unregistered WebSocket client")
|
||||
async def asend(self, peer, data):
|
||||
if hasattr(peer, 'hex'):
|
||||
print(f"[MOCK] Would send to {peer.hex()}: {data}")
|
||||
else:
|
||||
print(f"[MOCK] Would send to {peer}: {data}")
|
||||
|
||||
# Forward data to the connected WebSocket client
|
||||
if self.websocket_client:
|
||||
try:
|
||||
await self.websocket_client.send(data)
|
||||
print(f"[MOCK] Forwarded to WebSocket client")
|
||||
except Exception as e:
|
||||
print(f"[MOCK] WebSocket client disconnected: {e}")
|
||||
self.websocket_client = None
|
||||
|
||||
class MockAIOESPNow:
|
||||
def __init__(self):
|
||||
self.espnow = MockESPNow()
|
||||
def active(self, value):
|
||||
self.espnow.active(value)
|
||||
return self.espnow
|
||||
def add_peer(self, peer):
|
||||
self.espnow.add_peer(peer)
|
||||
async def asend(self, peer, data):
|
||||
await self.espnow.asend(peer, data)
|
||||
|
||||
# Store reference to mock instance for WebSocket registration
|
||||
@property
|
||||
def mock_instance(self):
|
||||
return self.espnow
|
||||
|
||||
# Create mock ESPNow instance and store reference for WebSocket registration
|
||||
mock_espnow_instance = MockESPNow()
|
||||
mock_aioespnow = MockAIOESPNow()
|
||||
mock_aioespnow.espnow = mock_espnow_instance # Use the shared instance
|
||||
|
||||
# Create mock ESPNow instance and store reference for WebSocket registration
|
||||
mock_espnow_instance = MockESPNow()
|
||||
mock_aioespnow = MockAIOESPNow()
|
||||
mock_aioespnow.espnow = mock_espnow_instance # Use the shared instance
|
||||
|
||||
# Install mocks in sys.modules before any imports
|
||||
sys.modules['machine'] = MockMachine()
|
||||
# Store the mock instance in the module so it can be accessed
|
||||
aioespnow_module = type('module', (), {'AIOESPNow': MockAIOESPNow, '_mock_instance': mock_espnow_instance})()
|
||||
sys.modules['aioespnow'] = aioespnow_module
|
||||
class MockWLAN:
|
||||
def __init__(self, interface):
|
||||
self.interface = interface
|
||||
def active(self, value):
|
||||
print(f"[MOCK] WLAN({self.interface}) active: {value}")
|
||||
|
||||
sys.modules['network'] = type('module', (), {
|
||||
'WLAN': MockWLAN,
|
||||
'STA_IF': 0
|
||||
})()
|
||||
|
||||
# Mock asyncio.sleep_ms for regular Python
|
||||
_original_sleep = asyncio.sleep
|
||||
async def sleep_ms(ms):
|
||||
await _original_sleep(ms / 1000.0)
|
||||
|
||||
# Patch asyncio.sleep_ms
|
||||
asyncio.sleep_ms = sleep_ms
|
||||
|
||||
# Patch sys.print_exception for regular Python (MicroPython has this, regular Python doesn't)
|
||||
if not hasattr(sys, 'print_exception'):
|
||||
import traceback
|
||||
sys.print_exception = lambda e, file=None: traceback.print_exception(type(e), e, e.__traceback__, file=file)
|
||||
|
||||
# Patch builtins.open to redirect /db/ paths to project db directory
|
||||
import builtins
|
||||
_original_open = builtins.open
|
||||
def patched_open(file, mode='r', *args, **kwargs):
|
||||
if isinstance(file, str):
|
||||
if file.startswith('/db/'):
|
||||
# Redirect to project db directory
|
||||
filename = os.path.basename(file)
|
||||
file = os.path.join(project_root, 'db', filename)
|
||||
elif not os.path.isabs(file):
|
||||
# For relative paths starting with templates/ or static/,
|
||||
# always resolve to src/ directory
|
||||
if file.startswith('templates/') or file.startswith('static/'):
|
||||
file = os.path.join(src_path, file)
|
||||
# For other relative paths, check if they exist in current dir
|
||||
# If not, try src/ directory
|
||||
elif not os.path.exists(file):
|
||||
src_file = os.path.join(src_path, file)
|
||||
if os.path.exists(src_file):
|
||||
file = src_file
|
||||
return _original_open(file, mode, *args, **kwargs)
|
||||
builtins.open = patched_open
|
||||
|
||||
# Also patch os.mkdir to handle /db path
|
||||
original_mkdir = os.mkdir
|
||||
def patched_mkdir(path):
|
||||
if path == "/db":
|
||||
# Use project db directory instead
|
||||
db_path = os.path.join(project_root, "db")
|
||||
if not os.path.exists(db_path):
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
else:
|
||||
original_mkdir(path)
|
||||
os.mkdir = patched_mkdir
|
||||
|
||||
# Create a flag to stop the infinite loop
|
||||
_stop_flag = False
|
||||
|
||||
# Patch gc.collect to check stop flag
|
||||
import gc as gc_module
|
||||
_original_collect = gc_module.collect
|
||||
def collect():
|
||||
global _stop_flag
|
||||
if _stop_flag:
|
||||
raise KeyboardInterrupt("Stop requested")
|
||||
return _original_collect()
|
||||
gc_module.collect = collect
|
||||
|
||||
# Change to src directory for file paths (where templates and static are)
|
||||
# main.py expects templates/ and static/ to be relative to the working directory
|
||||
os.chdir(src_path)
|
||||
|
||||
# Override settings path for local development
|
||||
# Import settings module and patch the path before main imports it
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("settings", os.path.join(src_path, "settings.py"))
|
||||
settings_module = importlib.util.module_from_spec(spec)
|
||||
sys.modules['settings'] = settings_module
|
||||
spec.loader.exec_module(settings_module)
|
||||
settings_module.Settings.SETTINGS_FILE = os.path.join(project_root, 'settings.json')
|
||||
|
||||
# Patch the Model class file path before importing
|
||||
# We need to monkey-patch the model.py file's behavior
|
||||
import importlib.util
|
||||
model_spec = importlib.util.spec_from_file_location("models.model", os.path.join(src_path, "models", "model.py"))
|
||||
model_module = importlib.util.module_from_spec(model_spec)
|
||||
|
||||
# Patch os.mkdir in the model module's context
|
||||
original_mkdir = os.mkdir
|
||||
def patched_mkdir(path):
|
||||
if path == "/db":
|
||||
db_path = os.path.join(project_root, "db")
|
||||
if not os.path.exists(db_path):
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
else:
|
||||
original_mkdir(path)
|
||||
|
||||
# Set up the module's namespace with patched os
|
||||
model_module.__dict__['os'] = type('os', (), {'mkdir': patched_mkdir, 'path': os.path})()
|
||||
model_spec.loader.exec_module(model_module)
|
||||
sys.modules['models.model'] = model_module
|
||||
|
||||
# Now patch the Model class to fix file paths
|
||||
# The issue is that Model.__init__ sets self.file and immediately calls load()
|
||||
# before we can patch it. We need to replace __init__ completely.
|
||||
# Also clear any existing singleton instances
|
||||
Model = model_module.Model
|
||||
# Clear singleton instances for all Model subclasses
|
||||
for attr_name in dir(model_module):
|
||||
attr = getattr(model_module, attr_name)
|
||||
if isinstance(attr, type) and issubclass(attr, Model) and attr != Model:
|
||||
if hasattr(attr, '_instance'):
|
||||
delattr(attr, '_instance')
|
||||
|
||||
original_save = Model.save
|
||||
original_load = Model.load
|
||||
original_set_defaults = Model.set_defaults
|
||||
|
||||
def patched_init(self):
|
||||
# Only initialize once (check if already initialized)
|
||||
if hasattr(self, '_initialized'):
|
||||
return
|
||||
|
||||
# Create db directory if it doesn't exist (use project db, not /db)
|
||||
db_path = os.path.join(project_root, "db")
|
||||
if not os.path.exists(db_path):
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
|
||||
self.class_name = self.__class__.__name__
|
||||
# Set file path to project db directory from the start
|
||||
self.file = os.path.join(project_root, 'db', f"{self.class_name.lower()}.json")
|
||||
super(Model, self).__init__()
|
||||
|
||||
# Now call load with the correct path already set
|
||||
# Call the patched load method (defined below)
|
||||
Model.load(self)
|
||||
self._initialized = True
|
||||
|
||||
def patched_save(self):
|
||||
# Ensure file path is correct before saving (this will also fix print statements)
|
||||
if hasattr(self, 'file') and self.file.startswith('/db/'):
|
||||
filename = os.path.basename(self.file)
|
||||
self.file = os.path.join(project_root, 'db', filename)
|
||||
# Also ensure the directory exists
|
||||
db_dir = os.path.dirname(self.file)
|
||||
if not os.path.exists(db_dir):
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
return original_save(self)
|
||||
|
||||
def patched_load(self):
|
||||
# Ensure file path is correct before loading
|
||||
if hasattr(self, 'file') and self.file.startswith('/db/'):
|
||||
filename = os.path.basename(self.file)
|
||||
self.file = os.path.join(project_root, 'db', filename)
|
||||
try:
|
||||
with open(self.file, 'r') as file:
|
||||
import json
|
||||
loaded_settings = json.load(file)
|
||||
# Use dict.update() directly, not the subclass's update() method
|
||||
dict.update(self, loaded_settings)
|
||||
print(f"{self.class_name} loaded successfully.")
|
||||
except FileNotFoundError:
|
||||
# File doesn't exist yet - this is normal on first run
|
||||
print(f"No existing {self.class_name} file found, creating defaults.")
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
except Exception as e:
|
||||
# Other errors - log and create defaults
|
||||
print(f"Error loading {self.class_name}: {type(e).__name__}: {e}")
|
||||
self.set_defaults()
|
||||
self.save()
|
||||
|
||||
# Apply patches - load must be patched before init uses it
|
||||
Model.load = patched_load
|
||||
Model.__init__ = patched_init
|
||||
Model.save = patched_save
|
||||
|
||||
# Patch with_websocket decorator before importing main to register WebSocket connections
|
||||
from microdot.websocket import with_websocket as original_with_websocket
|
||||
|
||||
def patched_with_websocket(f):
|
||||
"""Patched with_websocket decorator that registers connections with mock ESPNow."""
|
||||
@original_with_websocket
|
||||
async def wrapped_handler(request, ws):
|
||||
# Register WebSocket connection with mock ESPNow
|
||||
mock_espnow_instance.register_websocket(ws)
|
||||
try:
|
||||
# Call original handler
|
||||
await f(request, ws)
|
||||
finally:
|
||||
# Unregister when connection closes
|
||||
mock_espnow_instance.unregister_websocket(ws)
|
||||
return wrapped_handler
|
||||
|
||||
# Now import main (which will use the patched settings module and model)
|
||||
# Import as a module file directly to avoid package import issues
|
||||
main_spec = importlib.util.spec_from_file_location("main", os.path.join(src_path, "main.py"))
|
||||
main_module = importlib.util.module_from_spec(main_spec)
|
||||
|
||||
# Patch with_websocket in the main module before executing it
|
||||
main_module.__dict__['with_websocket'] = patched_with_websocket
|
||||
|
||||
main_spec.loader.exec_module(main_module)
|
||||
main = main_module.main
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
"""Handle Ctrl+C gracefully."""
|
||||
global _stop_flag
|
||||
print("\nShutting down server...")
|
||||
_stop_flag = True
|
||||
# Force exit since main has an infinite loop
|
||||
sys.exit(0)
|
||||
|
||||
async def run_web():
|
||||
"""Run main with port 5000."""
|
||||
print("Starting LED Controller Web Server (Local Development)")
|
||||
print("=" * 60)
|
||||
print(f"Server will run on http://localhost:5000")
|
||||
print("Press Ctrl+C to stop")
|
||||
print("=" * 60)
|
||||
|
||||
# Set up signal handler
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
try:
|
||||
# Call main with port 5000
|
||||
await main(port=5000)
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down server...")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(run_web())
|
||||
except KeyboardInterrupt:
|
||||
print("\nExiting...")
|
||||
except SystemExit:
|
||||
pass
|
||||
Reference in New Issue
Block a user