Fix OTA capability check and separate user apps/repo paths

- internal/data: Add OTACapable method to Device checking for firmware >= 1.4.4
- web/templates: Update firmware update UI to check for OTA capability
- internal/gitutils: Fix EnsureRepo to handle non-repo directories by re-cloning
- internal/server: Move user git repo sync location to 'repo' directory to avoid conflict with 'apps' uploads
- internal/apps: Update ListUserApps to scan both 'apps' and 'repo' directories
- CHANGELOG: Update OTA requirements note
This commit is contained in:
Ingmar Stein
2025-12-20 10:51:01 +01:00
parent 4c04fbcc61
commit f67dee9b5e
6 changed files with 55 additions and 8 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ The entire backend has been rewritten in Go (1.25+). This change offers:
* `reset-password`: Manually reset a user's password.
* `health`: Perform health checks against the running server.
* **Passkey Authentication:** Added support for passkey authentication (requires HTTPS on some browsers) for more secure and convenient logins.
* **Over-The-Air (OTA) Updates:** Devices running compatible firmware can now be updated directly from the web interface. Updates are delivered via WebSocket commands or HTTP headers, streamlining the firmware management process.
* **Over-The-Air (OTA) Updates:** Devices running compatible firmware can now be updated directly from the web interface. Updates are delivered via WebSocket commands or HTTP headers, streamlining the firmware management process. Requires device firmware version >= 1.4.4.
* **App Configuration Export/Import:** Users can now export app configurations to a JSON file and import them back into existing app installations. This makes it easy to backup configurations or replicate complex setups across different apps.
* **ZIP-packaged App Support:** Users can now upload and run apps packaged as ZIP files. This enables more complex apps that split logic across multiple files, reference external assets like images, and include metadata via manifest files.
+27 -4
View File
@@ -161,7 +161,30 @@ func scanSystemApps(dataDir string) ([]AppMetadata, error) {
}
func ListUserApps(dataDir, username string) ([]AppMetadata, error) {
userAppsDir := filepath.Join(dataDir, "users", username, "apps")
var allApps []AppMetadata
// 1. Uploaded Apps
uploaded, err := scanUserAppsDir(dataDir, username, "apps", "User uploaded app")
if err != nil {
// Just log error, don't fail completely
slog.Warn("Failed to list uploaded user apps", "error", err)
} else {
allApps = append(allApps, uploaded...)
}
// 2. Repo Apps
repoApps, err := scanUserAppsDir(dataDir, username, "repo", "Git Repository app")
if err != nil {
slog.Warn("Failed to list user repo apps", "error", err)
} else {
allApps = append(allApps, repoApps...)
}
return allApps, nil
}
func scanUserAppsDir(dataDir, username, subDir, defaultSummary string) ([]AppMetadata, error) {
userAppsDir := filepath.Join(dataDir, "users", username, subDir)
var apps []AppMetadata
entries, err := os.ReadDir(userAppsDir)
@@ -169,7 +192,7 @@ func ListUserApps(dataDir, username string) ([]AppMetadata, error) {
return apps, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read user apps directory: %w", err)
return nil, fmt.Errorf("failed to read user apps directory %s: %w", subDir, err)
}
for _, entry := range entries {
@@ -184,7 +207,7 @@ func ListUserApps(dataDir, username string) ([]AppMetadata, error) {
Name: appName,
PackageName: appName,
Author: username,
Summary: "User uploaded app",
Summary: defaultSummary,
},
}
@@ -204,7 +227,7 @@ func ListUserApps(dataDir, username string) ([]AppMetadata, error) {
if starFile != "" {
userApp.FileName = starFile
userApp.Path = filepath.Join("users", username, "apps", appName, starFile)
userApp.Path = filepath.Join("users", username, subDir, appName, starFile)
// Infer Preview/Preview2x from convention if files exist
baseFileName := strings.TrimSuffix(starFile, ".star")
+17
View File
@@ -8,6 +8,8 @@ import (
"strconv"
"strings"
"time"
"golang.org/x/mod/semver"
)
// --- Enums & Value Types ---
@@ -648,3 +650,18 @@ func (d *Device) GetEffectiveBrightness() int {
}
return brightness
}
func (d *Device) OTACapable() bool {
if !d.Type.SupportsFirmware() {
return false
}
v := d.Info.FirmwareVersion
if v == "" {
return false
}
if !strings.HasPrefix(v, "v") {
v = "v" + v
}
return semver.Compare(v, "v1.4.4") >= 0
}
+7
View File
@@ -125,6 +125,13 @@ func EnsureRepo(path string, repoURL string, token string, update bool) error {
// Repo exists, open it
r, err := git.PlainOpen(path)
if err != nil {
if errors.Is(err, git.ErrRepositoryNotExists) {
slog.Warn("Directory exists but is not a valid git repo, re-cloning", "path", path)
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to remove invalid repo directory: %w", err)
}
return EnsureRepo(path, repoURL, token, update)
}
// If not a git repo, maybe remove and re-clone?
// For safety, error out.
return fmt.Errorf("failed to open repo: %w", err)
+2 -2
View File
@@ -186,7 +186,7 @@ func (s *Server) handleSetUserRepo(w http.ResponseWriter, r *http.Request) {
return
}
appsPath := filepath.Join(s.DataDir, "users", user.Username, "apps")
appsPath := filepath.Join(s.DataDir, "users", user.Username, "repo")
if err := gitutils.EnsureRepo(appsPath, repoURL, s.Config.GitHubToken, true); err != nil {
slog.Error("Failed to sync user repo", "error", err)
}
@@ -198,7 +198,7 @@ func (s *Server) handleRefreshUserRepo(w http.ResponseWriter, r *http.Request) {
user := GetUser(r)
if user.AppRepoURL != "" {
appsPath := filepath.Join(s.DataDir, "users", user.Username, "apps")
appsPath := filepath.Join(s.DataDir, "users", user.Username, "repo")
if err := gitutils.EnsureRepo(appsPath, user.AppRepoURL, s.Config.GitHubToken, true); err != nil {
slog.Error("Failed to refresh user repo", "error", err)
}
+1 -1
View File
@@ -535,7 +535,7 @@
</table>
</div>
{{ if .FirmwareAvailable }}
{{ if and .FirmwareAvailable .Device.OTACapable }}
<div class="device-settings-section">
<h2>{{ t .Localizer "Firmware Update" }}</h2>
<div style="padding: 10px;">