Skip to content

Commit d0b3eec

Browse files
committed
layout system refactor
1 parent b33c90e commit d0b3eec

File tree

22 files changed

+761
-863
lines changed

22 files changed

+761
-863
lines changed

services/host-agent/internal/api/routes.go

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,6 @@ func (s *Server) handleInstall(w http.ResponseWriter, r *http.Request) {
308308
return
309309
}
310310

311-
// Add app to all users' layouts (non-system apps only)
312-
catalogApp, _ := s.catalog.Get(name)
313-
if catalogApp != nil && !catalogApp.IsSystem {
314-
if err := s.userStore.AddAppToAllUsersLayouts(name); err != nil {
315-
s.logger.Warn("failed to add app to users layouts", "app", name, "error", err)
316-
}
317-
}
318-
319311
// Trigger reconciliation to configure dependent apps
320312
s.triggerReconcile()
321313

@@ -359,11 +351,6 @@ func (s *Server) handleUninstall(w http.ResponseWriter, r *http.Request) {
359351
return
360352
}
361353

362-
// Remove app from all users' layouts
363-
if err := s.userStore.RemoveAppFromAllUsersLayouts(name); err != nil {
364-
s.logger.Warn("failed to remove app from users layouts", "app", name, "error", err)
365-
}
366-
367354
// Trigger reconciliation to update dependent apps
368355
s.triggerReconcile()
369356

@@ -563,22 +550,18 @@ func (s *Server) handleGetLayout(w http.ResponseWriter, r *http.Request) {
563550
return
564551
}
565552

566-
layout, err := s.userStore.GetLayout(user.ID)
553+
elements, err := s.userStore.GetLayout(user.ID)
567554
if err != nil {
568555
s.logger.Error("failed to get layout", "error", err)
569556
respondError(w, http.StatusInternalServerError, "failed to get layout")
570557
return
571558
}
572559

573-
if layout == nil {
574-
// Return empty defaults
575-
layout = &store.Layout{
576-
Items: []store.GridItem{},
577-
WidgetConfigs: map[string]map[string]any{},
578-
}
560+
if elements == nil {
561+
elements = []store.GridElement{}
579562
}
580563

581-
respondJSON(w, http.StatusOK, layout)
564+
respondJSON(w, http.StatusOK, elements)
582565
}
583566

584567
// handleSetLayout updates the user's layout
@@ -589,13 +572,13 @@ func (s *Server) handleSetLayout(w http.ResponseWriter, r *http.Request) {
589572
return
590573
}
591574

