Drop files here to upload
+or click to browse
+diff --git a/AGENTS.md b/AGENTS.md index 9bdf4ed..381b8cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ > **Purpose**: Memungkinkan AI agent berikutnya melanjutkan development tanpa mengulang atau merusak. > **Last Updated**: 2026-07-07 -> **Current Phase**: Sprint 1 + 2 + 3 selesai, Sprint 4 berikutnya +> **Current Phase**: Sprint 1 + 2 + 3 + 4 selesai, Sprint 5 berikutnya --- @@ -116,7 +116,7 @@ Alasan: ### ❌ Belum Dimulai / In Progress - **Sprint 2 (DONE)**: Snippets + Keychain - **Sprint 3 (DONE)**: Settings + Brief -- Sprint 4: SFTP Dual-Pane +- **Sprint 4 (DONE)**: SFTP Dual-Pane - Sprint 5: Terminal + xterm.js - Sprint 6: Storage Integration diff --git a/app/backend/internal/handler/sftp.go b/app/backend/internal/handler/sftp.go new file mode 100644 index 0000000..019b007 --- /dev/null +++ b/app/backend/internal/handler/sftp.go @@ -0,0 +1,78 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "git.tukangketik.id/swanadiva/hostkeeper/v2/internal/model" +) + +var fileStore = model.NewFileStore() + +func SFTP(c *fiber.Ctx) error { + local, remote := fileStore.All() + return c.Render("sftp", fiber.Map{ + "View": "sftp", + "Title": "SFTP", + "LocalFiles": local, + "RemoteFiles": remote, + "LocalBreadcrumb": fileStore.LocalBreadcrumb, + "RemoteBreadcrumb": fileStore.RemoteBreadcrumb, + "NavItems": navItems, + }) +} + +func SFTPPane(c *fiber.Ctx) error { + pane := c.Query("pane", "local") + dir := c.Query("dir", "/") + local, remote := fileStore.All() + files := local + breadcrumb := fileStore.LocalBreadcrumb + if pane == "remote" { + files = remote + breadcrumb = fileStore.RemoteBreadcrumb + } + return c.Render("sftp_pane", fiber.Map{ + "Pane": pane, + "Files": files, + "Breadcrumb": breadcrumb, + "CurrentDir": dir, + }) +} + +func CreateFolder(c *fiber.Ctx) error { + pane := c.FormValue("pane", "local") + name := c.FormValue("name") + if name != "" { + item := model.FileItem{ + Name: name, Size: "—", Modified: "Just now", + Type: "folder", Permissions: "drwxr-xr-x", + } + if pane == "remote" { + fileStore.Remote = append([]model.FileItem{item}, fileStore.Remote...) + } else { + fileStore.Local = append([]model.FileItem{item}, fileStore.Local...) + } + } + files := fileStore.Local + if pane == "remote" { files = fileStore.Remote } + return c.Render("sftp_pane", fiber.Map{ + "Pane": pane, "Files": files, + "Breadcrumb": fileStore.LocalBreadcrumb, + }) +} + +func DeleteFile(c *fiber.Ctx) error { + pane := c.Query("pane", "local") + name := c.Query("name") + files := &fileStore.Local + if pane == "remote" { files = &fileStore.Remote } + for i, f := range *files { + if f.Name == name { + *files = append((*files)[:i], (*files)[i+1:]...) + break + } + } + return c.Render("sftp_pane", fiber.Map{ + "Pane": pane, "Files": *files, + "Breadcrumb": fileStore.LocalBreadcrumb, + }) +} diff --git a/app/backend/internal/model/file.go b/app/backend/internal/model/file.go new file mode 100644 index 0000000..d39e066 --- /dev/null +++ b/app/backend/internal/model/file.go @@ -0,0 +1,60 @@ +package model + +type FileItem struct { + Name string `json:"name"` + Size string `json:"size"` + Modified string `json:"modified"` + Type string `json:"type"` // folder | file | image | code | lock + Extension string `json:"extension"` + Permissions string `json:"permissions"` + Path string `json:"path"` +} + +type FileStore struct { + Local []FileItem + Remote []FileItem + LocalBreadcrumb []Breadcrumb + RemoteBreadcrumb []Breadcrumb +} + +type Breadcrumb struct { + Name string + Path string +} + +func NewFileStore() *FileStore { + return &FileStore{ + Local: defaultLocalFiles(), + Remote: defaultRemoteFiles(), + LocalBreadcrumb: []Breadcrumb{{Name: "~", Path: "/home/user"}, {Name: "projects", Path: "/home/user/projects"}}, + RemoteBreadcrumb: []Breadcrumb{{Name: "/", Path: "/"}, {Name: "var", Path: "/var"}, {Name: "www", Path: "/var/www"}}, + } +} + +func (s *FileStore) All() (local, remote []FileItem) { return s.Local, s.Remote } + +func defaultLocalFiles() []FileItem { + return []FileItem{ + {Name: "Downloads", Size: "—", Modified: "2 hours ago", Type: "folder", Permissions: "drwxr-xr-x", Path: "/home/user/Downloads"}, + {Name: "Documents", Size: "—", Modified: "Yesterday", Type: "folder", Permissions: "drwxr-xr-x", Path: "/home/user/Documents"}, + {Name: "projects", Size: "—", Modified: "Just now", Type: "folder", Permissions: "drwxrwxr-x", Path: "/home/user/projects"}, + {Name: "config.yml", Size: "1.4 KB", Modified: "Yesterday", Type: "code", Extension: "yml", Permissions: "-rw-r--r--", Path: "/home/user/projects/config.yml"}, + {Name: "README.md", Size: "3.2 KB", Modified: "2 days ago", Type: "code", Extension: "md", Permissions: "-rw-r--r--", Path: "/home/user/projects/README.md"}, + {Name: "screenshot.png", Size: "240 KB", Modified: "3 days ago", Type: "image", Extension: "png", Permissions: "-rw-r--r--", Path: "/home/user/Downloads/screenshot.png"}, + {Name: "archive.zip", Size: "42 MB", Modified: "1 week ago", Type: "file", Extension: "zip", Permissions: "-rw-r--r--", Path: "/home/user/Downloads/archive.zip"}, + {Name: "vault.enc", Size: "8 KB", Modified: "2 weeks ago", Type: "lock", Extension: "enc", Permissions: "-rw-------", Path: "/home/user/vault.enc"}, + } +} + +func defaultRemoteFiles() []FileItem { + return []FileItem{ + {Name: "html", Size: "—", Modified: "2 hours ago", Type: "folder", Permissions: "drwxr-xr-x", Path: "/var/www/html"}, + {Name: "assets", Size: "—", Modified: "Yesterday", Type: "folder", Permissions: "drwxr-xr-x", Path: "/var/www/assets"}, + {Name: "index.html", Size: "4.1 KB", Modified: "Just now", Type: "code", Extension: "html", Permissions: "-rw-r--r--", Path: "/var/www/index.html"}, + {Name: "app.py", Size: "12.8 KB", Modified: "2 hours ago", Type: "code", Extension: "py", Permissions: "-rwxr-xr-x", Path: "/var/www/app.py"}, + {Name: "Dockerfile", Size: "512 B", Modified: "Yesterday", Type: "code", Extension: "Dockerfile", Permissions: "-rw-r--r--", Path: "/var/www/Dockerfile"}, + {Name: "compose.yml", Size: "2.1 KB", Modified: "Yesterday", Type: "code", Extension: "yml", Permissions: "-rw-r--r--", Path: "/var/www/compose.yml"}, + {Name: ".env", Size: "156 B", Modified: "3 days ago", Type: "lock", Extension: "env", Permissions: "-rw-------", Path: "/var/www/.env"}, + {Name: "logo.svg", Size: "6.7 KB", Modified: "1 week ago", Type: "image", Extension: "svg", Permissions: "-rw-r--r--", Path: "/var/www/assets/logo.svg"}, + } +} diff --git a/app/backend/internal/model/transfer.go b/app/backend/internal/model/transfer.go new file mode 100644 index 0000000..308ca3d --- /dev/null +++ b/app/backend/internal/model/transfer.go @@ -0,0 +1,22 @@ +package model + +type Transfer struct { + ID string `json:"id"` + FileName string `json:"fileName"` + Source string `json:"source"` + Destination string `json:"destination"` + Progress int `json:"progress"` + Speed string `json:"speed"` + Type string `json:"type"` // upload | download + Status string `json:"status"` // queued | transferring | completed | error +} + +type TransferStore struct { + Transfers []Transfer +} + +func NewTransferStore() *TransferStore { + return &TransferStore{} +} + +func (s *TransferStore) All() []Transfer { return s.Transfers } diff --git a/app/backend/main.go b/app/backend/main.go index 00c40d9..a5061ee 100644 --- a/app/backend/main.go +++ b/app/backend/main.go @@ -18,6 +18,15 @@ func main() { engine.AddFunc("svg", func(name string) string { return "" }) + engine.AddFunc("dict", func(values ...interface{}) map[string]interface{} { + m := make(map[string]interface{}) + for i := 0; i < len(values)-1; i += 2 { + if key, ok := values[i].(string); ok { + m[key] = values[i+1] + } + } + return m + }) app := fiber.New(fiber.Config{ Views: engine, @@ -43,6 +52,10 @@ func main() { app.Get("/settings", handler.Settings) app.Post("/settings", handler.UpdateConfig) app.Get("/brief", handler.Brief) + app.Get("/sftp", handler.SFTP) + app.Get("/sftp/pane", handler.SFTPPane) + app.Post("/sftp/mkdir", handler.CreateFolder) + app.Delete("/sftp/rm", handler.DeleteFile) app.Get("/api/health", handler.Health) log.Fatal(app.Listen(":1947")) diff --git a/app/backend/views/sftp.html b/app/backend/views/sftp.html new file mode 100644 index 0000000..9161ff9 --- /dev/null +++ b/app/backend/views/sftp.html @@ -0,0 +1,128 @@ + + +
+ + +Drop files here to upload
+or click to browse
+This directory is empty
+