loopwind 0.25.6 → 0.25.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/.astro/types.d.ts +1 -0
- package/app/dist/_astro/callback.Ci5gaEfJ.css +1 -0
- package/app/dist/auth/callback/index.html +81 -0
- package/app/dist/device/index.html +70 -0
- package/app/dist/index.html +327 -0
- package/app/package-lock.json +9239 -0
- package/app/package.json +23 -0
- package/app/wrangler.toml +8 -0
- package/dist/cli.js +54 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +60 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +5 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/publish.d.ts +10 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +155 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/templates.d.ts +5 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +60 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/unpublish.d.ts +5 -0
- package/dist/commands/unpublish.d.ts.map +1 -0
- package/dist/commands/unpublish.js +54 -0
- package/dist/commands/unpublish.js.map +1 -0
- package/dist/commands/whoami.d.ts +5 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +30 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api.d.ts +92 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +149 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/auth.d.ts +41 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +89 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/bundler.d.ts +18 -0
- package/dist/lib/bundler.d.ts.map +1 -0
- package/dist/lib/bundler.js +105 -0
- package/dist/lib/bundler.js.map +1 -0
- package/dist/lib/helpers.d.ts +35 -2
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/helpers.js +91 -13
- package/dist/lib/helpers.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +9 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/sdk/edge.d.ts +65 -0
- package/dist/sdk/edge.d.ts.map +1 -0
- package/dist/sdk/edge.js +359 -0
- package/dist/sdk/edge.js.map +1 -0
- package/dist/sdk/errors.d.ts +64 -0
- package/dist/sdk/errors.d.ts.map +1 -0
- package/dist/sdk/errors.js +94 -0
- package/dist/sdk/errors.js.map +1 -0
- package/dist/sdk/index.d.ts +29 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +30 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/render.d.ts +52 -0
- package/dist/sdk/render.d.ts.map +1 -0
- package/dist/sdk/render.js +432 -0
- package/dist/sdk/render.js.map +1 -0
- package/dist/sdk/types.d.ts +185 -0
- package/dist/sdk/types.d.ts.map +1 -0
- package/dist/sdk/types.js +5 -0
- package/dist/sdk/types.js.map +1 -0
- package/dist/types/template.d.ts +18 -0
- package/dist/types/template.d.ts.map +1 -1
- package/package.json +27 -4
- package/plans/PLATFORM.md +1637 -237
- package/plans/PLATFORM_IMPLEMENTATION.md +1347 -530
- package/plans/SDK.md +797 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite +0 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-shm +0 -0
- package/platform/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/ebad93a0a7be9c5768c512f3e30740b64d2b6e575277a40d77044af5ae8fd3f2.sqlite-wal +0 -0
- package/platform/migrations/0001_initial.sql +90 -0
- package/platform/package-lock.json +3253 -0
- package/platform/package.json +30 -0
- package/platform/wrangler.toml +43 -0
- package/tests-sdk/createRenderer.test.ts +251 -0
- package/tests-sdk/errors.test.ts +230 -0
- package/tests-sdk/render.test.ts +241 -0
- package/tests-sdk/tw.test.ts +277 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="astro/client" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-black:#000;--spacing:.25rem;--container-md:28rem;--container-6xl:72rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.1em;--radius-lg:.75rem;--radius-xl:1rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-background:#0a0f0d;--color-foreground:#f5faf9;--color-card:#1a2422;--color-popover:#141a19;--color-primary:#2dd4bf;--color-primary-foreground:#141a19;--color-muted:#222d2b;--color-muted-foreground:#9ea8a6;--color-accent:#283532;--color-destructive:#ef4444;--color-border:#2e3d3a;--color-input:#222d2b}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing)*0)}.top-full{top:100%}.right-0{right:calc(var(--spacing)*0)}.z-50{z-index:50}.mx-auto{margin-inline:auto}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-6{margin-top:calc(var(--spacing)*6)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing)*4)}.h-5{height:calc(var(--spacing)*5)}.h-8{height:calc(var(--spacing)*8)}.h-12{height:calc(var(--spacing)*12)}.max-h-64{max-height:calc(var(--spacing)*64)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing)*4)}.w-5{width:calc(var(--spacing)*5)}.w-8{width:calc(var(--spacing)*8)}.w-12{width:calc(var(--spacing)*12)}.w-64{width:calc(var(--spacing)*64)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.max-w-md{max-width:var(--container-md)}.flex-1{flex:1}.animate-spin{animation:var(--animate-spin)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-border{border-color:var(--color-border)}.border-destructive\/20{border-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.border-destructive\/20{border-color:color-mix(in oklab,var(--color-destructive)20%,transparent)}}.border-primary{border-color:var(--color-primary)}.border-primary\/20{border-color:#2dd4bf33}@supports (color:color-mix(in lab,red,red)){.border-primary\/20{border-color:color-mix(in oklab,var(--color-primary)20%,transparent)}}.border-t-transparent{border-top-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-background{background-color:var(--color-background)}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-card{background-color:var(--color-card)}.bg-destructive\/10{background-color:#ef44441a}@supports (color:color-mix(in lab,red,red)){.bg-destructive\/10{background-color:color-mix(in oklab,var(--color-destructive)10%,transparent)}}.bg-input{background-color:var(--color-input)}.bg-muted{background-color:var(--color-muted)}.bg-popover{background-color:var(--color-popover)}.bg-primary{background-color:var(--color-primary)}.bg-primary\/10{background-color:#2dd4bf1a}@supports (color:color-mix(in lab,red,red)){.bg-primary\/10{background-color:color-mix(in oklab,var(--color-primary)10%,transparent)}}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-6{padding-inline:calc(var(--spacing)*6)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.py-3{padding-block:calc(var(--spacing)*3)}.py-4{padding-block:calc(var(--spacing)*4)}.py-8{padding-block:calc(var(--spacing)*8)}.pt-2{padding-top:calc(var(--spacing)*2)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.text-destructive{color:var(--color-destructive)}.text-foreground{color:var(--color-foreground)}.text-muted-foreground{color:var(--color-muted-foreground)}.text-primary{color:var(--color-primary)}.text-primary-foreground{color:var(--color-primary-foreground)}.text-primary\/80{color:#2dd4bfcc}@supports (color:color-mix(in lab,red,red)){.text-primary\/80{color:color-mix(in oklab,var(--color-primary)80%,transparent)}}.uppercase{text-transform:uppercase}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.placeholder\:text-muted-foreground::placeholder{color:var(--color-muted-foreground)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-card:hover{background-color:var(--color-card)}.hover\:bg-primary\/90:hover{background-color:#2dd4bfe6}@supports (color:color-mix(in lab,red,red)){.hover\:bg-primary\/90:hover{background-color:color-mix(in oklab,var(--color-primary)90%,transparent)}}.hover\:text-destructive\/80:hover{color:#ef4444cc}@supports (color:color-mix(in lab,red,red)){.hover\:text-destructive\/80:hover{color:color-mix(in oklab,var(--color-destructive)80%,transparent)}}.hover\:text-foreground:hover{color:var(--color-foreground)}}.focus\:border-primary:focus{border-color:var(--color-primary)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (min-width:48rem){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}*{border-color:var(--color-border)}body{background-color:var(--color-background);color:var(--color-foreground);font-feature-settings:"rlig" 1,"calt" 1;font-family:system-ui,-apple-system,sans-serif}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background-color:var(--color-background)}::-webkit-scrollbar-thumb{background-color:var(--color-muted);border-radius:.25rem}::-webkit-scrollbar-thumb:hover{background-color:#9ea8a64d}@supports (color:color-mix(in lab,red,red)){::-webkit-scrollbar-thumb:hover{background-color:color-mix(in oklab,var(--color-muted-foreground)30%,transparent)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes spin{to{transform:rotate(360deg)}}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="build-version" content="1767728883264"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><title>Authentication | Loopwind</title><link rel="stylesheet" href="/_astro/callback.Ci5gaEfJ.css"></head> <body class="min-h-screen bg-background"> <div class="min-h-screen flex items-center justify-center p-4"> <div class="max-w-md w-full"> <div class="text-center mb-8"> <div class="flex items-center justify-center gap-3"> <svg width="40" height="40" viewBox="0 0 100 100" fill="none" class="text-primary"> <path d="M50 15 C20 15, 10 45, 30 65 C45 80, 70 75, 75 55 C80 35, 60 25, 50 35 C40 45, 45 60, 55 60 C65 60, 70 50, 65 42" stroke="currentColor" stroke-width="8" stroke-linecap="round" fill="none"></path> </svg> <span class="text-3xl font-bold text-foreground">Loopwind</span> </div> </div> <div class="bg-card rounded-xl border border-border p-8"> <div id="processing" class="text-center"> <div class="animate-spin w-12 h-12 border-4 border-primary border-t-transparent rounded-full mx-auto mb-4"></div> <p class="text-muted-foreground">Completing authentication...</p> </div> <div id="success" class="hidden text-center"> <div class="text-primary text-5xl mb-4">✓</div> <h2 class="text-xl font-semibold text-foreground mb-2">Success!</h2> <p class="text-muted-foreground" id="success-message">You're authenticated.</p> <a href="/" id="dashboard-link" class="inline-block mt-6 bg-primary text-primary-foreground py-2 px-6 rounded-lg hover:bg-primary/90 transition-colors">
|
|
2
|
+
Go to Dashboard
|
|
3
|
+
</a> </div> <div id="device-success" class="hidden text-center"> <div class="text-primary text-5xl mb-4">✓</div> <h2 class="text-xl font-semibold text-foreground mb-2">CLI Authorized!</h2> <p class="text-muted-foreground">You can close this window and return to your terminal.</p> </div> <div id="error" class="hidden text-center"> <div class="text-destructive text-5xl mb-4">✗</div> <h2 class="text-xl font-semibold text-foreground mb-2">Error</h2> <p class="text-muted-foreground" id="error-message"></p> </div> </div> </div> </div> </body></html> <script>(function(){const API_BASE = "https://loopwind-api.loopwind.dev";
|
|
4
|
+
|
|
5
|
+
// Read token from URL on client side (static page can't read query params at build time)
|
|
6
|
+
const params = new URLSearchParams(window.location.search);
|
|
7
|
+
const token = params.get('token');
|
|
8
|
+
const authError = params.get('error');
|
|
9
|
+
|
|
10
|
+
const processing = document.getElementById('processing');
|
|
11
|
+
const success = document.getElementById('success');
|
|
12
|
+
const deviceSuccess = document.getElementById('device-success');
|
|
13
|
+
const error = document.getElementById('error');
|
|
14
|
+
const errorMessage = document.getElementById('error-message');
|
|
15
|
+
|
|
16
|
+
// Check if there's a pending device code from /device page
|
|
17
|
+
const pendingDeviceCode = sessionStorage.getItem('device_code');
|
|
18
|
+
|
|
19
|
+
async function completeAuth() {
|
|
20
|
+
if (authError) {
|
|
21
|
+
processing.classList.add('hidden');
|
|
22
|
+
error.classList.remove('hidden');
|
|
23
|
+
errorMessage.textContent = authError;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!token) {
|
|
28
|
+
window.location.href = '/';
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
console.log('Completing auth with token:', token.substring(0, 20) + '...');
|
|
34
|
+
// Store token first
|
|
35
|
+
localStorage.setItem('loopwind_token', token);
|
|
36
|
+
console.log('Token stored in localStorage');
|
|
37
|
+
|
|
38
|
+
// If this is device flow, authorize the device code
|
|
39
|
+
if (pendingDeviceCode) {
|
|
40
|
+
sessionStorage.removeItem('device_code');
|
|
41
|
+
|
|
42
|
+
// Parse token to get user ID and org ID
|
|
43
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
44
|
+
|
|
45
|
+
// Authorize the device code
|
|
46
|
+
const authResponse = await fetch(`${API_BASE}/auth/device/authorize`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'Authorization': `Bearer ${token}`
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
user_code: pendingDeviceCode,
|
|
54
|
+
user_id: payload.sub,
|
|
55
|
+
org_id: payload.org
|
|
56
|
+
})
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!authResponse.ok) {
|
|
60
|
+
const err = await authResponse.json();
|
|
61
|
+
throw new Error(err.error || 'Failed to authorize device');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Show device success
|
|
65
|
+
processing.classList.add('hidden');
|
|
66
|
+
deviceSuccess.classList.remove('hidden');
|
|
67
|
+
} else {
|
|
68
|
+
// Regular web auth - show dashboard link immediately
|
|
69
|
+
processing.classList.add('hidden');
|
|
70
|
+
success.classList.remove('hidden');
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('Auth error:', e);
|
|
74
|
+
processing.classList.add('hidden');
|
|
75
|
+
error.classList.remove('hidden');
|
|
76
|
+
errorMessage.textContent = e.message;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
completeAuth();
|
|
81
|
+
})();</script>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="build-version" content="1767728883266"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><title>Device Authorization | Loopwind</title><link rel="stylesheet" href="/_astro/callback.Ci5gaEfJ.css"></head> <body class="min-h-screen bg-background"> <div class="min-h-screen flex items-center justify-center p-4"> <div class="max-w-md w-full"> <div class="text-center mb-8"> <div class="flex items-center justify-center gap-3"> <svg width="40" height="40" viewBox="0 0 100 100" fill="none" class="text-primary"> <path d="M50 15 C20 15, 10 45, 30 65 C45 80, 70 75, 75 55 C80 35, 60 25, 50 35 C40 45, 45 60, 55 60 C65 60, 70 50, 65 42" stroke="currentColor" stroke-width="8" stroke-linecap="round" fill="none"></path> </svg> <span class="text-3xl font-bold text-foreground">Loopwind</span> </div> <p class="text-muted-foreground mt-2">Authorize CLI Access</p> </div> <div class="bg-card rounded-xl border border-border p-8"> <div id="auth-section"> <!-- Step 1: Enter or confirm code --> <div id="code-step"> <p class="text-muted-foreground mb-4">
|
|
2
|
+
Enter the code shown in your terminal to authorize the CLI:
|
|
3
|
+
</p> <input type="text" id="user-code" placeholder="XXX-XXX" class="w-full text-center text-3xl font-mono tracking-widest p-4 bg-input border-2 border-border text-foreground rounded-lg focus:border-primary focus:outline-none uppercase placeholder:text-muted-foreground" maxlength="7"> <button id="verify-btn" class="w-full mt-6 bg-primary text-primary-foreground py-3 px-4 rounded-lg font-medium hover:bg-primary/90 transition-colors">
|
|
4
|
+
Continue with GitHub
|
|
5
|
+
</button> </div> <!-- Step 2: Authorize with GitHub --> <div id="github-step" class="hidden"> <p class="text-muted-foreground mb-4 text-center">
|
|
6
|
+
Sign in with GitHub to authorize the CLI
|
|
7
|
+
</p> <a id="github-link" href="#" class="flex items-center justify-center gap-3 w-full bg-card hover:bg-accent text-foreground py-3 px-4 rounded-lg font-medium border border-border transition-colors"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path> </svg>
|
|
8
|
+
Sign in with GitHub
|
|
9
|
+
</a> <p class="text-sm text-muted-foreground mt-4 text-center">
|
|
10
|
+
Code: <span id="display-code" class="font-mono font-bold text-foreground"></span> </p> </div> <!-- Error message --> <div id="error-msg" class="hidden mt-4 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive text-sm"></div> <!-- Success message --> <div id="success-msg" class="hidden mt-4 p-4 bg-primary/10 border border-primary/20 rounded-lg text-center"> <div class="text-primary font-medium">CLI Authorized!</div> <p class="text-primary/80 text-sm mt-1">You can close this window and return to your terminal.</p> </div> </div> </div> <p class="text-center text-muted-foreground text-sm mt-6"> <a href="https://loopwind.dev" class="hover:text-foreground transition-colors">loopwind.dev</a> </p> </div> </div> </body></html> <script>(function(){const API_BASE = "https://loopwind-api.loopwind.dev";
|
|
11
|
+
|
|
12
|
+
const codeInput = document.getElementById('user-code');
|
|
13
|
+
const verifyBtn = document.getElementById('verify-btn');
|
|
14
|
+
const codeStep = document.getElementById('code-step');
|
|
15
|
+
const githubStep = document.getElementById('github-step');
|
|
16
|
+
const githubLink = document.getElementById('github-link');
|
|
17
|
+
const displayCode = document.getElementById('display-code');
|
|
18
|
+
const errorMsg = document.getElementById('error-msg');
|
|
19
|
+
const successMsg = document.getElementById('success-msg');
|
|
20
|
+
|
|
21
|
+
// Read code from URL on client side (static page can't read at build time)
|
|
22
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
23
|
+
const initialCode = urlParams.get('code');
|
|
24
|
+
|
|
25
|
+
// Pre-fill code from URL if present
|
|
26
|
+
if (initialCode) {
|
|
27
|
+
codeInput.value = initialCode.toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Format code input (XXX-XXX)
|
|
31
|
+
codeInput.addEventListener('input', (e) => {
|
|
32
|
+
let value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
33
|
+
if (value.length > 3) {
|
|
34
|
+
value = value.slice(0, 3) + '-' + value.slice(3, 6);
|
|
35
|
+
}
|
|
36
|
+
e.target.value = value;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Verify code and show GitHub button
|
|
40
|
+
verifyBtn.addEventListener('click', async () => {
|
|
41
|
+
const code = codeInput.value.trim().toUpperCase();
|
|
42
|
+
|
|
43
|
+
if (!/^[A-Z0-9]{3}-[A-Z0-9]{3}$/.test(code)) {
|
|
44
|
+
showError('Please enter a valid code (e.g., ABC-123)');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Store code for after GitHub auth
|
|
49
|
+
sessionStorage.setItem('device_code', code);
|
|
50
|
+
|
|
51
|
+
// Show GitHub step
|
|
52
|
+
codeStep.classList.add('hidden');
|
|
53
|
+
githubStep.classList.remove('hidden');
|
|
54
|
+
displayCode.textContent = code;
|
|
55
|
+
|
|
56
|
+
// Set GitHub link - just redirect to API's GitHub auth
|
|
57
|
+
githubLink.href = `${API_BASE}/auth/github`;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function showError(msg) {
|
|
61
|
+
errorMsg.textContent = msg;
|
|
62
|
+
errorMsg.classList.remove('hidden');
|
|
63
|
+
setTimeout(() => errorMsg.classList.add('hidden'), 5000);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Auto-advance if code in URL
|
|
67
|
+
if (initialCode) {
|
|
68
|
+
setTimeout(() => verifyBtn.click(), 500);
|
|
69
|
+
}
|
|
70
|
+
})();</script>
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="build-version" content="1767728883268"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><title>Dashboard | Loopwind</title><link rel="stylesheet" href="/_astro/callback.Ci5gaEfJ.css"></head> <body class="min-h-screen bg-background"> <div id="loading" class="min-h-screen flex items-center justify-center"> <div class="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full"></div> </div> <div id="login-prompt" class="hidden min-h-screen flex items-center justify-center p-4"> <div class="max-w-md w-full text-center"> <div class="flex items-center justify-center gap-3 mb-4"> <svg width="40" height="40" viewBox="0 0 100 100" fill="none" class="text-primary"> <path d="M50 15 C20 15, 10 45, 30 65 C45 80, 70 75, 75 55 C80 35, 60 25, 50 35 C40 45, 45 60, 55 60 C65 60, 70 50, 65 42" stroke="currentColor" stroke-width="8" stroke-linecap="round" fill="none"></path> </svg> <span class="text-3xl font-bold text-foreground">Loopwind</span> </div> <p class="text-muted-foreground mb-8">Sign in to manage your templates and API keys.</p> <a href="https://loopwind-api.loopwind.dev/auth/github" class="inline-flex items-center gap-3 bg-card hover:bg-accent text-foreground py-3 px-6 rounded-lg font-medium border border-border transition-colors"> <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path> </svg>
|
|
2
|
+
Sign in with GitHub
|
|
3
|
+
</a> </div> </div> <div id="dashboard" class="hidden min-h-screen"> <!-- Header --> <header class="border-b border-border"> <div class="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between"> <a href="/" class="flex items-center gap-2"> <svg width="28" height="28" viewBox="0 0 100 100" fill="none" class="text-primary"> <path d="M50 15 C20 15, 10 45, 30 65 C45 80, 70 75, 75 55 C80 35, 60 25, 50 35 C40 45, 45 60, 55 60 C65 60, 70 50, 65 42" stroke="currentColor" stroke-width="8" stroke-linecap="round" fill="none"></path> </svg> <span class="text-xl font-bold text-foreground">Loopwind</span> </a> <div class="flex items-center gap-4"> <!-- Organization Switcher --> <div class="relative"> <button id="org-switcher" class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-card transition-colors"> <span id="org-name" class="text-foreground font-medium">Loading...</span> <svg class="w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg> </button> <div id="org-dropdown" class="hidden absolute right-0 top-full mt-1 w-64 bg-popover rounded-lg shadow-lg border border-border py-2 z-50"> <div id="org-list" class="max-h-64 overflow-y-auto"> <!-- Organizations will be inserted here --> </div> <div class="border-t border-border mt-2 pt-2 px-2"> <button id="create-org-btn" class="w-full text-left px-3 py-2 text-sm text-primary hover:bg-accent rounded-lg">
|
|
4
|
+
+ Create new organization
|
|
5
|
+
</button> </div> </div> </div> <button id="logout-btn" class="text-muted-foreground hover:text-foreground transition-colors">Sign out</button> </div> </div> </header> <!-- Content --> <main class="max-w-6xl mx-auto px-4 py-8"> <!-- Stats --> <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> <div class="bg-card rounded-lg border border-border p-6"> <div class="text-sm text-muted-foreground mb-1">Templates</div> <div id="template-count" class="text-3xl font-bold text-foreground">-</div> </div> <div class="bg-card rounded-lg border border-border p-6"> <div class="text-sm text-muted-foreground mb-1">API Keys</div> <div id="key-count" class="text-3xl font-bold text-foreground">-</div> </div> <div class="bg-card rounded-lg border border-border p-6"> <div class="text-sm text-muted-foreground mb-1">Renders (30d)</div> <div class="text-3xl font-bold text-foreground">-</div> </div> </div> <!-- Templates --> <div class="bg-card rounded-lg border border-border mb-8"> <div class="px-6 py-4 border-b border-border flex items-center justify-between"> <h2 class="text-lg font-semibold text-foreground">Templates</h2> </div> <div id="templates-list" class="p-6"> <p class="text-muted-foreground">Loading templates...</p> </div> </div> <!-- API Keys --> <div class="bg-card rounded-lg border border-border"> <div class="px-6 py-4 border-b border-border flex items-center justify-between"> <h2 class="text-lg font-semibold text-foreground">API Keys</h2> <button id="create-key-btn" class="bg-primary text-primary-foreground text-sm py-2 px-4 rounded-lg hover:bg-primary/90 transition-colors">
|
|
6
|
+
Create Key
|
|
7
|
+
</button> </div> <div id="keys-list" class="p-6"> <p class="text-muted-foreground">Loading API keys...</p> </div> </div> </main> </div> <div id="key-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"> <div class="bg-card rounded-xl border border-border shadow-xl max-w-md w-full p-6"> <h3 class="text-lg font-semibold text-foreground mb-4">Create API Key</h3> <input type="text" id="key-name" placeholder="Key name (e.g., Production)" class="w-full p-3 bg-input border border-border text-foreground rounded-lg mb-4 focus:border-primary focus:outline-none placeholder:text-muted-foreground"> <div class="flex gap-3"> <button id="cancel-key" class="flex-1 py-2 border border-border text-foreground rounded-lg hover:bg-accent transition-colors">
|
|
8
|
+
Cancel
|
|
9
|
+
</button> <button id="confirm-key" class="flex-1 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors">
|
|
10
|
+
Create
|
|
11
|
+
</button> </div> </div> </div> <div id="key-created-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"> <div class="bg-card rounded-xl border border-border shadow-xl max-w-md w-full p-6"> <h3 class="text-lg font-semibold text-foreground mb-2">API Key Created</h3> <p class="text-sm text-muted-foreground mb-4">Copy this key now. You won't be able to see it again.</p> <div class="bg-muted p-3 rounded-lg font-mono text-sm text-foreground break-all mb-4" id="new-key-value"></div> <button id="close-key-modal" class="w-full py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors">
|
|
12
|
+
Done
|
|
13
|
+
</button> </div> </div> <div id="org-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"> <div class="bg-card rounded-xl border border-border shadow-xl max-w-md w-full p-6"> <h3 class="text-lg font-semibold text-foreground mb-4">Create Organization</h3> <input type="text" id="org-name-input" placeholder="Organization name" class="w-full p-3 bg-input border border-border text-foreground rounded-lg mb-4 focus:border-primary focus:outline-none placeholder:text-muted-foreground"> <div class="flex gap-3"> <button id="cancel-org" class="flex-1 py-2 border border-border text-foreground rounded-lg hover:bg-accent transition-colors">
|
|
14
|
+
Cancel
|
|
15
|
+
</button> <button id="confirm-org" class="flex-1 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors">
|
|
16
|
+
Create
|
|
17
|
+
</button> </div> </div> </div> </body></html> <script>(function(){const API_BASE = "https://loopwind-api.loopwind.dev";
|
|
18
|
+
|
|
19
|
+
const loading = document.getElementById('loading');
|
|
20
|
+
const loginPrompt = document.getElementById('login-prompt');
|
|
21
|
+
const dashboard = document.getElementById('dashboard');
|
|
22
|
+
const orgName = document.getElementById('org-name');
|
|
23
|
+
const templateCount = document.getElementById('template-count');
|
|
24
|
+
const keyCount = document.getElementById('key-count');
|
|
25
|
+
const templatesList = document.getElementById('templates-list');
|
|
26
|
+
const keysList = document.getElementById('keys-list');
|
|
27
|
+
const orgSwitcher = document.getElementById('org-switcher');
|
|
28
|
+
const orgDropdown = document.getElementById('org-dropdown');
|
|
29
|
+
const orgList = document.getElementById('org-list');
|
|
30
|
+
|
|
31
|
+
// Modal elements
|
|
32
|
+
const keyModal = document.getElementById('key-modal');
|
|
33
|
+
const keyCreatedModal = document.getElementById('key-created-modal');
|
|
34
|
+
const keyNameInput = document.getElementById('key-name');
|
|
35
|
+
const newKeyValue = document.getElementById('new-key-value');
|
|
36
|
+
const orgModal = document.getElementById('org-modal');
|
|
37
|
+
const orgNameInput = document.getElementById('org-name-input');
|
|
38
|
+
|
|
39
|
+
let token = localStorage.getItem('loopwind_token');
|
|
40
|
+
let currentOrg = null;
|
|
41
|
+
let organizations = [];
|
|
42
|
+
|
|
43
|
+
// Helper to make API requests with org header
|
|
44
|
+
function apiHeaders() {
|
|
45
|
+
const headers = { 'Authorization': `Bearer ${token}` };
|
|
46
|
+
if (currentOrg) {
|
|
47
|
+
headers['X-Org-Id'] = currentOrg.id;
|
|
48
|
+
}
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function init() {
|
|
53
|
+
console.log('Init - token:', token ? token.substring(0, 20) + '...' : 'none');
|
|
54
|
+
|
|
55
|
+
if (!token) {
|
|
56
|
+
loading.classList.add('hidden');
|
|
57
|
+
loginPrompt.classList.remove('hidden');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Load organizations first
|
|
63
|
+
console.log('Fetching organizations...');
|
|
64
|
+
const orgsResponse = await fetch(`${API_BASE}/organizations`, {
|
|
65
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!orgsResponse.ok) {
|
|
69
|
+
throw new Error('Failed to fetch organizations');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const orgsData = await orgsResponse.json();
|
|
73
|
+
organizations = orgsData.organizations || [];
|
|
74
|
+
console.log('Organizations:', organizations);
|
|
75
|
+
|
|
76
|
+
// Get current org - prefer saved preference, fall back to token's org
|
|
77
|
+
const savedOrgId = localStorage.getItem('loopwind_current_org');
|
|
78
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
79
|
+
currentOrg = organizations.find(o => o.id === savedOrgId)
|
|
80
|
+
|| organizations.find(o => o.id === payload.org)
|
|
81
|
+
|| organizations[0];
|
|
82
|
+
|
|
83
|
+
if (!currentOrg) {
|
|
84
|
+
throw new Error('No organization found');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
orgName.textContent = currentOrg.name;
|
|
88
|
+
renderOrgList();
|
|
89
|
+
|
|
90
|
+
// Load data for current org
|
|
91
|
+
await Promise.all([loadTemplates(), loadKeys()]);
|
|
92
|
+
|
|
93
|
+
loading.classList.add('hidden');
|
|
94
|
+
dashboard.classList.remove('hidden');
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error('Init error:', e);
|
|
97
|
+
localStorage.removeItem('loopwind_token');
|
|
98
|
+
loading.classList.add('hidden');
|
|
99
|
+
loginPrompt.classList.remove('hidden');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderOrgList() {
|
|
104
|
+
orgList.innerHTML = organizations.map(org => `
|
|
105
|
+
<button
|
|
106
|
+
class="w-full text-left px-4 py-2 hover:bg-accent flex items-center justify-between ${org.id === currentOrg?.id ? 'bg-accent' : ''}"
|
|
107
|
+
data-org-id="${org.id}"
|
|
108
|
+
>
|
|
109
|
+
<span class="font-medium text-foreground">${org.name}</span>
|
|
110
|
+
${org.id === currentOrg?.id ? '<svg class="w-4 h-4 text-primary" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
|
|
111
|
+
</button>
|
|
112
|
+
`).join('');
|
|
113
|
+
|
|
114
|
+
// Add click handlers
|
|
115
|
+
orgList.querySelectorAll('button').forEach(btn => {
|
|
116
|
+
btn.addEventListener('click', () => switchOrg(btn.dataset.orgId));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function switchOrg(orgId) {
|
|
121
|
+
const org = organizations.find(o => o.id === orgId);
|
|
122
|
+
if (!org || org.id === currentOrg?.id) {
|
|
123
|
+
orgDropdown.classList.add('hidden');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
currentOrg = org;
|
|
128
|
+
orgName.textContent = org.name;
|
|
129
|
+
orgDropdown.classList.add('hidden');
|
|
130
|
+
renderOrgList();
|
|
131
|
+
|
|
132
|
+
// Store current org preference
|
|
133
|
+
localStorage.setItem('loopwind_current_org', orgId);
|
|
134
|
+
|
|
135
|
+
// Reload data for new org
|
|
136
|
+
await Promise.all([loadTemplates(), loadKeys()]);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function loadTemplates() {
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(`${API_BASE}/templates`, {
|
|
142
|
+
headers: apiHeaders()
|
|
143
|
+
});
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
const templates = data.templates || [];
|
|
146
|
+
|
|
147
|
+
templateCount.textContent = templates.length;
|
|
148
|
+
|
|
149
|
+
if (templates.length === 0) {
|
|
150
|
+
templatesList.innerHTML = `
|
|
151
|
+
<div class="text-center py-8">
|
|
152
|
+
<p class="text-muted-foreground mb-2">No templates yet</p>
|
|
153
|
+
<p class="text-sm text-muted-foreground">Run <code class="bg-muted text-foreground px-2 py-1 rounded">loopwind publish</code> to publish your first template</p>
|
|
154
|
+
</div>
|
|
155
|
+
`;
|
|
156
|
+
} else {
|
|
157
|
+
templatesList.innerHTML = templates.map(t => `
|
|
158
|
+
<div class="flex items-center justify-between py-3 border-b border-border last:border-0">
|
|
159
|
+
<div>
|
|
160
|
+
<div class="font-medium text-foreground">${t.name}</div>
|
|
161
|
+
<div class="text-sm text-muted-foreground">v${t.version}</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="text-sm text-muted-foreground">${t.updated_at ? new Date(t.updated_at).toLocaleDateString() : '-'}</div>
|
|
164
|
+
</div>
|
|
165
|
+
`).join('');
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
templatesList.innerHTML = '<p class="text-destructive">Failed to load templates</p>';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadKeys() {
|
|
173
|
+
try {
|
|
174
|
+
const response = await fetch(`${API_BASE}/keys`, {
|
|
175
|
+
headers: apiHeaders()
|
|
176
|
+
});
|
|
177
|
+
const data = await response.json();
|
|
178
|
+
const keys = data.keys || [];
|
|
179
|
+
|
|
180
|
+
keyCount.textContent = keys.length;
|
|
181
|
+
|
|
182
|
+
if (keys.length === 0) {
|
|
183
|
+
keysList.innerHTML = `
|
|
184
|
+
<div class="text-center py-8">
|
|
185
|
+
<p class="text-muted-foreground">No API keys yet</p>
|
|
186
|
+
</div>
|
|
187
|
+
`;
|
|
188
|
+
} else {
|
|
189
|
+
keysList.innerHTML = keys.map(k => `
|
|
190
|
+
<div class="flex items-center justify-between py-3 border-b border-border last:border-0">
|
|
191
|
+
<div>
|
|
192
|
+
<div class="font-medium text-foreground">${k.name}</div>
|
|
193
|
+
<div class="text-sm text-muted-foreground">Created ${new Date(k.created_at).toLocaleDateString()}</div>
|
|
194
|
+
</div>
|
|
195
|
+
<button class="delete-key text-destructive hover:text-destructive/80 text-sm transition-colors" data-id="${k.id}">
|
|
196
|
+
Delete
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
`).join('');
|
|
200
|
+
|
|
201
|
+
// Add delete handlers
|
|
202
|
+
keysList.querySelectorAll('.delete-key').forEach(btn => {
|
|
203
|
+
btn.addEventListener('click', () => deleteKey(btn.dataset.id));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
keysList.innerHTML = '<p class="text-destructive">Failed to load keys</p>';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function createKey(name) {
|
|
212
|
+
const response = await fetch(`${API_BASE}/keys`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { ...apiHeaders(), 'Content-Type': 'application/json' },
|
|
215
|
+
body: JSON.stringify({ name })
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error('Failed to create key');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return response.json();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function deleteKey(id) {
|
|
226
|
+
if (!confirm('Delete this API key? This cannot be undone.')) return;
|
|
227
|
+
|
|
228
|
+
await fetch(`${API_BASE}/keys/${id}`, {
|
|
229
|
+
method: 'DELETE',
|
|
230
|
+
headers: apiHeaders()
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
loadKeys();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function createOrg(name) {
|
|
237
|
+
const response = await fetch(`${API_BASE}/organizations`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { ...apiHeaders(), 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({ name })
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
const err = await response.json();
|
|
245
|
+
throw new Error(err.error || 'Failed to create organization');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return response.json();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Event listeners
|
|
252
|
+
document.getElementById('logout-btn').addEventListener('click', () => {
|
|
253
|
+
localStorage.removeItem('loopwind_token');
|
|
254
|
+
localStorage.removeItem('loopwind_current_org');
|
|
255
|
+
window.location.reload();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Org switcher toggle
|
|
259
|
+
orgSwitcher.addEventListener('click', (e) => {
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
orgDropdown.classList.toggle('hidden');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Close dropdown when clicking outside
|
|
265
|
+
document.addEventListener('click', () => {
|
|
266
|
+
orgDropdown.classList.add('hidden');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Create org
|
|
270
|
+
document.getElementById('create-org-btn').addEventListener('click', () => {
|
|
271
|
+
orgDropdown.classList.add('hidden');
|
|
272
|
+
orgNameInput.value = '';
|
|
273
|
+
orgModal.classList.remove('hidden');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
document.getElementById('cancel-org').addEventListener('click', () => {
|
|
277
|
+
orgModal.classList.add('hidden');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
document.getElementById('confirm-org').addEventListener('click', async () => {
|
|
281
|
+
const name = orgNameInput.value.trim();
|
|
282
|
+
if (!name) return;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const newOrg = await createOrg(name);
|
|
286
|
+
organizations.push(newOrg);
|
|
287
|
+
renderOrgList();
|
|
288
|
+
orgModal.classList.add('hidden');
|
|
289
|
+
// Switch to the new org
|
|
290
|
+
await switchOrg(newOrg.id);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
alert(e.message);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Create key
|
|
297
|
+
document.getElementById('create-key-btn').addEventListener('click', () => {
|
|
298
|
+
keyNameInput.value = '';
|
|
299
|
+
keyModal.classList.remove('hidden');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
document.getElementById('cancel-key').addEventListener('click', () => {
|
|
303
|
+
keyModal.classList.add('hidden');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
document.getElementById('confirm-key').addEventListener('click', async () => {
|
|
307
|
+
const name = keyNameInput.value.trim();
|
|
308
|
+
if (!name) return;
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await createKey(name);
|
|
312
|
+
keyModal.classList.add('hidden');
|
|
313
|
+
newKeyValue.textContent = result.key;
|
|
314
|
+
keyCreatedModal.classList.remove('hidden');
|
|
315
|
+
loadKeys();
|
|
316
|
+
} catch (e) {
|
|
317
|
+
alert('Failed to create key');
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
document.getElementById('close-key-modal').addEventListener('click', () => {
|
|
322
|
+
keyCreatedModal.classList.add('hidden');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Initialize
|
|
326
|
+
init();
|
|
327
|
+
})();</script>
|