592-
var layout store.Layout
593-
if err := json.NewDecoder(r.Body).Decode(&layout); err != nil {
575+
var elements []store.GridElement
576+
if err := json.NewDecoder(r.Body).Decode(&elements); err != nil {
594577
respondError(w, http.StatusBadRequest, "invalid request body")
595578
return
596579
}
597580

598-
if err := s.userStore.SetLayout(user.ID, &layout); err != nil {
581+
if err := s.userStore.SetLayout(user.ID, elements); err != nil {
599582
s.logger.Error("failed to set layout", "error", err)
600583
respondError(w, http.StatusInternalServerError, "failed to save layout")
601584
return

services/host-agent/internal/db/schema.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
CREATE TABLE IF NOT EXISTS users (
55
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
66
username TEXT UNIQUE NOT NULL,
7-
layout JSONB DEFAULT '{"items":[],"widgetConfigs":{}}',
7+
layout JSONB DEFAULT '[]',
88
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
99
);
1010

services/host-agent/internal/store/users.go

Lines changed: 23 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ type User struct {
1414
CreatedAt time.Time `json:"created_at"`
1515
}
1616

17-
// GridItem represents an item (app or widget) in the layout grid
18-
type GridItem struct {
17+
// GridElement represents an element (app or widget) in the layout grid
18+
type GridElement struct {
1919
Type string `json:"type"` // "app" or "widget"
2020
ID string `json:"id"` // app name or widget id
2121
Col int `json:"col"` // 1-based column position
@@ -24,12 +24,6 @@ type GridItem struct {
2424
Rowspan int `json:"rowspan"` // number of rows to span
2525
}
2626

27-
// Layout represents the user's home page layout
28-
type Layout struct {
29-
Items []GridItem `json:"items"`
30-
WidgetConfigs map[string]map[string]any `json:"widgetConfigs"`
31-
}
32-
3327
// UserStore manages users in the database
3428
type UserStore struct {
3529
db *sql.DB
@@ -78,204 +72,49 @@ func (s *UserStore) GetByUsername(username string) (*User, error) {
7872
return &user, nil
7973
}
8074

81-
// GetLayout returns the user's layout preferences
82-
func (s *UserStore) GetLayout(userID string) (*Layout, error) {
83-
var prefsJSON []byte
75+
// GetLayout returns the user's layout as an array of grid elements
76+
func (s *UserStore) GetLayout(userID string) ([]GridElement, error) {
77+
var layoutJSON []byte
8478
err := s.db.QueryRow(
85-
"SELECT COALESCE(layout, '{\"items\":[],\"widgetConfigs\":{}}') FROM users WHERE id = $1",
79+
"SELECT COALESCE(layout, '[]') FROM users WHERE id = $1",
8680
userID,
87-
).Scan(&prefsJSON)
81+
).Scan(&layoutJSON)
8882
if err == sql.ErrNoRows {
8983
return nil, nil
9084
}
9185
if err != nil {
9286
return nil, fmt.Errorf("failed to get layout: %w", err)
9387
}
9488

95-
var prefs Layout
96-
if err := json.Unmarshal(prefsJSON, &prefs); err != nil {
89+
// Try to parse as array first (new format)
90+
var elements []GridElement
91+
if err := json.Unmarshal(layoutJSON, &elements); err == nil {
92+
return elements, nil
93+
}
94+
95+
// Fall back to old format {elements: [...], widgetConfigs: {...}}
96+
var oldFormat struct {
97+
Elements []GridElement `json:"elements"`
98+
}
99+
if err := json.Unmarshal(layoutJSON, &oldFormat); err != nil {
97100
return nil, fmt.Errorf("failed to parse layout: %w", err)
98101
}
99-
return &prefs, nil
102+
return oldFormat.Elements, nil
100103
}
101104

102-
// SetLayout updates the user's layout preferences
103-
func (s *UserStore) SetLayout(userID string, prefs *Layout) error {
104-
prefsJSON, err := json.Marshal(prefs)
105+
// SetLayout updates the user's layout
106+
func (s *UserStore) SetLayout(userID string, elements []GridElement) error {
107+
layoutJSON, err := json.Marshal(elements)
105108
if err != nil {
106109
return fmt.Errorf("failed to marshal layout: %w", err)
107110
}
108111

109112
_, err = s.db.Exec(
110113
"UPDATE users SET layout = $1 WHERE id = $2",
111-
prefsJSON, userID,
114+
layoutJSON, userID,
112115
)
113116
if err != nil {
114117
return fmt.Errorf("failed to update layout: %w", err)
115118
}
116119
return nil
117120
}
118-
119-
// AddAppToLayout adds an app to the user's layout at the next available position
120-
func (s *UserStore) AddAppToLayout(userID string, appName string) error {
121-
prefs, err := s.GetLayout(userID)
122-
if err != nil {
123-
return err
124-
}
125-
if prefs == nil {
126-
prefs = &Layout{Items: []GridItem{}, WidgetConfigs: map[string]map[string]any{}}
127-
}
128-
129-
// Check if app already exists
130-
for _, item := range prefs.Items {
131-
if item.Type == "app" && item.ID == appName {
132-
return nil // Already exists
133-
}
134-
}
135-
136-
// Find next available position (simple algorithm: find first empty 1x1 slot)
137-
pos := findNextAvailablePosition(prefs.Items, 1, 1)
138-
139-
prefs.Items = append(prefs.Items, GridItem{
140-
Type: "app",
141-
ID: appName,
142-
Col: pos.col,
143-
Row: pos.row,
144-
Colspan: 1,
145-
Rowspan: 1,
146-
})
147-
148-
return s.SetLayout(userID, prefs)
149-
}
150-
151-
// RemoveAppFromLayout removes an app from the user's layout
152-
func (s *UserStore) RemoveAppFromLayout(userID string, appName string) error {
153-
prefs, err := s.GetLayout(userID)
154-
if err != nil {
155-
return err
156-
}
157-
if prefs == nil {
158-
return nil // No prefs, nothing to remove
159-
}
160-
161-
// Filter out the app
162-
newItems := make([]GridItem, 0, len(prefs.Items))
163-
for _, item := range prefs.Items {
164-
if !(item.Type == "app" && item.ID == appName) {
165-
newItems = append(newItems, item)
166-
}
167-
}
168-
prefs.Items = newItems
169-
170-
return s.SetLayout(userID, prefs)
171-
}
172-
173-
// position helper for grid placement
174-
type position struct {
175-
col int
176-
row int
177-
}
178-
179-
// findNextAvailablePosition finds the next available grid position for an item
180-
func findNextAvailablePosition(items []GridItem, colspan, rowspan int) position {
181-
const gridCols = 6
182-
183-
// Find max row currently in use
184-
maxRow := 0
185-
for _, item := range items {
186-
endRow := item.Row + item.Rowspan - 1
187-
if endRow > maxRow {
188-
maxRow = endRow
189-
}
190-
}
191-
192-
// Search for available space row by row
193-
for row := 1; row <= maxRow+10; row++ {
194-
for col := 1; col <= gridCols-colspan+1; col++ {
195-
canPlace := true
196-
for c := col; c < col+colspan && canPlace; c++ {
197-
for r := row; r < row+rowspan && canPlace; r++ {
198-
if isCellOccupied(items, c, r) {
199-
canPlace = false
200-
}
201-
}
202-
}
203-
if canPlace {
204-
return position{col: col, row: row}
205-
}
206-
}
207-
}
208-
209-
// Fallback: place at end
210-
return position{col: 1, row: maxRow + 1}
211-
}
212-
213-
// isCellOccupied checks if a cell is occupied by any item
214-
func isCellOccupied(items []GridItem, col, row int) bool {
215-
for _, item := range items {
216-
endCol := item.Col + item.Colspan - 1
217-
endRow := item.Row + item.Rowspan - 1
218-
if col >= item.Col && col <= endCol && row >= item.Row && row <= endRow {
219-
return true
220-
}
221-
}
222-
return false
223-
}
224-
225-
// AddAppToAllUsersLayouts adds an app to all users' layouts
226-
// Called when an app is installed
227-
func (s *UserStore) AddAppToAllUsersLayouts(appName string) error {
228-
// Get all user IDs
229-
rows, err := s.db.Query("SELECT id FROM users")
230-
if err != nil {
231-
return fmt.Errorf("failed to get users: %w", err)
232-
}
233-
defer rows.Close()
234-
235-
var userIDs []string
236-
for rows.Next() {
237-
var id string
238-
if err := rows.Scan(&id); err != nil {
239-
return fmt.Errorf("failed to scan user id: %w", err)
240-
}
241-
userIDs = append(userIDs, id)
242-
}
243-
244-
// Add app to each user's layout
245-
for _, userID := range userIDs {
246-
if err := s.AddAppToLayout(userID, appName); err != nil {
247-
return fmt.Errorf("failed to add app to user %s layout: %w", userID, err)
248-
}
249-
}
250-
251-
return nil
252-
}
253-
254-
// RemoveAppFromAllUsersLayouts removes an app from all users' layouts
255-
// Called when an app is uninstalled
256-
func (s *UserStore) RemoveAppFromAllUsersLayouts(appName string) error {
257-
// Get all user IDs
258-
rows, err := s.db.Query("SELECT id FROM users")
259-
if err != nil {
260-
return fmt.Errorf("failed to get users: %w", err)
261-
}
262-
defer rows.Close()
263-
264-
var userIDs []string
265-
for rows.Next() {
266-
var id string
267-
if err := rows.Scan(&id); err != nil {
268-
return fmt.Errorf("failed to scan user id: %w", err)
269-
}
270-
userIDs = append(userIDs, id)
271-
}
272-
273-
// Remove app from each user's layout
274-
for _, userID := range userIDs {
275-
if err := s.RemoveAppFromLayout(userID, appName); err != nil {
276-
return fmt.Errorf("failed to remove app from user %s layout: %w", userID, err)
277-
}
278-
}
279-
280-
return nil
281-
}

services/host-agent/web/src/lib/api/apps.ts

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)