Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# BoxBox Project Context

> **Last updated:** 2026-05-06
> **Last updated:** 2026-06-12
> **Purpose:** Single source of truth for the non-`docs/` Markdown files and current codebase state.
> **Scope:** Consolidates project truth from the previous non-`docs/` Markdown set. The old audit, roadmap, refactor-plan, and nested starter README files were removed on 2026-05-06 after this file became the local source of truth.
> **Excluded by request:** Contents of `docs/` Markdown files are not merged here, though this file notes known drift between `docs/` and the implementation where it affects project truth.
Expand Down Expand Up @@ -321,7 +321,7 @@ Backend testing gaps remain:

- SvelteKit app is built as a static SPA with fallback.
- Root layout initializes Svelte Query and handles auth redirects.
- Main app flow includes login, browse, settings, and a manual `/test` component/demo route.
- Main app flow includes login, browse, and settings. The old unauthenticated `/test` component/demo route was removed during the 2026-06-12 cleanup.
- `browse/+page.svelte` is the main file manager screen and composes the active UI.

### Important frontend directories
Expand All @@ -347,7 +347,7 @@ Older frontend refactor files are stale in several areas. These are now implemen
- `src/lib/utils/fileTypes.ts` exists and is the central file-type source.
- `src/lib/utils/format.ts` exists for formatting.
- Design tokens are defined in `src/routes/layout.css`.
- `src/lib/components/ui/` exists with base components including `Button`, `Input`, `Select`, `Toggle`, `Card`, `Modal`, `Spinner`, `Badge`, `ProgressBar`, `ContextMenu`, `Toast`, and `InlineRename`.
- `src/lib/components/ui/` exists with base components including `Button`, `Input`, `Select`, `Toggle`, `Modal`, `Spinner`, `Badge`, `ProgressBar`, `ContextMenu`, `Toast`, and `InlineRename`.
- No `<style>` blocks were found under `frontend/src` during this audit.
- `FileBrowser.svelte` referenced by old docs no longer exists.

Expand Down Expand Up @@ -409,14 +409,12 @@ Current upload utility supports:
- Upload progress callbacks.
- `getUploadStatus()`.
- `resumeUpload()`.
- An `UploadManager` helper class.

Upload integration status:

- `uploadStore.addFiles()` generates the upload ID for UI queue tracking.
- The upload ID is passed through `UploadOptions` into chunk upload requests.
- Normal queue flow now calls `resumeUpload()` first and falls back to a fresh upload with the same upload ID if no server session exists.
- `UploadManager` also passes its generated upload ID into `uploadFile()`.

### WebSocket frontend

Expand Down Expand Up @@ -462,7 +460,7 @@ Current video state:
- No Vitest config was found.
- No Playwright config was found.
- No frontend automated test files were found.
- A manual `/test` route exists as a component/demo page.
- No manual component/demo route remains.

Verification status:

Expand Down Expand Up @@ -686,7 +684,7 @@ The test roadmap remains mostly aspirational for frontend and partially implemen
### Frontend now

- No automated frontend test infrastructure found.
- Manual `/test` route exists.
- No manual `/test` route remains.

### Frontend next tests

Expand Down Expand Up @@ -723,6 +721,8 @@ A pragmatic target:
- MIME type duplication in backend stream handler has been fixed.
- Frontend config/storage/file-type/design-system foundation is now implemented.
- Base UI components exist.
- The old frontend `/test` route and its demo-only components were removed.
- The old frontend `UploadManager` helper class was removed; the active upload queue is `upload.svelte.ts`.
- No component `<style>` blocks were found under `frontend/src`.
- `FileBrowser.svelte` referenced by older docs no longer exists.
- More than one store is now migrated to runes: `clipboard`, `upload`, and `toast` are Svelte 5-style stores.
Expand Down
17 changes: 8 additions & 9 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func main() {
defer cancel()

// Initialize components
server, hub, jobService, authService, streamHandler, _, err := initializeServer(ctx, cfg)
server, hub, jobService, authService, streamHandler, err := initializeServer(cfg)
if err != nil {
log.Fatal().Err(err).Msg("Failed to initialize server")
}
Expand Down Expand Up @@ -90,11 +90,11 @@ func main() {
}()

// Wait for shutdown signal
waitForShutdown(ctx, cancel, server, jobService, authService, streamHandler)
waitForShutdown(cancel, server, jobService, authService, streamHandler)
}

