From 35296b3bd208e7c23bacd171cda3148d494a2562 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Wed, 22 Oct 2025 22:28:02 +0000 Subject: [PATCH] Major update to Apps page. Add instance switcher. --- package.json | 2 + pnpm-lock.yaml | 706 +++++++++++++++++++++++- src/components/AppSidebar.tsx | 7 +- src/components/AppsComponent.tsx | 194 +++++-- src/components/InstanceSwitcher.tsx | 216 ++++++++ src/components/apps/AppConfigDialog.tsx | 9 + src/components/apps/AppDetailModal.tsx | 606 ++++++++++++++++++++ src/hooks/useApps.ts | 55 ++ src/index.css | 1 + src/services/api/apps.ts | 43 ++ src/services/api/types/app.ts | 88 +++ 11 files changed, 1882 insertions(+), 45 deletions(-) create mode 100644 src/components/InstanceSwitcher.tsx create mode 100644 src/components/apps/AppDetailModal.tsx diff --git a/package.json b/package.json index 20084ce..e8a9eed 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.58.1", + "react-markdown": "^10.1.0", "react-router": "^7.9.4", "react-router-dom": "^7.9.4", "tailwind-merge": "^3.3.1", @@ -40,6 +41,7 @@ }, "devDependencies": { "@eslint/js": "^9.25.0", + "@tailwindcss/typography": "^0.5.19", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@types/node": "^24.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167ca49..6104b86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: react-hook-form: specifier: ^7.58.1 version: 7.58.1(react@19.1.0) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.1.8)(react@19.1.0) react-router: specifier: ^7.9.4 version: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -75,6 +78,9 @@ importers: '@eslint/js': specifier: ^9.25.0 version: 9.29.0 + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.1.10) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -122,7 +128,7 @@ importers: version: 6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.0.3)(jiti@2.4.2)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1) packages: @@ -1084,6 +1090,11 @@ packages: resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==} engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.10': resolution: {integrity: sha512-QWnD5HDY2IADv+vYR82lOhqOlS1jSCUUAmfem52cXAhRTKxpDh3ARX8TTXJTCCO7Rv7cD2Nlekabv02bwP3a2A==} peerDependencies: @@ -1138,18 +1149,33 @@ packages: '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@24.0.3': resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} @@ -1161,6 +1187,12 @@ packages: '@types/react@19.1.8': resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.34.1': resolution: {integrity: sha512-STXcN6ebF6li4PxwNeFnqF8/2BNDvBupf2OPx2yWNzr6mKNGF7q49VM00Pz5FaomJyqvbXpY6PhO+T9w139YEQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1220,6 +1252,9 @@ packages: resolution: {integrity: sha512-xoh5rJ+tgsRKoXnkBPFRLZ7rjKM0AfVbC68UZ/ECXoDbfggb9RbEySN359acY1vS3qZ0jVTVWzbtfapwm5ztxw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitejs/plugin-react@4.5.2': resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1302,6 +1337,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1334,6 +1372,9 @@ packages: caniuse-lite@1.0.30001723: resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1342,6 +1383,18 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -1364,6 +1417,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1385,6 +1441,11 @@ packages: css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + cssstyle@5.3.1: resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==} engines: {node: '>=20'} @@ -1408,6 +1469,9 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-named-character-reference@1.2.0: + resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1426,6 +1490,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -1508,6 +1575,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1519,6 +1589,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1605,10 +1678,19 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1641,6 +1723,18 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1649,10 +1743,17 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1779,6 +1880,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -1801,6 +1905,30 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -1808,6 +1936,69 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1866,6 +2057,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1895,6 +2089,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1907,6 +2105,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1928,6 +2129,12 @@ packages: react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} @@ -1987,6 +2194,12 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2047,12 +2260,18 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -2064,6 +2283,12 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + style-to-js@1.1.18: + resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} + + style-to-object@1.0.11: + resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2126,6 +2351,12 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -2157,6 +2388,24 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -2186,6 +2435,15 @@ packages: '@types/react': optional: true + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2326,6 +2584,9 @@ packages: zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -3151,6 +3412,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.10 '@tailwindcss/oxide-win32-x64-msvc': 4.1.10 + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.10)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.10 + '@tailwindcss/vite@4.1.10(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.10 @@ -3222,14 +3488,32 @@ snapshots: dependencies: '@types/deep-eql': 4.0.2 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.7': {} '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@24.0.3': dependencies: undici-types: 7.8.0 @@ -3242,6 +3526,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + '@typescript-eslint/eslint-plugin@8.34.1(@typescript-eslint/parser@8.34.1(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.29.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -3334,6 +3622,8 @@ snapshots: '@typescript-eslint/types': 8.34.1 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1))': dependencies: '@babel/core': 7.27.4 @@ -3425,6 +3715,8 @@ snapshots: assertion-error@2.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} bidi-js@1.0.3: @@ -3457,6 +3749,8 @@ snapshots: caniuse-lite@1.0.30001723: {} + ccount@2.0.1: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3470,6 +3764,14 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + check-error@2.1.1: {} chownr@3.0.0: {} @@ -3486,6 +3788,8 @@ snapshots: color-name@1.1.4: {} + comma-separated-tokens@2.0.3: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -3505,6 +3809,8 @@ snapshots: css.escape@1.5.1: {} + cssesc@3.0.0: {} + cssstyle@5.3.1(postcss@8.5.6): dependencies: '@asamuzakjp/css-color': 4.0.5 @@ -3526,6 +3832,10 @@ snapshots: decimal.js@10.6.0: {} + decode-named-character-reference@1.2.0: + dependencies: + character-entities: 2.0.2 + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -3536,6 +3846,10 @@ snapshots: detect-node-es@1.1.0: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -3658,6 +3972,8 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -3666,6 +3982,8 @@ snapshots: expect-type@1.2.2: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3735,10 +4053,36 @@ snapshots: has-flag@4.0.0: {} + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.18 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3770,14 +4114,29 @@ snapshots: indent-string@4.0.0: {} + inline-style-parser@0.2.4: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} isexe@2.0.0: {} @@ -3890,6 +4249,8 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + loupe@3.2.1: {} lru-cache@11.2.2: {} @@ -3908,10 +4269,232 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdn-data@2.12.2: {} merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.2.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.1 + decode-named-character-reference: 1.2.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3964,6 +4547,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -3982,6 +4575,11 @@ snapshots: picomatch@4.0.2: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -3996,6 +4594,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + property-information@7.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4011,6 +4611,24 @@ snapshots: react-is@17.0.2: {} + react-markdown@10.1.0(@types/react@19.1.8)(react@19.1.0): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.1.8 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 + react: 19.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + react-refresh@0.17.0: {} react-remove-scroll-bar@2.3.8(@types/react@19.1.8)(react@19.1.0): @@ -4061,6 +4679,23 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -4123,10 +4758,17 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + stackback@0.0.2: {} std-env@3.9.0: {} + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -4137,6 +4779,14 @@ snapshots: dependencies: js-tokens: 9.0.1 + style-to-js@1.1.18: + dependencies: + style-to-object: 1.0.11 + + style-to-object@1.0.11: + dependencies: + inline-style-parser: 0.2.4 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -4191,6 +4841,10 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.1.0(typescript@5.8.3): dependencies: typescript: 5.8.3 @@ -4217,6 +4871,39 @@ snapshots: undici-types@7.8.0: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.1.3(browserslist@4.25.0): dependencies: browserslist: 4.25.0 @@ -4242,6 +4929,18 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 + util-deprecate@1.0.2: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + vite-node@3.2.4(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1): dependencies: cac: 6.7.14 @@ -4277,7 +4976,7 @@ snapshots: jiti: 2.4.2 lightningcss: 1.30.1 - vitest@3.2.4(@types/node@24.0.3)(jiti@2.4.2)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -4303,6 +5002,7 @@ snapshots: vite-node: 3.2.4(@types/node@24.0.3)(jiti@2.4.2)(lightningcss@1.30.1) why-is-node-running: 2.3.0 optionalDependencies: + '@types/debug': 4.1.12 '@types/node': 24.0.3 jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: @@ -4360,3 +5060,5 @@ snapshots: yocto-queue@0.1.0: {} zod@3.25.67: {} + + zwitch@2.0.4: {} diff --git a/src/components/AppSidebar.tsx b/src/components/AppSidebar.tsx index 9778600..d0006e0 100644 --- a/src/components/AppSidebar.tsx +++ b/src/components/AppSidebar.tsx @@ -16,6 +16,7 @@ import { } from './ui/sidebar'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; import { useTheme } from '../contexts/ThemeContext'; +import { InstanceSwitcher } from './InstanceSwitcher'; export function AppSidebar() { const { theme, setTheme } = useTheme(); @@ -61,15 +62,17 @@ export function AppSidebar() { return ( -
+

