From 14d862d3da8864d1022130e3b897024e10f25c29 Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Wed, 31 Jul 2024 18:44:52 +0300 Subject: [PATCH 1/7] app_store UI: mirror checks and info --- .../app_store/app_store/src/http_api.rs | 54 +++- .../packages/app_store/app_store/src/state.rs | 7 + .../packages/app_store/ui/src/abis/helpers.ts | 14 +- .../app_store/ui/src/components/Header.tsx | 10 +- kinode/packages/app_store/ui/src/index.css | 256 ++++++++++++++---- .../app_store/ui/src/pages/AppPage.tsx | 168 +++++++++--- .../app_store/ui/src/pages/MyAppsPage.tsx | 76 ------ .../packages/app_store/ui/src/store/index.ts | 12 +- .../packages/app_store/ui/src/types/Apps.ts | 7 + 9 files changed, 432 insertions(+), 172 deletions(-) delete mode 100644 kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index bed2d8ad3..eb8915ac5 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -1,4 +1,4 @@ -use crate::state::{PackageListing, State}; +use crate::state::{MirrorCheckFile, PackageListing, State}; use crate::DownloadResponse; use kinode_process_lib::{ http::{ @@ -7,6 +7,7 @@ use kinode_process_lib::{ }, println, Address, NodeId, PackageId, ProcessId, Request, }; +use kinode_process_lib::{SendError, SendErrorKind}; use serde_json::json; use std::collections::HashMap; @@ -22,6 +23,7 @@ pub fn init_frontend(our: &Address) { "/apps/:id/mirror", "/apps/:id/auto-update", "/apps/rebuild-index", + "/mirrorcheck/:node", ] { bind_http_path(path, true, false).expect("failed to bind http path"); } @@ -173,6 +175,7 @@ fn make_widget() -> String { /// - get detail about a specific app: GET /apps/:id /// - get capabilities for a specific downloaded app: GET /apps/:id/caps /// +/// - get online/offline mirrors for a listed app: GET /mirrorcheck/:node /// - install a downloaded app, download a listed app: POST /apps/:id /// - uninstall/delete a downloaded app: DELETE /apps/:id /// - update a downloaded app: PUT /apps/:id @@ -218,6 +221,7 @@ fn gen_package_info(id: &PackageId, listing: &PackageListing) -> serde_json::Val None => false, }, "metadata_hash": listing.metadata_hash, + "metadata_uri": listing.metadata_uri, "metadata": listing.metadata, "state": match &listing.state { Some(state) => json!({ @@ -259,6 +263,54 @@ fn serve_paths( .collect(); return Ok((StatusCode::OK, None, serde_json::to_vec(&all)?)); } + // GET online/offline mirrors for a listed app + "/mirrorcheck/:node" => { + if method != Method::GET { + return Ok(( + StatusCode::METHOD_NOT_ALLOWED, + None, + format!("Invalid method {method} for {bound_path}").into_bytes(), + )); + } + let Some(node) = url_params.get("node") else { + return Ok(( + StatusCode::BAD_REQUEST, + None, + format!("Missing node").into_bytes(), + )); + }; + if let Err(SendError { kind, .. }) = Request::to((node, "net", "distro", "sys")) + .body(b"checking your mirror status...") + .send_and_await_response(3) + .unwrap() + { + match kind { + SendErrorKind::Timeout => { + let check_reponse = MirrorCheckFile { + node: node.to_string(), + is_online: false, + error: Some(format!("node {} timed out", node).to_string()), + }; + return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?)); + } + SendErrorKind::Offline => { + let check_reponse = MirrorCheckFile { + node: node.to_string(), + is_online: false, + error: Some(format!("node {} is offline", node).to_string()), + }; + return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?)); + } + } + } else { + let check_reponse = MirrorCheckFile { + node: node.to_string(), + is_online: true, + error: None, + }; + return Ok((StatusCode::OK, None, serde_json::to_vec(&check_reponse)?)); + } + } // GET detail about a specific app // install an app: POST // update a downloaded app: PUT diff --git a/kinode/packages/app_store/app_store/src/state.rs b/kinode/packages/app_store/app_store/src/state.rs index 99fd6ca0d..23388e6fa 100644 --- a/kinode/packages/app_store/app_store/src/state.rs +++ b/kinode/packages/app_store/app_store/src/state.rs @@ -49,6 +49,13 @@ pub struct MirroringFile { pub auto_update: bool, } +#[derive(Debug, Deserialize, Serialize)] +pub struct MirrorCheckFile { + pub node: NodeId, + pub is_online: bool, + pub error: Option, +} + #[derive(Debug, Deserialize, Serialize)] pub struct RequestedPackage { pub from: NodeId, diff --git a/kinode/packages/app_store/ui/src/abis/helpers.ts b/kinode/packages/app_store/ui/src/abis/helpers.ts index 051c0b4d5..099227f2f 100644 --- a/kinode/packages/app_store/ui/src/abis/helpers.ts +++ b/kinode/packages/app_store/ui/src/abis/helpers.ts @@ -2,27 +2,27 @@ import { multicallAbi, kinomapAbi, mechAbi, KINOMAP, MULTICALL, KINO_ACCOUNT_IMP import { encodeFunctionData, encodePacked, stringToHex } from "viem"; export function encodeMulticalls(metadataUri: string, metadataHash: string) { - const metadataUriCall = encodeFunctionData({ + const metadataHashCall = encodeFunctionData({ abi: kinomapAbi, functionName: 'note', args: [ - encodePacked(["bytes"], [stringToHex("~metadata-uri")]), - encodePacked(["bytes"], [stringToHex(metadataUri)]), + encodePacked(["bytes"], [stringToHex("~metadata-hash")]), + encodePacked(["bytes"], [stringToHex(metadataHash)]), ] }) - const metadataHashCall = encodeFunctionData({ + const metadataUriCall = encodeFunctionData({ abi: kinomapAbi, functionName: 'note', args: [ - encodePacked(["bytes"], [stringToHex("~metadata-hash")]), - encodePacked(["bytes"], [stringToHex(metadataHash)]), + encodePacked(["bytes"], [stringToHex("~metadata-uri")]), + encodePacked(["bytes"], [stringToHex(metadataUri)]), ] }) const calls = [ + { target: KINOMAP, callData: metadataHashCall }, { target: KINOMAP, callData: metadataUriCall }, - { target: KINOMAP, callData: metadataHashCall } ]; const multicall = encodeFunctionData({ diff --git a/kinode/packages/app_store/ui/src/components/Header.tsx b/kinode/packages/app_store/ui/src/components/Header.tsx index 1a9568881..f19dd3f36 100644 --- a/kinode/packages/app_store/ui/src/components/Header.tsx +++ b/kinode/packages/app_store/ui/src/components/Header.tsx @@ -1,16 +1,18 @@ import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { STORE_PATH, PUBLISH_PATH } from '../constants/path'; import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { FaHome } from "react-icons/fa"; const Header: React.FC = () => { - const location = useLocation(); - return (
diff --git a/kinode/packages/app_store/ui/src/index.css b/kinode/packages/app_store/ui/src/index.css index 666f37275..2a38deb9b 100644 --- a/kinode/packages/app_store/ui/src/index.css +++ b/kinode/packages/app_store/ui/src/index.css @@ -108,13 +108,44 @@ /* App Page Styles */ .app-page { - max-width: 800px; + max-width: 1000px; margin: 0 auto; + padding: 20px; +} + +.app-header { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +.app-icon { + width: 100px; + height: 100px; + margin-right: 20px; + border-radius: 12px; + object-fit: cover; +} + +.app-title { + flex: 1; +} + +.app-title h2 { + margin: 0; + color: var(--orange); +} + +.app-id { + font-family: monospace; + color: light-dark(var(--gray), var(--off-white)); + margin-top: 5px; } .app-description { - margin-bottom: 1rem; + margin-bottom: 20px; color: light-dark(var(--gray), var(--off-white)); + line-height: 1.5; } .app-details { @@ -127,66 +158,148 @@ flex: 1; } -.app-details-list { - list-style-type: none; - padding: 0; +.info-section { + margin-bottom: 20px; } -.app-details-list li { +.info-section h3 { + color: var(--orange); + margin-bottom: 10px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + background-color: light-dark(var(--tan), var(--maroon)); + border-radius: 4px; +} + +.detail-list li { display: flex; justify-content: space-between; align-items: center; - padding: 0.5rem 0; - border-bottom: 1px solid light-dark(var(--gray), var(--maroon)); + padding: 10px 0; + border-bottom: 1px solid light-dark(var(--gray-light), var(--gray)); } -.app-details-list li:last-child { +.detail-list li:last-child { border-bottom: none; } .status-icon { - font-size: 1rem; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: light-dark(var(--gray-light), var(--gray)); } -.status-icon.installed, -.status-icon.mirroring, -.status-icon.auto-update { +.installed, +.verified, +.approved, +.mirroring, +.auto-update { color: var(--orange); } -.status-icon.not-installed, -.status-icon.not-mirroring, -.status-icon.no-auto-update { +.not-installed, +.not-verified, +.not-approved, +.not-mirroring, +.no-auto-update { color: var(--ansi-red); } -.app-actions { - display: flex; - flex-direction: column; - gap: 0.5rem; +.hash { + font-family: monospace; + font-size: 0.9em; + word-break: break-all; + color: light-dark(var(--gray), var(--off-white)); } -.app-actions button { +.toggle-button { display: flex; align-items: center; justify-content: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; + padding: 5px 10px; + font-size: 0.9em; + background-color: light-dark(var(--gray-light), var(--gray)); + color: light-dark(var(--off-black), var(--off-white)); + border: none; + border-radius: 20px; + cursor: pointer; + transition: all 0.3s ease; +} + +.toggle-button.active { + background-color: var(--orange); + color: var(--white); +} + +.toggle-button svg { + margin-right: 5px; } -.app-screenshots { +.app-actions { display: flex; - overflow-x: auto; - gap: 1rem; - padding: 1rem 0; - margin-top: 1rem; + flex-direction: column; + gap: 10px; + min-width: 150px; +} + +.screenshot-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; } .app-screenshot { max-width: 200px; - border-radius: 4px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + max-height: 200px; + border-radius: 8px; + object-fit: cover; +} + +button, +.external-link { + padding: 10px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + transition: background-color 0.3s ease; +} + +button:hover, +.external-link:hover { + opacity: 0.9; +} + +button svg, +.external-link svg { + margin-right: 5px; +} + +.primary { + background-color: var(--orange); + color: var(--white); +} + +.secondary { + background-color: var(--gray); + color: var(--white); +} + +.external-link { + background-color: var(--blue); + color: var(--white); } /* My Apps Page Styles */ @@ -504,30 +617,71 @@ } .mirrors-list { - position: absolute; - top: 100%; - left: 0; - background-color: white; - border: 1px solid var(--gray-light); - border-radius: 4px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); - list-style-type: none; - padding: 0; - margin: 0; - z-index: 10; - max-height: 200px; - overflow-y: auto; + margin-top: 10px; } -.mirrors-list li { - padding: 8px 12px; - border-bottom: 1px solid var(--gray-light); +.mirror-item { + display: flex; + align-items: center; + margin-bottom: 8px; } -.mirrors-list li:last-child { - border-bottom: none; +.mirror-address { + flex-grow: 1; + margin-right: 10px; + font-size: 0.9em; +} + +.check-button { + background: none; + border: none; + cursor: pointer; + padding: 5px; + margin-right: 10px; + display: inline-flex !important; + /* Force display */ + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +.check-button svg { + width: 16px; + height: 16px; + color: var(--off-black); + /* Ensure visibility */ +} + +.spinning { + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.mirror-status { + display: flex; + align-items: center; +} + +.online { + color: green; +} + +.offline { + color: red; } -.mirrors-list li:hover { - background-color: var(--gray-lighter); +.error-message { + margin-left: 5px; + font-size: 0.8em; + color: #888; } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index b7d45a81b..04dddc6e6 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -1,13 +1,17 @@ -import React from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; -import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes } from "react-icons/fa"; +import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt } from "react-icons/fa"; import useAppsStore from "../store"; import { appId } from "../utils/app"; +import { MirrorCheckFile } from "../types/Apps"; export default function AppPage() { - const { installApp, updateApp, uninstallApp, setMirroring, setAutoUpdate, apps } = useAppsStore(); + const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps } = useAppsStore(); const { id } = useParams(); const app = apps.find(a => appId(a) === id); + const [showMetadata, setShowMetadata] = useState(true); + const [showLocalInfo, setShowLocalInfo] = useState(true); + const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({}); if (!app) { return

App details not found for {id}

; @@ -17,54 +21,154 @@ export default function AppPage() { const handleUpdate = () => app && updateApp(app); const handleUninstall = () => app && uninstallApp(app); const handleMirror = () => app && setMirroring(app, !app.state?.mirroring); + const handleApproveCaps = () => app && approveCaps(app); const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update); + const handleLaunch = () => { + console.log("Launching app:", app.package); + window.open(`/${app.package}:${app.publisher}`, '_blank'); + }; + + + const handleCheckMirror = (mirror: string) => { + setMirrorStatuses(prev => ({ ...prev, [mirror]: null })); // Set to loading + checkMirror(mirror) + .then(status => setMirrorStatuses(prev => ({ ...prev, [mirror]: status }))) + .catch(error => { + console.error(`Failed to check mirror ${mirror}:`, error); + setMirrorStatuses(prev => ({ ...prev, [mirror]: { node: mirror, is_online: false, error: "Request failed" } })); + }); + }; return (
-

{app.metadata?.name || app.package}

-

{app.metadata?.description || "No description available"}

+
+ {app.metadata?.image && ( + {app.metadata?.name + )} +
+

{app.metadata?.name || app.package}

+

{`${app.package}.${app.publisher}`}

+
+
+ +
{app.metadata?.description || "No description available"}
+
-
    -
  • Version: {app.metadata?.properties?.current_version || "Unknown"}
  • -
  • Developer: {app.publisher}
  • -
  • Mirrors: {app.metadata?.properties?.mirrors?.length || 0}
  • -
  • - Installed: - {app.installed ? : } -
  • -
  • - Mirroring: - {app.state?.mirroring ? : } -
  • -
  • - Auto-Update: - {app.state?.auto_update ? : } -
  • -
+
+

setShowMetadata(!showMetadata)}> + Metadata {showMetadata ? : } +

+ {showMetadata && ( +
    +
  • Version: {app.metadata?.properties?.current_version || "Unknown"}
  • +
  • ~metadata-uri {app.metadata_uri}
  • +
  • ~metadata-hash {app.metadata_hash}
  • +
  • + Mirrors: +
      + {app.metadata?.properties?.mirrors?.map((mirror) => ( +
    • + {mirror} + + {mirrorStatuses[mirror] && ( + + {mirrorStatuses[mirror]?.is_online ? ( + <> Online + ) : ( + <> + + Offline + {mirrorStatuses[mirror]?.error && ( + + ({mirrorStatuses[mirror]?.error}) + + )} + + )} + + )} +
    • + ))} +
    +
  • +
+ )} +
+
+

setShowLocalInfo(!showLocalInfo)}> + Local Information {showLocalInfo ? : } +

+ {showLocalInfo && ( +
    +
  • + Installed: + {app.installed ? : } +
  • +
  • Installed Version: {app.state?.our_version || "Not installed"}
  • +
  • + Verified: + {app.state?.verified ? : } +
  • +
  • License: {app.metadata?.properties?.license || "Not specified"}
  • +
  • + Capabilities Approved: + +
  • +
  • + Mirroring: + +
  • +
  • + Auto-Update: + +
  • +
  • Manifest Hash: {app.state?.manifest_hash || "N/A"}
  • +
+ )} +
{app.installed ? ( <> + ) : ( - + + )} + {app.metadata?.external_url && ( + + External Link + )} - -
+ {app.metadata?.properties?.screenshots && (
- {app.metadata.properties.screenshots.map((screenshot, index) => ( - {`Screenshot - ))} +

Screenshots

+
+ {app.metadata.properties.screenshots.map((screenshot, index) => ( + {`Screenshot + ))} +
)}
diff --git a/kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx b/kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx deleted file mode 100644 index 30249412c..000000000 --- a/kinode/packages/app_store/ui/src/pages/MyAppsPage.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { FaUpload } from "react-icons/fa"; -import { useNavigate, Link } from "react-router-dom"; - -import { AppInfo } from "../types/Apps"; -import useAppsStore from "../store"; -import { PUBLISH_PATH } from "../constants/path"; -import { appId } from "../utils/app"; - -export default function MyAppsPage() { - const { apps, getApps } = useAppsStore(); - const navigate = useNavigate(); - - const [searchQuery, setSearchQuery] = useState(""); - - useEffect(() => { - getApps(); - }, [getApps]); - - const filteredApps = apps.filter((app) => - app.package.toLowerCase().includes(searchQuery.toLowerCase()) || - app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const categorizedApps = { - installed: filteredApps.filter(app => app.installed), - downloaded: filteredApps.filter(app => !app.installed && app.state), - available: filteredApps.filter(app => !app.state) - }; - - return ( -
-
-

My Packages

- -
- - setSearchQuery(e.target.value)} - className="search-input" - /> - -
- {Object.entries(categorizedApps).map(([category, apps]) => ( - apps.length > 0 && ( -
-

{category.charAt(0).toUpperCase() + category.slice(1)}

- {apps.map((app) => ( - - ))} -
- ) - ))} -
-
- ); -} - -interface AppEntryProps { - app: AppInfo; -} - -const AppEntry: React.FC = ({ app }) => { - return ( - -

{app.metadata?.name || app.package}

-

{app.metadata?.description || "No description available"}

- - ); -}; \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/store/index.ts b/kinode/packages/app_store/ui/src/store/index.ts index d59e81670..0b47b4769 100644 --- a/kinode/packages/app_store/ui/src/store/index.ts +++ b/kinode/packages/app_store/ui/src/store/index.ts @@ -1,6 +1,6 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -import { AppInfo, PackageManifest } from '../types/Apps' +import { AppInfo, MirrorCheckFile, PackageManifest } from '../types/Apps' import { HTTP_STATUS } from '../constants/http' import { appId } from '../utils/app' @@ -10,6 +10,7 @@ interface AppsStore { apps: AppInfo[] getApps: () => Promise getApp: (id: string) => Promise + checkMirror: (node: string) => Promise installApp: (app: AppInfo) => Promise updateApp: (app: AppInfo) => Promise uninstallApp: (app: AppInfo) => Promise @@ -44,6 +45,14 @@ const useAppsStore = create()( throw new Error(`Failed to get app: ${id}`) }, + checkMirror: async (node: string) => { + const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`) + if (res.status === HTTP_STATUS.OK) { + return await res.json() + } + throw new Error(`Failed to check mirror status for node: ${node}`) + }, + installApp: async (app: AppInfo) => { const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'POST' }) if (res.status !== HTTP_STATUS.CREATED) { @@ -91,6 +100,7 @@ const useAppsStore = create()( if (res.status !== HTTP_STATUS.OK) { throw new Error(`Failed to approve caps for app: ${appId(app)}`) } + await get().getApp(appId(app)) }, setMirroring: async (app: AppInfo, mirroring: boolean) => { diff --git a/kinode/packages/app_store/ui/src/types/Apps.ts b/kinode/packages/app_store/ui/src/types/Apps.ts index 7ccceb24b..e22637fe2 100644 --- a/kinode/packages/app_store/ui/src/types/Apps.ts +++ b/kinode/packages/app_store/ui/src/types/Apps.ts @@ -10,11 +10,18 @@ export interface AppListing { package: string publisher: string metadata_hash: string + metadata_uri: string metadata?: OnchainPackageMetadata installed: boolean state?: PackageState } +export interface MirrorCheckFile { + node: string; + is_online: boolean; + error: string | null; +} + export interface Erc721Properties { package_name: string; publisher: string; From b8811180e23e14b84c5642d4152d74b2605078bc Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 13:47:16 +0300 Subject: [PATCH 2/7] app_store: fix uninstall removing listing, separate backend api, add progress update to FT --- .../app_store/app_store/src/http_api.rs | 286 ++++++------------ .../packages/app_store/app_store/src/lib.rs | 41 ++- .../packages/app_store/app_store/src/state.rs | 5 +- .../app_store/ft_worker/src/ft_worker_lib.rs | 5 + .../packages/app_store/ft_worker/src/lib.rs | 12 + .../packages/app_store/ui/src/store/index.ts | 27 ++ kinode/packages/app_store/ui/src/utils/ws.ts | 11 + 7 files changed, 179 insertions(+), 208 deletions(-) create mode 100644 kinode/packages/app_store/ui/src/utils/ws.ts diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index eb8915ac5..74337a18e 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -19,6 +19,8 @@ pub fn init_frontend(our: &Address) { for path in [ "/apps", "/apps/:id", + "/apps/:id/download", + "/apps/:id/install", "/apps/:id/caps", "/apps/:id/mirror", "/apps/:id/auto-update", @@ -27,14 +29,8 @@ pub fn init_frontend(our: &Address) { ] { bind_http_path(path, true, false).expect("failed to bind http path"); } - serve_ui( - &our, - "ui", - true, - false, - vec!["/", "/my-apps", "/app/:id", "/publish"], - ) - .expect("failed to serve static UI"); + serve_ui(&our, "ui", true, false, vec!["/", "/app/:id", "/publish"]) + .expect("failed to serve static UI"); bind_ws_path("/", true, true).expect("failed to bind ws path"); @@ -176,9 +172,10 @@ fn make_widget() -> String { /// - get capabilities for a specific downloaded app: GET /apps/:id/caps /// /// - get online/offline mirrors for a listed app: GET /mirrorcheck/:node -/// - install a downloaded app, download a listed app: POST /apps/:id +/// - download a listed app: POST /apps/:id/download +/// - install a downloaded app: POST /apps/:id/install /// - uninstall/delete a downloaded app: DELETE /apps/:id -/// - update a downloaded app: PUT /apps/:id +/// - update a downloaded app: PUT /apps/:id FIX /// - approve capabilities for a downloaded app: POST /apps/:id/caps /// - start mirroring a downloaded app: PUT /apps/:id/mirror /// - stop mirroring a downloaded app: DELETE /apps/:id/mirror @@ -312,9 +309,7 @@ fn serve_paths( } } // GET detail about a specific app - // install an app: POST // update a downloaded app: PUT - // uninstall an app: DELETE "/apps/:id" => { let Ok(package_id) = get_package_id(url_params) else { return Ok(( @@ -341,194 +336,6 @@ fn serve_paths( .into_bytes(), )) } - Method::POST => { - let Some(listing) = state.packages.get(&package_id) else { - return Ok(( - StatusCode::NOT_FOUND, - None, - format!("App not found: {package_id}").into_bytes(), - )); - }; - if listing.state.is_some() { - // install a downloaded app - crate::handle_install(state, &package_id)?; - Ok((StatusCode::CREATED, None, format!("Installed").into_bytes())) - } else { - // download a listed app - let pkg_listing: &PackageListing = state - .packages - .get(&package_id) - .ok_or(anyhow::anyhow!("No package"))?; - // from POST body, look for download_from field and use that as the mirror - let body = crate::get_blob() - .ok_or(anyhow::anyhow!("missing blob"))? - .bytes; - let body_json: serde_json::Value = - serde_json::from_slice(&body).unwrap_or_default(); - let mirrors: &Vec = pkg_listing - .metadata - .as_ref() - .expect("Package does not have metadata") - .properties - .mirrors - .as_ref(); - - let download_from = body_json - .get("download_from") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .or_else(|| mirrors.first().map(|mirror| mirror.to_string())); - - // if no specific mirror specified, loop through and ping them. - if let Some(download_from) = download_from { - // TODO choose more on frontend. - let mirror = false; - let auto_update = false; - let desired_version_hash = None; - match crate::start_download( - state, - package_id, - download_from, - mirror, - auto_update, - desired_version_hash, - ) { - DownloadResponse::Started => Ok(( - StatusCode::CREATED, - None, - format!("Downloading").into_bytes(), - )), - other => Ok(( - StatusCode::SERVICE_UNAVAILABLE, - None, - format!("Failed to download: {other:?}").into_bytes(), - )), - } - } else { - let online_mirrors: Vec = mirrors - .iter() - .filter_map(|mirror| { - let target = Address::new( - mirror, - ProcessId::new(Some("net"), "distro", "sys"), - ); - let request = Request::new().target(target).body(vec![]).send(); - - match request { - Ok(_) => Some(mirror.clone()), - Err(_) => None, - } - }) - .collect(); - - println!("all mirrors: {:?}", mirrors); - println!("online mirrors: {:?}", online_mirrors); - - let mut failed_mirrors = Vec::new(); - for online_mirror in &online_mirrors { - let mirror = true; - let auto_update = false; - let desired_version_hash = None; - match crate::start_download( - state, - package_id.clone(), - online_mirror.to_string(), - mirror, - auto_update, - desired_version_hash, - ) { - DownloadResponse::Started => { - return Ok(( - StatusCode::CREATED, - None, - format!( - "Download started from mirror: {}", - online_mirror - ) - .into_bytes(), - )); - } - _ => { - failed_mirrors.push(online_mirror.to_string()); - continue; - } - } - } - let mut failed_mirrors = Vec::new(); - for online_mirror in &online_mirrors { - let mirror = true; - let auto_update = false; - let desired_version_hash = None; - match crate::start_download( - state, - package_id.clone(), - online_mirror.to_string(), - mirror, - auto_update, - desired_version_hash, - ) { - DownloadResponse::Started => { - return Ok(( - StatusCode::CREATED, - None, - format!( - "Download started from mirror: {}", - online_mirror - ) - .into_bytes(), - )); - } - _ => { - failed_mirrors.push(online_mirror.to_string()); - continue; - } - } - } - Ok(( - StatusCode::SERVICE_UNAVAILABLE, - None, - format!( - "Failed to start download from any mirrors. Failed mirrors: {:?}", - failed_mirrors - ).into_bytes(), - )) - } - } - } - Method::PUT => { - // update a downloaded app - let listing: &PackageListing = state - .packages - .get(&package_id) - .ok_or(anyhow::anyhow!("No package listing"))?; - let Some(ref pkg_state) = listing.state else { - return Err(anyhow::anyhow!("No package state")); - }; - let download_from = pkg_state - .mirrored_from - .as_ref() - .ok_or(anyhow::anyhow!("No mirror for package {package_id}"))? - .to_string(); - match crate::start_download( - state, - package_id, - download_from, - pkg_state.mirroring, - pkg_state.auto_update, - None, - ) { - DownloadResponse::Started => Ok(( - StatusCode::CREATED, - None, - format!("Downloading").into_bytes(), - )), - _ => Ok(( - StatusCode::SERVICE_UNAVAILABLE, - None, - format!("Failed to download").into_bytes(), - )), - } - } Method::DELETE => { // uninstall an app state.uninstall(&package_id)?; @@ -545,6 +352,85 @@ fn serve_paths( )), } } + // PUT /apps/:id/download + // download a listed app from a mirror + "/apps/:id/download" => { + let Ok(package_id) = get_package_id(url_params) else { + return Ok(( + StatusCode::BAD_REQUEST, + None, + format!("Missing id").into_bytes(), + )); + }; + // download a listed app + let pkg_listing: &PackageListing = state + .packages + .get(&package_id) + .ok_or(anyhow::anyhow!("No package"))?; + // from POST body, look for download_from field and use that as the mirror + let body = crate::get_blob() + .ok_or(anyhow::anyhow!("missing blob"))? + .bytes; + let body_json: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default(); + let mirrors: &Vec = pkg_listing + .metadata + .as_ref() + .expect("Package does not have metadata") + .properties + .mirrors + .as_ref(); + + let download_from = body_json + .get("download_from") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .or_else(|| mirrors.first().map(|mirror| mirror.to_string())) + .ok_or_else(|| anyhow::anyhow!("No download_from specified!"))?; + + let mirror = false; + let auto_update = false; + // TODO choose on frontend? + let desired_version_hash = None; + match crate::start_download( + state, + package_id, + download_from, + mirror, + auto_update, + desired_version_hash, + ) { + DownloadResponse::Started => Ok(( + StatusCode::CREATED, + None, + format!("Downloading").into_bytes(), + )), + other => Ok(( + StatusCode::SERVICE_UNAVAILABLE, + None, + format!("Failed to download: {other:?}").into_bytes(), + )), + } + } + // POST /apps/:id/install + // install a downloaded app + "/apps/:id/install" => { + let Ok(package_id) = get_package_id(url_params) else { + return Ok(( + StatusCode::BAD_REQUEST, + None, + format!("Missing id").into_bytes(), + )); + }; + + match crate::handle_install(state, &package_id) { + Ok(_) => Ok((StatusCode::CREATED, None, vec![])), + Err(e) => Ok(( + StatusCode::SERVICE_UNAVAILABLE, + None, + e.to_string().into_bytes(), + )), + } + } // GET caps for a specific downloaded app // approve capabilities for a downloaded app: POST "/apps/:id/caps" => { diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index 0f5b11a28..7f079e8bb 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -1,7 +1,7 @@ #![feature(let_chains)] //! App Store: //! acts as both a local package manager and a protocol to share packages across the network. -//! packages are apps; apps are packages. we use an onchain app listing contract to determine +//! packages are apps; apps are packages. we use the kimap contract to determine //! what apps are available to download and what node(s) to download them from. //! //! once we know that list, we can request a package from a node and download it locally. @@ -22,8 +22,9 @@ use ft_worker_lib::{ spawn_receive_transfer, spawn_transfer, FTWorkerCommand, FTWorkerResult, FileTransferContext, }; use kinode_process_lib::{ - await_message, call_init, eth, get_blob, http, kimap, println, vfs, Address, LazyLoadBlob, - Message, PackageId, Request, Response, + await_message, call_init, eth, get_blob, + http::{self, WsMessageType}, + println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response, }; use serde::{Deserialize, Serialize}; use state::{AppStoreLogError, PackageState, RequestedPackage, State}; @@ -53,7 +54,7 @@ pub const APP_SHARE_TIMEOUT: u64 = 120; // 120s #[cfg(not(feature = "simulation-mode"))] const KIMAP_ADDRESS: &str = kimap::KIMAP_ADDRESS; #[cfg(feature = "simulation-mode")] -const KIMAP_ADDRESS: &str = "0x0165878A594ca255338adfa4d48449f69242Eb8F"; // note temp kimap address! +const KIMAP_ADDRESS: &str = "0x0165878A594ca255338adfa4d48449f69242Eb8F"; #[cfg(not(feature = "simulation-mode"))] const KIMAP_FIRST_BLOCK: u64 = kimap::KIMAP_FIRST_BLOCK; @@ -135,11 +136,33 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> { let resp = handle_remote_request(state, message.source(), remote_request); Response::new().body(serde_json::to_vec(&resp)?).send()?; } + Req::FTWorkerCommand(_) => { + spawn_receive_transfer(&state.our, message.body())?; + } Req::FTWorkerResult(FTWorkerResult::ReceiveSuccess(name)) => { handle_receive_download(state, &name)?; } - Req::FTWorkerCommand(_) => { - spawn_receive_transfer(&state.our, message.body())?; + Req::FTWorkerResult(FTWorkerResult::ProgressUpdate { + file_name, + chunks_received, + total_chunks, + }) => { + // forward progress to UI + let ws_blob = LazyLoadBlob { + mime: Some("application/json".to_string()), + bytes: serde_json::json!({ + "kind": "progress", + "data": { + "file_name": file_name, + "chunks_received": chunks_received, + "total_chunks": total_chunks, + } + }) + .to_string() + .as_bytes() + .to_vec(), + }; + http::send_ws_push(6969, WsMessageType::Text, ws_blob); } Req::FTWorkerResult(r) => { println!("got weird ft_worker result: {r:?}"); @@ -566,8 +589,12 @@ fn handle_ft_worker_result(ft_worker_result: FTWorkerResult, context: &[u8]) -> .as_secs_f64(), ); Ok(()) + } else if let FTWorkerResult::Err(e) = ft_worker_result { + Err(anyhow::anyhow!("failed to share app: {e:?}")) } else { - Err(anyhow::anyhow!("failed to share app")) + Err(anyhow::anyhow!( + "failed to share app: unknown FTWorkerResult {ft_worker_result:?}" + )) } } diff --git a/kinode/packages/app_store/app_store/src/state.rs b/kinode/packages/app_store/app_store/src/state.rs index b25dcedce..f4f5bd4f3 100644 --- a/kinode/packages/app_store/app_store/src/state.rs +++ b/kinode/packages/app_store/app_store/src/state.rs @@ -353,7 +353,10 @@ impl State { pub fn uninstall(&mut self, package_id: &PackageId) -> anyhow::Result<()> { utils::uninstall(package_id)?; - self.packages.remove(package_id); + let Some(listing) = self.packages.get_mut(package_id) else { + return Err(anyhow::anyhow!("package not found")); + }; + listing.state = None; // kinode_process_lib::set_state(&serde_json::to_vec(self)?); println!("uninstalled {package_id}"); Ok(()) diff --git a/kinode/packages/app_store/ft_worker/src/ft_worker_lib.rs b/kinode/packages/app_store/ft_worker/src/ft_worker_lib.rs index 36d0a7bfd..88fa03ebb 100644 --- a/kinode/packages/app_store/ft_worker/src/ft_worker_lib.rs +++ b/kinode/packages/app_store/ft_worker/src/ft_worker_lib.rs @@ -34,6 +34,11 @@ pub enum FTWorkerResult { SendSuccess, /// string is name of file. bytes in blob ReceiveSuccess(String), + ProgressUpdate { + file_name: String, + chunks_received: u64, + total_chunks: u64, + }, Err(TransferError), } diff --git a/kinode/packages/app_store/ft_worker/src/lib.rs b/kinode/packages/app_store/ft_worker/src/lib.rs index 2e2e785e4..2bbdf378b 100644 --- a/kinode/packages/app_store/ft_worker/src/lib.rs +++ b/kinode/packages/app_store/ft_worker/src/lib.rs @@ -155,6 +155,18 @@ fn handle_receive( }; chunks_received += 1; file_bytes.extend(blob.bytes); + // send progress update to parent + Request::to(parent_process.clone()) + .body( + serde_json::to_vec(&FTWorkerResult::ProgressUpdate { + file_name: file_name.to_string(), + chunks_received, + total_chunks, + }) + .unwrap(), + ) + .send() + .unwrap(); if chunks_received == total_chunks { break; } diff --git a/kinode/packages/app_store/ui/src/store/index.ts b/kinode/packages/app_store/ui/src/store/index.ts index 0b47b4769..8f63ad828 100644 --- a/kinode/packages/app_store/ui/src/store/index.ts +++ b/kinode/packages/app_store/ui/src/store/index.ts @@ -3,11 +3,15 @@ import { persist } from 'zustand/middleware' import { AppInfo, MirrorCheckFile, PackageManifest } from '../types/Apps' import { HTTP_STATUS } from '../constants/http' import { appId } from '../utils/app' +import KinodeClientApi from "@kinode/client-api"; +import { WEBSOCKET_URL } from '../utils/ws' const BASE_URL = '/main:app_store:sys' interface AppsStore { apps: AppInfo[] + ws: KinodeClientApi + downloads: Map getApps: () => Promise getApp: (id: string) => Promise checkMirror: (node: string) => Promise @@ -27,6 +31,29 @@ const useAppsStore = create()( (set, get) => ({ apps: [], + downloads: new Map(), + + ws: new KinodeClientApi({ + uri: WEBSOCKET_URL, + nodeId: window.our?.node, + processId: "main:app_store:sys", + onMessage: (message) => { + const data = JSON.parse(message); + console.log('we got a json message', data) + if (data.kind === 'progress') { + const appId = data.data.name.split('/').pop().split('.').shift(); + set((state) => { + const newDownloads = new Map(state.downloads); + newDownloads.set(appId, [data.data.chunks_received, data.data.total_chunks]); + return { downloads: newDownloads }; + }); + } + }, + onOpen: (_e) => { + console.log('open') + }, + }), + getApps: async () => { const res = await fetch(`${BASE_URL}/apps`) if (res.status === HTTP_STATUS.OK) { diff --git a/kinode/packages/app_store/ui/src/utils/ws.ts b/kinode/packages/app_store/ui/src/utils/ws.ts new file mode 100644 index 000000000..7291c9448 --- /dev/null +++ b/kinode/packages/app_store/ui/src/utils/ws.ts @@ -0,0 +1,11 @@ +// TODO: remove as much as possible of this.. +const BASE_URL = "/main:app_store:sys/"; + +if (window.our) window.our.process = BASE_URL?.replace("/", ""); + +export const PROXY_TARGET = `${(import.meta.env.VITE_NODE_URL || `http://localhost:8080`)}${BASE_URL}`; + +// This env also has BASE_URL which should match the process + package name +export const WEBSOCKET_URL = import.meta.env.DEV + ? `${PROXY_TARGET.replace('http', 'ws')}` + : undefined; \ No newline at end of file From 4311ecc83edcd673b29e5edfd52279b52c5239f1 Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 14:05:38 +0300 Subject: [PATCH 3/7] hotfix --- kinode/packages/app_store/app_store/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index 7f079e8bb..016c74939 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -24,7 +24,7 @@ use ft_worker_lib::{ use kinode_process_lib::{ await_message, call_init, eth, get_blob, http::{self, WsMessageType}, - println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response, + kimap, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response, }; use serde::{Deserialize, Serialize}; use state::{AppStoreLogError, PackageState, RequestedPackage, State}; From ca0996aca729a911207e42ace7b65765cad5b9dd Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 14:27:27 +0300 Subject: [PATCH 4/7] how did this sneak in???? --- Cargo.lock | 54 +++++++++---------- .../packages/app_store/app_store/src/state.rs | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffacae250..3cf395abd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,7 +199,7 @@ dependencies = [ "itoa", "serde", "serde_json", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -583,7 +583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbcba3ca07cf7975f15d871b721fb18031eec8bce51103907f6dcce00b255d98" dependencies = [ "serde", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -1394,9 +1394,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" dependencies = [ "clap_builder", "clap_derive", @@ -1404,9 +1404,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", @@ -1416,9 +1416,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3008,9 +3008,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3313,7 +3313,7 @@ dependencies = [ [[package]] name = "kinode_process_lib" version = "0.9.0" -source = "git+https://github.com/kinode-dao/process_lib?branch=develop#05ab125d3e9e733f59301253c51bdf1b14f61140" +source = "git+https://github.com/kinode-dao/process_lib?branch=develop#51800f9c144b3b69ed52406b4b2ae4c5aa078aec" dependencies = [ "alloy", "alloy-primitives", @@ -5708,14 +5708,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a44eede9b727419af8095cb2d72fab15487a541f54647ad4414b34096ee4631" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.18", + "toml_edit 0.22.20", ] [[package]] @@ -5740,15 +5740,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.18" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1490595c74d930da779e944f5ba2ecdf538af67df1a9848cbd156af43c1b7cf0" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.16", + "winnow 0.6.18", ] [[package]] @@ -6330,9 +6330,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.214.0" +version = "0.215.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff694f02a8d7a50b6922b197ae03883fbf18cdb2ae9fbee7b6148456f5f44041" +checksum = "4fb56df3e06b8e6b77e37d2969a50ba51281029a9aeb3855e76b7f49b6418847" dependencies = [ "leb128", ] @@ -6723,24 +6723,24 @@ dependencies = [ [[package]] name = "wast" -version = "214.0.0" +version = "215.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694bcdb24c49c8709bd8713768b71301a11e823923eee355d530f1d8d0a7f8e9" +checksum = "1ff1d00d893593249e60720be04a7c1f42f1c4dc3806a2869f4e66ab61eb54cb" dependencies = [ "bumpalo", "leb128", "memchr", "unicode-width", - "wasm-encoder 0.214.0", + "wasm-encoder 0.215.0", ] [[package]] name = "wat" -version = "1.214.0" +version = "1.215.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347249eb56773fa728df2656cfe3a8c19437ded61a922a0b5e0839d9790e278e" +checksum = "670bf4d9c8cf76ae242d70ded47c546525b6dafaa6871f9bcb065344bf2b4e3d" dependencies = [ - "wast 214.0.0", + "wast 215.0.0", ] [[package]] @@ -7010,9 +7010,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.16" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] diff --git a/kinode/packages/app_store/app_store/src/state.rs b/kinode/packages/app_store/app_store/src/state.rs index f4f5bd4f3..891b28999 100644 --- a/kinode/packages/app_store/app_store/src/state.rs +++ b/kinode/packages/app_store/app_store/src/state.rs @@ -375,7 +375,7 @@ impl State { let block_number: u64 = log.block_number.ok_or(AppStoreLogError::NoBlockNumber)?; let note: kimap::Note = - kimap::decode_note_log(&log).ok_or(AppStoreLogError::DecodeLogError)?; + kimap::decode_note_log(&log).map_err(|_| AppStoreLogError::DecodeLogError)?; let package_id = note .parent_path From c47e41a87b98e5775ffee88a451f58ca838ad0bb Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 17:48:21 +0300 Subject: [PATCH 5/7] app_store: new download flow --- .../packages/app_store/app_store/src/lib.rs | 10 +- .../packages/app_store/app_store/src/state.rs | 10 +- .../packages/app_store/ft_worker/src/lib.rs | 2 +- kinode/packages/app_store/ui/src/App.tsx | 3 +- kinode/packages/app_store/ui/src/index.css | 402 +++++++++--------- .../app_store/ui/src/pages/AppPage.tsx | 224 ++++++++-- .../packages/app_store/ui/src/store/index.ts | 36 +- 7 files changed, 423 insertions(+), 264 deletions(-) diff --git a/kinode/packages/app_store/app_store/src/lib.rs b/kinode/packages/app_store/app_store/src/lib.rs index 016c74939..4f75a4e5b 100644 --- a/kinode/packages/app_store/app_store/src/lib.rs +++ b/kinode/packages/app_store/app_store/src/lib.rs @@ -24,7 +24,7 @@ use ft_worker_lib::{ use kinode_process_lib::{ await_message, call_init, eth, get_blob, http::{self, WsMessageType}, - kimap, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response, + kimap, println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response, }; use serde::{Deserialize, Serialize}; use state::{AppStoreLogError, PackageState, RequestedPackage, State}; @@ -162,7 +162,9 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> { .as_bytes() .to_vec(), }; - http::send_ws_push(6969, WsMessageType::Text, ws_blob); + for channel_id in state.ui_ws_channels.iter() { + http::send_ws_push(*channel_id, WsMessageType::Text, ws_blob.clone()); + } } Req::FTWorkerResult(r) => { println!("got weird ft_worker result: {r:?}"); @@ -192,6 +194,10 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> { } if let http::HttpServerRequest::Http(req) = incoming { http_api::handle_http_request(state, &req)?; + } else if let http::HttpServerRequest::WebSocketOpen { channel_id, .. } = incoming { + state.ui_ws_channels.insert(channel_id); + } else if let http::HttpServerRequest::WebSocketClose { 0: channel_id } = incoming { + state.ui_ws_channels.remove(&channel_id); } } } diff --git a/kinode/packages/app_store/app_store/src/state.rs b/kinode/packages/app_store/app_store/src/state.rs index 891b28999..0c7505204 100644 --- a/kinode/packages/app_store/app_store/src/state.rs +++ b/kinode/packages/app_store/app_store/src/state.rs @@ -117,6 +117,8 @@ pub struct State { pub requested_packages: HashMap, /// the APIs we have outstanding requests to download (not persisted) pub requested_apis: HashMap, + /// UI websocket connected channel_IDs + pub ui_ws_channels: HashSet, } #[derive(Deserialize)] @@ -152,6 +154,7 @@ impl State { downloaded_apis: s.downloaded_apis, requested_packages: HashMap::new(), requested_apis: HashMap::new(), + ui_ws_channels: HashSet::new(), } } @@ -166,6 +169,7 @@ impl State { downloaded_apis: HashSet::new(), requested_packages: HashMap::new(), requested_apis: HashMap::new(), + ui_ws_channels: HashSet::new(), }; state.populate_packages_from_filesystem()?; Ok(state) @@ -210,8 +214,10 @@ impl State { mirroring: package_state.mirroring, auto_update: package_state.auto_update, })?)?; - if utils::extract_api(package_id)? { - self.downloaded_apis.insert(package_id.to_owned()); + if let Ok(extracted) = utils::extract_api(package_id) { + if extracted { + self.downloaded_apis.insert(package_id.to_owned()); + } } listing.state = Some(package_state); // kinode_process_lib::set_state(&serde_json::to_vec(self)?); diff --git a/kinode/packages/app_store/ft_worker/src/lib.rs b/kinode/packages/app_store/ft_worker/src/lib.rs index 2bbdf378b..480b5b058 100644 --- a/kinode/packages/app_store/ft_worker/src/lib.rs +++ b/kinode/packages/app_store/ft_worker/src/lib.rs @@ -64,7 +64,7 @@ fn handle_send(our: &Address, target: &Address, file_name: &str, timeout: u64) - let file_bytes = blob.bytes; let mut file_size = file_bytes.len() as u64; let mut offset: u64 = 0; - let chunk_size: u64 = 1048576; // 1MB, can be changed + let chunk_size: u64 = 262144; // 256KB let total_chunks = (file_size as f64 / chunk_size as f64).ceil() as u64; // send a file to another worker // start by telling target to expect a file, diff --git a/kinode/packages/app_store/ui/src/App.tsx b/kinode/packages/app_store/ui/src/App.tsx index aa14c28fd..453cd5845 100644 --- a/kinode/packages/app_store/ui/src/App.tsx +++ b/kinode/packages/app_store/ui/src/App.tsx @@ -3,9 +3,10 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; import StorePage from "./pages/StorePage"; import AppPage from "./pages/AppPage"; -import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path"; import PublishPage from "./pages/PublishPage"; import Header from "./components/Header"; +import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path"; + const BASE_URL = import.meta.env.BASE_URL; if (window.our) window.our.process = BASE_URL?.replace("/", ""); diff --git a/kinode/packages/app_store/ui/src/index.css b/kinode/packages/app_store/ui/src/index.css index 2a38deb9b..af8a73900 100644 --- a/kinode/packages/app_store/ui/src/index.css +++ b/kinode/packages/app_store/ui/src/index.css @@ -17,6 +17,7 @@ font-size: 1.5rem; margin: 0; margin-right: 2rem; + color: var(--orange); } .header-left nav { @@ -44,9 +45,36 @@ } .app-content { - padding: 2rem; - flex-grow: 1; - overflow-y: auto; + display: flex; + gap: 2rem; +} + +.app-info-column { + flex: 2; +} + +.app-actions-column { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.app-actions { + display: flex; + flex-direction: column; + gap: 1rem; +} + +@media (max-width: 768px) { + .app-content { + flex-direction: column; + } + + .app-info-column, + .app-actions-column { + flex: 1; + } } .special-appstore-background { @@ -58,14 +86,63 @@ background-position: 0 0, 0 10px, 10px -10px, -10px 0px; } +/* Common Styles */ +button, +.external-link { + padding: 10px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + transition: background-color 0.3s ease; +} + +button:hover, +.external-link:hover { + opacity: 0.9; +} + +button svg, +.external-link svg { + margin-right: 5px; +} + +.primary { + background-color: var(--orange); + color: var(--white); +} + +.secondary { + background-color: var(--gray); + color: var(--white); +} + +.external-link { + background-color: var(--blue); + color: var(--white); +} /* Store Page Styles */ .store-page { padding: 2rem; } +.store-header { + display: flex; + justify-content: space-between; + align-items: stretch; + margin-bottom: 2rem; + gap: 1rem; +} + .search-bar { - margin-bottom: 1rem; + flex-grow: 1; + display: flex; + align-items: stretch; } .search-bar input { @@ -74,36 +151,77 @@ font-size: 1rem; border: 1px solid var(--gray); border-radius: 4px; + height: 38px; } -.app-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1rem; - margin-top: 2rem; +.filter-button, +.store-header button { + height: 38px; + padding: 0 1rem; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + align-self: stretch; } -.app-card { - background-color: light-dark(var(--white), var(--off-black)); - border: 1px solid var(--gray); - border-radius: 8px; - padding: 1rem; - transition: transform 0.3s ease, box-shadow 0.3s ease; +.store-header>* { + margin: 0; } -.app-card:hover { - transform: translateY(-5px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +.store-header button { + flex-shrink: 0; } -.app-card h3 { - margin-bottom: 0.5rem; +.app-list table { + width: 100%; + border-collapse: collapse; +} + +.app-list th, +.app-list td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid var(--gray); +} + +.app-list th { + font-weight: bold; color: var(--orange); } -.app-card p { +.app-row:hover { + background-color: light-dark(var(--tan), var(--maroon)); +} + +.app-name { + font-weight: bold; + color: var(--blue); + text-decoration: none; +} + +.publisher, +.version, +.mirrors { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status { + padding: 0.25rem 0.5rem; + border-radius: 4px; font-size: 0.9rem; - color: light-dark(var(--gray), var(--off-white)); +} + +.status.installed { + background-color: var(--off-black); + color: var(--white); +} + +.status.not-installed { + background-color: var(--gray); + color: var(--white); } /* App Page Styles */ @@ -263,45 +381,6 @@ object-fit: cover; } -button, -.external-link { - padding: 10px 15px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 14px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - transition: background-color 0.3s ease; -} - -button:hover, -.external-link:hover { - opacity: 0.9; -} - -button svg, -.external-link svg { - margin-right: 5px; -} - -.primary { - background-color: var(--orange); - color: var(--white); -} - -.secondary { - background-color: var(--gray); - color: var(--white); -} - -.external-link { - background-color: var(--blue); - color: var(--white); -} - /* My Apps Page Styles */ .my-apps-page { padding: 2rem; @@ -437,7 +516,7 @@ button svg, } .message.success { - background-color: #4CAF50; + background-color: var(--green); color: var(--white); } @@ -476,133 +555,7 @@ button svg, background-color: #c62828; } -/* Store Page Styles */ -.store-page { - padding: 2rem; -} - -.store-header { - display: flex; - justify-content: space-between; - align-items: stretch; - margin-bottom: 2rem; - gap: 1rem; -} - -.search-bar { - flex-grow: 1; - display: flex; - align-items: stretch; -} - -.search-bar input { - width: 100%; - padding: 0.5rem; - font-size: 1rem; - border: 1px solid var(--gray); - border-radius: 4px; - height: 38px; -} - -.filter-button, -.store-header button { - height: 38px; - padding: 0 1rem; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; - align-self: stretch; -} - -/* Add these new styles */ -.store-header>* { - margin: 0; -} - -.store-header button { - flex-shrink: 0; -} - -.app-list table { - width: 100%; - border-collapse: collapse; -} - -.app-list th, -.app-list td { - padding: 1rem; - text-align: left; - border-bottom: 1px solid var(--gray); -} - -.app-list th { - font-weight: bold; - color: var(--orange); -} - -.app-row:hover { - background-color: light-dark(var(--tan), var(--maroon)); -} - -.app-name { - font-weight: bold; - color: var(--blue); - text-decoration: none; -} - -.publisher, -.version, -.mirrors { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.status { - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-size: 0.9rem; -} - -.status.installed { - background-color: var(--off-black); - color: var(--white); -} - -.status.not-installed { - background-color: var(--gray); - color: var(--white); -} - -.app-info-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-bottom: 1rem; -} - -.app-info-item { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.app-info-item>span:first-child { - font-weight: bold; - margin-bottom: 0.5rem; -} - -.app-info-item a { - color: var(--blue); - text-decoration: none; -} - -.app-info-item a:hover { - text-decoration: underline; -} - +/* Mirrors Dropdown Styles */ .mirrors-dropdown { position: relative; } @@ -613,7 +566,7 @@ button svg, cursor: pointer; display: flex; align-items: center; - color: var(--off-black); + color: light-dark(var(--off-black), var(--off-white)); } .mirrors-list { @@ -638,8 +591,7 @@ button svg, cursor: pointer; padding: 5px; margin-right: 10px; - display: inline-flex !important; - /* Force display */ + display: inline-flex; align-items: center; justify-content: center; width: 24px; @@ -649,8 +601,7 @@ button svg, .check-button svg { width: 16px; height: 16px; - color: var(--off-black); - /* Ensure visibility */ + color: light-dark(var(--off-black), var(--off-white)); } .spinning { @@ -673,15 +624,72 @@ button svg, } .online { - color: green; + color: var(--green); } .offline { - color: red; + color: var(--ansi-red); } .error-message { margin-left: 5px; font-size: 0.8em; - color: #888; + color: var(--gray); +} + +.progress-container { + margin-top: 20px; +} + +.progress-bar { + width: 100%; + height: 24px; + background-color: var(--gray-light); + border-radius: 12px; + overflow: hidden; + position: relative; +} + +.progress { + height: 100%; + background-color: var(--blue); + transition: width 0.3s ease; +} + +.progress-percentage { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--white); + font-size: 14px; + font-weight: bold; + text-shadow: 0 0 2px rgba(0, 0, 0, 0.5); +} + +.capabilities-section { + margin-top: 20px; +} + +.capabilities-section h3 { + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px; + background-color: var(--gray-light); + border-radius: 5px; +} + +.capabilities { + background-color: var(--off-white); + padding: 10px; + border-radius: 5px; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-all; } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index 04dddc6e6..89b44741e 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -1,43 +1,111 @@ -import React, { useState } from "react"; -import { useParams } from "react-router-dom"; -import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt } from "react-icons/fa"; +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt, FaSpinner, FaPlay, FaExclamationTriangle } from "react-icons/fa"; import useAppsStore from "../store"; import { appId } from "../utils/app"; import { MirrorCheckFile } from "../types/Apps"; export default function AppPage() { - const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps } = useAppsStore(); + const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps, downloadApp, getCaps, getApp } = useAppsStore(); const { id } = useParams(); + const app = apps.find(a => appId(a) === id); const [showMetadata, setShowMetadata] = useState(true); const [showLocalInfo, setShowLocalInfo] = useState(true); const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({}); + const [selectedMirror, setSelectedMirror] = useState(null); + const [isDownloading, setIsDownloading] = useState(false); + const [isInstalling, setIsInstalling] = useState(false); + const [error, setError] = useState(null); + const [caps, setCaps] = useState(null); + const [showCaps, setShowCaps] = useState(false); + const [localProgress, setLocalProgress] = useState(null); + + useEffect(() => { + if (app) { + checkMirrors(); + fetchCaps(); + } + }, [app]); if (!app) { return

App details not found for {id}

; } - const handleInstall = () => app && installApp(app); - const handleUpdate = () => app && updateApp(app); - const handleUninstall = () => app && uninstallApp(app); - const handleMirror = () => app && setMirroring(app, !app.state?.mirroring); - const handleApproveCaps = () => app && approveCaps(app); - const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update); + const checkMirrors = async () => { + const mirrors = [app.publisher, ...(app.metadata?.properties?.mirrors || [])]; + const statuses: { [mirror: string]: MirrorCheckFile | null } = {}; + for (const mirror of mirrors) { + const status = await checkMirror(mirror); + statuses[mirror] = status; + } + setMirrorStatuses(statuses); + setSelectedMirror(statuses[app.publisher]?.is_online ? app.publisher : mirrors.find(m => statuses[m]?.is_online) || null); + }; + + const fetchCaps = async () => { + try { + const appCaps = await getCaps(app); + setCaps(appCaps); + } catch (error) { + console.error('Failed to fetch capabilities:', error); + setError(`Failed to fetch capabilities: ${error instanceof Error ? error.message : String(error)}`); + } + }; + + const handleDownload = async () => { + if (selectedMirror) { + setError(null); + setIsDownloading(true); + setLocalProgress(0); + try { + await downloadApp(app, selectedMirror); + setLocalProgress(100); + setTimeout(() => { + setIsDownloading(false); + setLocalProgress(null); + }, 3000); + } catch (error) { + console.error('Download failed:', error); + setError(`Download failed: ${error instanceof Error ? error.message : String(error)}`); + setIsDownloading(false); + setLocalProgress(null); + } + } + }; + + const handleInstall = async () => { + setIsInstalling(true); + setError(null); + try { + if (!caps?.approved) { + await approveCaps(app); + } + await installApp(app); + await getApp(app.package); + } catch (error) { + console.error('Installation failed:', error); + setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`); + } finally { + setIsInstalling(false); + } + }; + + const handleUpdate = () => updateApp(app); + const handleUninstall = () => uninstallApp(app); + const handleMirror = () => setMirroring(app, !app.state?.mirroring); + const handleAutoUpdate = () => setAutoUpdate(app, !app.state?.auto_update); const handleLaunch = () => { console.log("Launching app:", app.package); - window.open(`/${app.package}:${app.publisher}`, '_blank'); + window.open(`/${app.package}${app.package}:${app.publisher}`, '_blank'); }; + const isDownloaded = app.state !== undefined; + const isInstalled = app.installed; - const handleCheckMirror = (mirror: string) => { - setMirrorStatuses(prev => ({ ...prev, [mirror]: null })); // Set to loading - checkMirror(mirror) - .then(status => setMirrorStatuses(prev => ({ ...prev, [mirror]: status }))) - .catch(error => { - console.error(`Failed to check mirror ${mirror}:`, error); - setMirrorStatuses(prev => ({ ...prev, [mirror]: { node: mirror, is_online: false, error: "Request failed" } })); - }); - }; + const progressPercentage = localProgress !== null + ? localProgress + : isDownloaded ? 100 : 0; return (
@@ -53,8 +121,8 @@ export default function AppPage() {
{app.metadata?.description || "No description available"}
-
-
+
+

setShowMetadata(!showMetadata)}> Metadata {showMetadata ? : } @@ -67,27 +135,27 @@ export default function AppPage() {
  • Mirrors:
      - {app.metadata?.properties?.mirrors?.map((mirror) => ( + {Object.entries(mirrorStatuses).map(([mirror, status]) => (
    • {mirror} - {mirrorStatuses[mirror] && ( + {status && ( - {mirrorStatuses[mirror]?.is_online ? ( + {status.is_online ? ( <> Online ) : ( <> Offline - {mirrorStatuses[mirror]?.error && ( + {status.error && ( - ({mirrorStatuses[mirror]?.error}) + ({status.error}) )} @@ -109,7 +177,7 @@ export default function AppPage() {
      • Installed: - {app.installed ? : } + {isInstalled ? : }
      • Installed Version: {app.state?.our_version || "Not installed"}
      • @@ -119,7 +187,7 @@ export default function AppPage() {
      • License: {app.metadata?.properties?.license || "Not specified"}
      • Capabilities Approved: - @@ -143,21 +211,88 @@ export default function AppPage() { )}
  • -
    - {app.installed ? ( - <> - - - - - ) : ( - + +
    +
    + {isInstalled ? ( + <> + + + + + ) : ( + <> +
    + +
    + + + + )} + {app.metadata?.external_url && ( + + External Link + + )} +
    + + {(isDownloading || isDownloaded) && ( +
    +
    +
    +
    {progressPercentage}%
    +
    +
    )} - {app.metadata?.external_url && ( - - External Link - + + {error && ( +
    + {error} +
    )} + +
    +

    setShowCaps(!showCaps)}> + Requested Capabilities {showCaps ? : } +

    + {showCaps && caps && ( +
    {JSON.stringify(caps, null, 2)}
    + )} +
    @@ -173,4 +308,5 @@ export default function AppPage() { )}
    ); + } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/store/index.ts b/kinode/packages/app_store/ui/src/store/index.ts index 8f63ad828..8e7482b79 100644 --- a/kinode/packages/app_store/ui/src/store/index.ts +++ b/kinode/packages/app_store/ui/src/store/index.ts @@ -11,7 +11,7 @@ const BASE_URL = '/main:app_store:sys' interface AppsStore { apps: AppInfo[] ws: KinodeClientApi - downloads: Map + downloads: Record getApps: () => Promise getApp: (id: string) => Promise checkMirror: (node: string) => Promise @@ -31,7 +31,7 @@ const useAppsStore = create()( (set, get) => ({ apps: [], - downloads: new Map(), + downloads: {}, ws: new KinodeClientApi({ uri: WEBSOCKET_URL, @@ -39,14 +39,19 @@ const useAppsStore = create()( processId: "main:app_store:sys", onMessage: (message) => { const data = JSON.parse(message); - console.log('we got a json message', data) if (data.kind === 'progress') { - const appId = data.data.name.split('/').pop().split('.').shift(); - set((state) => { - const newDownloads = new Map(state.downloads); - newDownloads.set(appId, [data.data.chunks_received, data.data.total_chunks]); - return { downloads: newDownloads }; - }); + const appId = data.data.file_name.slice(1).replace('.zip', ''); + console.log('got app id with progress: ', appId, data.data.chunks_received, data.data.total_chunks) + set((state) => ({ + downloads: { + ...state.downloads, + [appId]: [data.data.chunks_received, data.data.total_chunks] + } + })); + + if (data.data.chunks_received === data.data.total_chunks) { + get().getApp(appId); + } } }, onOpen: (_e) => { @@ -81,7 +86,7 @@ const useAppsStore = create()( }, installApp: async (app: AppInfo) => { - const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'POST' }) + const res = await fetch(`${BASE_URL}/apps/${appId(app)}/install`, { method: 'POST' }) if (res.status !== HTTP_STATUS.CREATED) { throw new Error(`Failed to install app: ${appId(app)}`) } @@ -89,11 +94,8 @@ const useAppsStore = create()( }, updateApp: async (app: AppInfo) => { - const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'PUT' }) - if (res.status !== HTTP_STATUS.CREATED) { - throw new Error(`Failed to update app: ${appId(app)}`) - } - await get().getApp(appId(app)) + // Note: The backend doesn't have a specific update endpoint, so we might need to implement this differently + throw new Error('Update functionality not implemented') }, uninstallApp: async (app: AppInfo) => { @@ -105,8 +107,8 @@ const useAppsStore = create()( }, downloadApp: async (app: AppInfo, downloadFrom: string) => { - const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { - method: 'POST', + const res = await fetch(`${BASE_URL}/apps/${appId(app)}/download`, { + method: 'PUT', body: JSON.stringify({ download_from: downloadFrom }), }) if (res.status !== HTTP_STATUS.CREATED) { From c507d7c384a0b5be5ec637ba415094f8bfca1922 Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 17:55:32 +0300 Subject: [PATCH 6/7] quickfix --- kinode/packages/app_store/ui/src/pages/AppPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index 89b44741e..a8a292317 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -100,7 +100,7 @@ export default function AppPage() { window.open(`/${app.package}${app.package}:${app.publisher}`, '_blank'); }; - const isDownloaded = app.state !== undefined; + const isDownloaded = app.state !== null; const isInstalled = app.installed; const progressPercentage = localProgress !== null From cebdf10affd2fa2dd32f610cdf89611279bf4a2a Mon Sep 17 00:00:00 2001 From: bitful-pannul Date: Thu, 1 Aug 2024 17:57:00 +0300 Subject: [PATCH 7/7] doublequickfix --- kinode/packages/app_store/ui/src/pages/AppPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index a8a292317..24a77b960 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -97,7 +97,7 @@ export default function AppPage() { const handleAutoUpdate = () => setAutoUpdate(app, !app.state?.auto_update); const handleLaunch = () => { console.log("Launching app:", app.package); - window.open(`/${app.package}${app.package}:${app.publisher}`, '_blank'); + window.open(`/${app.package}:${app.package}:${app.publisher}`, '_blank'); }; const isDownloaded = app.state !== null;