// initializeServer creates and configures all server components
func initializeServer(ctx context.Context, cfg *model.ServerConfig) (*http.Server, *websocket.Hub, service.JobService, service.AuthService, *handler.StreamHandler, *handler.SettingsHandler, error) {
func initializeServer(cfg *model.ServerConfig) (*http.Server, *websocket.Hub, service.JobService, service.AuthService, *handler.StreamHandler, error) {
// Create filesystem abstraction (using real OS filesystem)
fs := filesystem.NewOsFS()

Expand Down Expand Up @@ -146,7 +146,7 @@ func initializeServer(ctx context.Context, cfg *model.ServerConfig) (*http.Serve
})

jobService := service.NewJobService(fs, hub, service.JobServiceConfig{
Workers: 4,
Workers: config.DefaultJobWorkers,
MountPoints: mountPoints,
})

Expand All @@ -159,7 +159,7 @@ func initializeServer(ctx context.Context, cfg *model.ServerConfig) (*http.Serve
// Create handlers
authHandler := handler.NewAuthHandler(authService)
fileHandler := handler.NewFileHandler(fileService)
streamHandler := handler.NewStreamHandler(fileService, cfg.ChunkSizeMB)
streamHandler := handler.NewStreamHandler(fileService, cfg.ChunkSizeMB, cfg.MaxUploadMB)
jobHandler := handler.NewJobHandler(jobService)
searchHandler := handler.NewSearchHandler(searchService)
wsHandler := handler.NewWebSocketHandler(hub, authService, cfg.AllowedOrigins)
Expand All @@ -169,7 +169,6 @@ func initializeServer(ctx context.Context, cfg *model.ServerConfig) (*http.Serve
// Create router
router := createRouter(cfg, authService, authHandler, fileHandler, streamHandler, jobHandler, searchHandler, wsHandler, systemHandler, settingsHandler, mountPoints)

// Create HTTP server
// Create HTTP server
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Expand All @@ -179,7 +178,7 @@ func initializeServer(ctx context.Context, cfg *model.ServerConfig) (*http.Serve
IdleTimeout: config.HTTPIdleTimeout,
}

return server, hub, jobService, authService, streamHandler, settingsHandler, nil
return server, hub, jobService, authService, streamHandler, nil
}

// createRouter sets up chi router with all routes and middleware
Expand Down Expand Up @@ -282,7 +281,7 @@ func createRouter(
}

// waitForShutdown handles graceful shutdown on interrupt signals
func waitForShutdown(ctx context.Context, cancel context.CancelFunc, server *http.Server, jobService service.JobService, authService service.AuthService, streamHandler *handler.StreamHandler) {
func waitForShutdown(cancel context.CancelFunc, server *http.Server, jobService service.JobService, authService service.AuthService, streamHandler *handler.StreamHandler) {
// Create channel to receive OS signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
Expand All @@ -295,7 +294,7 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, server *htt
cancel()

// Create shutdown context with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), config.ShutdownTimeout)
defer shutdownCancel()

// Stop job service
Expand Down
4 changes: 2 additions & 2 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ func Load(configPath string) (*model.ServerConfig, error) {
// Set defaults
v.SetDefault("port", 80)
v.SetDefault("host", "0.0.0.0")
v.SetDefault("max_upload_mb", 10240) // 10GB default
v.SetDefault("chunk_size_mb", 5) // 5MB chunks
v.SetDefault("max_upload_mb", DefaultMaxUploadMB) // 10GB default
v.SetDefault("chunk_size_mb", 5) // 5MB chunks
v.SetDefault("rate_limit_rps", 10.0)
v.SetDefault("data_dir", DefaultDataDir)

Expand Down
3 changes: 3 additions & 0 deletions backend/internal/config/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ const (

// Upload configuration constants
const (
// DefaultMaxUploadMB is the default maximum accepted upload size in megabytes
DefaultMaxUploadMB = 10240

// DefaultChunkSizeMB is the default chunk size for uploads in megabytes
DefaultChunkSizeMB = 10

Expand Down
14 changes: 0 additions & 14 deletions backend/internal/handler/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,3 @@ func HandleServiceError(w http.ResponseWriter, err error) {
// Default to internal server error
writeError(w, "Internal server error", model.ErrCodeInternalError, http.StatusInternalServerError)
}

// HandleServiceErrorWithLog converts service errors to HTTP responses and returns
// whether the error was handled (true) or was an internal error (false)
func HandleServiceErrorWithLog(w http.ResponseWriter, err error) bool {
for _, mapping := range serviceErrorMappings {
if errors.Is(err, mapping.Error) {
writeError(w, mapping.Message, mapping.Code, mapping.StatusCode)
return true
}
}
// Internal server error - caller should log the actual error
writeError(w, "Internal server error", model.ErrCodeInternalError, http.StatusInternalServerError)
return false
}
6 changes: 2 additions & 4 deletions backend/internal/handler/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"io"
"net/http"
"strconv"
"strings"

"github.com/go-chi/chi/v5"
"github.com/homelab/filemanager/internal/model"
"github.com/homelab/filemanager/internal/pkg/validator"
"github.com/homelab/filemanager/internal/service"
)

Expand Down Expand Up @@ -140,14 +140,12 @@ func (h *FileHandler) CreateItem(w http.ResponseWriter, r *http.Request) {
itemType = "directory"
}

// Validate item name
if req.Name == "" {
writeError(w, "Name is required", model.ErrCodeValidationError, http.StatusBadRequest)
return
}

// Check for invalid characters in name
if strings.ContainsAny(req.Name, "/\\") || req.Name == "." || req.Name == ".." || strings.ContainsRune(req.Name, 0) {
if !validator.IsValidFileName(req.Name) {
writeError(w, "Name cannot contain path separators or special path names", model.ErrCodeValidationError, http.StatusBadRequest)
return
}
Expand Down
65 changes: 1 addition & 64 deletions backend/internal/handler/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,82 +7,19 @@ import (
"github.com/homelab/filemanager/internal/model"
)

// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Code string `json:"code"`
Details string `json:"details,omitempty"`
}

// writeError writes an error response with the specified message, code, and status
func writeError(w http.ResponseWriter, message, code string, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{
json.NewEncoder(w).Encode(model.ErrorResponse{
Error: message,
Code: code,
})
}

// writeErrorWithDetails writes an error response with additional details
func writeErrorWithDetails(w http.ResponseWriter, message, code, details string, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(ErrorResponse{
Error: message,
Code: code,
Details: details,
})
}

// writeJSON writes a JSON response with the specified data and status code
func writeJSON(w http.ResponseWriter, data interface{}, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}

// writeSuccess writes a simple success response
func writeSuccess(w http.ResponseWriter, message string) {
writeJSON(w, map[string]string{"message": message}, http.StatusOK)
}

// writeCreated writes a 201 Created response with the specified data
func writeCreated(w http.ResponseWriter, data interface{}) {
writeJSON(w, data, http.StatusCreated)
}

// writeNoContent writes a 204 No Content response
func writeNoContent(w http.ResponseWriter) {
w.WriteHeader(http.StatusNoContent)
}

// writeBadRequest writes a 400 Bad Request error response
func writeBadRequest(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodeValidationError, http.StatusBadRequest)
}

// writeUnauthorized writes a 401 Unauthorized error response
func writeUnauthorized(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodeUnauthorized, http.StatusUnauthorized)
}

// writeForbidden writes a 403 Forbidden error response
func writeForbidden(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodePermissionDenied, http.StatusForbidden)
}

// writeNotFound writes a 404 Not Found error response
func writeNotFound(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodeNotFound, http.StatusNotFound)
}

// writeConflict writes a 409 Conflict error response
func writeConflict(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodeConflict, http.StatusConflict)
}

// writeInternalError writes a 500 Internal Server Error response
func writeInternalError(w http.ResponseWriter, message string) {
writeError(w, message, model.ErrCodeInternalError, http.StatusInternalServerError)
}
Loading