Wild Cloud

-

{instanceId}

+
+ +
diff --git a/src/components/AppsComponent.tsx b/src/components/AppsComponent.tsx index 82b9ff0..1863b59 100644 --- a/src/components/AppsComponent.tsx +++ b/src/components/AppsComponent.tsx @@ -20,17 +20,22 @@ import { Archive, RotateCcw, Settings, + Eye, } from 'lucide-react'; import { useInstanceContext } from '../hooks/useInstanceContext'; import { useAvailableApps, useDeployedApps, useAppBackups } from '../hooks/useApps'; import { BackupRestoreModal } from './BackupRestoreModal'; import { AppConfigDialog } from './apps/AppConfigDialog'; +import { AppDetailModal } from './apps/AppDetailModal'; import type { App } from '../services/api'; interface MergedApp extends App { deploymentStatus?: 'added' | 'deployed'; + url?: string; } +type TabView = 'available' | 'installed'; + export function AppsComponent() { const { currentInstance } = useInstanceContext(); const { data: availableAppsData, isLoading: loadingAvailable, error: availableError } = useAvailableApps(); @@ -46,6 +51,7 @@ export function AppsComponent() { isDeleting } = useDeployedApps(currentInstance); + const [activeTab, setActiveTab] = useState('available'); const [searchTerm, setSearchTerm] = useState(''); const [selectedCategory, setSelectedCategory] = useState('all'); const [configDialogOpen, setConfigDialogOpen] = useState(false); @@ -53,6 +59,8 @@ export function AppsComponent() { const [backupModalOpen, setBackupModalOpen] = useState(false); const [restoreModalOpen, setRestoreModalOpen] = useState(false); const [selectedAppForBackup, setSelectedAppForBackup] = useState(null); + const [detailModalOpen, setDetailModalOpen] = useState(false); + const [selectedAppForDetail, setSelectedAppForDetail] = useState(null); // Fetch backups for the selected app const { @@ -64,18 +72,27 @@ export function AppsComponent() { isRestoring, } = useAppBackups(currentInstance, selectedAppForBackup); - // Merge available and deployed apps - // DeployedApps now includes status: 'added' | 'deployed' + // Merge available and deployed apps with URL from deployment const applications: MergedApp[] = (availableAppsData?.apps || []).map(app => { const deployedApp = deployedApps.find(d => d.name === app.name); return { ...app, - deploymentStatus: deployedApp?.status as 'added' | 'deployed' | undefined, // 'added' or 'deployed' from API + deploymentStatus: deployedApp?.status as 'added' | 'deployed' | undefined, + url: deployedApp?.url, }; }); const isLoading = loadingAvailable || loadingDeployed; + // Filter for available apps (not added or deployed) + const availableApps = applications.filter(app => !app.deploymentStatus); + + // Filter for installed apps (added or deployed) + const installedApps = applications.filter(app => app.deploymentStatus); + + // Count running apps - apps that are deployed (not just added) + const runningApps = installedApps.filter(app => app.deploymentStatus === 'deployed').length; + const getStatusIcon = (status?: string) => { switch (status) { case 'running': @@ -88,6 +105,8 @@ export function AppsComponent() { return ; case 'added': return ; + case 'deployed': + return ; case 'available': return ; default: @@ -145,12 +164,37 @@ export function AppsComponent() { } }; - const handleAppAction = (app: MergedApp, action: 'configure' | 'deploy' | 'delete' | 'backup' | 'restore') => { + // Separate component for app icon with error handling + const AppIcon = ({ app }: { app: MergedApp }) => { + const [imageError, setImageError] = useState(false); + + return ( +
+ {app.icon && !imageError ? ( + {app.name} setImageError(true)} + /> + ) : ( + getCategoryIcon(app.category) + )} +
+ ); + }; + + const handleAppAction = (app: MergedApp, action: 'configure' | 'deploy' | 'delete' | 'backup' | 'restore' | 'view') => { if (!currentInstance) return; switch (action) { case 'configure': - // Open config dialog for adding or reconfiguring app + console.log('[AppsComponent] Configuring app:', { + name: app.name, + hasDefaultConfig: !!app.defaultConfig, + defaultConfigKeys: app.defaultConfig ? Object.keys(app.defaultConfig) : [], + fullApp: app, + }); setSelectedAppForConfig(app); setConfigDialogOpen(true); break; @@ -170,19 +214,21 @@ export function AppsComponent() { setSelectedAppForBackup(app.name); setRestoreModalOpen(true); break; + case 'view': + setSelectedAppForDetail(app.name); + setDetailModalOpen(true); + break; } }; const handleConfigSave = (config: Record) => { if (!selectedAppForConfig) return; - // Call addApp with the configuration addApp({ name: selectedAppForConfig.name, config: config, }); - // Close dialog setConfigDialogOpen(false); setSelectedAppForConfig(null); }; @@ -199,15 +245,15 @@ export function AppsComponent() { const categories = ['all', 'database', 'web', 'security', 'monitoring', 'communication', 'storage']; - const filteredApps = applications.filter(app => { + const appsToDisplay = activeTab === 'available' ? availableApps : installedApps; + + const filteredApps = appsToDisplay.filter(app => { const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) || app.description.toLowerCase().includes(searchTerm.toLowerCase()); const matchesCategory = selectedCategory === 'all' || app.category === selectedCategory; return matchesSearch && matchesCategory; }); - const runningApps = applications.filter(app => app.status?.status === 'running').length; - // Show message if no instance is selected if (!currentInstance) { return ( @@ -248,12 +294,12 @@ export function AppsComponent() { What are Apps in your Personal Cloud?

- Apps are the useful programs that make your personal cloud valuable - like having a personal Netflix - (media server), Google Drive (file storage), or Gmail (email server) running on your own hardware. + Apps are the useful programs that make your personal cloud valuable - like having a personal Netflix + (media server), Google Drive (file storage), or Gmail (email server) running on your own hardware. Instead of relying on big tech companies, you control your data and services.

- Your cluster can run databases, web servers, photo galleries, password managers, backup services, and much more. + Your cluster can run databases, web servers, photo galleries, password managers, backup services, and much more. Each app runs in its own secure container, so they don't interfere with each other and can be easily managed.

+ {/* Tab Navigation */} +
+ + +
+
@@ -288,14 +350,14 @@ export function AppsComponent() { className="w-full pl-10 pr-4 py-2 border rounded-lg bg-background" />
-
+
{categories.map(category => ( @@ -311,7 +373,7 @@ export function AppsComponent() { Loading apps... ) : ( - `${runningApps} applications running • ${applications.length} total available` + `${runningApps} applications running • ${installedApps.length} installed • ${availableApps.length} available` )}
@@ -322,14 +384,45 @@ export function AppsComponent() {

Loading applications...

+ ) : activeTab === 'available' ? ( + // Available Apps Grid +
+ {filteredApps.map((app) => ( + +
+
+ +
+
+

{app.name}

+
+ {app.version && ( + + {app.version} + + )} +
+
+

{app.description}

+ +
+
+ ))} +
) : ( -
+ // Installed Apps List +
{filteredApps.map((app) => (
-
- {getCategoryIcon(app.category)} -
+

{app.name}

@@ -338,10 +431,23 @@ export function AppsComponent() { {app.version} )} - {getStatusIcon(app.status?.status)} + {getStatusIcon(app.status?.status || app.deploymentStatus)}

{app.description}

+ {/* Show ingress URL if available */} + {app.url && ( + + + {app.url} + + )} + {app.status?.status === 'running' && (
{app.status.namespace && ( @@ -350,31 +456,14 @@ export function AppsComponent() { {app.status.replicas && (
Replicas: {app.status.replicas}
)} - {app.status.resources && ( -
- Resources: {app.status.resources.cpu} CPU, {app.status.resources.memory} RAM -
- )}
)} - {app.status?.message && ( -

{app.status.message}

- )}
{getStatusBadge(app)}
- {/* Available: not added yet */} - {!app.deploymentStatus && ( - - )} + {/* Available: not added yet - shouldn't show here */} {/* Added: in config but not deployed */} {app.deploymentStatus === 'added' && ( @@ -408,6 +497,14 @@ export function AppsComponent() { {/* Deployed: running in Kubernetes */} {app.deploymentStatus === 'deployed' && ( <> + {app.status?.status === 'running' && ( <>
); -} \ No newline at end of file +} diff --git a/src/components/InstanceSwitcher.tsx b/src/components/InstanceSwitcher.tsx new file mode 100644 index 0000000..100e872 --- /dev/null +++ b/src/components/InstanceSwitcher.tsx @@ -0,0 +1,216 @@ +import { useState } from 'react'; +import { useNavigate, useLocation, useParams } from 'react-router'; +import { Plus } from 'lucide-react'; +import { useInstances } from '../hooks/useInstances'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + SelectSeparator, +} from './ui/select'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; +import { Label } from './ui/label'; + +const ADD_INSTANCE_VALUE = '__add_new__'; + +export function InstanceSwitcher() { + const navigate = useNavigate(); + const location = useLocation(); + const { instanceId } = useParams<{ instanceId: string }>(); + const { instances, isLoading, error, createInstance, isCreating } = useInstances(); + + const [dialogOpen, setDialogOpen] = useState(false); + const [newInstanceName, setNewInstanceName] = useState(''); + + const handleInstanceChange = (value: string) => { + // Check if user selected "Add new instance" + if (value === ADD_INSTANCE_VALUE) { + setDialogOpen(true); + return; + } + + if (!instanceId) return; + + // Extract the page path after /instances/:instanceId + const instancePrefix = `/instances/${instanceId}`; + const pagePath = location.pathname.startsWith(instancePrefix) + ? location.pathname.slice(instancePrefix.length) + : '/dashboard'; + + // Navigate to the same page in the new instance + navigate(`/instances/${value}${pagePath || '/dashboard'}`); + }; + + const handleCreateInstance = (e: React.FormEvent) => { + e.preventDefault(); + if (!newInstanceName.trim()) return; + + createInstance( + { name: newInstanceName.trim() }, + { + onSuccess: () => { + setDialogOpen(false); + setNewInstanceName(''); + // Navigate to the new instance's dashboard + navigate(`/instances/${newInstanceName.trim()}/dashboard`); + }, + } + ); + }; + + // Loading state + if (isLoading) { + return ( + + ); + } + + // Error state + if (error) { + return ( + + ); + } + + // No instances state - show dialog immediately + if (!instances || instances.length === 0) { + return ( + <> + + + + +
+ + Create New Instance + + Enter a name for your new Wild Cloud instance. + + +
+
+ + setNewInstanceName(e.target.value)} + disabled={isCreating} + autoFocus + /> +
+
+ + + + +
+
+
+ + ); + } + + return ( + <> + + + + +
+ + Create New Instance + + Enter a name for your new Wild Cloud instance. + + +
+
+ + setNewInstanceName(e.target.value)} + disabled={isCreating} + autoFocus + /> +
+
+ + + + +
+
+
+ + ); +} diff --git a/src/components/apps/AppConfigDialog.tsx b/src/components/apps/AppConfigDialog.tsx index 7d9388c..8362984 100644 --- a/src/components/apps/AppConfigDialog.tsx +++ b/src/components/apps/AppConfigDialog.tsx @@ -37,6 +37,15 @@ export function AppConfigDialog({ if (app && open) { const initialConfig: Record = {}; + // Debug logging to diagnose the issue + console.log('[AppConfigDialog] App data:', { + name: app.name, + hasDefaultConfig: !!app.defaultConfig, + defaultConfigKeys: app.defaultConfig ? Object.keys(app.defaultConfig) : [], + hasExistingConfig: !!existingConfig, + existingConfigKeys: existingConfig ? Object.keys(existingConfig) : [], + }); + // Start with default config if (app.defaultConfig) { Object.entries(app.defaultConfig).forEach(([key, value]) => { diff --git a/src/components/apps/AppDetailModal.tsx b/src/components/apps/AppDetailModal.tsx new file mode 100644 index 0000000..23334ea --- /dev/null +++ b/src/components/apps/AppDetailModal.tsx @@ -0,0 +1,606 @@ +import { useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useAppEnhanced, useAppLogs, useAppEvents, useAppReadme } from '@/hooks/useApps'; +import { + RefreshCw, + Eye, + Settings, + Activity, + FileText, + ExternalLink, + AlertCircle, + CheckCircle, +} from 'lucide-react'; + +interface AppDetailModalProps { + instanceName: string; + appName: string; + open: boolean; + onClose: () => void; +} + +type ViewMode = 'overview' | 'configuration' | 'status' | 'logs'; + +export function AppDetailModal({ + instanceName, + appName, + open, + onClose, +}: AppDetailModalProps) { + const [viewMode, setViewMode] = useState('overview'); + const [showSecrets, setShowSecrets] = useState(false); + const [logParams, setLogParams] = useState({ tail: 100, sinceSeconds: 3600 }); + + const { data: appDetails, isLoading, refetch } = useAppEnhanced(instanceName, appName); + const { data: logs, refetch: refetchLogs } = useAppLogs( + instanceName, + appName, + viewMode === 'logs' ? logParams : undefined + ); + const { data: eventsData } = useAppEvents(instanceName, appName, 20); + const { data: readmeContent, isLoading: readmeLoading } = useAppReadme(instanceName, appName); + + const getPodStatusColor = (status: string) => { + if (status.toLowerCase().includes('running')) return 'text-green-600 dark:text-green-400'; + if (status.toLowerCase().includes('pending')) return 'text-yellow-600 dark:text-yellow-400'; + if (status.toLowerCase().includes('failed')) return 'text-red-600 dark:text-red-400'; + return 'text-muted-foreground'; + }; + + const getStatusBadge = (status: string) => { + const variants: Record = { + running: 'success', + error: 'destructive', + deploying: 'outline', + stopped: 'warning', + added: 'outline', + deployed: 'outline', + }; + + return ( + + {status.charAt(0).toUpperCase() + status.slice(1)} + + ); + }; + + return ( + !open && onClose()}> + + + + {appName} + {appDetails && getStatusBadge(appDetails.status)} + + + {appDetails?.description || 'Application details and configuration'} + + + + {/* View Mode Selector */} +
+ + + + +
+ + {/* Overview Tab */} + {viewMode === 'overview' && ( +
+
+ +
+ + {isLoading ? ( +
+ + +
+ ) : appDetails ? ( + <> + + + Application Information + + +
+
+

Name

+

{appDetails.name}

+
+
+

Version

+

{appDetails.version || 'N/A'}

+
+
+

Namespace

+

{appDetails.namespace}

+
+
+

Status

+

{appDetails.status}

+
+
+ + {appDetails.url && ( + + )} + + {appDetails.description && ( +
+

Description

+

{appDetails.description}

+
+ )} + + {appDetails.manifest?.dependencies && appDetails.manifest.dependencies.length > 0 && ( +
+

Dependencies

+
+ {appDetails.manifest.dependencies.map((dep) => ( + + {dep} + + ))} +
+
+ )} +
+
+ + {/* README Documentation */} + {readmeContent && ( + + + + + README + + + + {readmeLoading ? ( + + ) : ( +
+ { + return inline ? ( + + {children} + + ) : ( + + {children} + + ); + }, + // Make links open in new tab + a: ({node, children, href, ...props}) => ( + + {children} + + ), + }} + > + {readmeContent} + +
+ )} +
+
+ )} + + ) : ( +

No information available

+ )} +
+ )} + + {/* Configuration Tab */} + {viewMode === 'configuration' && ( +
+
+ +
+ + {isLoading ? ( +
+ + +
+ ) : appDetails ? ( + <> + {/* Configuration Values */} + {((appDetails.config && Object.keys(appDetails.config).length > 0) || + (appDetails.manifest?.defaultConfig && Object.keys(appDetails.manifest.defaultConfig).length > 0)) && ( + + + Configuration + Current configuration values + + +
+ {Object.entries(appDetails.config || appDetails.manifest?.defaultConfig || {}).map(([key, value]) => ( +
+ {key}: + + {typeof value === 'object' && value !== null + ? JSON.stringify(value, null, 2) + : String(value)} + +
+ ))} +
+
+
+ )} + + {/* Secrets */} + {appDetails.manifest?.requiredSecrets && appDetails.manifest.requiredSecrets.length > 0 && ( + + + + Secrets + + + Sensitive configuration values (redacted) + + +
+ {appDetails.manifest.requiredSecrets.map((secret) => ( +
+ {secret}: + + {showSecrets ? '**hidden**' : '••••••••'} + +
+ ))} +
+
+
+ )} + + ) : ( +

No configuration available

+ )} +
+ )} + + {/* Status Tab */} + {viewMode === 'status' && ( +
+
+ +
+ + {isLoading ? ( +
+ + +
+ ) : appDetails?.runtime ? ( + <> + {/* Replicas */} + {appDetails.runtime.replicas && ( + + + Replicas + + +
+
+

Desired

+

{appDetails.runtime.replicas.desired}

+
+
+

Current

+

{appDetails.runtime.replicas.current}

+
+
+

Ready

+

{appDetails.runtime.replicas.ready}

+
+
+

Available

+

{appDetails.runtime.replicas.available}

+
+
+
+
+ )} + + {/* Pods */} + {appDetails.runtime.pods && appDetails.runtime.pods.length > 0 && ( + + + Pods + {appDetails.runtime.pods.length} pod(s) + + +
+ {appDetails.runtime.pods.map((pod) => ( +
+
+
+

{pod.name}

+ {pod.node && ( +

Node: {pod.node}

+ )} +
+
+ + {pod.status} + +
+
+
+
+ Ready:{' '} + {pod.ready} +
+
+ Restarts:{' '} + {pod.restarts} +
+
+ Age:{' '} + {pod.age} +
+
+ {pod.ip && ( +
+ IP:{' '} + {pod.ip} +
+ )} +
+ ))} +
+
+
+ )} + + {/* Resource Usage */} + {appDetails.runtime.resources && ( + + + Resource Usage + + + {appDetails.runtime.resources.cpu && ( +
+
+ CPU + + {appDetails.runtime.resources.cpu.used} / {appDetails.runtime.resources.cpu.limit} + +
+
+
+
+

+ {appDetails.runtime.resources.cpu.percentage.toFixed(1)}% used +

+
+ )} + + {appDetails.runtime.resources.memory && ( +
+
+ Memory + + {appDetails.runtime.resources.memory.used} / {appDetails.runtime.resources.memory.limit} + +
+
+
+
+

+ {appDetails.runtime.resources.memory.percentage.toFixed(1)}% used +

+
+ )} + + {appDetails.runtime.resources.storage && ( +
+
+ Storage + + {appDetails.runtime.resources.storage.used} / {appDetails.runtime.resources.storage.limit} + +
+
+
+
+

+ {appDetails.runtime.resources.storage.percentage.toFixed(1)}% used +

+
+ )} + + + )} + + {/* Recent Events */} + {eventsData?.events && eventsData.events.length > 0 && ( + + + Recent Events + + +
+ {eventsData.events.map((event, idx) => ( +
+ {event.type === 'Warning' ? ( + + ) : ( + + )} +
+

{event.reason}

+

{event.message}

+

+ {event.timestamp} {event.count > 1 && `(${event.count}x)`} +

+
+
+ ))} +
+
+
+ )} + + ) : ( +

No status information available

+ )} +
+ )} + + {/* Logs Tab */} + {viewMode === 'logs' && ( +
+
+
+ +
+ +
+ + + +
+ {logs && logs.logs && Array.isArray(logs.logs) && logs.logs.length > 0 ? ( + logs.logs.map((line, idx) => { + // Handle both string format and object format {timestamp, message, pod} + if (typeof line === 'string') { + return ( +
+ {line} +
+ ); + } else if (line && typeof line === 'object' && 'message' in line) { + // Display timestamp and message nicely + const timestamp = line.timestamp ? new Date(line.timestamp).toLocaleTimeString() : ''; + return ( +
+ {timestamp && [{timestamp}] } + {line.message} +
+ ); + } else { + return ( +
+ {JSON.stringify(line)} +
+ ); + } + }) + ) : logs && typeof logs === 'object' && !Array.isArray(logs) ? ( + // Handle case where logs might be an object with different structure +
+ {JSON.stringify(logs, null, 2)} +
+ ) : ( +

No logs available

+ )} +
+
+
+
+ )} + +
+ ); +} diff --git a/src/hooks/useApps.ts b/src/hooks/useApps.ts index fd50a00..9ee89d8 100644 --- a/src/hooks/useApps.ts +++ b/src/hooks/useApps.ts @@ -108,3 +108,58 @@ export function useAppBackups(instanceName: string | null | undefined, appName: restoreResult: restoreMutation.data, }; } + +// Enhanced hooks for app details and runtime status +export function useAppEnhanced(instanceName: string | null | undefined, appName: string | null | undefined) { + return useQuery({ + queryKey: ['instances', instanceName, 'apps', appName, 'enhanced'], + queryFn: () => appsApi.getEnhanced(instanceName!, appName!), + enabled: !!instanceName && !!appName, + refetchInterval: 10000, // Poll every 10 seconds + }); +} + +export function useAppRuntime(instanceName: string | null | undefined, appName: string | null | undefined) { + return useQuery({ + queryKey: ['instances', instanceName, 'apps', appName, 'runtime'], + queryFn: () => appsApi.getRuntime(instanceName!, appName!), + enabled: !!instanceName && !!appName, + refetchInterval: 5000, // Poll every 5 seconds + }); +} + +export function useAppLogs( + instanceName: string | null | undefined, + appName: string | null | undefined, + params?: { tail?: number; sinceSeconds?: number; pod?: string } +) { + return useQuery({ + queryKey: ['instances', instanceName, 'apps', appName, 'logs', params], + queryFn: () => appsApi.getLogs(instanceName!, appName!, params), + enabled: !!instanceName && !!appName, + refetchInterval: false, // Manual refresh only + }); +} + +export function useAppEvents( + instanceName: string | null | undefined, + appName: string | null | undefined, + limit?: number +) { + return useQuery({ + queryKey: ['instances', instanceName, 'apps', appName, 'events', limit], + queryFn: () => appsApi.getEvents(instanceName!, appName!, limit), + enabled: !!instanceName && !!appName, + refetchInterval: 10000, // Poll every 10 seconds + }); +} + +export function useAppReadme(instanceName: string | null | undefined, appName: string | null | undefined) { + return useQuery({ + queryKey: ['instances', instanceName, 'apps', appName, 'readme'], + queryFn: () => appsApi.getReadme(instanceName!, appName!), + enabled: !!instanceName && !!appName, + staleTime: 5 * 60 * 1000, // 5 minutes - READMEs don't change often + retry: false, // Don't retry if README not found (404) + }); +} diff --git a/src/index.css b/src/index.css index 98de84c..0f34d46 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@plugin "@tailwindcss/typography"; @custom-variant dark (&:is(.dark *)); diff --git a/src/services/api/apps.ts b/src/services/api/apps.ts index 070c895..c2e1cc8 100644 --- a/src/services/api/apps.ts +++ b/src/services/api/apps.ts @@ -6,6 +6,10 @@ import type { AppAddResponse, AppStatus, OperationResponse, + EnhancedApp, + RuntimeStatus, + LogEntry, + KubernetesEvent, } from './types'; export const appsApi = { @@ -39,6 +43,33 @@ export const appsApi = { return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/status`); }, + // Enhanced app details endpoints + async getEnhanced(instanceName: string, appName: string): Promise { + return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/enhanced`); + }, + + async getRuntime(instanceName: string, appName: string): Promise { + return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/runtime`); + }, + + async getLogs( + instanceName: string, + appName: string, + params?: { tail?: number; sinceSeconds?: number; pod?: string } + ): Promise { + const queryParams = new URLSearchParams(); + if (params?.tail) queryParams.append('tail', params.tail.toString()); + if (params?.sinceSeconds) queryParams.append('sinceSeconds', params.sinceSeconds.toString()); + if (params?.pod) queryParams.append('pod', params.pod); + + const query = queryParams.toString(); + return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/logs${query ? `?${query}` : ''}`); + }, + + async getEvents(instanceName: string, appName: string, limit = 20): Promise<{ events: KubernetesEvent[] }> { + return apiClient.get(`/api/v1/instances/${instanceName}/apps/${appName}/events?limit=${limit}`); + }, + // Backup operations async backup(instanceName: string, appName: string): Promise { return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/backup`); @@ -51,4 +82,16 @@ export const appsApi = { async restore(instanceName: string, appName: string, backupId: string): Promise { return apiClient.post(`/api/v1/instances/${instanceName}/apps/${appName}/restore`, { backup_id: backupId }); }, + + // README content + async getReadme(instanceName: string, appName: string): Promise { + const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5055'}/api/v1/instances/${instanceName}/apps/${appName}/readme`); + if (!response.ok) { + if (response.status === 404) { + return ''; // Return empty string if README not found + } + throw new Error(`Failed to fetch README: ${response.statusText}`); + } + return response.text(); + }, }; diff --git a/src/services/api/types/app.ts b/src/services/api/types/app.ts index 22bfe2a..149a534 100644 --- a/src/services/api/types/app.ts +++ b/src/services/api/types/app.ts @@ -10,6 +10,8 @@ export interface App { dependencies?: string[]; config?: Record; status?: AppStatus; + readme?: string; + documentation?: string; } export interface AppRequirement { @@ -38,6 +40,92 @@ export interface AppResources { storage?: string; } +// Enhanced types for app details with runtime status +export interface ContainerInfo { + name: string; + image: string; + ready: boolean; + restartCount: number; + state: string; // "running", "waiting", "terminated" +} + +export interface PodInfo { + name: string; + status: string; + ready: string; // "1/1" + restarts: number; + age: string; + node: string; + ip: string; + containers?: ContainerInfo[]; +} + +export interface ReplicaInfo { + desired: number; + current: number; + ready: number; + available: number; +} + +export interface ResourceMetric { + used: string; + requested: string; + limit: string; + percentage: number; +} + +export interface ResourceUsage { + cpu: ResourceMetric; + memory: ResourceMetric; + storage?: ResourceMetric; +} + +export interface KubernetesEvent { + type: string; + reason: string; + message: string; + timestamp: string; + count: number; +} + +export interface RuntimeStatus { + pods: PodInfo[]; + replicas?: ReplicaInfo; + resources?: ResourceUsage; + recentEvents?: KubernetesEvent[]; +} + +export interface AppManifest { + name: string; + description: string; + version: string; + category?: string; + icon?: string; + dependencies?: string[]; + defaultConfig?: Record; + requiredSecrets?: string[]; +} + +export interface EnhancedApp { + name: string; + status: string; + version?: string; + namespace: string; + url?: string; + description?: string; + icon?: string; + manifest?: AppManifest; + config?: Record; + runtime?: RuntimeStatus; + readme?: string; + documentation?: string; +} + +export interface LogEntry { + pod: string; + logs: string[]; +} + export interface AppListResponse { apps: App[]; }