refactor: Update session management and template rendering
- Renamed `is_online` field to `is_active` in the Sessions struct for clarity. - Updated SQL queries and session validation logic to reflect the new field name. - Enhanced login route to validate session against the database, improving security. - Refactored HTML rendering to utilize Templ components for better maintainability. - Removed deprecated HTML fragments and streamlined template structure. - Adjusted CSS styles for improved UI consistency and responsiveness.
- commit
- 10909c5743ca9f579686073e1be47b2bc397d03b
- author
- bamen · · 2025-03-30 09:14 UTC
- parent
- 0ea01e3
- signature
- unsigned
40 files · +402 -1515
M go.mod +2 -1
| @@ -5,7 +5,6 @@ go 1.24.1 | |||
| 5 | 5 | require ( | |
| 6 | 6 | github.com/AlecAivazis/survey/v2 v2.3.7 | |
| 7 | 7 | github.com/Masterminds/semver/v3 v3.3.1 | |
| 8 | - | github.com/PuerkitoBio/goquery v1.10.2 | |
| 9 | 8 | github.com/a-h/templ v0.3.833 | |
| 10 | 9 | github.com/charmbracelet/bubbles v0.20.0 | |
| 11 | 10 | github.com/charmbracelet/lipgloss v1.0.0 | |
| @@ -25,6 +24,8 @@ require ( | |||
| 25 | 24 | modernc.org/sqlite v1.36.0 | |
| 26 | 25 | ) | |
| 27 | 26 | ||
| 27 | + | require github.com/PuerkitoBio/goquery v1.10.2 // indirect | |
| 28 | + | ||
| 28 | 29 | require ( | |
| 29 | 30 | cloud.google.com/go/auth v0.13.0 // indirect | |
| 30 | 31 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect | |
| @@ -5,7 +5,6 @@ go 1.24.1 | |||||
| 5 | require ( | 5 | require ( | ||
| 6 | github.com/AlecAivazis/survey/v2 v2.3.7 | 6 | github.com/AlecAivazis/survey/v2 v2.3.7 | ||
| 7 | github.com/Masterminds/semver/v3 v3.3.1 | 7 | github.com/Masterminds/semver/v3 v3.3.1 | ||
| 8 | - | github.com/PuerkitoBio/goquery v1.10.2 | |||
| 9 | github.com/a-h/templ v0.3.833 | 8 | github.com/a-h/templ v0.3.833 | ||
| 10 | github.com/charmbracelet/bubbles v0.20.0 | 9 | github.com/charmbracelet/bubbles v0.20.0 | ||
| 11 | github.com/charmbracelet/lipgloss v1.0.0 | 10 | github.com/charmbracelet/lipgloss v1.0.0 | ||
| @@ -25,6 +24,8 @@ require ( | |||||
| 25 | modernc.org/sqlite v1.36.0 | 24 | modernc.org/sqlite v1.36.0 | ||
| 26 | ) | 25 | ) | ||
| 27 | 26 | ||||
| 27 | + | require github.com/PuerkitoBio/goquery v1.10.2 // indirect | |||
| 28 | + | ||||
| 28 | require ( | 29 | require ( | ||
| 29 | cloud.google.com/go/auth v0.13.0 // indirect | 30 | cloud.google.com/go/auth v0.13.0 // indirect | ||
| 30 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect | 31 | cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect | ||
M go.sum +0 -2 generated file: collapsed
M internal/db/db_struct.go +1 -1
| @@ -26,7 +26,7 @@ type Sessions struct { | |||
| 26 | 26 | BrowserInfo string `sql:"browser_info"` | |
| 27 | 27 | AccessToken string `sql:"access_token"` | |
| 28 | 28 | Expires string `sql:"expires"` | |
| 29 | - | IsOnline bool `sql:"is_online"` | |
| 29 | + | IsActive bool `sql:"is_active"` | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | type Provider struct { | |
| @@ -26,7 +26,7 @@ type Sessions struct { | |||||
| 26 | BrowserInfo string `sql:"browser_info"` | 26 | BrowserInfo string `sql:"browser_info"` | ||
| 27 | AccessToken string `sql:"access_token"` | 27 | AccessToken string `sql:"access_token"` | ||
| 28 | Expires string `sql:"expires"` | 28 | Expires string `sql:"expires"` | ||
| 29 | - | IsOnline bool `sql:"is_online"` | 29 | + | IsActive bool `sql:"is_active"` |
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | type Provider struct { | 32 | type Provider struct { | ||
M internal/db/queries/sessions.go +62 -22
| @@ -20,19 +20,21 @@ type SessionQueries struct { | |||
| 20 | 20 | UpdateSessionExpiry string | |
| 21 | 21 | CheckSessionExists string | |
| 22 | 22 | InvalidateSession string | |
| 23 | + | MarkSessionActive string | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 25 | 26 | // NewSessionQueries returns a new instance of SessionQueries | |
| 26 | 27 | func NewSessionQueries() *SessionQueries { | |
| 27 | 28 | return &SessionQueries{ | |
| 28 | - | InsertSession: "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)", | |
| 29 | - | GetSessionByToken: "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_online FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?", | |
| 30 | - | UpdateSessionToken: "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?", | |
| 29 | + | InsertSession: "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_active) VALUES (?, ?, ?, ?, ?, ?)", | |
| 30 | + | GetSessionByToken: "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_active FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?", | |
| 31 | + | UpdateSessionToken: "UPDATE sessions SET access_token = ?, expires = ?, is_active = true WHERE id = ?", | |
| 31 | 32 | DeleteSession: "DELETE FROM sessions WHERE account_id = ? AND id = ?", | |
| 32 | 33 | GetSessionExpiry: "SELECT expires FROM sessions WHERE account_id = ? AND id = ?", | |
| 33 | 34 | UpdateSessionExpiry: "UPDATE sessions SET expires = ? WHERE account_id = ? AND id = ?", | |
| 34 | 35 | CheckSessionExists: "SELECT EXISTS(SELECT 1 FROM sessions WHERE id = ?)", | |
| 35 | - | InvalidateSession: "UPDATE sessions SET is_online = ? WHERE id = ?", | |
| 36 | + | InvalidateSession: "UPDATE sessions SET is_active = ? WHERE id = ?", | |
| 37 | + | MarkSessionActive: "UPDATE sessions SET is_active = true WHERE id = ?", | |
| 36 | 38 | } | |
| 37 | 39 | } | |
| 38 | 40 | ||
| @@ -58,7 +60,7 @@ func CreateDBSession(database *sql.DB, browserInfo string, accessToken string, a | |||
| 58 | 60 | AccessToken: accessToken, | |
| 59 | 61 | BrowserInfo: browserInfo, | |
| 60 | 62 | Expires: expireTime, | |
| 61 | - | IsOnline: true, | |
| 63 | + | IsActive: true, | |
| 62 | 64 | } | |
| 63 | 65 | ||
| 64 | 66 | logger.Debug("Session created in database", "session_id", sessionID, "account_id", accountID, "expires", expireTime) | |
| @@ -79,7 +81,7 @@ func GetDBUserSession(database *sql.DB, accessToken string, browserInfo string) | |||
| 79 | 81 | &session.AccessToken, | |
| 80 | 82 | &session.BrowserInfo, | |
| 81 | 83 | &session.Expires, | |
| 82 | - | &session.IsOnline, | |
| 84 | + | &session.IsActive, | |
| 83 | 85 | ) | |
| 84 | 86 | ||
| 85 | 87 | if err != nil { | |
| @@ -130,12 +132,19 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||
| 130 | 132 | // If the session exists, update its token and expiry | |
| 131 | 133 | if existingSession != nil { | |
| 132 | 134 | logger.Debug("Existing session found, updating token and expiry", "sessionID", existingSession.ID, "accountID", accountID) | |
| 133 | - | _, err := database.Exec( | |
| 134 | - | NewSessionQueries().UpdateSessionToken, | |
| 135 | - | accessToken, | |
| 136 | - | expireTime, | |
| 137 | - | existingSession.ID, | |
| 135 | + | updateQuery := NewSessionQueries().UpdateSessionToken | |
| 136 | + | updateParams := []interface{}{accessToken, expireTime, existingSession.ID} | |
| 137 | + | logger.Debug("Executing session update", "query", updateQuery, "params", updateParams) | |
| 138 | + | result, err := database.Exec( | |
| 139 | + | updateQuery, | |
| 140 | + | updateParams..., | |
| 138 | 141 | ) | |
| 142 | + | if err != nil { | |
| 143 | + | logger.Error("Database Exec failed during session update", "error", err, "session_id", existingSession.ID) | |
| 144 | + | } else { | |
| 145 | + | rowsAffected, _ := result.RowsAffected() | |
| 146 | + | logger.Debug("Database Exec success during session update", "session_id", existingSession.ID, "rows_affected", rowsAffected) | |
| 147 | + | } | |
| 139 | 148 | if err != nil { | |
| 140 | 149 | logger.Error("Failed to update existing session", "error", err, "session_id", existingSession.ID) | |
| 141 | 150 | return nil, fmt.Errorf("error updating session: %w", err) | |
| @@ -143,17 +152,27 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||
| 143 | 152 | // Update the struct we have in memory | |
| 144 | 153 | existingSession.AccessToken = accessToken | |
| 145 | 154 | existingSession.Expires = expireTime | |
| 146 | - | existingSession.IsOnline = true // Ensure it's marked online | |
| 155 | + | existingSession.IsActive = true // Ensure it's marked active | |
| 147 | 156 | logger.Info("Session updated successfully", "sessionID", existingSession.ID) | |
| 148 | 157 | return existingSession, nil | |
| 149 | 158 | } else { | |
| 150 | 159 | // If the session does not exist, create a new one | |
| 151 | 160 | logger.Debug("No existing session found, creating new session", "accountID", accountID) | |
| 152 | 161 | sessionID := uuid.NewString() // Use uuid directly | |
| 153 | - | _, err := database.Exec( | |
| 154 | - | NewSessionQueries().InsertSession, | |
| 155 | - | sessionID, accountID, accessToken, browserInfo, expireTime, true, | |
| 162 | + | insertQuery := NewSessionQueries().InsertSession | |
| 163 | + | insertParams := []interface{}{sessionID, accountID, accessToken, browserInfo, expireTime, true} | |
| 164 | + | logger.Debug("Executing session insert", "query", insertQuery, "params", insertParams) | |
| 165 | + | result, err := database.Exec( | |
| 166 | + | insertQuery, | |
| 167 | + | insertParams..., | |
| 156 | 168 | ) | |
| 169 | + | if err != nil { | |
| 170 | + | logger.Error("Database Exec failed during session insert", "error", err, "accountID", accountID) | |
| 171 | + | } else { | |
| 172 | + | rowsAffected, _ := result.RowsAffected() | |
| 173 | + | lastInsertId, _ := result.LastInsertId() // Might be 0/irrelevant for SQLite UUID PKs, but log anyway | |
| 174 | + | logger.Debug("Database Exec success during session insert", "accountID", accountID, "rows_affected", rowsAffected, "last_insert_id", lastInsertId) | |
| 175 | + | } | |
| 157 | 176 | if err != nil { | |
| 158 | 177 | logger.Error("Failed to create new session", "error", err, "accountID", accountID) | |
| 159 | 178 | return nil, fmt.Errorf("error creating session: %w", err) | |
| @@ -165,7 +184,7 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||
| 165 | 184 | AccessToken: accessToken, | |
| 166 | 185 | BrowserInfo: browserInfo, | |
| 167 | 186 | Expires: expireTime, | |
| 168 | - | IsOnline: true, | |
| 187 | + | IsActive: true, | |
| 169 | 188 | } | |
| 170 | 189 | logger.Info("New session created successfully", "sessionID", newSession.ID) | |
| 171 | 190 | return newSession, nil | |
| @@ -176,14 +195,14 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||
| 176 | 195 | func GetSessionByAccountAndBrowser(database *sql.DB, accountID string, browserInfo string) (*db.Sessions, error) { | |
| 177 | 196 | session := &db.Sessions{} | |
| 178 | 197 | // Define the query inline or add to SessionQueries struct | |
| 179 | - | query := "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions WHERE account_id = ? AND browser_info = ?" | |
| 198 | + | query := "SELECT id, account_id, access_token, browser_info, expires, is_active FROM sessions WHERE account_id = ? AND browser_info = ?" | |
| 180 | 199 | err := database.QueryRow(query, accountID, browserInfo).Scan( | |
| 181 | 200 | &session.ID, | |
| 182 | 201 | &session.AccountID, | |
| 183 | 202 | &session.AccessToken, | |
| 184 | 203 | &session.BrowserInfo, | |
| 185 | 204 | &session.Expires, | |
| 186 | - | &session.IsOnline, | |
| 205 | + | &session.IsActive, | |
| 187 | 206 | ) | |
| 188 | 207 | if err != nil { | |
| 189 | 208 | // Return sql.ErrNoRows if not found | |
| @@ -268,8 +287,8 @@ func CheckDBSessionExists(database *sql.DB, sessionID string) (bool, error) { | |||
| 268 | 287 | return sessionExists, nil | |
| 269 | 288 | } | |
| 270 | 289 | ||
| 271 | - | // InvalidateDBSession marks a session as offline | |
| 272 | - | func InvalidateDBSession(database *sql.DB, sessionID string) error { | |
| 290 | + | // MarkSessionInactive marks a session as inactive (sets is_active to false) | |
| 291 | + | func MarkSessionInactive(database *sql.DB, sessionID string) error { | |
| 273 | 292 | _, err := database.Exec( | |
| 274 | 293 | NewSessionQueries().InvalidateSession, | |
| 275 | 294 | false, | |
| @@ -277,10 +296,31 @@ func InvalidateDBSession(database *sql.DB, sessionID string) error { | |||
| 277 | 296 | ) | |
| 278 | 297 | ||
| 279 | 298 | if err != nil { | |
| 280 | - | logger.Error("Failed to invalidate session", "error", err, "session_id", sessionID) | |
| 299 | + | logger.Error("Failed to mark session inactive", "error", err, "session_id", sessionID) | |
| 281 | 300 | return err | |
| 282 | 301 | } | |
| 283 | 302 | ||
| 284 | - | logger.Debug("Session invalidated", "sessionID", sessionID) | |
| 303 | + | logger.Debug("Session marked as inactive", "sessionID", sessionID) | |
| 304 | + | return nil | |
| 305 | + | } | |
| 306 | + | ||
| 307 | + | // MarkSessionActive sets the is_active flag to true for a given session ID. | |
| 308 | + | func MarkSessionActive(database *sql.DB, sessionID string) error { | |
| 309 | + | query := NewSessionQueries().MarkSessionActive | |
| 310 | + | result, err := database.Exec(query, sessionID) | |
| 311 | + | ||
| 312 | + | if err != nil { | |
| 313 | + | logger.Error("Failed to mark session active in database", "error", err, "session_id", sessionID) | |
| 314 | + | return fmt.Errorf("failed to execute MarkSessionActive query: %w", err) | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | rowsAffected, _ := result.RowsAffected() | |
| 318 | + | if rowsAffected == 0 { | |
| 319 | + | logger.Warn("MarkSessionActive query executed but did not affect any rows", "sessionID", sessionID) | |
| 320 | + | // This might indicate the session ID didn't exist, though the query technically succeeded. | |
| 321 | + | // Consider if this should return an error or not. For now, logging a warning. | |
| 322 | + | } | |
| 323 | + | ||
| 324 | + | logger.Debug("Marked session active in database", "sessionID", sessionID, "rows_affected", rowsAffected) | |
| 285 | 325 | return nil | |
| 286 | 326 | } | |
| @@ -20,19 +20,21 @@ type SessionQueries struct { | |||||
| 20 | UpdateSessionExpiry string | 20 | UpdateSessionExpiry string | ||
| 21 | CheckSessionExists string | 21 | CheckSessionExists string | ||
| 22 | InvalidateSession string | 22 | InvalidateSession string | ||
| 23 | + | MarkSessionActive string | |||
| 23 | } | 24 | } | ||
| 24 | 25 | ||||
| 25 | // NewSessionQueries returns a new instance of SessionQueries | 26 | // NewSessionQueries returns a new instance of SessionQueries | ||
| 26 | func NewSessionQueries() *SessionQueries { | 27 | func NewSessionQueries() *SessionQueries { | ||
| 27 | return &SessionQueries{ | 28 | return &SessionQueries{ | ||
| 28 | - | InsertSession: "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)", | 29 | + | InsertSession: "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_active) VALUES (?, ?, ?, ?, ?, ?)", |
| 29 | - | GetSessionByToken: "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_online FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?", | 30 | + | GetSessionByToken: "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_active FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?", |
| 30 | - | UpdateSessionToken: "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?", | 31 | + | UpdateSessionToken: "UPDATE sessions SET access_token = ?, expires = ?, is_active = true WHERE id = ?", |
| 31 | DeleteSession: "DELETE FROM sessions WHERE account_id = ? AND id = ?", | 32 | DeleteSession: "DELETE FROM sessions WHERE account_id = ? AND id = ?", | ||
| 32 | GetSessionExpiry: "SELECT expires FROM sessions WHERE account_id = ? AND id = ?", | 33 | GetSessionExpiry: "SELECT expires FROM sessions WHERE account_id = ? AND id = ?", | ||
| 33 | UpdateSessionExpiry: "UPDATE sessions SET expires = ? WHERE account_id = ? AND id = ?", | 34 | UpdateSessionExpiry: "UPDATE sessions SET expires = ? WHERE account_id = ? AND id = ?", | ||
| 34 | CheckSessionExists: "SELECT EXISTS(SELECT 1 FROM sessions WHERE id = ?)", | 35 | CheckSessionExists: "SELECT EXISTS(SELECT 1 FROM sessions WHERE id = ?)", | ||
| 35 | - | InvalidateSession: "UPDATE sessions SET is_online = ? WHERE id = ?", | 36 | + | InvalidateSession: "UPDATE sessions SET is_active = ? WHERE id = ?", |
| 37 | + | MarkSessionActive: "UPDATE sessions SET is_active = true WHERE id = ?", | |||
| 36 | } | 38 | } | ||
| 37 | } | 39 | } | ||
| 38 | 40 | ||||
| @@ -58,7 +60,7 @@ func CreateDBSession(database *sql.DB, browserInfo string, accessToken string, a | |||||
| 58 | AccessToken: accessToken, | 60 | AccessToken: accessToken, | ||
| 59 | BrowserInfo: browserInfo, | 61 | BrowserInfo: browserInfo, | ||
| 60 | Expires: expireTime, | 62 | Expires: expireTime, | ||
| 61 | - | IsOnline: true, | 63 | + | IsActive: true, |
| 62 | } | 64 | } | ||
| 63 | 65 | ||||
| 64 | logger.Debug("Session created in database", "session_id", sessionID, "account_id", accountID, "expires", expireTime) | 66 | logger.Debug("Session created in database", "session_id", sessionID, "account_id", accountID, "expires", expireTime) | ||
| @@ -79,7 +81,7 @@ func GetDBUserSession(database *sql.DB, accessToken string, browserInfo string) | |||||
| 79 | &session.AccessToken, | 81 | &session.AccessToken, | ||
| 80 | &session.BrowserInfo, | 82 | &session.BrowserInfo, | ||
| 81 | &session.Expires, | 83 | &session.Expires, | ||
| 82 | - | &session.IsOnline, | 84 | + | &session.IsActive, |
| 83 | ) | 85 | ) | ||
| 84 | 86 | ||||
| 85 | if err != nil { | 87 | if err != nil { | ||
| @@ -130,12 +132,19 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||||
| 130 | // If the session exists, update its token and expiry | 132 | // If the session exists, update its token and expiry | ||
| 131 | if existingSession != nil { | 133 | if existingSession != nil { | ||
| 132 | logger.Debug("Existing session found, updating token and expiry", "sessionID", existingSession.ID, "accountID", accountID) | 134 | logger.Debug("Existing session found, updating token and expiry", "sessionID", existingSession.ID, "accountID", accountID) | ||
| 133 | - | _, err := database.Exec( | 135 | + | updateQuery := NewSessionQueries().UpdateSessionToken |
| 134 | - | NewSessionQueries().UpdateSessionToken, | 136 | + | updateParams := []interface{}{accessToken, expireTime, existingSession.ID} |
| 135 | - | accessToken, | 137 | + | logger.Debug("Executing session update", "query", updateQuery, "params", updateParams) |
| 136 | - | expireTime, | 138 | + | result, err := database.Exec( |
| 137 | - | existingSession.ID, | 139 | + | updateQuery, |
| 140 | + | updateParams..., | |||
| 138 | ) | 141 | ) | ||
| 142 | + | if err != nil { | |||
| 143 | + | logger.Error("Database Exec failed during session update", "error", err, "session_id", existingSession.ID) | |||
| 144 | + | } else { | |||
| 145 | + | rowsAffected, _ := result.RowsAffected() | |||
| 146 | + | logger.Debug("Database Exec success during session update", "session_id", existingSession.ID, "rows_affected", rowsAffected) | |||
| 147 | + | } | |||
| 139 | if err != nil { | 148 | if err != nil { | ||
| 140 | logger.Error("Failed to update existing session", "error", err, "session_id", existingSession.ID) | 149 | logger.Error("Failed to update existing session", "error", err, "session_id", existingSession.ID) | ||
| 141 | return nil, fmt.Errorf("error updating session: %w", err) | 150 | return nil, fmt.Errorf("error updating session: %w", err) | ||
| @@ -143,17 +152,27 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||||
| 143 | // Update the struct we have in memory | 152 | // Update the struct we have in memory | ||
| 144 | existingSession.AccessToken = accessToken | 153 | existingSession.AccessToken = accessToken | ||
| 145 | existingSession.Expires = expireTime | 154 | existingSession.Expires = expireTime | ||
| 146 | - | existingSession.IsOnline = true // Ensure it's marked online | 155 | + | existingSession.IsActive = true // Ensure it's marked active |
| 147 | logger.Info("Session updated successfully", "sessionID", existingSession.ID) | 156 | logger.Info("Session updated successfully", "sessionID", existingSession.ID) | ||
| 148 | return existingSession, nil | 157 | return existingSession, nil | ||
| 149 | } else { | 158 | } else { | ||
| 150 | // If the session does not exist, create a new one | 159 | // If the session does not exist, create a new one | ||
| 151 | logger.Debug("No existing session found, creating new session", "accountID", accountID) | 160 | logger.Debug("No existing session found, creating new session", "accountID", accountID) | ||
| 152 | sessionID := uuid.NewString() // Use uuid directly | 161 | sessionID := uuid.NewString() // Use uuid directly | ||
| 153 | - | _, err := database.Exec( | 162 | + | insertQuery := NewSessionQueries().InsertSession |
| 154 | - | NewSessionQueries().InsertSession, | 163 | + | insertParams := []interface{}{sessionID, accountID, accessToken, browserInfo, expireTime, true} |
| 155 | - | sessionID, accountID, accessToken, browserInfo, expireTime, true, | 164 | + | logger.Debug("Executing session insert", "query", insertQuery, "params", insertParams) |
| 165 | + | result, err := database.Exec( | |||
| 166 | + | insertQuery, | |||
| 167 | + | insertParams..., | |||
| 156 | ) | 168 | ) | ||
| 169 | + | if err != nil { | |||
| 170 | + | logger.Error("Database Exec failed during session insert", "error", err, "accountID", accountID) | |||
| 171 | + | } else { | |||
| 172 | + | rowsAffected, _ := result.RowsAffected() | |||
| 173 | + | lastInsertId, _ := result.LastInsertId() // Might be 0/irrelevant for SQLite UUID PKs, but log anyway | |||
| 174 | + | logger.Debug("Database Exec success during session insert", "accountID", accountID, "rows_affected", rowsAffected, "last_insert_id", lastInsertId) | |||
| 175 | + | } | |||
| 157 | if err != nil { | 176 | if err != nil { | ||
| 158 | logger.Error("Failed to create new session", "error", err, "accountID", accountID) | 177 | logger.Error("Failed to create new session", "error", err, "accountID", accountID) | ||
| 159 | return nil, fmt.Errorf("error creating session: %w", err) | 178 | return nil, fmt.Errorf("error creating session: %w", err) | ||
| @@ -165,7 +184,7 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||||
| 165 | AccessToken: accessToken, | 184 | AccessToken: accessToken, | ||
| 166 | BrowserInfo: browserInfo, | 185 | BrowserInfo: browserInfo, | ||
| 167 | Expires: expireTime, | 186 | Expires: expireTime, | ||
| 168 | - | IsOnline: true, | 187 | + | IsActive: true, |
| 169 | } | 188 | } | ||
| 170 | logger.Info("New session created successfully", "sessionID", newSession.ID) | 189 | logger.Info("New session created successfully", "sessionID", newSession.ID) | ||
| 171 | return newSession, nil | 190 | return newSession, nil | ||
| @@ -176,14 +195,14 @@ func CreateOrUpdateSession(database *sql.DB, accountID string, accessToken strin | |||||
| 176 | func GetSessionByAccountAndBrowser(database *sql.DB, accountID string, browserInfo string) (*db.Sessions, error) { | 195 | func GetSessionByAccountAndBrowser(database *sql.DB, accountID string, browserInfo string) (*db.Sessions, error) { | ||
| 177 | session := &db.Sessions{} | 196 | session := &db.Sessions{} | ||
| 178 | // Define the query inline or add to SessionQueries struct | 197 | // Define the query inline or add to SessionQueries struct | ||
| 179 | - | query := "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions WHERE account_id = ? AND browser_info = ?" | 198 | + | query := "SELECT id, account_id, access_token, browser_info, expires, is_active FROM sessions WHERE account_id = ? AND browser_info = ?" |
| 180 | err := database.QueryRow(query, accountID, browserInfo).Scan( | 199 | err := database.QueryRow(query, accountID, browserInfo).Scan( | ||
| 181 | &session.ID, | 200 | &session.ID, | ||
| 182 | &session.AccountID, | 201 | &session.AccountID, | ||
| 183 | &session.AccessToken, | 202 | &session.AccessToken, | ||
| 184 | &session.BrowserInfo, | 203 | &session.BrowserInfo, | ||
| 185 | &session.Expires, | 204 | &session.Expires, | ||
| 186 | - | &session.IsOnline, | 205 | + | &session.IsActive, |
| 187 | ) | 206 | ) | ||
| 188 | if err != nil { | 207 | if err != nil { | ||
| 189 | // Return sql.ErrNoRows if not found | 208 | // Return sql.ErrNoRows if not found | ||
| @@ -268,8 +287,8 @@ func CheckDBSessionExists(database *sql.DB, sessionID string) (bool, error) { | |||||
| 268 | return sessionExists, nil | 287 | return sessionExists, nil | ||
| 269 | } | 288 | } | ||
| 270 | 289 | ||||
| 271 | - | // InvalidateDBSession marks a session as offline | 290 | + | // MarkSessionInactive marks a session as inactive (sets is_active to false) |
| 272 | - | func InvalidateDBSession(database *sql.DB, sessionID string) error { | 291 | + | func MarkSessionInactive(database *sql.DB, sessionID string) error { |
| 273 | _, err := database.Exec( | 292 | _, err := database.Exec( | ||
| 274 | NewSessionQueries().InvalidateSession, | 293 | NewSessionQueries().InvalidateSession, | ||
| 275 | false, | 294 | false, | ||
| @@ -277,10 +296,31 @@ func InvalidateDBSession(database *sql.DB, sessionID string) error { | |||||
| 277 | ) | 296 | ) | ||
| 278 | 297 | ||||
| 279 | if err != nil { | 298 | if err != nil { | ||
| 280 | - | logger.Error("Failed to invalidate session", "error", err, "session_id", sessionID) | 299 | + | logger.Error("Failed to mark session inactive", "error", err, "session_id", sessionID) |
| 281 | return err | 300 | return err | ||
| 282 | } | 301 | } | ||
| 283 | 302 | ||||
| 284 | - | logger.Debug("Session invalidated", "sessionID", sessionID) | 303 | + | logger.Debug("Session marked as inactive", "sessionID", sessionID) |
| 304 | + | return nil | |||
| 305 | + | } | |||
| 306 | + | ||||
| 307 | + | // MarkSessionActive sets the is_active flag to true for a given session ID. | |||
| 308 | + | func MarkSessionActive(database *sql.DB, sessionID string) error { | |||
| 309 | + | query := NewSessionQueries().MarkSessionActive | |||
| 310 | + | result, err := database.Exec(query, sessionID) | |||
| 311 | + | ||||
| 312 | + | if err != nil { | |||
| 313 | + | logger.Error("Failed to mark session active in database", "error", err, "session_id", sessionID) | |||
| 314 | + | return fmt.Errorf("failed to execute MarkSessionActive query: %w", err) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | rowsAffected, _ := result.RowsAffected() | |||
| 318 | + | if rowsAffected == 0 { | |||
| 319 | + | logger.Warn("MarkSessionActive query executed but did not affect any rows", "sessionID", sessionID) | |||
| 320 | + | // This might indicate the session ID didn't exist, though the query technically succeeded. | |||
| 321 | + | // Consider if this should return an error or not. For now, logging a warning. | |||
| 322 | + | } | |||
| 323 | + | ||||
| 324 | + | logger.Debug("Marked session active in database", "sessionID", sessionID, "rows_affected", rowsAffected) | |||
| 285 | return nil | 325 | return nil | ||
| 286 | } | 326 | } | ||
M internal/httpserve/handlers/admin_view.go +67 -32
| @@ -3,64 +3,99 @@ package handlers | |||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | 5 | ||
| 6 | - | "net/http" | |
| 7 | - | ||
| 8 | 6 | "github.com/bnema/gordon/internal/server" | |
| 7 | + | "github.com/bnema/gordon/internal/templating/models/templ/pages/admin" | |
| 9 | 8 | "github.com/bnema/gordon/internal/templating/render" | |
| 9 | + | "github.com/bnema/gordon/pkg/docker" | |
| 10 | 10 | "github.com/labstack/echo/v4" | |
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | - | var ( | |
| 14 | - | mainPath = "html/admin" | |
| 15 | - | fragmentsPath = "html/fragments" | |
| 16 | - | ) | |
| 17 | - | ||
| 18 | 13 | // GetLocalizedData returns the data for the localization | |
| 19 | - | func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) { | |
| 14 | + | func GetLocalizedData(c echo.Context, a *server.App) (map[string]string, error) { | |
| 20 | 15 | lang := c.Get("LangKey") | |
| 21 | 16 | if lang == nil { | |
| 22 | 17 | return nil, fmt.Errorf("LangKey not found in context") | |
| 23 | 18 | } | |
| 24 | 19 | ||
| 25 | - | yamlData, err := render.GetLocalization(lang.(string), a) | |
| 20 | + | langStr, ok := lang.(string) | |
| 21 | + | if !ok { | |
| 22 | + | return nil, fmt.Errorf("LangKey is not a string") | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | yamlDataInterface, err := render.GetLocalization(langStr, a) | |
| 26 | 26 | if err != nil { | |
| 27 | 27 | return nil, fmt.Errorf("failed to get localization: %w", err) | |
| 28 | 28 | } | |
| 29 | - | return map[string]interface{}{ | |
| 30 | - | "Lang": yamlData, | |
| 31 | - | "LogoutURL": a.Config.Admin.Path + "/logout", | |
| 32 | - | "AdminPath": a.Config.Admin.Path, | |
| 33 | - | }, nil | |
| 34 | - | } | |
| 35 | 29 | ||
| 36 | - | // renderAdminPage is a generalized function to render admin pages | |
| 37 | - | func renderAdminPage(c echo.Context, a *server.App, templateName string) error { | |
| 38 | - | data, err := GetLocalizedData(c, a) | |
| 39 | - | if err != nil { | |
| 40 | - | return err | |
| 30 | + | // Convert map[string]interface{} to map[string]string | |
| 31 | + | langContent := make(map[string]string) | |
| 32 | + | for key, value := range yamlDataInterface { | |
| 33 | + | strValue, ok := value.(string) | |
| 34 | + | if !ok { | |
| 35 | + | // Handle cases where the value is not a string, e.g., log a warning or skip | |
| 36 | + | // For now, we'll skip non-string values | |
| 37 | + | continue | |
| 38 | + | } | |
| 39 | + | langContent[key] = strValue | |
| 41 | 40 | } | |
| 42 | 41 | ||
| 43 | - | data["AdminPath"] = a.Config.Admin.Path | |
| 42 | + | // Return map[string]string directly | |
| 43 | + | return langContent, nil | |
| 44 | + | } | |
| 44 | 45 | ||
| 45 | - | rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath) | |
| 46 | + | // AdminRoute is the route for the admin panel | |
| 47 | + | func AdminRoute(c echo.Context, a *server.App) error { | |
| 48 | + | langContent, err := GetLocalizedData(c, a) | |
| 46 | 49 | if err != nil { | |
| 47 | - | return err | |
| 50 | + | return sendError(c, err) // Assuming sendError exists | |
| 48 | 51 | } | |
| 49 | 52 | ||
| 50 | - | renderedHTML, err := rendererData.Render(data, a) | |
| 51 | - | if err != nil { | |
| 52 | - | return err | |
| 53 | + | data := admin.IndexPageData{ | |
| 54 | + | Title: "Admin Dashboard", // Or get from LangContent if available | |
| 55 | + | BuildVersion: a.Config.Build.BuildVersion, | |
| 56 | + | AdminPath: a.Config.Admin.Path, | |
| 57 | + | UserSettings: a.Config.Admin.Path + "/user", // Assuming this path | |
| 58 | + | LogoutURL: a.Config.Admin.Path + "/logout", | |
| 59 | + | LangContent: langContent, | |
| 53 | 60 | } | |
| 54 | 61 | ||
| 55 | - | return c.HTML(http.StatusOK, renderedHTML) | |
| 56 | - | } | |
| 62 | + | // Use the Templ renderer | |
| 63 | + | renderer := render.NewTemplRenderer(a) | |
| 64 | + | component := admin.IndexPage(data) | |
| 57 | 65 | ||
| 58 | - | // AdminRoute is the route for the admin panel | |
| 59 | - | func AdminRoute(c echo.Context, a *server.App) error { | |
| 60 | - | return renderAdminPage(c, a, "index.gohtml") | |
| 66 | + | return renderer.RenderTempl(c, component) | |
| 61 | 67 | } | |
| 62 | 68 | ||
| 63 | 69 | // AdminManagerRoute is the route of the manager page | |
| 64 | 70 | func AdminManagerRoute(c echo.Context, a *server.App) error { | |
| 65 | - | return renderAdminPage(c, a, "manager.gohtml") | |
| 71 | + | // Fetch container and image counts | |
| 72 | + | containers, err := docker.ListRunningContainers() | |
| 73 | + | if err != nil { | |
| 74 | + | return sendError(c, fmt.Errorf("failed to list containers: %w", err)) | |
| 75 | + | } | |
| 76 | + | images, err := docker.ListContainerImages() | |
| 77 | + | if err != nil { | |
| 78 | + | return sendError(c, fmt.Errorf("failed to list images: %w", err)) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | containerCount := len(containers) | |
| 82 | + | imageCount := len(images) | |
| 83 | + | ||
| 84 | + | title := "Container Manager" // Set a default title | |
| 85 | + | userSettingsURL := a.Config.Admin.Path + "/user" // Assuming this path | |
| 86 | + | logoutURL := a.Config.Admin.Path + "/logout" | |
| 87 | + | ||
| 88 | + | // Use the Templ renderer | |
| 89 | + | renderer := render.NewTemplRenderer(a) | |
| 90 | + | component := admin.ManagerPage( | |
| 91 | + | title, | |
| 92 | + | a.Config.Build.BuildVersion, | |
| 93 | + | a.Config.Admin.Path, | |
| 94 | + | userSettingsURL, | |
| 95 | + | logoutURL, | |
| 96 | + | containerCount, | |
| 97 | + | imageCount, | |
| 98 | + | ) | |
| 99 | + | ||
| 100 | + | return renderer.RenderTempl(c, component) | |
| 66 | 101 | } | |
| @@ -3,64 +3,99 @@ package handlers | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | 5 | ||||
| 6 | - | "net/http" | |||
| 7 | - | ||||
| 8 | "github.com/bnema/gordon/internal/server" | 6 | "github.com/bnema/gordon/internal/server" | ||
| 7 | + | "github.com/bnema/gordon/internal/templating/models/templ/pages/admin" | |||
| 9 | "github.com/bnema/gordon/internal/templating/render" | 8 | "github.com/bnema/gordon/internal/templating/render" | ||
| 9 | + | "github.com/bnema/gordon/pkg/docker" | |||
| 10 | "github.com/labstack/echo/v4" | 10 | "github.com/labstack/echo/v4" | ||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | - | var ( | |||
| 14 | - | mainPath = "html/admin" | |||
| 15 | - | fragmentsPath = "html/fragments" | |||
| 16 | - | ) | |||
| 17 | - | ||||
| 18 | // GetLocalizedData returns the data for the localization | 13 | // GetLocalizedData returns the data for the localization | ||
| 19 | - | func GetLocalizedData(c echo.Context, a *server.App) (map[string]interface{}, error) { | 14 | + | func GetLocalizedData(c echo.Context, a *server.App) (map[string]string, error) { |
| 20 | lang := c.Get("LangKey") | 15 | lang := c.Get("LangKey") | ||
| 21 | if lang == nil { | 16 | if lang == nil { | ||
| 22 | return nil, fmt.Errorf("LangKey not found in context") | 17 | return nil, fmt.Errorf("LangKey not found in context") | ||
| 23 | } | 18 | } | ||
| 24 | 19 | ||||
| 25 | - | yamlData, err := render.GetLocalization(lang.(string), a) | 20 | + | langStr, ok := lang.(string) |
| 21 | + | if !ok { | |||
| 22 | + | return nil, fmt.Errorf("LangKey is not a string") | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | yamlDataInterface, err := render.GetLocalization(langStr, a) | |||
| 26 | if err != nil { | 26 | if err != nil { | ||
| 27 | return nil, fmt.Errorf("failed to get localization: %w", err) | 27 | return nil, fmt.Errorf("failed to get localization: %w", err) | ||
| 28 | } | 28 | } | ||
| 29 | - | return map[string]interface{}{ | |||
| 30 | - | "Lang": yamlData, | |||
| 31 | - | "LogoutURL": a.Config.Admin.Path + "/logout", | |||
| 32 | - | "AdminPath": a.Config.Admin.Path, | |||
| 33 | - | }, nil | |||
| 34 | - | } | |||
| 35 | 29 | ||||
| 36 | - | // renderAdminPage is a generalized function to render admin pages | 30 | + | // Convert map[string]interface{} to map[string]string |
| 37 | - | func renderAdminPage(c echo.Context, a *server.App, templateName string) error { | 31 | + | langContent := make(map[string]string) |
| 38 | - | data, err := GetLocalizedData(c, a) | 32 | + | for key, value := range yamlDataInterface { |
| 39 | - | if err != nil { | 33 | + | strValue, ok := value.(string) |
| 40 | - | return err | 34 | + | if !ok { |
| 35 | + | // Handle cases where the value is not a string, e.g., log a warning or skip | |||
| 36 | + | // For now, we'll skip non-string values | |||
| 37 | + | continue | |||
| 38 | + | } | |||
| 39 | + | langContent[key] = strValue | |||
| 41 | } | 40 | } | ||
| 42 | 41 | ||||
| 43 | - | data["AdminPath"] = a.Config.Admin.Path | 42 | + | // Return map[string]string directly |
| 43 | + | return langContent, nil | |||
| 44 | + | } | |||
| 44 | 45 | ||||
| 45 | - | rendererData, err := render.GetHTMLRenderer(mainPath, templateName, a.TemplateFS, a, fragmentsPath) | 46 | + | // AdminRoute is the route for the admin panel |
| 47 | + | func AdminRoute(c echo.Context, a *server.App) error { | |||
| 48 | + | langContent, err := GetLocalizedData(c, a) | |||
| 46 | if err != nil { | 49 | if err != nil { | ||
| 47 | - | return err | 50 | + | return sendError(c, err) // Assuming sendError exists |
| 48 | } | 51 | } | ||
| 49 | 52 | ||||
| 50 | - | renderedHTML, err := rendererData.Render(data, a) | 53 | + | data := admin.IndexPageData{ |
| 51 | - | if err != nil { | 54 | + | Title: "Admin Dashboard", // Or get from LangContent if available |
| 52 | - | return err | 55 | + | BuildVersion: a.Config.Build.BuildVersion, |
| 56 | + | AdminPath: a.Config.Admin.Path, | |||
| 57 | + | UserSettings: a.Config.Admin.Path + "/user", // Assuming this path | |||
| 58 | + | LogoutURL: a.Config.Admin.Path + "/logout", | |||
| 59 | + | LangContent: langContent, | |||
| 53 | } | 60 | } | ||
| 54 | 61 | ||||
| 55 | - | return c.HTML(http.StatusOK, renderedHTML) | 62 | + | // Use the Templ renderer |
| 56 | - | } | 63 | + | renderer := render.NewTemplRenderer(a) |
| 64 | + | component := admin.IndexPage(data) | |||
| 57 | 65 | ||||
| 58 | - | // AdminRoute is the route for the admin panel | 66 | + | return renderer.RenderTempl(c, component) |
| 59 | - | func AdminRoute(c echo.Context, a *server.App) error { | |||
| 60 | - | return renderAdminPage(c, a, "index.gohtml") | |||
| 61 | } | 67 | } | ||
| 62 | 68 | ||||
| 63 | // AdminManagerRoute is the route of the manager page | 69 | // AdminManagerRoute is the route of the manager page | ||
| 64 | func AdminManagerRoute(c echo.Context, a *server.App) error { | 70 | func AdminManagerRoute(c echo.Context, a *server.App) error { | ||
| 65 | - | return renderAdminPage(c, a, "manager.gohtml") | 71 | + | // Fetch container and image counts |
| 72 | + | containers, err := docker.ListRunningContainers() | |||
| 73 | + | if err != nil { | |||
| 74 | + | return sendError(c, fmt.Errorf("failed to list containers: %w", err)) | |||
| 75 | + | } | |||
| 76 | + | images, err := docker.ListContainerImages() | |||
| 77 | + | if err != nil { | |||
| 78 | + | return sendError(c, fmt.Errorf("failed to list images: %w", err)) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | containerCount := len(containers) | |||
| 82 | + | imageCount := len(images) | |||
| 83 | + | ||||
| 84 | + | title := "Container Manager" // Set a default title | |||
| 85 | + | userSettingsURL := a.Config.Admin.Path + "/user" // Assuming this path | |||
| 86 | + | logoutURL := a.Config.Admin.Path + "/logout" | |||
| 87 | + | ||||
| 88 | + | // Use the Templ renderer | |||
| 89 | + | renderer := render.NewTemplRenderer(a) | |||
| 90 | + | component := admin.ManagerPage( | |||
| 91 | + | title, | |||
| 92 | + | a.Config.Build.BuildVersion, | |||
| 93 | + | a.Config.Admin.Path, | |||
| 94 | + | userSettingsURL, | |||
| 95 | + | logoutURL, | |||
| 96 | + | containerCount, | |||
| 97 | + | imageCount, | |||
| 98 | + | ) | |||
| 99 | + | ||||
| 100 | + | return renderer.RenderTempl(c, component) | |||
| 66 | } | 101 | } | ||
M internal/httpserve/handlers/container_management.go +36 -128
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/gordon/internal/server" | |
| 11 | 11 | "github.com/bnema/gordon/internal/templating/load" | |
| 12 | + | components "github.com/bnema/gordon/internal/templating/models/templ/components" | |
| 12 | 13 | "github.com/bnema/gordon/internal/templating/render" | |
| 13 | 14 | "github.com/bnema/gordon/pkg/docker" | |
| 14 | 15 | "github.com/bnema/gordon/pkg/humanize" | |
| @@ -88,35 +89,6 @@ type ContainerDisplay struct { | |||
| 88 | 89 | IsGordonContainer bool | |
| 89 | 90 | } | |
| 90 | 91 | ||
| 91 | - | // renderHTML is a generalized function to render HTML | |
| 92 | - | func renderHTML(c echo.Context, a *server.App, path, templateName string, data map[string]interface{}) error { | |
| 93 | - | logger.Debug("Rendering HTML", "path", path, "template", templateName, "data_keys", getKeysFromMap(data)) | |
| 94 | - | ||
| 95 | - | rendererData, err := render.GetHTMLRenderer(path, templateName, a.TemplateFS, a) | |
| 96 | - | if err != nil { | |
| 97 | - | logger.Error("Error getting HTML renderer", "error", err) | |
| 98 | - | return err | |
| 99 | - | } | |
| 100 | - | ||
| 101 | - | renderedHTML, err := rendererData.Render(data, a) | |
| 102 | - | if err != nil { | |
| 103 | - | logger.Error("Error rendering HTML", "error", err) | |
| 104 | - | return err | |
| 105 | - | } | |
| 106 | - | ||
| 107 | - | logger.Debug("HTML rendered successfully", "content_length", len(renderedHTML)) | |
| 108 | - | return c.HTML(http.StatusOK, renderedHTML) | |
| 109 | - | } | |
| 110 | - | ||
| 111 | - | // getKeysFromMap returns a slice of keys from a map | |
| 112 | - | func getKeysFromMap(data map[string]interface{}) []string { | |
| 113 | - | keys := make([]string, 0, len(data)) | |
| 114 | - | for k := range data { | |
| 115 | - | keys = append(keys, k) | |
| 116 | - | } | |
| 117 | - | return keys | |
| 118 | - | } | |
| 119 | - | ||
| 120 | 92 | // ActionSuccess returns the success HTML fragment | |
| 121 | 93 | func ActionSuccess(a *server.App) string { | |
| 122 | 94 | // Create a data structure to pass to the template | |
| @@ -135,60 +107,54 @@ func ActionSuccess(a *server.App) string { | |||
| 135 | 107 | ||
| 136 | 108 | // ImageManagerComponent handles the /image-manager route (HTMX route) | |
| 137 | 109 | func ImageManagerComponent(c echo.Context, a *server.App) error { | |
| 138 | - | logger.Debug("ImageManagerComponent called", "url", c.Request().URL.String()) | |
| 139 | - | ||
| 140 | 110 | images, err := docker.ListContainerImages() | |
| 141 | 111 | if err != nil { | |
| 142 | - | logger.Error("Error listing container images:", "error", err) | |
| 143 | 112 | return sendError(c, err) | |
| 144 | 113 | } | |
| 145 | 114 | ||
| 146 | - | logger.Debug("Images found", "count", len(images)) | |
| 147 | - | for i, img := range images { | |
| 148 | - | logger.Debug("Image details", "index", i, "id", img.ID, "repoTags", img.RepoTags) | |
| 149 | - | } | |
| 150 | - | ||
| 151 | - | var humanReadableImages []HumanReadableContainerImage | |
| 152 | - | ||
| 115 | + | var humanReadableImages []components.HumanReadableImage | |
| 153 | 116 | for _, img := range images { | |
| 154 | - | shortID := img.ID[7:19] | |
| 117 | + | shortID := img.ID[7:19] // Display a shorter version of the ID | |
| 155 | 118 | safelyInteractWithIDMap(Update, shortID, img.ID) | |
| 156 | - | createdTime := time.Unix(img.Created, 0) | |
| 157 | - | createdStr := humanize.TimeAgo(createdTime) | |
| 119 | + | createdStr := humanize.TimeAgo(time.Unix(img.Created, 0)) | |
| 158 | 120 | sizeStr := humanize.BytesToReadableSize(img.Size) | |
| 159 | 121 | ||
| 160 | - | for _, repoTag := range img.RepoTags { | |
| 161 | - | // Split the repo tag into repository and tag | |
| 122 | + | // Parse RepoTags to extract repository and tag | |
| 123 | + | var repository, tag string | |
| 124 | + | repoTag := "<none>:<none>" | |
| 125 | + | if len(img.RepoTags) > 0 { | |
| 126 | + | repoTag = img.RepoTags[0] | |
| 162 | 127 | parts := strings.Split(repoTag, ":") | |
| 163 | - | repository := parts[0] | |
| 164 | - | tag := "latest" | |
| 165 | - | if len(parts) > 1 { | |
| 128 | + | if len(parts) == 2 { | |
| 129 | + | repository = parts[0] | |
| 166 | 130 | tag = parts[1] | |
| 131 | + | } else { | |
| 132 | + | repository = repoTag // Handle cases like "image" without a tag | |
| 133 | + | tag = "latest" | |
| 167 | 134 | } | |
| 168 | - | ||
| 169 | - | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | |
| 170 | - | ID: img.ID, | |
| 171 | - | ShortID: shortID, | |
| 172 | - | CreatedStr: createdStr, | |
| 173 | - | SizeStr: sizeStr, | |
| 174 | - | Name: repoTag, | |
| 175 | - | RepoDigests: img.RepoDigests, | |
| 176 | - | RepoTags: img.RepoTags, | |
| 177 | - | // Add fields that match template expectations | |
| 178 | - | Repository: repository, | |
| 179 | - | Tag: tag, | |
| 180 | - | Size: sizeStr, | |
| 181 | - | Created: createdStr, | |
| 182 | - | }) | |
| 183 | 135 | } | |
| 184 | - | } | |
| 185 | 136 | ||
| 186 | - | data := map[string]interface{}{ | |
| 187 | - | "Images": humanReadableImages, | |
| 137 | + | // Create instance of components.HumanReadableImage | |
| 138 | + | humanReadableImages = append(humanReadableImages, components.HumanReadableImage{ | |
| 139 | + | ID: img.ID, | |
| 140 | + | ShortID: shortID, | |
| 141 | + | Created: createdStr, | |
| 142 | + | Size: sizeStr, | |
| 143 | + | Name: repoTag, | |
| 144 | + | RepoDigests: img.RepoDigests, | |
| 145 | + | RepoTags: img.RepoTags, | |
| 146 | + | Repository: repository, | |
| 147 | + | Tag: tag, | |
| 148 | + | }) | |
| 188 | 149 | } | |
| 189 | 150 | ||
| 190 | 151 | logger.Debug("Rendering imagelist template with", len(humanReadableImages), "images") | |
| 191 | - | return renderHTML(c, a, "html/fragments", "imagelist.gohtml", data) | |
| 152 | + | ||
| 153 | + | // Use the Templ renderer | |
| 154 | + | renderer := render.NewTemplRenderer(a) | |
| 155 | + | component := components.ImageList(humanReadableImages) | |
| 156 | + | ||
| 157 | + | return renderer.RenderTempl(c, component) | |
| 192 | 158 | } | |
| 193 | 159 | ||
| 194 | 160 | // ImageManagerDelete handles the /image-manager/delete route | |
| @@ -226,69 +192,11 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||
| 226 | 192 | logger.Debug("Container details", "index", i, "id", container.ID, "names", container.Names, "state", container.State) | |
| 227 | 193 | } | |
| 228 | 194 | ||
| 229 | - | var humanReadableContainers []HumanReadableContainer | |
| 230 | - | ||
| 231 | - | for _, container := range containers { | |
| 232 | - | localContainer := container // Make a local copy | |
| 233 | - | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |
| 234 | - | stateColor := "blue" | |
| 235 | - | ||
| 236 | - | if container.State == "running" { | |
| 237 | - | stateColor = "green" | |
| 238 | - | } else if container.State == "exited" { | |
| 239 | - | stateColor = "red" | |
| 240 | - | } | |
| 241 | - | var ports []string | |
| 242 | - | for _, port := range container.Ports { | |
| 243 | - | portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort) | |
| 244 | - | ports = append(ports, portStr) | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | for _, name := range container.Names { | |
| 248 | - | name = name[1:] | |
| 249 | - | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |
| 250 | - | Container: &localContainer, | |
| 251 | - | SizeStr: sizeStr, | |
| 252 | - | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 253 | - | StateColor: stateColor, | |
| 254 | - | Name: name, | |
| 255 | - | Ports: ports, | |
| 256 | - | ProxyPort: extractProxyPort(container.Labels), | |
| 257 | - | CreatedStr: humanize.TimeAgo(time.Unix(container.Created, 0)), | |
| 258 | - | }) | |
| 259 | - | } | |
| 260 | - | } | |
| 261 | - | yamlData, err := GetLocalizedData(c, a) | |
| 262 | - | if err != nil { | |
| 263 | - | return err | |
| 264 | - | } | |
| 265 | - | data := map[string]interface{}{ | |
| 266 | - | "Lang": yamlData["Lang"], | |
| 267 | - | "Containers": humanReadableContainers, | |
| 268 | - | "NoContainer": len(humanReadableContainers) == 0, | |
| 269 | - | } | |
| 270 | - | ||
| 271 | - | logger.Debug("Rendering containerlist template", | |
| 272 | - | "humanReadableContainers", len(humanReadableContainers), | |
| 273 | - | "noContainer", len(humanReadableContainers) == 0) | |
| 274 | - | ||
| 275 | - | return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data) | |
| 276 | - | } | |
| 277 | - | ||
| 278 | - | func extractProxyPort(labels map[string]string) string { | |
| 279 | - | // Check for the gordon proxy port label | |
| 280 | - | if port, ok := labels["gordon.proxy.port"]; ok { | |
| 281 | - | return port | |
| 282 | - | } | |
| 283 | - | ||
| 284 | - | // Backward compatibility for Traefik-labeled containers | |
| 285 | - | for key, value := range labels { | |
| 286 | - | if strings.Contains(key, "loadbalancer.server.port") { | |
| 287 | - | return value | |
| 288 | - | } | |
| 289 | - | } | |
| 195 | + | // Use the Templ renderer | |
| 196 | + | renderer := render.NewTemplRenderer(a) | |
| 197 | + | component := components.ContainerList(containers, len(containers) == 0) | |
| 290 | 198 | ||
| 291 | - | return "" | |
| 199 | + | return renderer.RenderTempl(c, component) | |
| 292 | 200 | } | |
| 293 | 201 | ||
| 294 | 202 | // ContainerManagerDelete handles the /container-manager/delete route | |
| @@ -9,6 +9,7 @@ import ( | |||||
| 9 | 9 | ||||
| 10 | "github.com/bnema/gordon/internal/server" | 10 | "github.com/bnema/gordon/internal/server" | ||
| 11 | "github.com/bnema/gordon/internal/templating/load" | 11 | "github.com/bnema/gordon/internal/templating/load" | ||
| 12 | + | components "github.com/bnema/gordon/internal/templating/models/templ/components" | |||
| 12 | "github.com/bnema/gordon/internal/templating/render" | 13 | "github.com/bnema/gordon/internal/templating/render" | ||
| 13 | "github.com/bnema/gordon/pkg/docker" | 14 | "github.com/bnema/gordon/pkg/docker" | ||
| 14 | "github.com/bnema/gordon/pkg/humanize" | 15 | "github.com/bnema/gordon/pkg/humanize" | ||
| @@ -88,35 +89,6 @@ type ContainerDisplay struct { | |||||
| 88 | IsGordonContainer bool | 89 | IsGordonContainer bool | ||
| 89 | } | 90 | } | ||
| 90 | 91 | ||||
| 91 | - | // renderHTML is a generalized function to render HTML | |||
| 92 | - | func renderHTML(c echo.Context, a *server.App, path, templateName string, data map[string]interface{}) error { | |||
| 93 | - | logger.Debug("Rendering HTML", "path", path, "template", templateName, "data_keys", getKeysFromMap(data)) | |||
| 94 | - | ||||
| 95 | - | rendererData, err := render.GetHTMLRenderer(path, templateName, a.TemplateFS, a) | |||
| 96 | - | if err != nil { | |||
| 97 | - | logger.Error("Error getting HTML renderer", "error", err) | |||
| 98 | - | return err | |||
| 99 | - | } | |||
| 100 | - | ||||
| 101 | - | renderedHTML, err := rendererData.Render(data, a) | |||
| 102 | - | if err != nil { | |||
| 103 | - | logger.Error("Error rendering HTML", "error", err) | |||
| 104 | - | return err | |||
| 105 | - | } | |||
| 106 | - | ||||
| 107 | - | logger.Debug("HTML rendered successfully", "content_length", len(renderedHTML)) | |||
| 108 | - | return c.HTML(http.StatusOK, renderedHTML) | |||
| 109 | - | } | |||
| 110 | - | ||||
| 111 | - | // getKeysFromMap returns a slice of keys from a map | |||
| 112 | - | func getKeysFromMap(data map[string]interface{}) []string { | |||
| 113 | - | keys := make([]string, 0, len(data)) | |||
| 114 | - | for k := range data { | |||
| 115 | - | keys = append(keys, k) | |||
| 116 | - | } | |||
| 117 | - | return keys | |||
| 118 | - | } | |||
| 119 | - | ||||
| 120 | // ActionSuccess returns the success HTML fragment | 92 | // ActionSuccess returns the success HTML fragment | ||
| 121 | func ActionSuccess(a *server.App) string { | 93 | func ActionSuccess(a *server.App) string { | ||
| 122 | // Create a data structure to pass to the template | 94 | // Create a data structure to pass to the template | ||
| @@ -135,60 +107,54 @@ func ActionSuccess(a *server.App) string { | |||||
| 135 | 107 | ||||
| 136 | // ImageManagerComponent handles the /image-manager route (HTMX route) | 108 | // ImageManagerComponent handles the /image-manager route (HTMX route) | ||
| 137 | func ImageManagerComponent(c echo.Context, a *server.App) error { | 109 | func ImageManagerComponent(c echo.Context, a *server.App) error { | ||
| 138 | - | logger.Debug("ImageManagerComponent called", "url", c.Request().URL.String()) | |||
| 139 | - | ||||
| 140 | images, err := docker.ListContainerImages() | 110 | images, err := docker.ListContainerImages() | ||
| 141 | if err != nil { | 111 | if err != nil { | ||
| 142 | - | logger.Error("Error listing container images:", "error", err) | |||
| 143 | return sendError(c, err) | 112 | return sendError(c, err) | ||
| 144 | } | 113 | } | ||
| 145 | 114 | ||||
| 146 | - | logger.Debug("Images found", "count", len(images)) | 115 | + | var humanReadableImages []components.HumanReadableImage |
| 147 | - | for i, img := range images { | |||
| 148 | - | logger.Debug("Image details", "index", i, "id", img.ID, "repoTags", img.RepoTags) | |||
| 149 | - | } | |||
| 150 | - | ||||
| 151 | - | var humanReadableImages []HumanReadableContainerImage | |||
| 152 | - | ||||
| 153 | for _, img := range images { | 116 | for _, img := range images { | ||
| 154 | - | shortID := img.ID[7:19] | 117 | + | shortID := img.ID[7:19] // Display a shorter version of the ID |
| 155 | safelyInteractWithIDMap(Update, shortID, img.ID) | 118 | safelyInteractWithIDMap(Update, shortID, img.ID) | ||
| 156 | - | createdTime := time.Unix(img.Created, 0) | 119 | + | createdStr := humanize.TimeAgo(time.Unix(img.Created, 0)) |
| 157 | - | createdStr := humanize.TimeAgo(createdTime) | |||
| 158 | sizeStr := humanize.BytesToReadableSize(img.Size) | 120 | sizeStr := humanize.BytesToReadableSize(img.Size) | ||
| 159 | 121 | ||||
| 160 | - | for _, repoTag := range img.RepoTags { | 122 | + | // Parse RepoTags to extract repository and tag |
| 161 | - | // Split the repo tag into repository and tag | 123 | + | var repository, tag string |
| 124 | + | repoTag := "<none>:<none>" | |||
| 125 | + | if len(img.RepoTags) > 0 { | |||
| 126 | + | repoTag = img.RepoTags[0] | |||
| 162 | parts := strings.Split(repoTag, ":") | 127 | parts := strings.Split(repoTag, ":") | ||
| 163 | - | repository := parts[0] | 128 | + | if len(parts) == 2 { |
| 164 | - | tag := "latest" | 129 | + | repository = parts[0] |
| 165 | - | if len(parts) > 1 { | |||
| 166 | tag = parts[1] | 130 | tag = parts[1] | ||
| 131 | + | } else { | |||
| 132 | + | repository = repoTag // Handle cases like "image" without a tag | |||
| 133 | + | tag = "latest" | |||
| 167 | } | 134 | } | ||
| 168 | - | ||||
| 169 | - | humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{ | |||
| 170 | - | ID: img.ID, | |||
| 171 | - | ShortID: shortID, | |||
| 172 | - | CreatedStr: createdStr, | |||
| 173 | - | SizeStr: sizeStr, | |||
| 174 | - | Name: repoTag, | |||
| 175 | - | RepoDigests: img.RepoDigests, | |||
| 176 | - | RepoTags: img.RepoTags, | |||
| 177 | - | // Add fields that match template expectations | |||
| 178 | - | Repository: repository, | |||
| 179 | - | Tag: tag, | |||
| 180 | - | Size: sizeStr, | |||
| 181 | - | Created: createdStr, | |||
| 182 | - | }) | |||
| 183 | } | 135 | } | ||
| 184 | - | } | |||
| 185 | 136 | ||||
| 186 | - | data := map[string]interface{}{ | 137 | + | // Create instance of components.HumanReadableImage |
| 187 | - | "Images": humanReadableImages, | 138 | + | humanReadableImages = append(humanReadableImages, components.HumanReadableImage{ |
| 139 | + | ID: img.ID, | |||
| 140 | + | ShortID: shortID, | |||
| 141 | + | Created: createdStr, | |||
| 142 | + | Size: sizeStr, | |||
| 143 | + | Name: repoTag, | |||
| 144 | + | RepoDigests: img.RepoDigests, | |||
| 145 | + | RepoTags: img.RepoTags, | |||
| 146 | + | Repository: repository, | |||
| 147 | + | Tag: tag, | |||
| 148 | + | }) | |||
| 188 | } | 149 | } | ||
| 189 | 150 | ||||
| 190 | logger.Debug("Rendering imagelist template with", len(humanReadableImages), "images") | 151 | logger.Debug("Rendering imagelist template with", len(humanReadableImages), "images") | ||
| 191 | - | return renderHTML(c, a, "html/fragments", "imagelist.gohtml", data) | 152 | + | |
| 153 | + | // Use the Templ renderer | |||
| 154 | + | renderer := render.NewTemplRenderer(a) | |||
| 155 | + | component := components.ImageList(humanReadableImages) | |||
| 156 | + | ||||
| 157 | + | return renderer.RenderTempl(c, component) | |||
| 192 | } | 158 | } | ||
| 193 | 159 | ||||
| 194 | // ImageManagerDelete handles the /image-manager/delete route | 160 | // ImageManagerDelete handles the /image-manager/delete route | ||
| @@ -226,69 +192,11 @@ func ContainerManagerComponent(c echo.Context, a *server.App) error { | |||||
| 226 | logger.Debug("Container details", "index", i, "id", container.ID, "names", container.Names, "state", container.State) | 192 | logger.Debug("Container details", "index", i, "id", container.ID, "names", container.Names, "state", container.State) | ||
| 227 | } | 193 | } | ||
| 228 | 194 | ||||
| 229 | - | var humanReadableContainers []HumanReadableContainer | 195 | + | // Use the Templ renderer |
| 230 | - | 196 | + | renderer := render.NewTemplRenderer(a) | |
| 231 | - | for _, container := range containers { | 197 | + | component := components.ContainerList(containers, len(containers) == 0) |
| 232 | - | localContainer := container // Make a local copy | |||
| 233 | - | sizeStr := humanize.BytesToReadableSize(container.SizeRw) | |||
| 234 | - | stateColor := "blue" | |||
| 235 | - | ||||
| 236 | - | if container.State == "running" { | |||
| 237 | - | stateColor = "green" | |||
| 238 | - | } else if container.State == "exited" { | |||
| 239 | - | stateColor = "red" | |||
| 240 | - | } | |||
| 241 | - | var ports []string | |||
| 242 | - | for _, port := range container.Ports { | |||
| 243 | - | portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort) | |||
| 244 | - | ports = append(ports, portStr) | |||
| 245 | - | } | |||
| 246 | - | ||||
| 247 | - | for _, name := range container.Names { | |||
| 248 | - | name = name[1:] | |||
| 249 | - | humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{ | |||
| 250 | - | Container: &localContainer, | |||
| 251 | - | SizeStr: sizeStr, | |||
| 252 | - | UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)), | |||
| 253 | - | StateColor: stateColor, | |||
| 254 | - | Name: name, | |||
| 255 | - | Ports: ports, | |||
| 256 | - | ProxyPort: extractProxyPort(container.Labels), | |||
| 257 | - | CreatedStr: humanize.TimeAgo(time.Unix(container.Created, 0)), | |||
| 258 | - | }) | |||
| 259 | - | } | |||
| 260 | - | } | |||
| 261 | - | yamlData, err := GetLocalizedData(c, a) | |||
| 262 | - | if err != nil { | |||
| 263 | - | return err | |||
| 264 | - | } | |||
| 265 | - | data := map[string]interface{}{ | |||
| 266 | - | "Lang": yamlData["Lang"], | |||
| 267 | - | "Containers": humanReadableContainers, | |||
| 268 | - | "NoContainer": len(humanReadableContainers) == 0, | |||
| 269 | - | } | |||
| 270 | - | ||||
| 271 | - | logger.Debug("Rendering containerlist template", | |||
| 272 | - | "humanReadableContainers", len(humanReadableContainers), | |||
| 273 | - | "noContainer", len(humanReadableContainers) == 0) | |||
| 274 | - | ||||
| 275 | - | return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data) | |||
| 276 | - | } | |||
| 277 | - | ||||
| 278 | - | func extractProxyPort(labels map[string]string) string { | |||
| 279 | - | // Check for the gordon proxy port label | |||
| 280 | - | if port, ok := labels["gordon.proxy.port"]; ok { | |||
| 281 | - | return port | |||
| 282 | - | } | |||
| 283 | - | ||||
| 284 | - | // Backward compatibility for Traefik-labeled containers | |||
| 285 | - | for key, value := range labels { | |||
| 286 | - | if strings.Contains(key, "loadbalancer.server.port") { | |||
| 287 | - | return value | |||
| 288 | - | } | |||
| 289 | - | } | |||
| 290 | 198 | ||||
| 291 | - | return "" | 199 | + | return renderer.RenderTempl(c, component) |
| 292 | } | 200 | } | ||
| 293 | 201 | ||||
| 294 | // ContainerManagerDelete handles the /container-manager/delete route | 202 | // ContainerManagerDelete handles the /container-manager/delete route | ||
M internal/httpserve/handlers/container_transactions.go +22 -42
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/gordon/internal/server" | |
| 8 | 8 | "github.com/bnema/gordon/internal/templating/cmdparams" | |
| 9 | + | "github.com/bnema/gordon/internal/templating/models/templ/components" | |
| 9 | 10 | "github.com/bnema/gordon/internal/templating/render" | |
| 10 | 11 | "github.com/bnema/gordon/pkg/docker" | |
| 11 | 12 | "github.com/bnema/gordon/pkg/logger" | |
| @@ -37,41 +38,35 @@ func generateOrderedYAML(infoMap map[string]interface{}) (string, error) { | |||
| 37 | 38 | // ContainerManagerEditGET displays the edit container view | |
| 38 | 39 | func ContainerManagerEditGET(c echo.Context, a *server.App) error { | |
| 39 | 40 | containerID := c.Param("ID") | |
| 40 | - | ||
| 41 | - | // Get the container info | |
| 42 | 41 | containerInfo, err := docker.GetContainerInfo(containerID) | |
| 43 | 42 | if err != nil { | |
| 44 | 43 | return sendError(c, err) | |
| 45 | 44 | } | |
| 46 | 45 | ||
| 47 | - | // Trim the slash from the container name | |
| 48 | - | containerInfo.Name = strings.TrimPrefix(containerInfo.Name, "/") | |
| 49 | - | // Get the network names | |
| 50 | - | networkNames := make([]string, 0, len(containerInfo.NetworkSettings.Networks)) | |
| 51 | - | for networkName := range containerInfo.NetworkSettings.Networks { | |
| 52 | - | networkNames = append(networkNames, networkName) | |
| 53 | - | } | |
| 54 | - | ||
| 55 | - | // Prepare Ports | |
| 56 | - | portMappings := make([]string, 0) | |
| 57 | - | for port, bindings := range containerInfo.HostConfig.PortBindings { | |
| 58 | - | for _, binding := range bindings { | |
| 59 | - | portMappings = append(portMappings, fmt.Sprintf("%s:%s/%s", binding.HostPort, port.Port(), port.Proto())) | |
| 46 | + | portMappings := make(map[string]string) | |
| 47 | + | for internalPort, hostBindings := range containerInfo.HostConfig.PortBindings { | |
| 48 | + | if len(hostBindings) > 0 { | |
| 49 | + | hostBinding := hostBindings[0] | |
| 50 | + | portMappings[string(internalPort)] = fmt.Sprintf("%s:%s", hostBinding.HostIP, hostBinding.HostPort) | |
| 60 | 51 | } | |
| 61 | 52 | } | |
| 62 | 53 | ||
| 63 | - | // Prepare Volumes (Mounts) | |
| 64 | - | volumeMappings := make([]string, 0) | |
| 54 | + | volumeMappings := make(map[string]string) | |
| 65 | 55 | for _, mount := range containerInfo.Mounts { | |
| 66 | - | volumeMappings = append(volumeMappings, fmt.Sprintf("%s:%s", mount.Source, mount.Destination)) | |
| 56 | + | volumeMappings[mount.Source] = mount.Destination | |
| 67 | 57 | } | |
| 68 | 58 | ||
| 69 | - | // Initialize a map to store the information from the container | |
| 70 | - | infoMap := make(map[string]interface{}) | |
| 59 | + | networkNames := []string{} | |
| 60 | + | for name := range containerInfo.NetworkSettings.Networks { | |
| 61 | + | networkNames = append(networkNames, name) | |
| 62 | + | } | |
| 71 | 63 | ||
| 72 | - | // Populate the map with container details | |
| 73 | - | infoMap["Name"] = containerInfo.Name | |
| 74 | - | infoMap["Image"] = containerInfo.Config.Image | |
| 64 | + | infoMap := map[string]interface{}{ | |
| 65 | + | "Name": strings.TrimPrefix(containerInfo.Name, "/"), | |
| 66 | + | "Image": containerInfo.Config.Image, | |
| 67 | + | "CMD": containerInfo.Config.Cmd, | |
| 68 | + | "Entrypoint": containerInfo.Config.Entrypoint, | |
| 69 | + | } | |
| 75 | 70 | infoMap["Hostname"] = containerInfo.Config.Hostname | |
| 76 | 71 | infoMap["Ports"] = portMappings | |
| 77 | 72 | infoMap["Volumes"] = volumeMappings | |
| @@ -86,26 +81,11 @@ func ContainerManagerEditGET(c echo.Context, a *server.App) error { | |||
| 86 | 81 | return sendError(c, err) | |
| 87 | 82 | } | |
| 88 | 83 | ||
| 89 | - | // Pass the containerInfo to the data map | |
| 90 | - | data := map[string]interface{}{ | |
| 91 | - | "Title": "Edit container", | |
| 92 | - | "ID": containerID, | |
| 93 | - | "ContainerInfo": yamlString, | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | rendererData, err := render.GetHTMLRenderer("html/fragments", "editcontainer.gohtml", a.TemplateFS, a) | |
| 97 | - | ||
| 98 | - | if err != nil { | |
| 99 | - | return sendError(c, err) | |
| 100 | - | } | |
| 101 | - | ||
| 102 | - | renderedHTML, err := rendererData.Render(data, a) | |
| 103 | - | ||
| 104 | - | if err != nil { | |
| 105 | - | return sendError(c, err) | |
| 106 | - | } | |
| 84 | + | // Use the Templ renderer | |
| 85 | + | renderer := render.NewTemplRenderer(a) | |
| 86 | + | component := components.EditContainer(containerID, yamlString) | |
| 107 | 87 | ||
| 108 | - | return c.HTML(200, renderedHTML) | |
| 88 | + | return renderer.RenderTempl(c, component) | |
| 109 | 89 | } | |
| 110 | 90 | ||
| 111 | 91 | type TransactionStep func() error | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | 6 | ||||
| 7 | "github.com/bnema/gordon/internal/server" | 7 | "github.com/bnema/gordon/internal/server" | ||
| 8 | "github.com/bnema/gordon/internal/templating/cmdparams" | 8 | "github.com/bnema/gordon/internal/templating/cmdparams" | ||
| 9 | + | "github.com/bnema/gordon/internal/templating/models/templ/components" | |||
| 9 | "github.com/bnema/gordon/internal/templating/render" | 10 | "github.com/bnema/gordon/internal/templating/render" | ||
| 10 | "github.com/bnema/gordon/pkg/docker" | 11 | "github.com/bnema/gordon/pkg/docker" | ||
| 11 | "github.com/bnema/gordon/pkg/logger" | 12 | "github.com/bnema/gordon/pkg/logger" | ||
| @@ -37,41 +38,35 @@ func generateOrderedYAML(infoMap map[string]interface{}) (string, error) { | |||||
| 37 | // ContainerManagerEditGET displays the edit container view | 38 | // ContainerManagerEditGET displays the edit container view | ||
| 38 | func ContainerManagerEditGET(c echo.Context, a *server.App) error { | 39 | func ContainerManagerEditGET(c echo.Context, a *server.App) error { | ||
| 39 | containerID := c.Param("ID") | 40 | containerID := c.Param("ID") | ||
| 40 | - | ||||
| 41 | - | // Get the container info | |||
| 42 | containerInfo, err := docker.GetContainerInfo(containerID) | 41 | containerInfo, err := docker.GetContainerInfo(containerID) | ||
| 43 | if err != nil { | 42 | if err != nil { | ||
| 44 | return sendError(c, err) | 43 | return sendError(c, err) | ||
| 45 | } | 44 | } | ||
| 46 | 45 | ||||
| 47 | - | // Trim the slash from the container name | 46 | + | portMappings := make(map[string]string) |
| 48 | - | containerInfo.Name = strings.TrimPrefix(containerInfo.Name, "/") | 47 | + | for internalPort, hostBindings := range containerInfo.HostConfig.PortBindings { |
| 49 | - | // Get the network names | 48 | + | if len(hostBindings) > 0 { |
| 50 | - | networkNames := make([]string, 0, len(containerInfo.NetworkSettings.Networks)) | 49 | + | hostBinding := hostBindings[0] |
| 51 | - | for networkName := range containerInfo.NetworkSettings.Networks { | 50 | + | portMappings[string(internalPort)] = fmt.Sprintf("%s:%s", hostBinding.HostIP, hostBinding.HostPort) |
| 52 | - | networkNames = append(networkNames, networkName) | |||
| 53 | - | } | |||
| 54 | - | ||||
| 55 | - | // Prepare Ports | |||
| 56 | - | portMappings := make([]string, 0) | |||
| 57 | - | for port, bindings := range containerInfo.HostConfig.PortBindings { | |||
| 58 | - | for _, binding := range bindings { | |||
| 59 | - | portMappings = append(portMappings, fmt.Sprintf("%s:%s/%s", binding.HostPort, port.Port(), port.Proto())) | |||
| 60 | } | 51 | } | ||
| 61 | } | 52 | } | ||
| 62 | 53 | ||||
| 63 | - | // Prepare Volumes (Mounts) | 54 | + | volumeMappings := make(map[string]string) |
| 64 | - | volumeMappings := make([]string, 0) | |||
| 65 | for _, mount := range containerInfo.Mounts { | 55 | for _, mount := range containerInfo.Mounts { | ||
| 66 | - | volumeMappings = append(volumeMappings, fmt.Sprintf("%s:%s", mount.Source, mount.Destination)) | 56 | + | volumeMappings[mount.Source] = mount.Destination |
| 67 | } | 57 | } | ||
| 68 | 58 | ||||
| 69 | - | // Initialize a map to store the information from the container | 59 | + | networkNames := []string{} |
| 70 | - | infoMap := make(map[string]interface{}) | 60 | + | for name := range containerInfo.NetworkSettings.Networks { |
| 61 | + | networkNames = append(networkNames, name) | |||
| 62 | + | } | |||
| 71 | 63 | ||||
| 72 | - | // Populate the map with container details | 64 | + | infoMap := map[string]interface{}{ |
| 73 | - | infoMap["Name"] = containerInfo.Name | 65 | + | "Name": strings.TrimPrefix(containerInfo.Name, "/"), |
| 74 | - | infoMap["Image"] = containerInfo.Config.Image | 66 | + | "Image": containerInfo.Config.Image, |
| 67 | + | "CMD": containerInfo.Config.Cmd, | |||
| 68 | + | "Entrypoint": containerInfo.Config.Entrypoint, | |||
| 69 | + | } | |||
| 75 | infoMap["Hostname"] = containerInfo.Config.Hostname | 70 | infoMap["Hostname"] = containerInfo.Config.Hostname | ||
| 76 | infoMap["Ports"] = portMappings | 71 | infoMap["Ports"] = portMappings | ||
| 77 | infoMap["Volumes"] = volumeMappings | 72 | infoMap["Volumes"] = volumeMappings | ||
| @@ -86,26 +81,11 @@ func ContainerManagerEditGET(c echo.Context, a *server.App) error { | |||||
| 86 | return sendError(c, err) | 81 | return sendError(c, err) | ||
| 87 | } | 82 | } | ||
| 88 | 83 | ||||
| 89 | - | // Pass the containerInfo to the data map | 84 | + | // Use the Templ renderer |
| 90 | - | data := map[string]interface{}{ | 85 | + | renderer := render.NewTemplRenderer(a) |
| 91 | - | "Title": "Edit container", | 86 | + | component := components.EditContainer(containerID, yamlString) |
| 92 | - | "ID": containerID, | |||
| 93 | - | "ContainerInfo": yamlString, | |||
| 94 | - | } | |||
| 95 | - | ||||
| 96 | - | rendererData, err := render.GetHTMLRenderer("html/fragments", "editcontainer.gohtml", a.TemplateFS, a) | |||
| 97 | - | ||||
| 98 | - | if err != nil { | |||
| 99 | - | return sendError(c, err) | |||
| 100 | - | } | |||
| 101 | - | ||||
| 102 | - | renderedHTML, err := rendererData.Render(data, a) | |||
| 103 | - | ||||
| 104 | - | if err != nil { | |||
| 105 | - | return sendError(c, err) | |||
| 106 | - | } | |||
| 107 | 87 | ||||
| 108 | - | return c.HTML(200, renderedHTML) | 88 | + | return renderer.RenderTempl(c, component) |
| 109 | } | 89 | } | ||
| 110 | 90 | ||||
| 111 | type TransactionStep func() error | 91 | type TransactionStep func() error | ||
M internal/httpserve/handlers/login.go +79 -47
| @@ -4,16 +4,17 @@ import ( | |||
| 4 | 4 | "database/sql" | |
| 5 | 5 | "encoding/base64" | |
| 6 | 6 | "encoding/json" | |
| 7 | + | "errors" | |
| 7 | 8 | "fmt" | |
| 8 | 9 | "io" | |
| 9 | 10 | "net/http" | |
| 10 | 11 | "net/url" | |
| 12 | + | "strings" | |
| 11 | 13 | "time" | |
| 12 | 14 | ||
| 13 | 15 | "github.com/bnema/gordon/internal/db" | |
| 14 | 16 | "github.com/bnema/gordon/internal/db/queries" | |
| 15 | 17 | "github.com/bnema/gordon/internal/server" | |
| 16 | - | "github.com/bnema/gordon/internal/templating/render" | |
| 17 | 18 | "github.com/bnema/gordon/pkg/logger" | |
| 18 | 19 | "github.com/golang-jwt/jwt/v5" | |
| 19 | 20 | "github.com/gorilla/sessions" | |
| @@ -28,47 +29,6 @@ type JwtCustomClaims struct { | |||
| 28 | 29 | jwt.RegisteredClaims | |
| 29 | 30 | } | |
| 30 | 31 | ||
| 31 | - | // RenderLoginPage renders the login.html template (or redirects if already logged in) | |
| 32 | - | // NOTE: This function might be superseded by RenderTemplLoginPage depending on router setup. | |
| 33 | - | // Keeping it for now, but it needs adjustment if used. | |
| 34 | - | func RenderLoginPage(c echo.Context, a *server.App) error { | |
| 35 | - | // Check if user is already authenticated via session | |
| 36 | - | sess, _ := getSession(c) // Ignore error for check | |
| 37 | - | if sess != nil && sess.Values != nil { | |
| 38 | - | if authenticated, ok := sess.Values["authenticated"].(bool); ok && authenticated { | |
| 39 | - | // If authenticated, redirect to admin panel | |
| 40 | - | logger.Debug("User already authenticated, redirecting to admin panel") | |
| 41 | - | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |
| 42 | - | } | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | // Check for error message from previous attempts (e.g., GitHub auth failure) | |
| 46 | - | errorMsg := c.QueryParam("error") | |
| 47 | - | ||
| 48 | - | data := map[string]interface{}{ | |
| 49 | - | "Title": "Login", | |
| 50 | - | "Error": errorMsg, | |
| 51 | - | // Add other necessary data for the template | |
| 52 | - | } | |
| 53 | - | ||
| 54 | - | // Navigate inside the fs.FS to get the template | |
| 55 | - | // TODO: Verify the correct template path and name (e.g., "html/login/index.gohtml") | |
| 56 | - | path := "html/login" | |
| 57 | - | rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a) | |
| 58 | - | if err != nil { | |
| 59 | - | logger.Error("Failed to get HTML renderer for login page", "error", err) | |
| 60 | - | return err // Or render a generic error page | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | renderedHTML, err := rendererData.Render(data, a) | |
| 64 | - | if err != nil { | |
| 65 | - | logger.Error("Failed to render login page HTML", "error", err) | |
| 66 | - | return err // Or render a generic error page | |
| 67 | - | } | |
| 68 | - | ||
| 69 | - | return c.HTML(http.StatusOK, renderedHTML) | |
| 70 | - | } | |
| 71 | - | ||
| 72 | 32 | // StartOAuthGithub starts the Github OAuth flow. | |
| 73 | 33 | func StartOAuthGithub(c echo.Context, a *server.App) error { | |
| 74 | 34 | logger.Info("Attempting to get an existing session") | |
| @@ -121,6 +81,14 @@ func StartOAuthGithub(c echo.Context, a *server.App) error { | |||
| 121 | 81 | return fmt.Errorf("could not extend session expiration: %w", err) | |
| 122 | 82 | } | |
| 123 | 83 | ||
| 84 | + | // Explicitly mark the session as online since we've validated it | |
| 85 | + | logger.Info("Marking existing session as online", "sessionID", sessionID) | |
| 86 | + | err = queries.MarkSessionActive(a.DB, sessionID) | |
| 87 | + | if err != nil { | |
| 88 | + | logger.Error("Failed to mark session as online", "error", err, "sessionID", sessionID) | |
| 89 | + | return fmt.Errorf("failed to mark session online: %w", err) | |
| 90 | + | } | |
| 91 | + | ||
| 124 | 92 | logger.Info("Session is valid and extended. Redirecting to admin panel") | |
| 125 | 93 | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |
| 126 | 94 | } | |
| @@ -191,8 +159,43 @@ func OAuthCallback(c echo.Context, a *server.App) error { | |||
| 191 | 159 | ||
| 192 | 160 | user, accountID, session, err := handleUser(a, accessToken, browserInfo, userInfo) | |
| 193 | 161 | if err != nil { | |
| 194 | - | logger.Error("Failed to handle user", "error", err) | |
| 195 | - | return err | |
| 162 | + | logger.Error("Failed to handle user during OAuth callback", "error", err) | |
| 163 | + | ||
| 164 | + | // --- MODIFIED: Redirect to login with specific error based on err type --- | |
| 165 | + | loginPath := a.Config.Admin.Path + "/login" | |
| 166 | + | errorParam := "error=unknown_error" // Default error | |
| 167 | + | ||
| 168 | + | var httpErr *echo.HTTPError | |
| 169 | + | if errors.As(err, &httpErr) { | |
| 170 | + | switch httpErr.Code { | |
| 171 | + | case http.StatusUnauthorized: | |
| 172 | + | // Check the message to differentiate unauthorized reasons | |
| 173 | + | if strings.Contains(httpErr.Message.(string), "does not match the allowed email") { | |
| 174 | + | errorParam = "error=email_mismatch" | |
| 175 | + | } else if strings.Contains(httpErr.Message.(string), "no public email address") { | |
| 176 | + | errorParam = "error=no_email" | |
| 177 | + | } else { | |
| 178 | + | errorParam = "error=unauthorized" | |
| 179 | + | } | |
| 180 | + | case http.StatusInternalServerError: | |
| 181 | + | // Check for specific internal errors if needed, e.g., config issue | |
| 182 | + | if strings.Contains(httpErr.Message.(string), "Admin email not set") { | |
| 183 | + | errorParam = "error=config_email_missing" | |
| 184 | + | } else { | |
| 185 | + | errorParam = "error=server_error" | |
| 186 | + | } | |
| 187 | + | default: | |
| 188 | + | errorParam = "error=server_error" // Fallback for other HTTP errors | |
| 189 | + | } | |
| 190 | + | } else { | |
| 191 | + | // Handle non-HTTP errors (like DB connection issues, etc.) | |
| 192 | + | errorParam = "error=server_error" | |
| 193 | + | } | |
| 194 | + | ||
| 195 | + | redirectURL := loginPath + "?" + errorParam | |
| 196 | + | logger.Debug("Redirecting user to login page with error", "url", redirectURL) | |
| 197 | + | return c.Redirect(http.StatusSeeOther, redirectURL) | |
| 198 | + | // --- END MODIFICATION --- | |
| 196 | 199 | } | |
| 197 | 200 | logger.Info("User handled successfully", "user_id", user.ID, "account_id", accountID) | |
| 198 | 201 | ||
| @@ -200,7 +203,8 @@ func OAuthCallback(c echo.Context, a *server.App) error { | |||
| 200 | 203 | err = setSessionValues(c, sess, accountID, session) | |
| 201 | 204 | if err != nil { | |
| 202 | 205 | // Error is already logged within setSessionValues | |
| 203 | - | return err // Return immediately if saving failed | |
| 206 | + | // Redirect to login page with a server error message | |
| 207 | + | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error") | |
| 204 | 208 | } | |
| 205 | 209 | // Log success ONLY after setSessionValues returns without error | |
| 206 | 210 | logger.Info("Session values set and saved successfully") | |
| @@ -260,7 +264,35 @@ func handleUser(a *server.App, accessToken, browserInfo string, userInfo *db.Git | |||
| 260 | 264 | ||
| 261 | 265 | // --- Case 2: Subsequent GitHub logins --- | |
| 262 | 266 | if providerCount > 0 { | |
| 263 | - | logger.Debug("Existing provider(s) found. Checking if incoming GitHub login matches.") | |
| 267 | + | logger.Debug("Existing provider(s) found. Checking if incoming GitHub user matches.") | |
| 268 | + | ||
| 269 | + | // --- ADDED: Check if incoming GitHub user email matches configured email --- | |
| 270 | + | configuredEmail := a.Config.ReverseProxy.Email | |
| 271 | + | if configuredEmail == "" { | |
| 272 | + | logger.Error("Configuration Error: ReverseProxy.Email is not set in config.yml. Cannot validate user email.") | |
| 273 | + | // Prevent login if the email isn't configured, as it's required for validation. | |
| 274 | + | return nil, "", nil, echo.NewHTTPError(http.StatusInternalServerError, "Server configuration error: Admin email not set.") | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | githubEmail := "" | |
| 278 | + | if len(userInfo.Emails) > 0 { | |
| 279 | + | githubEmail = userInfo.Emails[0] // Assuming the first email is primary | |
| 280 | + | } | |
| 281 | + | ||
| 282 | + | if githubEmail == "" { | |
| 283 | + | logger.Warn("Unauthorized GitHub login attempt: User has no public email address on GitHub.", "github_login", userInfo.Login) | |
| 284 | + | return nil, "", nil, echo.NewHTTPError(http.StatusUnauthorized, "GitHub account must have a public primary email address.") | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | if !strings.EqualFold(githubEmail, configuredEmail) { | |
| 288 | + | logger.Warn("Unauthorized GitHub login attempt: Email mismatch.", | |
| 289 | + | "github_login", userInfo.Login, | |
| 290 | + | "github_email", githubEmail, | |
| 291 | + | "configured_email", configuredEmail) | |
| 292 | + | return nil, "", nil, echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("This GitHub account's email (%s) does not match the allowed email.", githubEmail)) | |
| 293 | + | } | |
| 294 | + | logger.Debug("GitHub user email matches configured admin email.", "email", githubEmail) | |
| 295 | + | // --- END: Email Check --- | |
| 264 | 296 | ||
| 265 | 297 | // --- Check if incoming GitHub user matches an existing provider --- | |
| 266 | 298 | existingUser, existingAccountID, err := queries.GetUserByProviderLogin(a.DB, "github", userInfo.Login) | |
| @@ -347,7 +379,7 @@ func setSessionValues(c echo.Context, sess *sessions.Session, accountID string, | |||
| 347 | 379 | sess.Values["accountID"] = accountID | |
| 348 | 380 | sess.Values["expires"] = session.Expires | |
| 349 | 381 | sess.Values["sessionID"] = session.ID | |
| 350 | - | sess.Values["isOnline"] = session.IsOnline | |
| 382 | + | sess.Values["isActive"] = session.IsActive | |
| 351 | 383 | ||
| 352 | 384 | if err := sess.Save(c.Request(), c.Response()); err != nil { | |
| 353 | 385 | logger.Error("Failed to save session cookie", "error", err, "sessionID", session.ID) | |
| @@ -4,16 +4,17 @@ import ( | |||||
| 4 | "database/sql" | 4 | "database/sql" | ||
| 5 | "encoding/base64" | 5 | "encoding/base64" | ||
| 6 | "encoding/json" | 6 | "encoding/json" | ||
| 7 | + | "errors" | |||
| 7 | "fmt" | 8 | "fmt" | ||
| 8 | "io" | 9 | "io" | ||
| 9 | "net/http" | 10 | "net/http" | ||
| 10 | "net/url" | 11 | "net/url" | ||
| 12 | + | "strings" | |||
| 11 | "time" | 13 | "time" | ||
| 12 | 14 | ||||
| 13 | "github.com/bnema/gordon/internal/db" | 15 | "github.com/bnema/gordon/internal/db" | ||
| 14 | "github.com/bnema/gordon/internal/db/queries" | 16 | "github.com/bnema/gordon/internal/db/queries" | ||
| 15 | "github.com/bnema/gordon/internal/server" | 17 | "github.com/bnema/gordon/internal/server" | ||
| 16 | - | "github.com/bnema/gordon/internal/templating/render" | |||
| 17 | "github.com/bnema/gordon/pkg/logger" | 18 | "github.com/bnema/gordon/pkg/logger" | ||
| 18 | "github.com/golang-jwt/jwt/v5" | 19 | "github.com/golang-jwt/jwt/v5" | ||
| 19 | "github.com/gorilla/sessions" | 20 | "github.com/gorilla/sessions" | ||
| @@ -28,47 +29,6 @@ type JwtCustomClaims struct { | |||||
| 28 | jwt.RegisteredClaims | 29 | jwt.RegisteredClaims | ||
| 29 | } | 30 | } | ||
| 30 | 31 | ||||
| 31 | - | // RenderLoginPage renders the login.html template (or redirects if already logged in) | |||
| 32 | - | // NOTE: This function might be superseded by RenderTemplLoginPage depending on router setup. | |||
| 33 | - | // Keeping it for now, but it needs adjustment if used. | |||
| 34 | - | func RenderLoginPage(c echo.Context, a *server.App) error { | |||
| 35 | - | // Check if user is already authenticated via session | |||
| 36 | - | sess, _ := getSession(c) // Ignore error for check | |||
| 37 | - | if sess != nil && sess.Values != nil { | |||
| 38 | - | if authenticated, ok := sess.Values["authenticated"].(bool); ok && authenticated { | |||
| 39 | - | // If authenticated, redirect to admin panel | |||
| 40 | - | logger.Debug("User already authenticated, redirecting to admin panel") | |||
| 41 | - | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |||
| 42 | - | } | |||
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | // Check for error message from previous attempts (e.g., GitHub auth failure) | |||
| 46 | - | errorMsg := c.QueryParam("error") | |||
| 47 | - | ||||
| 48 | - | data := map[string]interface{}{ | |||
| 49 | - | "Title": "Login", | |||
| 50 | - | "Error": errorMsg, | |||
| 51 | - | // Add other necessary data for the template | |||
| 52 | - | } | |||
| 53 | - | ||||
| 54 | - | // Navigate inside the fs.FS to get the template | |||
| 55 | - | // TODO: Verify the correct template path and name (e.g., "html/login/index.gohtml") | |||
| 56 | - | path := "html/login" | |||
| 57 | - | rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a) | |||
| 58 | - | if err != nil { | |||
| 59 | - | logger.Error("Failed to get HTML renderer for login page", "error", err) | |||
| 60 | - | return err // Or render a generic error page | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | renderedHTML, err := rendererData.Render(data, a) | |||
| 64 | - | if err != nil { | |||
| 65 | - | logger.Error("Failed to render login page HTML", "error", err) | |||
| 66 | - | return err // Or render a generic error page | |||
| 67 | - | } | |||
| 68 | - | ||||
| 69 | - | return c.HTML(http.StatusOK, renderedHTML) | |||
| 70 | - | } | |||
| 71 | - | ||||
| 72 | // StartOAuthGithub starts the Github OAuth flow. | 32 | // StartOAuthGithub starts the Github OAuth flow. | ||
| 73 | func StartOAuthGithub(c echo.Context, a *server.App) error { | 33 | func StartOAuthGithub(c echo.Context, a *server.App) error { | ||
| 74 | logger.Info("Attempting to get an existing session") | 34 | logger.Info("Attempting to get an existing session") | ||
| @@ -121,6 +81,14 @@ func StartOAuthGithub(c echo.Context, a *server.App) error { | |||||
| 121 | return fmt.Errorf("could not extend session expiration: %w", err) | 81 | return fmt.Errorf("could not extend session expiration: %w", err) | ||
| 122 | } | 82 | } | ||
| 123 | 83 | ||||
| 84 | + | // Explicitly mark the session as online since we've validated it | |||
| 85 | + | logger.Info("Marking existing session as online", "sessionID", sessionID) | |||
| 86 | + | err = queries.MarkSessionActive(a.DB, sessionID) | |||
| 87 | + | if err != nil { | |||
| 88 | + | logger.Error("Failed to mark session as online", "error", err, "sessionID", sessionID) | |||
| 89 | + | return fmt.Errorf("failed to mark session online: %w", err) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 124 | logger.Info("Session is valid and extended. Redirecting to admin panel") | 92 | logger.Info("Session is valid and extended. Redirecting to admin panel") | ||
| 125 | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | 93 | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | ||
| 126 | } | 94 | } | ||
| @@ -191,8 +159,43 @@ func OAuthCallback(c echo.Context, a *server.App) error { | |||||
| 191 | 159 | ||||
| 192 | user, accountID, session, err := handleUser(a, accessToken, browserInfo, userInfo) | 160 | user, accountID, session, err := handleUser(a, accessToken, browserInfo, userInfo) | ||
| 193 | if err != nil { | 161 | if err != nil { | ||
| 194 | - | logger.Error("Failed to handle user", "error", err) | 162 | + | logger.Error("Failed to handle user during OAuth callback", "error", err) |
| 195 | - | return err | 163 | + | |
| 164 | + | // --- MODIFIED: Redirect to login with specific error based on err type --- | |||
| 165 | + | loginPath := a.Config.Admin.Path + "/login" | |||
| 166 | + | errorParam := "error=unknown_error" // Default error | |||
| 167 | + | ||||
| 168 | + | var httpErr *echo.HTTPError | |||
| 169 | + | if errors.As(err, &httpErr) { | |||
| 170 | + | switch httpErr.Code { | |||
| 171 | + | case http.StatusUnauthorized: | |||
| 172 | + | // Check the message to differentiate unauthorized reasons | |||
| 173 | + | if strings.Contains(httpErr.Message.(string), "does not match the allowed email") { | |||
| 174 | + | errorParam = "error=email_mismatch" | |||
| 175 | + | } else if strings.Contains(httpErr.Message.(string), "no public email address") { | |||
| 176 | + | errorParam = "error=no_email" | |||
| 177 | + | } else { | |||
| 178 | + | errorParam = "error=unauthorized" | |||
| 179 | + | } | |||
| 180 | + | case http.StatusInternalServerError: | |||
| 181 | + | // Check for specific internal errors if needed, e.g., config issue | |||
| 182 | + | if strings.Contains(httpErr.Message.(string), "Admin email not set") { | |||
| 183 | + | errorParam = "error=config_email_missing" | |||
| 184 | + | } else { | |||
| 185 | + | errorParam = "error=server_error" | |||
| 186 | + | } | |||
| 187 | + | default: | |||
| 188 | + | errorParam = "error=server_error" // Fallback for other HTTP errors | |||
| 189 | + | } | |||
| 190 | + | } else { | |||
| 191 | + | // Handle non-HTTP errors (like DB connection issues, etc.) | |||
| 192 | + | errorParam = "error=server_error" | |||
| 193 | + | } | |||
| 194 | + | ||||
| 195 | + | redirectURL := loginPath + "?" + errorParam | |||
| 196 | + | logger.Debug("Redirecting user to login page with error", "url", redirectURL) | |||
| 197 | + | return c.Redirect(http.StatusSeeOther, redirectURL) | |||
| 198 | + | // --- END MODIFICATION --- | |||
| 196 | } | 199 | } | ||
| 197 | logger.Info("User handled successfully", "user_id", user.ID, "account_id", accountID) | 200 | logger.Info("User handled successfully", "user_id", user.ID, "account_id", accountID) | ||
| 198 | 201 | ||||
| @@ -200,7 +203,8 @@ func OAuthCallback(c echo.Context, a *server.App) error { | |||||
| 200 | err = setSessionValues(c, sess, accountID, session) | 203 | err = setSessionValues(c, sess, accountID, session) | ||
| 201 | if err != nil { | 204 | if err != nil { | ||
| 202 | // Error is already logged within setSessionValues | 205 | // Error is already logged within setSessionValues | ||
| 203 | - | return err // Return immediately if saving failed | 206 | + | // Redirect to login page with a server error message |
| 207 | + | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login?error=server_error") | |||
| 204 | } | 208 | } | ||
| 205 | // Log success ONLY after setSessionValues returns without error | 209 | // Log success ONLY after setSessionValues returns without error | ||
| 206 | logger.Info("Session values set and saved successfully") | 210 | logger.Info("Session values set and saved successfully") | ||
| @@ -260,7 +264,35 @@ func handleUser(a *server.App, accessToken, browserInfo string, userInfo *db.Git | |||||
| 260 | 264 | ||||
| 261 | // --- Case 2: Subsequent GitHub logins --- | 265 | // --- Case 2: Subsequent GitHub logins --- | ||
| 262 | if providerCount > 0 { | 266 | if providerCount > 0 { | ||
| 263 | - | logger.Debug("Existing provider(s) found. Checking if incoming GitHub login matches.") | 267 | + | logger.Debug("Existing provider(s) found. Checking if incoming GitHub user matches.") |
| 268 | + | ||||
| 269 | + | // --- ADDED: Check if incoming GitHub user email matches configured email --- | |||
| 270 | + | configuredEmail := a.Config.ReverseProxy.Email | |||
| 271 | + | if configuredEmail == "" { | |||
| 272 | + | logger.Error("Configuration Error: ReverseProxy.Email is not set in config.yml. Cannot validate user email.") | |||
| 273 | + | // Prevent login if the email isn't configured, as it's required for validation. | |||
| 274 | + | return nil, "", nil, echo.NewHTTPError(http.StatusInternalServerError, "Server configuration error: Admin email not set.") | |||
| 275 | + | } | |||
| 276 | + | ||||
| 277 | + | githubEmail := "" | |||
| 278 | + | if len(userInfo.Emails) > 0 { | |||
| 279 | + | githubEmail = userInfo.Emails[0] // Assuming the first email is primary | |||
| 280 | + | } | |||
| 281 | + | ||||
| 282 | + | if githubEmail == "" { | |||
| 283 | + | logger.Warn("Unauthorized GitHub login attempt: User has no public email address on GitHub.", "github_login", userInfo.Login) | |||
| 284 | + | return nil, "", nil, echo.NewHTTPError(http.StatusUnauthorized, "GitHub account must have a public primary email address.") | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | if !strings.EqualFold(githubEmail, configuredEmail) { | |||
| 288 | + | logger.Warn("Unauthorized GitHub login attempt: Email mismatch.", | |||
| 289 | + | "github_login", userInfo.Login, | |||
| 290 | + | "github_email", githubEmail, | |||
| 291 | + | "configured_email", configuredEmail) | |||
| 292 | + | return nil, "", nil, echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("This GitHub account's email (%s) does not match the allowed email.", githubEmail)) | |||
| 293 | + | } | |||
| 294 | + | logger.Debug("GitHub user email matches configured admin email.", "email", githubEmail) | |||
| 295 | + | // --- END: Email Check --- | |||
| 264 | 296 | ||||
| 265 | // --- Check if incoming GitHub user matches an existing provider --- | 297 | // --- Check if incoming GitHub user matches an existing provider --- | ||
| 266 | existingUser, existingAccountID, err := queries.GetUserByProviderLogin(a.DB, "github", userInfo.Login) | 298 | existingUser, existingAccountID, err := queries.GetUserByProviderLogin(a.DB, "github", userInfo.Login) | ||
| @@ -347,7 +379,7 @@ func setSessionValues(c echo.Context, sess *sessions.Session, accountID string, | |||||
| 347 | sess.Values["accountID"] = accountID | 379 | sess.Values["accountID"] = accountID | ||
| 348 | sess.Values["expires"] = session.Expires | 380 | sess.Values["expires"] = session.Expires | ||
| 349 | sess.Values["sessionID"] = session.ID | 381 | sess.Values["sessionID"] = session.ID | ||
| 350 | - | sess.Values["isOnline"] = session.IsOnline | 382 | + | sess.Values["isActive"] = session.IsActive |
| 351 | 383 | ||||
| 352 | if err := sess.Save(c.Request(), c.Response()); err != nil { | 384 | if err := sess.Save(c.Request(), c.Response()); err != nil { | ||
| 353 | logger.Error("Failed to save session cookie", "error", err, "sessionID", session.ID) | 385 | logger.Error("Failed to save session cookie", "error", err, "sessionID", session.ID) | ||
M internal/httpserve/handlers/logout.go +1 -1
| @@ -23,7 +23,7 @@ func Logout(c echo.Context, a *server.App) error { | |||
| 23 | 23 | // Invalidate the session in the database | |
| 24 | 24 | sessionID, ok := sess.Values["sessionID"].(string) | |
| 25 | 25 | if ok { | |
| 26 | - | err := queries.InvalidateDBSession(a.DB, sessionID) | |
| 26 | + | err := queries.MarkSessionInactive(a.DB, sessionID) | |
| 27 | 27 | if err != nil { | |
| 28 | 28 | return fmt.Errorf("failed to invalidate session: %w", err) | |
| 29 | 29 | } | |
| @@ -23,7 +23,7 @@ func Logout(c echo.Context, a *server.App) error { | |||||
| 23 | // Invalidate the session in the database | 23 | // Invalidate the session in the database | ||
| 24 | sessionID, ok := sess.Values["sessionID"].(string) | 24 | sessionID, ok := sess.Values["sessionID"].(string) | ||
| 25 | if ok { | 25 | if ok { | ||
| 26 | - | err := queries.InvalidateDBSession(a.DB, sessionID) | 26 | + | err := queries.MarkSessionInactive(a.DB, sessionID) |
| 27 | if err != nil { | 27 | if err != nil { | ||
| 28 | return fmt.Errorf("failed to invalidate session: %w", err) | 28 | return fmt.Errorf("failed to invalidate session: %w", err) | ||
| 29 | } | 29 | } | ||
M internal/httpserve/handlers/session_db.go +10 -8
| @@ -49,13 +49,13 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error { | |||
| 49 | 49 | return fmt.Errorf("account does not exist") | |
| 50 | 50 | } | |
| 51 | 51 | ||
| 52 | - | isValidInDB, err := IsSessionValidInDB(a, sessionID) | |
| 52 | + | isValidInDB, err := IsSessionActiveInDB(a, sessionID) | |
| 53 | 53 | if err != nil { | |
| 54 | 54 | logger.Error("Failed to check session validity in DB", "error", err, "sessionID", sessionID) | |
| 55 | 55 | return fmt.Errorf("failed checking session validity in DB: %w", err) | |
| 56 | 56 | } | |
| 57 | 57 | if !isValidInDB { | |
| 58 | - | logger.Warn("Session is invalid in DB (expired or not online)", "sessionID", sessionID) | |
| 58 | + | logger.Warn("Session is invalid in DB (expired or not active)", "sessionID", sessionID) | |
| 59 | 59 | return fmt.Errorf("session invalid or expired") | |
| 60 | 60 | } | |
| 61 | 61 | ||
| @@ -73,12 +73,12 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error { | |||
| 73 | 73 | return nil | |
| 74 | 74 | } | |
| 75 | 75 | ||
| 76 | - | func IsSessionValidInDB(a *server.App, sessionID string) (bool, error) { | |
| 76 | + | func IsSessionActiveInDB(a *server.App, sessionID string) (bool, error) { | |
| 77 | 77 | var dbExpiresStr string | |
| 78 | - | var isOnline bool | |
| 78 | + | var isActive bool | |
| 79 | 79 | ||
| 80 | - | query := "SELECT expires, is_online FROM sessions WHERE id = ?" | |
| 81 | - | err := a.DB.QueryRow(query, sessionID).Scan(&dbExpiresStr, &isOnline) | |
| 80 | + | query := "SELECT expires, is_active FROM sessions WHERE id = ?" | |
| 81 | + | err := a.DB.QueryRow(query, sessionID).Scan(&dbExpiresStr, &isActive) | |
| 82 | 82 | ||
| 83 | 83 | if err != nil { | |
| 84 | 84 | if err == sql.ErrNoRows { | |
| @@ -89,8 +89,10 @@ func IsSessionValidInDB(a *server.App, sessionID string) (bool, error) { | |||
| 89 | 89 | return false, err | |
| 90 | 90 | } | |
| 91 | 91 | ||
| 92 | - | if !isOnline { | |
| 93 | - | logger.Warn("Session found in DB but marked as not online", "sessionID", sessionID) | |
| 92 | + | logger.Debug("Session validity check: Read DB values", "sessionID", sessionID, "isActive_read", isActive, "expires_read", dbExpiresStr) | |
| 93 | + | ||
| 94 | + | if !isActive { | |
| 95 | + | logger.Warn("Session found in DB but marked as not active", "sessionID", sessionID) | |
| 94 | 96 | return false, nil | |
| 95 | 97 | } | |
| 96 | 98 | ||
| @@ -49,13 +49,13 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error { | |||||
| 49 | return fmt.Errorf("account does not exist") | 49 | return fmt.Errorf("account does not exist") | ||
| 50 | } | 50 | } | ||
| 51 | 51 | ||||
| 52 | - | isValidInDB, err := IsSessionValidInDB(a, sessionID) | 52 | + | isValidInDB, err := IsSessionActiveInDB(a, sessionID) |
| 53 | if err != nil { | 53 | if err != nil { | ||
| 54 | logger.Error("Failed to check session validity in DB", "error", err, "sessionID", sessionID) | 54 | logger.Error("Failed to check session validity in DB", "error", err, "sessionID", sessionID) | ||
| 55 | return fmt.Errorf("failed checking session validity in DB: %w", err) | 55 | return fmt.Errorf("failed checking session validity in DB: %w", err) | ||
| 56 | } | 56 | } | ||
| 57 | if !isValidInDB { | 57 | if !isValidInDB { | ||
| 58 | - | logger.Warn("Session is invalid in DB (expired or not online)", "sessionID", sessionID) | 58 | + | logger.Warn("Session is invalid in DB (expired or not active)", "sessionID", sessionID) |
| 59 | return fmt.Errorf("session invalid or expired") | 59 | return fmt.Errorf("session invalid or expired") | ||
| 60 | } | 60 | } | ||
| 61 | 61 | ||||
| @@ -73,12 +73,12 @@ func ValidateSessionAndUser(c echo.Context, a *server.App) error { | |||||
| 73 | return nil | 73 | return nil | ||
| 74 | } | 74 | } | ||
| 75 | 75 | ||||
| 76 | - | func IsSessionValidInDB(a *server.App, sessionID string) (bool, error) { | 76 | + | func IsSessionActiveInDB(a *server.App, sessionID string) (bool, error) { |
| 77 | var dbExpiresStr string | 77 | var dbExpiresStr string | ||
| 78 | - | var isOnline bool | 78 | + | var isActive bool |
| 79 | 79 | ||||
| 80 | - | query := "SELECT expires, is_online FROM sessions WHERE id = ?" | 80 | + | query := "SELECT expires, is_active FROM sessions WHERE id = ?" |
| 81 | - | err := a.DB.QueryRow(query, sessionID).Scan(&dbExpiresStr, &isOnline) | 81 | + | err := a.DB.QueryRow(query, sessionID).Scan(&dbExpiresStr, &isActive) |
| 82 | 82 | ||||
| 83 | if err != nil { | 83 | if err != nil { | ||
| 84 | if err == sql.ErrNoRows { | 84 | if err == sql.ErrNoRows { | ||
| @@ -89,8 +89,10 @@ func IsSessionValidInDB(a *server.App, sessionID string) (bool, error) { | |||||
| 89 | return false, err | 89 | return false, err | ||
| 90 | } | 90 | } | ||
| 91 | 91 | ||||
| 92 | - | if !isOnline { | 92 | + | logger.Debug("Session validity check: Read DB values", "sessionID", sessionID, "isActive_read", isActive, "expires_read", dbExpiresStr) |
| 93 | - | logger.Warn("Session found in DB but marked as not online", "sessionID", sessionID) | 93 | + | |
| 94 | + | if !isActive { | |||
| 95 | + | logger.Warn("Session found in DB but marked as not active", "sessionID", sessionID) | |||
| 94 | return false, nil | 96 | return false, nil | ||
| 95 | } | 97 | } | ||
| 96 | 98 | ||||
M internal/httpserve/handlers/upload_image.go +5 -13
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "os" | |
| 8 | 8 | ||
| 9 | 9 | "github.com/bnema/gordon/internal/server" | |
| 10 | + | "github.com/bnema/gordon/internal/templating/models/templ/components" | |
| 10 | 11 | "github.com/bnema/gordon/internal/templating/render" | |
| 11 | 12 | "github.com/bnema/gordon/pkg/docker" | |
| 12 | 13 | "github.com/bnema/gordon/pkg/filestore" | |
| @@ -18,20 +19,11 @@ const MaxUploadSize = 10 * 1024 * 1024 * 1024 // 10GB | |||
| 18 | 19 | ||
| 19 | 20 | // UploadImageHandler handles the /upload-image route to show the form | |
| 20 | 21 | func UploadImageGETHandler(c echo.Context, a *server.App) error { | |
| 21 | - | data := map[string]interface{}{ | |
| 22 | - | "Title": "Upload Image", | |
| 23 | - | } | |
| 22 | + | // Use the Templ renderer | |
| 23 | + | renderer := render.NewTemplRenderer(a) | |
| 24 | + | component := components.UploadImageForm() | |
| 24 | 25 | ||
| 25 | - | rendererData, err := render.GetHTMLRenderer("html/fragments", "uploadimage.gohtml", a.TemplateFS, a) | |
| 26 | - | if err != nil { | |
| 27 | - | return sendError(c, err) | |
| 28 | - | } | |
| 29 | - | ||
| 30 | - | renderedHTML, err := rendererData.Render(data, a) | |
| 31 | - | if err != nil { | |
| 32 | - | return err | |
| 33 | - | } | |
| 34 | - | return c.HTML(200, renderedHTML) | |
| 26 | + | return renderer.RenderTempl(c, component) | |
| 35 | 27 | } | |
| 36 | 28 | ||
| 37 | 29 | // UploadImageHandler handles the /upload-image | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "os" | 7 | "os" | ||
| 8 | 8 | ||||
| 9 | "github.com/bnema/gordon/internal/server" | 9 | "github.com/bnema/gordon/internal/server" | ||
| 10 | + | "github.com/bnema/gordon/internal/templating/models/templ/components" | |||
| 10 | "github.com/bnema/gordon/internal/templating/render" | 11 | "github.com/bnema/gordon/internal/templating/render" | ||
| 11 | "github.com/bnema/gordon/pkg/docker" | 12 | "github.com/bnema/gordon/pkg/docker" | ||
| 12 | "github.com/bnema/gordon/pkg/filestore" | 13 | "github.com/bnema/gordon/pkg/filestore" | ||
| @@ -18,20 +19,11 @@ const MaxUploadSize = 10 * 1024 * 1024 * 1024 // 10GB | |||||
| 18 | 19 | ||||
| 19 | // UploadImageHandler handles the /upload-image route to show the form | 20 | // UploadImageHandler handles the /upload-image route to show the form | ||
| 20 | func UploadImageGETHandler(c echo.Context, a *server.App) error { | 21 | func UploadImageGETHandler(c echo.Context, a *server.App) error { | ||
| 21 | - | data := map[string]interface{}{ | 22 | + | // Use the Templ renderer |
| 22 | - | "Title": "Upload Image", | 23 | + | renderer := render.NewTemplRenderer(a) |
| 23 | - | } | 24 | + | component := components.UploadImageForm() |
| 24 | 25 | ||||
| 25 | - | rendererData, err := render.GetHTMLRenderer("html/fragments", "uploadimage.gohtml", a.TemplateFS, a) | 26 | + | return renderer.RenderTempl(c, component) |
| 26 | - | if err != nil { | |||
| 27 | - | return sendError(c, err) | |||
| 28 | - | } | |||
| 29 | - | ||||
| 30 | - | renderedHTML, err := rendererData.Render(data, a) | |||
| 31 | - | if err != nil { | |||
| 32 | - | return err | |||
| 33 | - | } | |||
| 34 | - | return c.HTML(200, renderedHTML) | |||
| 35 | } | 27 | } | ||
| 36 | 28 | ||||
| 37 | // UploadImageHandler handles the /upload-image | 29 | // UploadImageHandler handles the /upload-image | ||
M internal/httpserve/middleware/secure.go +2 -0
| @@ -2,6 +2,7 @@ package middleware | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "log" | |
| 5 | + | "net/http" | |
| 5 | 6 | "os" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/gordon/internal/server" | |
| @@ -38,6 +39,7 @@ func InitSessionMiddleware(a *server.App) echo.MiddlewareFunc { | |||
| 38 | 39 | HttpOnly: true, | |
| 39 | 40 | Secure: isHttps, | |
| 40 | 41 | MaxAge: 86400, | |
| 42 | + | SameSite: http.SameSiteLaxMode, | |
| 41 | 43 | } | |
| 42 | 44 | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 43 | 45 | return func(c echo.Context) error { | |
| @@ -2,6 +2,7 @@ package middleware | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "log" | 4 | "log" | ||
| 5 | + | "net/http" | |||
| 5 | "os" | 6 | "os" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/gordon/internal/server" | 8 | "github.com/bnema/gordon/internal/server" | ||
| @@ -38,6 +39,7 @@ func InitSessionMiddleware(a *server.App) echo.MiddlewareFunc { | |||||
| 38 | HttpOnly: true, | 39 | HttpOnly: true, | ||
| 39 | Secure: isHttps, | 40 | Secure: isHttps, | ||
| 40 | MaxAge: 86400, | 41 | MaxAge: 86400, | ||
| 42 | + | SameSite: http.SameSiteLaxMode, | |||
| 41 | } | 43 | } | ||
| 42 | return func(next echo.HandlerFunc) echo.HandlerFunc { | 44 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 43 | return func(c echo.Context) error { | 45 | return func(c echo.Context) error { | ||
M internal/httpserve/router.go +16 -4
| @@ -132,14 +132,26 @@ func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) { | |||
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | 134 | e.GET(adminPath+"/login", func(c echo.Context) error { | |
| 135 | - | // Check if already logged in, if so redirect to admin panel, otherwise render login page | |
| 136 | - | sess, _ := session.Get("session", c) // Use echo-contrib session | |
| 135 | + | // Check if already logged in based on cookie | |
| 136 | + | sess, _ := session.Get("session", c) // Ignore error for initial check | |
| 137 | 137 | if sess != nil && sess.Values != nil { | |
| 138 | 138 | if authenticated, ok := sess.Values["authenticated"].(bool); ok && authenticated { | |
| 139 | - | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |
| 139 | + | // Cookie claims authenticated. NOW, validate against the DB too. | |
| 140 | + | err := handlers.ValidateSessionAndUser(c, a) // Use the same validation function as the middleware | |
| 141 | + | if err == nil { | |
| 142 | + | // Session is valid in BOTH cookie AND DB. Proceed to admin panel. | |
| 143 | + | log.Debug("Login page check: Valid session found (cookie & DB), redirecting to admin panel") | |
| 144 | + | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |
| 145 | + | } | |
| 146 | + | // If DB validation failed, log it and fall through to render login page | |
| 147 | + | log.Warn("Login page check: Cookie session present but DB validation failed", "error", err) | |
| 148 | + | // NOTE: Consider invalidating the inconsistent cookie here? | |
| 149 | + | // handlers.InvalidateSessionCookie(c, sess) // Optional: Force cookie cleanup | |
| 140 | 150 | } | |
| 141 | 151 | } | |
| 142 | - | // If not authenticated, render the login page (which should now only contain GitHub login) | |
| 152 | + | // If no session cookie, not authenticated in cookie, or DB validation failed: render login page. | |
| 153 | + | log.Debug("Login page check: No valid session found or DB validation failed, rendering login page") | |
| 154 | + | // Always use the Templ-based login page renderer | |
| 143 | 155 | return handlers.RenderTemplLoginPage(c, a) | |
| 144 | 156 | }) | |
| 145 | 157 | // Removed POST /login/submit-token route | |
| @@ -132,14 +132,26 @@ func bindLoginRoute(e *echo.Echo, a *server.App, adminPath string) { | |||||
| 132 | } | 132 | } | ||
| 133 | 133 | ||||
| 134 | e.GET(adminPath+"/login", func(c echo.Context) error { | 134 | e.GET(adminPath+"/login", func(c echo.Context) error { | ||
| 135 | - | // Check if already logged in, if so redirect to admin panel, otherwise render login page | 135 | + | // Check if already logged in based on cookie |
| 136 | - | sess, _ := session.Get("session", c) // Use echo-contrib session | 136 | + | sess, _ := session.Get("session", c) // Ignore error for initial check |
| 137 | if sess != nil && sess.Values != nil { | 137 | if sess != nil && sess.Values != nil { | ||
| 138 | if authenticated, ok := sess.Values["authenticated"].(bool); ok && authenticated { | 138 | if authenticated, ok := sess.Values["authenticated"].(bool); ok && authenticated { | ||
| 139 | - | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | 139 | + | // Cookie claims authenticated. NOW, validate against the DB too. |
| 140 | + | err := handlers.ValidateSessionAndUser(c, a) // Use the same validation function as the middleware | |||
| 141 | + | if err == nil { | |||
| 142 | + | // Session is valid in BOTH cookie AND DB. Proceed to admin panel. | |||
| 143 | + | log.Debug("Login page check: Valid session found (cookie & DB), redirecting to admin panel") | |||
| 144 | + | return c.Redirect(http.StatusFound, a.Config.Admin.Path) | |||
| 145 | + | } | |||
| 146 | + | // If DB validation failed, log it and fall through to render login page | |||
| 147 | + | log.Warn("Login page check: Cookie session present but DB validation failed", "error", err) | |||
| 148 | + | // NOTE: Consider invalidating the inconsistent cookie here? | |||
| 149 | + | // handlers.InvalidateSessionCookie(c, sess) // Optional: Force cookie cleanup | |||
| 140 | } | 150 | } | ||
| 141 | } | 151 | } | ||
| 142 | - | // If not authenticated, render the login page (which should now only contain GitHub login) | 152 | + | // If no session cookie, not authenticated in cookie, or DB validation failed: render login page. |
| 153 | + | log.Debug("Login page check: No valid session found or DB validation failed, rendering login page") | |||
| 154 | + | // Always use the Templ-based login page renderer | |||
| 143 | return handlers.RenderTemplLoginPage(c, a) | 155 | return handlers.RenderTemplLoginPage(c, a) | ||
| 144 | }) | 156 | }) | ||
| 145 | // Removed POST /login/submit-token route | 157 | // Removed POST /login/submit-token route | ||
M internal/server/sqlite_init.go +2 -2
| @@ -630,7 +630,7 @@ func (p *ProviderWrapper) ScanFromRows(rows *sql.Rows) error { | |||
| 630 | 630 | } | |
| 631 | 631 | ||
| 632 | 632 | func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error { | |
| 633 | - | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline) | |
| 633 | + | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsActive) | |
| 634 | 634 | } | |
| 635 | 635 | ||
| 636 | 636 | func (c *ClientsWrapper) ScanFromRows(rows *sql.Rows) error { | |
| @@ -681,7 +681,7 @@ func PopulateStructWithTables(a *App) error { | |||
| 681 | 681 | ||
| 682 | 682 | // Populate the Sessions table | |
| 683 | 683 | sessionsWrapper := &SessionsWrapper{Sessions: &a.DBTables.Sessions} | |
| 684 | - | if err := populateTableFromDB(a, "SELECT id, account_id, browser_info, access_token, expires, is_online FROM sessions", sessionsWrapper); err != nil { | |
| 684 | + | if err := populateTableFromDB(a, "SELECT id, account_id, browser_info, access_token, expires, is_active FROM sessions", sessionsWrapper); err != nil { | |
| 685 | 685 | return err | |
| 686 | 686 | } | |
| 687 | 687 | ||
| @@ -630,7 +630,7 @@ func (p *ProviderWrapper) ScanFromRows(rows *sql.Rows) error { | |||||
| 630 | } | 630 | } | ||
| 631 | 631 | ||||
| 632 | func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error { | 632 | func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error { | ||
| 633 | - | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline) | 633 | + | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsActive) |
| 634 | } | 634 | } | ||
| 635 | 635 | ||||
| 636 | func (c *ClientsWrapper) ScanFromRows(rows *sql.Rows) error { | 636 | func (c *ClientsWrapper) ScanFromRows(rows *sql.Rows) error { | ||
| @@ -681,7 +681,7 @@ func PopulateStructWithTables(a *App) error { | |||||
| 681 | 681 | ||||
| 682 | // Populate the Sessions table | 682 | // Populate the Sessions table | ||
| 683 | sessionsWrapper := &SessionsWrapper{Sessions: &a.DBTables.Sessions} | 683 | sessionsWrapper := &SessionsWrapper{Sessions: &a.DBTables.Sessions} | ||
| 684 | - | if err := populateTableFromDB(a, "SELECT id, account_id, browser_info, access_token, expires, is_online FROM sessions", sessionsWrapper); err != nil { | 684 | + | if err := populateTableFromDB(a, "SELECT id, account_id, browser_info, access_token, expires, is_active FROM sessions", sessionsWrapper); err != nil { |
| 685 | return err | 685 | return err | ||
| 686 | } | 686 | } | ||
| 687 | 687 | ||||
D internal/templating/models/html/admin/createcontainerfull.gohtml +0 -21
| @@ -1,21 +0,0 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | {{template "header.gohtml" .}} | |
| 4 | - | ||
| 5 | - | <body class="bg-app flex flex-col lg:flex-row p-0"> | |
| 6 | - | {{template "menu.gohtml" .}} | |
| 7 | - | <!-- Main content here --> | |
| 8 | - | <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6"> | |
| 9 | - | <div class="flex flex-col md:flex-row w-full flex-grow"> | |
| 10 | - | <div class="card flex-grow"> | |
| 11 | - | <h1 class="card-header mb-4">Edit Container</h1> | |
| 12 | - | <!-- Include the existing edit container fragment here --> | |
| 13 | - | {{template "createcontainer.gohtml" .}} | |
| 14 | - | </div> | |
| 15 | - | </div> | |
| 16 | - | </div> | |
| 17 | - | ||
| 18 | - | <!-- footer --> | |
| 19 | - | {{template "footer.gohtml" .}} | |
| 20 | - | </body> | |
| 21 | - | </html>⏎̸ | |
| @@ -1,21 +0,0 @@ | |||||
| 1 | - | <!DOCTYPE html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | {{template "header.gohtml" .}} | |||
| 4 | - | ||||
| 5 | - | <body class="bg-app flex flex-col lg:flex-row p-0"> | |||
| 6 | - | {{template "menu.gohtml" .}} | |||
| 7 | - | <!-- Main content here --> | |||
| 8 | - | <div class="flex-1 flex flex-col space-y-4 pt-16 md:pt-32 md:px-6"> | |||
| 9 | - | <div class="flex flex-col md:flex-row w-full flex-grow"> | |||
| 10 | - | <div class="card flex-grow"> | |||
| 11 | - | <h1 class="card-header mb-4">Edit Container</h1> | |||
| 12 | - | <!-- Include the existing edit container fragment here --> | |||
| 13 | - | {{template "createcontainer.gohtml" .}} | |||
| 14 | - | </div> | |||
| 15 | - | </div> | |||
| 16 | - | </div> | |||
| 17 | - | ||||
| 18 | - | <!-- footer --> | |||
| 19 | - | {{template "footer.gohtml" .}} | |||
| 20 | - | </body> | |||
| 21 | - | </html>⏎̸ | |||
D internal/templating/models/html/admin/dashboard.gohtml +0 -9
| @@ -1,9 +0,0 @@ | |||
| 1 | - | <div id="dashboard"> | |
| 2 | - | <h1 | |
| 3 | - | class="text-5xl fw100 text-indigo-600 animate-bounce-alt animate-count-infinite animate-duration-1s" | |
| 4 | - | > | |
| 5 | - | Hello World from dashboard.html | |
| 6 | - | </h1> | |
| 7 | - | <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded --> | |
| 8 | - | <script src="libs/chart.js"></script> | |
| 9 | - | </div> | |
| @@ -1,9 +0,0 @@ | |||||
| 1 | - | <div id="dashboard"> | |||
| 2 | - | <h1 | |||
| 3 | - | class="text-5xl fw100 text-indigo-600 animate-bounce-alt animate-count-infinite animate-duration-1s" | |||
| 4 | - | > | |||
| 5 | - | Hello World from dashboard.html | |||
| 6 | - | </h1> | |||
| 7 | - | <!-- We load Chart.js here to avoid loading it in every page end when all the rest is loaded --> | |||
| 8 | - | <script src="libs/chart.js"></script> | |||
| 9 | - | </div> | |||
D internal/templating/models/html/admin/index.gohtml +0 -29
| @@ -1,29 +0,0 @@ | |||
| 1 | - | <!doctype html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | {{template "header.gohtml" .}} | |
| 4 | - | ||
| 5 | - | <body class="bg-app flex flex-col md:flex-row p-4 md:p-0"> | |
| 6 | - | {{template "menu.gohtml" .}} | |
| 7 | - | <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4"> | |
| 8 | - | <div | |
| 9 | - | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"> | |
| 10 | - | <div class="card flex-grow"> | |
| 11 | - | <h2 class="card-header">{{index .Lang "dashboard" "content" "title"}}</h2> | |
| 12 | - | </div> | |
| 13 | - | <div class="w-full md:w-2/6 flex flex-col space-y-4"> | |
| 14 | - | <div class="card"> | |
| 15 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |
| 16 | - | </div> | |
| 17 | - | <div class="card"> | |
| 18 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |
| 19 | - | </div> | |
| 20 | - | <div class="card"> | |
| 21 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |
| 22 | - | </div> | |
| 23 | - | </div> | |
| 24 | - | </div> | |
| 25 | - | </div> | |
| 26 | - | <!-- footer --> | |
| 27 | - | {{template "footer.gohtml" .}} | |
| 28 | - | </body> | |
| 29 | - | </html> | |
| @@ -1,29 +0,0 @@ | |||||
| 1 | - | <!doctype html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | {{template "header.gohtml" .}} | |||
| 4 | - | ||||
| 5 | - | <body class="bg-app flex flex-col md:flex-row p-4 md:p-0"> | |||
| 6 | - | {{template "menu.gohtml" .}} | |||
| 7 | - | <div class="flex-1 flex flex-col md:pt-10 space-y-4 p-4 md:ml-4"> | |||
| 8 | - | <div | |||
| 9 | - | class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow"> | |||
| 10 | - | <div class="card flex-grow"> | |||
| 11 | - | <h2 class="card-header">{{index .Lang "dashboard" "content" "title"}}</h2> | |||
| 12 | - | </div> | |||
| 13 | - | <div class="w-full md:w-2/6 flex flex-col space-y-4"> | |||
| 14 | - | <div class="card"> | |||
| 15 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |||
| 16 | - | </div> | |||
| 17 | - | <div class="card"> | |||
| 18 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |||
| 19 | - | </div> | |||
| 20 | - | <div class="card"> | |||
| 21 | - | <div class="w-full h-44 rounded-sm bg-indigo-100"></div> | |||
| 22 | - | </div> | |||
| 23 | - | </div> | |||
| 24 | - | </div> | |||
| 25 | - | </div> | |||
| 26 | - | <!-- footer --> | |||
| 27 | - | {{template "footer.gohtml" .}} | |||
| 28 | - | </body> | |||
| 29 | - | </html> | |||
D internal/templating/models/html/admin/install.gohtml +0 -77
| @@ -1,77 +0,0 @@ | |||
| 1 | - | <!DOCTYPE html> | |
| 2 | - | <html lang="en"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8" /> | |
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | - | <title>{{ .header.meta.title }}</title> | |
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |
| 9 | - | <!-- Custom CSS + Tailwind --> | |
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 11 | - | <!-- Iconoir --> | |
| 12 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |
| 13 | - | <!-- Alpine --> | |
| 14 | - | <script src="/libs/alpine.min.js"></script> | |
| 15 | - | <!-- HTMX min js --> | |
| 16 | - | <script src="/libs/htmx.min.js"></script> | |
| 17 | - | <!-- HTMX SSE --> | |
| 18 | - | <script src="/libs/htmx-sse.js"></script> | |
| 19 | - | <!-- Custom JS from assets/js --> | |
| 20 | - | <script src="/assets/js/custom.js"></script> | |
| 21 | - | </head> | |
| 22 | - | ||
| 23 | - | <body | |
| 24 | - | class="IBMPlexSans bg-app md:p-8 flex flex-col items-center" | |
| 25 | - | hx-boost="true" | |
| 26 | - | > | |
| 27 | - | <!-- Logo and Title --> | |
| 28 | - | <div | |
| 29 | - | id="logo-mascot" | |
| 30 | - | class="flex flex-col justify-center items-center p-4 mb-4" | |
| 31 | - | > | |
| 32 | - | <div class="circle-shadow flex justify-center w-100"> | |
| 33 | - | <img | |
| 34 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |
| 35 | - | alt="logo" | |
| 36 | - | class="sm:h-24 h-32 xl:h-48" | |
| 37 | - | /> | |
| 38 | - | </div> | |
| 39 | - | <div class="flex justify-center mt-4"> | |
| 40 | - | <h1 | |
| 41 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-indigo-800 tracking-tighter" | |
| 42 | - | > | |
| 43 | - | Gordon | |
| 44 | - | </h1> | |
| 45 | - | <span class="gordon-v-subtitle text-indigo-600 text-sm ml-2" | |
| 46 | - | >v0.0.1</span | |
| 47 | - | > | |
| 48 | - | </div> | |
| 49 | - | </div> | |
| 50 | - | ||
| 51 | - | <!-- Main Content --> | |
| 52 | - | <div class="card w-2/3"> | |
| 53 | - | <!-- Built-in Proxy --> | |
| 54 | - | <div id="built-in-proxy" class="mt-8"> | |
| 55 | - | <div class="flex items-center mb-4"> | |
| 56 | - | <div class="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center"> | |
| 57 | - | <span class="text-white font-bold">✓</span> | |
| 58 | - | </div> | |
| 59 | - | <h2 class="text-lg ml-2 text-title">Reverse Proxy</h2> | |
| 60 | - | </div> | |
| 61 | - | ||
| 62 | - | <div class="bg-indigo-50 p-4 rounded-md"> | |
| 63 | - | <p class="mb-4">Gordon now uses a built-in reverse proxy to handle all domain routing and TLS certificates.</p> | |
| 64 | - | <p>The proxy automatically:</p> | |
| 65 | - | <ul class="list-disc ml-6 mb-4"> | |
| 66 | - | <li>Manages Let's Encrypt certificates</li> | |
| 67 | - | <li>Routes domains to the appropriate containers</li> | |
| 68 | - | <li>Handles HTTP to HTTPS redirects</li> | |
| 69 | - | <li>Provides proper headers for forwarded requests</li> | |
| 70 | - | </ul> | |
| 71 | - | <p>No additional configuration is required.</p> | |
| 72 | - | </div> | |
| 73 | - | </div> | |
| 74 | - | <!-- End Built-in Proxy --> | |
| 75 | - | </div> | |
| 76 | - | </body> | |
| 77 | - | </html> | |
| @@ -1,77 +0,0 @@ | |||||
| 1 | - | <!DOCTYPE html> | |||
| 2 | - | <html lang="en"> | |||
| 3 | - | <head> | |||
| 4 | - | <meta charset="UTF-8" /> | |||
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 6 | - | <title>{{ .header.meta.title }}</title> | |||
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |||
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |||
| 9 | - | <!-- Custom CSS + Tailwind --> | |||
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |||
| 11 | - | <!-- Iconoir --> | |||
| 12 | - | <link rel="stylesheet" href="/libs/iconoir.css" /> | |||
| 13 | - | <!-- Alpine --> | |||
| 14 | - | <script src="/libs/alpine.min.js"></script> | |||
| 15 | - | <!-- HTMX min js --> | |||
| 16 | - | <script src="/libs/htmx.min.js"></script> | |||
| 17 | - | <!-- HTMX SSE --> | |||
| 18 | - | <script src="/libs/htmx-sse.js"></script> | |||
| 19 | - | <!-- Custom JS from assets/js --> | |||
| 20 | - | <script src="/assets/js/custom.js"></script> | |||
| 21 | - | </head> | |||
| 22 | - | ||||
| 23 | - | <body | |||
| 24 | - | class="IBMPlexSans bg-app md:p-8 flex flex-col items-center" | |||
| 25 | - | hx-boost="true" | |||
| 26 | - | > | |||
| 27 | - | <!-- Logo and Title --> | |||
| 28 | - | <div | |||
| 29 | - | id="logo-mascot" | |||
| 30 | - | class="flex flex-col justify-center items-center p-4 mb-4" | |||
| 31 | - | > | |||
| 32 | - | <div class="circle-shadow flex justify-center w-100"> | |||
| 33 | - | <img | |||
| 34 | - | src="/assets/imgs/gordon-mascot-hq-trsp.png" | |||
| 35 | - | alt="logo" | |||
| 36 | - | class="sm:h-24 h-32 xl:h-48" | |||
| 37 | - | /> | |||
| 38 | - | </div> | |||
| 39 | - | <div class="flex justify-center mt-4"> | |||
| 40 | - | <h1 | |||
| 41 | - | class="gordon-title text-2xl lg:text-4xl xl:text-5xl text-indigo-800 tracking-tighter" | |||
| 42 | - | > | |||
| 43 | - | Gordon | |||
| 44 | - | </h1> | |||
| 45 | - | <span class="gordon-v-subtitle text-indigo-600 text-sm ml-2" | |||
| 46 | - | >v0.0.1</span | |||
| 47 | - | > | |||
| 48 | - | </div> | |||
| 49 | - | </div> | |||
| 50 | - | ||||
| 51 | - | <!-- Main Content --> | |||
| 52 | - | <div class="card w-2/3"> | |||
| 53 | - | <!-- Built-in Proxy --> | |||
| 54 | - | <div id="built-in-proxy" class="mt-8"> | |||
| 55 | - | <div class="flex items-center mb-4"> | |||
| 56 | - | <div class="w-8 h-8 bg-indigo-500 rounded-full flex items-center justify-center"> | |||
| 57 | - | <span class="text-white font-bold">✓</span> | |||
| 58 | - | </div> | |||
| 59 | - | <h2 class="text-lg ml-2 text-title">Reverse Proxy</h2> | |||
| 60 | - | </div> | |||
| 61 | - | ||||
| 62 | - | <div class="bg-indigo-50 p-4 rounded-md"> | |||
| 63 | - | <p class="mb-4">Gordon now uses a built-in reverse proxy to handle all domain routing and TLS certificates.</p> | |||
| 64 | - | <p>The proxy automatically:</p> | |||
| 65 | - | <ul class="list-disc ml-6 mb-4"> | |||
| 66 | - | <li>Manages Let's Encrypt certificates</li> | |||
| 67 | - | <li>Routes domains to the appropriate containers</li> | |||
| 68 | - | <li>Handles HTTP to HTTPS redirects</li> | |||
| 69 | - | <li>Provides proper headers for forwarded requests</li> | |||
| 70 | - | </ul> | |||
| 71 | - | <p>No additional configuration is required.</p> | |||
| 72 | - | </div> | |||
| 73 | - | </div> | |||
| 74 | - | <!-- End Built-in Proxy --> | |||
| 75 | - | </div> | |||
| 76 | - | </body> | |||
| 77 | - | </html> | |||
D internal/templating/models/html/admin/manager.gohtml +0 -76
| @@ -1,76 +0,0 @@ | |||
| 1 | - | <!-- manager.gohtml --> | |
| 2 | - | <!DOCTYPE html> | |
| 3 | - | <html lang="en" data-theme="gordon"> | |
| 4 | - | {{template "header.gohtml" .}} | |
| 5 | - | ||
| 6 | - | <body class="min-h-screen bg-base-200"> | |
| 7 | - | {{template "menu.gohtml" .}} | |
| 8 | - | ||
| 9 | - | <!-- Main content --> | |
| 10 | - | <div class="container mx-auto py-6 px-4 mt-16"> | |
| 11 | - | <div class="text-sm breadcrumbs mb-4"> | |
| 12 | - | <ul> | |
| 13 | - | <li><a href="{{ .AdminPath }}">Gordon</a></li> | |
| 14 | - | <li>Manager</li> | |
| 15 | - | </ul> | |
| 16 | - | </div> | |
| 17 | - | ||
| 18 | - | <!-- Stats overview --> | |
| 19 | - | <div class="stats shadow mb-6 w-full"> | |
| 20 | - | <div class="stat"> | |
| 21 | - | <div class="stat-figure text-primary"> | |
| 22 | - | <span class="iconf text-3xl"></span> | |
| 23 | - | </div> | |
| 24 | - | <div class="stat-title">Containers</div> | |
| 25 | - | <div class="stat-value" id="container-count">{{.ContainerCount}}</div> | |
| 26 | - | <div class="stat-desc">Docker containers managed by Gordon</div> | |
| 27 | - | </div> | |
| 28 | - | ||
| 29 | - | <div class="stat"> | |
| 30 | - | <div class="stat-figure text-secondary"> | |
| 31 | - | <span class="iconf text-3xl"></span> | |
| 32 | - | </div> | |
| 33 | - | <div class="stat-title">Images</div> | |
| 34 | - | <div class="stat-value" id="image-count">{{.ImageCount}}</div> | |
| 35 | - | <div class="stat-desc">Docker images available</div> | |
| 36 | - | </div> | |
| 37 | - | </div> | |
| 38 | - | ||
| 39 | - | <!-- Tabs for containers and images --> | |
| 40 | - | <div role="tablist" class="tabs tabs-boxed mb-4"> | |
| 41 | - | <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked /> | |
| 42 | - | <div role="tabpanel" class="tab-content p-0"> | |
| 43 | - | <!-- Container manager section --> | |
| 44 | - | <div id="container-manager" | |
| 45 | - | hx-get="/htmx/container-manager" | |
| 46 | - | hx-trigger="load" | |
| 47 | - | hx-swap="innerHTML"> | |
| 48 | - | <!-- Container content loaded via HTMX --> | |
| 49 | - | <div class="text-center p-4"> | |
| 50 | - | <span class="iconf text-3xl animate-spin inline-block"></span> | |
| 51 | - | Loading containers... | |
| 52 | - | </div> | |
| 53 | - | </div> | |
| 54 | - | </div> | |
| 55 | - | ||
| 56 | - | <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" /> | |
| 57 | - | <div role="tabpanel" class="tab-content p-0"> | |
| 58 | - | <!-- Image manager section --> | |
| 59 | - | <div id="image-manager" | |
| 60 | - | hx-get="/htmx/image-manager" | |
| 61 | - | hx-trigger="load" | |
| 62 | - | hx-swap="innerHTML"> | |
| 63 | - | <!-- Image content loaded via HTMX --> | |
| 64 | - | <div class="text-center p-4"> | |
| 65 | - | <span class="iconf text-3xl animate-spin inline-block"></span> | |
| 66 | - | Loading images... | |
| 67 | - | </div> | |
| 68 | - | </div> | |
| 69 | - | </div> | |
| 70 | - | </div> | |
| 71 | - | ||
| 72 | - | </div> | |
| 73 | - | ||
| 74 | - | {{template "footer.gohtml" .}} | |
| 75 | - | </body> | |
| 76 | - | </html> | |
| @@ -1,76 +0,0 @@ | |||||
| 1 | - | <!-- manager.gohtml --> | |||
| 2 | - | <!DOCTYPE html> | |||
| 3 | - | <html lang="en" data-theme="gordon"> | |||
| 4 | - | {{template "header.gohtml" .}} | |||
| 5 | - | ||||
| 6 | - | <body class="min-h-screen bg-base-200"> | |||
| 7 | - | {{template "menu.gohtml" .}} | |||
| 8 | - | ||||
| 9 | - | <!-- Main content --> | |||
| 10 | - | <div class="container mx-auto py-6 px-4 mt-16"> | |||
| 11 | - | <div class="text-sm breadcrumbs mb-4"> | |||
| 12 | - | <ul> | |||
| 13 | - | <li><a href="{{ .AdminPath }}">Gordon</a></li> | |||
| 14 | - | <li>Manager</li> | |||
| 15 | - | </ul> | |||
| 16 | - | </div> | |||
| 17 | - | ||||
| 18 | - | <!-- Stats overview --> | |||
| 19 | - | <div class="stats shadow mb-6 w-full"> | |||
| 20 | - | <div class="stat"> | |||
| 21 | - | <div class="stat-figure text-primary"> | |||
| 22 | - | <span class="iconf text-3xl"></span> | |||
| 23 | - | </div> | |||
| 24 | - | <div class="stat-title">Containers</div> | |||
| 25 | - | <div class="stat-value" id="container-count">{{.ContainerCount}}</div> | |||
| 26 | - | <div class="stat-desc">Docker containers managed by Gordon</div> | |||
| 27 | - | </div> | |||
| 28 | - | ||||
| 29 | - | <div class="stat"> | |||
| 30 | - | <div class="stat-figure text-secondary"> | |||
| 31 | - | <span class="iconf text-3xl"></span> | |||
| 32 | - | </div> | |||
| 33 | - | <div class="stat-title">Images</div> | |||
| 34 | - | <div class="stat-value" id="image-count">{{.ImageCount}}</div> | |||
| 35 | - | <div class="stat-desc">Docker images available</div> | |||
| 36 | - | </div> | |||
| 37 | - | </div> | |||
| 38 | - | ||||
| 39 | - | <!-- Tabs for containers and images --> | |||
| 40 | - | <div role="tablist" class="tabs tabs-boxed mb-4"> | |||
| 41 | - | <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked /> | |||
| 42 | - | <div role="tabpanel" class="tab-content p-0"> | |||
| 43 | - | <!-- Container manager section --> | |||
| 44 | - | <div id="container-manager" | |||
| 45 | - | hx-get="/htmx/container-manager" | |||
| 46 | - | hx-trigger="load" | |||
| 47 | - | hx-swap="innerHTML"> | |||
| 48 | - | <!-- Container content loaded via HTMX --> | |||
| 49 | - | <div class="text-center p-4"> | |||
| 50 | - | <span class="iconf text-3xl animate-spin inline-block"></span> | |||
| 51 | - | Loading containers... | |||
| 52 | - | </div> | |||
| 53 | - | </div> | |||
| 54 | - | </div> | |||
| 55 | - | ||||
| 56 | - | <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" /> | |||
| 57 | - | <div role="tabpanel" class="tab-content p-0"> | |||
| 58 | - | <!-- Image manager section --> | |||
| 59 | - | <div id="image-manager" | |||
| 60 | - | hx-get="/htmx/image-manager" | |||
| 61 | - | hx-trigger="load" | |||
| 62 | - | hx-swap="innerHTML"> | |||
| 63 | - | <!-- Image content loaded via HTMX --> | |||
| 64 | - | <div class="text-center p-4"> | |||
| 65 | - | <span class="iconf text-3xl animate-spin inline-block"></span> | |||
| 66 | - | Loading images... | |||
| 67 | - | </div> | |||
| 68 | - | </div> | |||
| 69 | - | </div> | |||
| 70 | - | </div> | |||
| 71 | - | ||||
| 72 | - | </div> | |||
| 73 | - | ||||
| 74 | - | {{template "footer.gohtml" .}} | |||
| 75 | - | </body> | |||
| 76 | - | </html> | |||
D internal/templating/models/html/admin/user.gohtml +0 -0
no textual change.
D internal/templating/models/html/fragments/components.gohtml +0 -76
| @@ -1,76 +0,0 @@ | |||
| 1 | - | <!-- @format --> | |
| 2 | - | ||
| 3 | - | <div id="header" class="hero bg-base-200"> | |
| 4 | - | <div class="hero-content text-center"> | |
| 5 | - | <div class="max-w-md"> | |
| 6 | - | <h1 class="text-5xl font-bold gordon-title"> | |
| 7 | - | {{ .body.header.helloworld }} | |
| 8 | - | </h1> | |
| 9 | - | <p class="py-6 IBMPlexSans">Welcome to Gordon Container Management system.</p> | |
| 10 | - | <button class="btn btn-primary">Get Started</button> | |
| 11 | - | </div> | |
| 12 | - | </div> | |
| 13 | - | </div> | |
| 14 | - | ||
| 15 | - | <div id="hello" class="alert alert-info shadow-lg mt-4 mb-4"> | |
| 16 | - | <div> | |
| 17 | - | <span class="iconfb"></span> | |
| 18 | - | <span class="IBMPlexSans">{{ .body.div.hello }}</span> | |
| 19 | - | </div> | |
| 20 | - | </div> | |
| 21 | - | ||
| 22 | - | <div | |
| 23 | - | id="header" | |
| 24 | - | hx-get="/htmx" | |
| 25 | - | hx-headers='{"HX-Fragment": "header"}' | |
| 26 | - | hx-trigger="load" | |
| 27 | - | hx-swap="outerHTML" | |
| 28 | - | ></div> | |
| 29 | - | ||
| 30 | - | <!-- App Fragment --> | |
| 31 | - | <div | |
| 32 | - | id="app" | |
| 33 | - | hx-get="/htmx" | |
| 34 | - | hx-headers='{"HX-Fragment": "hello"}' | |
| 35 | - | hx-trigger="load" | |
| 36 | - | hx-swap="outerHTML" | |
| 37 | - | ></div> | |
| 38 | - | ||
| 39 | - | <!-- Stats Cards --> | |
| 40 | - | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-4"> | |
| 41 | - | <div class="stat bg-base-100 shadow"> | |
| 42 | - | <div class="stat-figure text-primary"> | |
| 43 | - | <span class="iconfb text-3xl"></span> | |
| 44 | - | </div> | |
| 45 | - | <div class="stat-title">Containers</div> | |
| 46 | - | <div class="stat-value">31</div> | |
| 47 | - | <div class="stat-desc">Running containers</div> | |
| 48 | - | </div> | |
| 49 | - | ||
| 50 | - | <div class="stat bg-base-100 shadow"> | |
| 51 | - | <div class="stat-figure text-secondary"> | |
| 52 | - | <span class="iconfb text-3xl"></span> | |
| 53 | - | </div> | |
| 54 | - | <div class="stat-title">Images</div> | |
| 55 | - | <div class="stat-value">42</div> | |
| 56 | - | <div class="stat-desc">Available images</div> | |
| 57 | - | </div> | |
| 58 | - | ||
| 59 | - | <div class="stat bg-base-100 shadow"> | |
| 60 | - | <div class="stat-figure text-accent"> | |
| 61 | - | <span class="iconfb text-3xl"></span> | |
| 62 | - | </div> | |
| 63 | - | <div class="stat-title">Networks</div> | |
| 64 | - | <div class="stat-value">12</div> | |
| 65 | - | <div class="stat-desc">Container networks</div> | |
| 66 | - | </div> | |
| 67 | - | </div> | |
| 68 | - | ||
| 69 | - | <!-- Example of tabs --> | |
| 70 | - | <div class="tabs tabs-boxed my-4"> | |
| 71 | - | <a class="tab tab-active">Overview</a> | |
| 72 | - | <a class="tab">Containers</a> | |
| 73 | - | <a class="tab">Images</a> | |
| 74 | - | <a class="tab">Networks</a> | |
| 75 | - | <a class="tab">Volumes</a> | |
| 76 | - | </div> | |
| @@ -1,76 +0,0 @@ | |||||
| 1 | - | <!-- @format --> | |||
| 2 | - | ||||
| 3 | - | <div id="header" class="hero bg-base-200"> | |||
| 4 | - | <div class="hero-content text-center"> | |||
| 5 | - | <div class="max-w-md"> | |||
| 6 | - | <h1 class="text-5xl font-bold gordon-title"> | |||
| 7 | - | {{ .body.header.helloworld }} | |||
| 8 | - | </h1> | |||
| 9 | - | <p class="py-6 IBMPlexSans">Welcome to Gordon Container Management system.</p> | |||
| 10 | - | <button class="btn btn-primary">Get Started</button> | |||
| 11 | - | </div> | |||
| 12 | - | </div> | |||
| 13 | - | </div> | |||
| 14 | - | ||||
| 15 | - | <div id="hello" class="alert alert-info shadow-lg mt-4 mb-4"> | |||
| 16 | - | <div> | |||
| 17 | - | <span class="iconfb"></span> | |||
| 18 | - | <span class="IBMPlexSans">{{ .body.div.hello }}</span> | |||
| 19 | - | </div> | |||
| 20 | - | </div> | |||
| 21 | - | ||||
| 22 | - | <div | |||
| 23 | - | id="header" | |||
| 24 | - | hx-get="/htmx" | |||
| 25 | - | hx-headers='{"HX-Fragment": "header"}' | |||
| 26 | - | hx-trigger="load" | |||
| 27 | - | hx-swap="outerHTML" | |||
| 28 | - | ></div> | |||
| 29 | - | ||||
| 30 | - | <!-- App Fragment --> | |||
| 31 | - | <div | |||
| 32 | - | id="app" | |||
| 33 | - | hx-get="/htmx" | |||
| 34 | - | hx-headers='{"HX-Fragment": "hello"}' | |||
| 35 | - | hx-trigger="load" | |||
| 36 | - | hx-swap="outerHTML" | |||
| 37 | - | ></div> | |||
| 38 | - | ||||
| 39 | - | <!-- Stats Cards --> | |||
| 40 | - | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-4"> | |||
| 41 | - | <div class="stat bg-base-100 shadow"> | |||
| 42 | - | <div class="stat-figure text-primary"> | |||
| 43 | - | <span class="iconfb text-3xl"></span> | |||
| 44 | - | </div> | |||
| 45 | - | <div class="stat-title">Containers</div> | |||
| 46 | - | <div class="stat-value">31</div> | |||
| 47 | - | <div class="stat-desc">Running containers</div> | |||
| 48 | - | </div> | |||
| 49 | - | ||||
| 50 | - | <div class="stat bg-base-100 shadow"> | |||
| 51 | - | <div class="stat-figure text-secondary"> | |||
| 52 | - | <span class="iconfb text-3xl"></span> | |||
| 53 | - | </div> | |||
| 54 | - | <div class="stat-title">Images</div> | |||
| 55 | - | <div class="stat-value">42</div> | |||
| 56 | - | <div class="stat-desc">Available images</div> | |||
| 57 | - | </div> | |||
| 58 | - | ||||
| 59 | - | <div class="stat bg-base-100 shadow"> | |||
| 60 | - | <div class="stat-figure text-accent"> | |||
| 61 | - | <span class="iconfb text-3xl"></span> | |||
| 62 | - | </div> | |||
| 63 | - | <div class="stat-title">Networks</div> | |||
| 64 | - | <div class="stat-value">12</div> | |||
| 65 | - | <div class="stat-desc">Container networks</div> | |||
| 66 | - | </div> | |||
| 67 | - | </div> | |||
| 68 | - | ||||
| 69 | - | <!-- Example of tabs --> | |||
| 70 | - | <div class="tabs tabs-boxed my-4"> | |||
| 71 | - | <a class="tab tab-active">Overview</a> | |||
| 72 | - | <a class="tab">Containers</a> | |||
| 73 | - | <a class="tab">Images</a> | |||
| 74 | - | <a class="tab">Networks</a> | |||
| 75 | - | <a class="tab">Volumes</a> | |||
| 76 | - | </div> | |||
D internal/templating/models/html/fragments/containerlist.gohtml +0 -122
| @@ -1,122 +0,0 @@ | |||
| 1 | - | <!-- Container list using daisyUI components --> | |
| 2 | - | <div class="card w-full bg-base-300"> | |
| 3 | - | <div class="card-body p-4"> | |
| 4 | - | ||
| 5 | - | <!-- Action buttons --> | |
| 6 | - | <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4"> | |
| 7 | - | <h3 class="font-semibold text-base-content">Available Containers: {{ len .Containers }}</h3> | |
| 8 | - | <div class="btn-group"> | |
| 9 | - | <button | |
| 10 | - | class="btn btn-sm btn-primary" | |
| 11 | - | hx-get="/htmx/container-manager" | |
| 12 | - | hx-target="#container-manager" | |
| 13 | - | hx-swap="innerHTML"> | |
| 14 | - | <span class="iconf"></span> Refresh | |
| 15 | - | </button> | |
| 16 | - | </div> | |
| 17 | - | </div> | |
| 18 | - | ||
| 19 | - | <!-- Container list --> | |
| 20 | - | <table class="table"> | |
| 21 | - | <!-- head --> | |
| 22 | - | <thead> | |
| 23 | - | <tr> | |
| 24 | - | <th>Name</th> | |
| 25 | - | <th>Image</th> | |
| 26 | - | <th>Status</th> | |
| 27 | - | <th>Created</th> | |
| 28 | - | <th>Actions</th> | |
| 29 | - | </tr> | |
| 30 | - | </thead> | |
| 31 | - | <tbody> | |
| 32 | - | {{ if or .NoContainer (eq (len .Containers) 0) }} | |
| 33 | - | <tr> | |
| 34 | - | <td colspan="5" class="text-center py-4"> | |
| 35 | - | <div class="alert alert-info"> | |
| 36 | - | <span class="iconf">ℹ️</span> | |
| 37 | - | <span>No containers found. Upload an image first, then create a container.</span> | |
| 38 | - | </div> | |
| 39 | - | </td> | |
| 40 | - | </tr> | |
| 41 | - | {{ else }} | |
| 42 | - | {{ range .Containers }} | |
| 43 | - | <tr class="hover"> | |
| 44 | - | <td> | |
| 45 | - | <div class="flex items-center gap-3"> | |
| 46 | - | <div> | |
| 47 | - | <div class="font-bold">{{ .Names }}</div> | |
| 48 | - | <div class="text-sm opacity-50">{{ .ID }}</div> | |
| 49 | - | </div> | |
| 50 | - | </div> | |
| 51 | - | </td> | |
| 52 | - | <td>{{ .Image }}</td> | |
| 53 | - | <td> | |
| 54 | - | {{ if eq .State "running" }} | |
| 55 | - | <div class="badge badge-success badge-sm">Running</div> | |
| 56 | - | {{ else if eq .State "exited" }} | |
| 57 | - | <div class="badge badge-warning badge-sm">Stopped</div> | |
| 58 | - | {{ else }} | |
| 59 | - | <div class="badge badge-ghost badge-sm">{{ .State }}</div> | |
| 60 | - | {{ end }} | |
| 61 | - | </td> | |
| 62 | - | <td>{{ .Created }}</td> | |
| 63 | - | <th> | |
| 64 | - | <div class="dropdown dropdown-end"> | |
| 65 | - | <div tabindex="0" role="button" class="btn btn-ghost btn-xs"> | |
| 66 | - | <span class="iconf"></span> | |
| 67 | - | </div> | |
| 68 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-40"> | |
| 69 | - | {{ if eq .State "running" }} | |
| 70 | - | <li> | |
| 71 | - | <button | |
| 72 | - | class="text-warning flex items-center" | |
| 73 | - | hx-post="/htmx/container-manager/stop/{{ .ID }}" | |
| 74 | - | hx-target="#container-manager" | |
| 75 | - | hx-swap="innerHTML"> | |
| 76 | - | <span class="iconf mr-2"></span> Stop | |
| 77 | - | </button> | |
| 78 | - | </li> | |
| 79 | - | {{ else if eq .State "exited" }} | |
| 80 | - | <li> | |
| 81 | - | <button | |
| 82 | - | class="text-success flex items-center" | |
| 83 | - | hx-post="/htmx/container-manager/start/{{ .ID }}" | |
| 84 | - | hx-target="#container-manager" | |
| 85 | - | hx-swap="innerHTML"> | |
| 86 | - | <span class="iconf mr-2"></span> Start | |
| 87 | - | </button> | |
| 88 | - | </li> | |
| 89 | - | {{ end }} | |
| 90 | - | <li> | |
| 91 | - | <button | |
| 92 | - | class="text-info flex items-center" | |
| 93 | - | hx-get="/htmx/container-manager/edit/{{ .ID }}" | |
| 94 | - | hx-target="#container-manager" | |
| 95 | - | hx-swap="innerHTML"> | |
| 96 | - | <span class="iconf mr-2"></span> Edit | |
| 97 | - | </button> | |
| 98 | - | </li> | |
| 99 | - | <li> | |
| 100 | - | <button | |
| 101 | - | class="text-error flex items-center" | |
| 102 | - | hx-delete="/htmx/container-manager/delete/{{ .ID }}" | |
| 103 | - | hx-target="#container-manager" | |
| 104 | - | hx-swap="innerHTML" | |
| 105 | - | hx-confirm="Are you sure you want to delete this container?"> | |
| 106 | - | <span class="iconf mr-2"></span> Delete | |
| 107 | - | </button> | |
| 108 | - | </li> | |
| 109 | - | </ul> | |
| 110 | - | </div> | |
| 111 | - | </th> | |
| 112 | - | </tr> | |
| 113 | - | {{ end }} | |
| 114 | - | {{ end }} | |
| 115 | - | </tbody> | |
| 116 | - | </table> | |
| 117 | - | </div> | |
| 118 | - | </div> | |
| 119 | - | ||
| 120 | - | <script> | |
| 121 | - | console.debug('Container list rendered'); | |
| 122 | - | </script> | |
| @@ -1,122 +0,0 @@ | |||||
| 1 | - | <!-- Container list using daisyUI components --> | |||
| 2 | - | <div class="card w-full bg-base-300"> | |||
| 3 | - | <div class="card-body p-4"> | |||
| 4 | - | ||||
| 5 | - | <!-- Action buttons --> | |||
| 6 | - | <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4"> | |||
| 7 | - | <h3 class="font-semibold text-base-content">Available Containers: {{ len .Containers }}</h3> | |||
| 8 | - | <div class="btn-group"> | |||
| 9 | - | <button | |||
| 10 | - | class="btn btn-sm btn-primary" | |||
| 11 | - | hx-get="/htmx/container-manager" | |||
| 12 | - | hx-target="#container-manager" | |||
| 13 | - | hx-swap="innerHTML"> | |||
| 14 | - | <span class="iconf"></span> Refresh | |||
| 15 | - | </button> | |||
| 16 | - | </div> | |||
| 17 | - | </div> | |||
| 18 | - | ||||
| 19 | - | <!-- Container list --> | |||
| 20 | - | <table class="table"> | |||
| 21 | - | <!-- head --> | |||
| 22 | - | <thead> | |||
| 23 | - | <tr> | |||
| 24 | - | <th>Name</th> | |||
| 25 | - | <th>Image</th> | |||
| 26 | - | <th>Status</th> | |||
| 27 | - | <th>Created</th> | |||
| 28 | - | <th>Actions</th> | |||
| 29 | - | </tr> | |||
| 30 | - | </thead> | |||
| 31 | - | <tbody> | |||
| 32 | - | {{ if or .NoContainer (eq (len .Containers) 0) }} | |||
| 33 | - | <tr> | |||
| 34 | - | <td colspan="5" class="text-center py-4"> | |||
| 35 | - | <div class="alert alert-info"> | |||
| 36 | - | <span class="iconf">ℹ️</span> | |||
| 37 | - | <span>No containers found. Upload an image first, then create a container.</span> | |||
| 38 | - | </div> | |||
| 39 | - | </td> | |||
| 40 | - | </tr> | |||
| 41 | - | {{ else }} | |||
| 42 | - | {{ range .Containers }} | |||
| 43 | - | <tr class="hover"> | |||
| 44 | - | <td> | |||
| 45 | - | <div class="flex items-center gap-3"> | |||
| 46 | - | <div> | |||
| 47 | - | <div class="font-bold">{{ .Names }}</div> | |||
| 48 | - | <div class="text-sm opacity-50">{{ .ID }}</div> | |||
| 49 | - | </div> | |||
| 50 | - | </div> | |||
| 51 | - | </td> | |||
| 52 | - | <td>{{ .Image }}</td> | |||
| 53 | - | <td> | |||
| 54 | - | {{ if eq .State "running" }} | |||
| 55 | - | <div class="badge badge-success badge-sm">Running</div> | |||
| 56 | - | {{ else if eq .State "exited" }} | |||
| 57 | - | <div class="badge badge-warning badge-sm">Stopped</div> | |||
| 58 | - | {{ else }} | |||
| 59 | - | <div class="badge badge-ghost badge-sm">{{ .State }}</div> | |||
| 60 | - | {{ end }} | |||
| 61 | - | </td> | |||
| 62 | - | <td>{{ .Created }}</td> | |||
| 63 | - | <th> | |||
| 64 | - | <div class="dropdown dropdown-end"> | |||
| 65 | - | <div tabindex="0" role="button" class="btn btn-ghost btn-xs"> | |||
| 66 | - | <span class="iconf"></span> | |||
| 67 | - | </div> | |||
| 68 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-40"> | |||
| 69 | - | {{ if eq .State "running" }} | |||
| 70 | - | <li> | |||
| 71 | - | <button | |||
| 72 | - | class="text-warning flex items-center" | |||
| 73 | - | hx-post="/htmx/container-manager/stop/{{ .ID }}" | |||
| 74 | - | hx-target="#container-manager" | |||
| 75 | - | hx-swap="innerHTML"> | |||
| 76 | - | <span class="iconf mr-2"></span> Stop | |||
| 77 | - | </button> | |||
| 78 | - | </li> | |||
| 79 | - | {{ else if eq .State "exited" }} | |||
| 80 | - | <li> | |||
| 81 | - | <button | |||
| 82 | - | class="text-success flex items-center" | |||
| 83 | - | hx-post="/htmx/container-manager/start/{{ .ID }}" | |||
| 84 | - | hx-target="#container-manager" | |||
| 85 | - | hx-swap="innerHTML"> | |||
| 86 | - | <span class="iconf mr-2"></span> Start | |||
| 87 | - | </button> | |||
| 88 | - | </li> | |||
| 89 | - | {{ end }} | |||
| 90 | - | <li> | |||
| 91 | - | <button | |||
| 92 | - | class="text-info flex items-center" | |||
| 93 | - | hx-get="/htmx/container-manager/edit/{{ .ID }}" | |||
| 94 | - | hx-target="#container-manager" | |||
| 95 | - | hx-swap="innerHTML"> | |||
| 96 | - | <span class="iconf mr-2"></span> Edit | |||
| 97 | - | </button> | |||
| 98 | - | </li> | |||
| 99 | - | <li> | |||
| 100 | - | <button | |||
| 101 | - | class="text-error flex items-center" | |||
| 102 | - | hx-delete="/htmx/container-manager/delete/{{ .ID }}" | |||
| 103 | - | hx-target="#container-manager" | |||
| 104 | - | hx-swap="innerHTML" | |||
| 105 | - | hx-confirm="Are you sure you want to delete this container?"> | |||
| 106 | - | <span class="iconf mr-2"></span> Delete | |||
| 107 | - | </button> | |||
| 108 | - | </li> | |||
| 109 | - | </ul> | |||
| 110 | - | </div> | |||
| 111 | - | </th> | |||
| 112 | - | </tr> | |||
| 113 | - | {{ end }} | |||
| 114 | - | {{ end }} | |||
| 115 | - | </tbody> | |||
| 116 | - | </table> | |||
| 117 | - | </div> | |||
| 118 | - | </div> | |||
| 119 | - | ||||
| 120 | - | <script> | |||
| 121 | - | console.debug('Container list rendered'); | |||
| 122 | - | </script> | |||
D internal/templating/models/html/fragments/createcontainer.gohtml +0 -140
| @@ -1,140 +0,0 @@ | |||
| 1 | - | <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"> | |
| 2 | - | <!-- Left Side: Docker Run Options --> | |
| 3 | - | <div class="flex-grow card p-6"> | |
| 4 | - | <h2 class="card-title pb-4">Deploy a container based on {{.ImageName}}</h2> | |
| 5 | - | {{if .ErrorMessage}} | |
| 6 | - | <div class="alert alert-error"> | |
| 7 | - | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| 8 | - | <span>{{.ErrorMessage}}</span> | |
| 9 | - | </div> | |
| 10 | - | {{ end }} | |
| 11 | - | <form id="create-container-form-{{.ShortID}}" action="/create-container/{{.ShortID}}" method="post" | |
| 12 | - | hx-post="/htmx/create-container/{{.ShortID}}" | |
| 13 | - | hx-swap="innerHTML" | |
| 14 | - | hx-target="#create-container-{{.ShortID}}" | |
| 15 | - | hx-target-*="#error-message-create-container-{{.ShortID}}"> | |
| 16 | - | <!-- Image Name --> | |
| 17 | - | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4"> | |
| 18 | - | <div class="md:col-span-1"> | |
| 19 | - | <label class="label"> | |
| 20 | - | <span class="label-text">Image name</span> | |
| 21 | - | </label> | |
| 22 | - | <input type="text" name="image_name" class="input input-bordered input-disabled w-full" | |
| 23 | - | value="{{.ImageName}}" readonly /> | |
| 24 | - | </div> | |
| 25 | - | ||
| 26 | - | <div class="md:col-span-2"> | |
| 27 | - | <label class="label"> | |
| 28 | - | <span class="label-text">Image ID</span> | |
| 29 | - | </label> | |
| 30 | - | <input type="text" name="image_id" class="input input-bordered input-disabled w-full" | |
| 31 | - | value="{{.ImageID}}" readonly /> | |
| 32 | - | </div> | |
| 33 | - | </div> | |
| 34 | - | ||
| 35 | - | <!-- Container Name --> | |
| 36 | - | <div class="form-control pb-4"> | |
| 37 | - | <label class="label"> | |
| 38 | - | <span class="label-text">Container name</span> | |
| 39 | - | </label> | |
| 40 | - | <input type="text" name="container_name" class="input input-bordered w-full" value="{{.ContainerName}}" /> | |
| 41 | - | </div> | |
| 42 | - | ||
| 43 | - | <!-- Http(s) / Subdomain / Domain --> | |
| 44 | - | <div class="grid grid-cols-1 md:grid-cols-12 gap-4 pb-4"> | |
| 45 | - | <!-- Protocol --> | |
| 46 | - | <div class="md:col-span-3"> | |
| 47 | - | <label class="label"> | |
| 48 | - | <span class="label-text">Protocol</span> | |
| 49 | - | </label> | |
| 50 | - | <select name="container_protocol" class="select select-bordered w-full"> | |
| 51 | - | <option value="https">https</option> | |
| 52 | - | <option value="http">http</option> | |
| 53 | - | </select> | |
| 54 | - | </div> | |
| 55 | - | ||
| 56 | - | <!-- Subdomain --> | |
| 57 | - | <div class="md:col-span-4"> | |
| 58 | - | <label class="label"> | |
| 59 | - | <span class="label-text">Subdomain</span> | |
| 60 | - | </label> | |
| 61 | - | <input type="text" name="container_subdomain" class="input input-bordered w-full" value="{{.ContainerSubdomain}}" /> | |
| 62 | - | </div> | |
| 63 | - | ||
| 64 | - | <!-- Domain --> | |
| 65 | - | <div class="md:col-span-5"> | |
| 66 | - | <label class="label"> | |
| 67 | - | <span class="label-text">Domain</span> | |
| 68 | - | </label> | |
| 69 | - | <input type="text" name="container_domain" class="input input-bordered w-full" value="{{.ContainerDomain}}" /> | |
| 70 | - | </div> | |
| 71 | - | </div> | |
| 72 | - | ||
| 73 | - | <!-- Exposed Ports --> | |
| 74 | - | <div class="form-control pb-4"> | |
| 75 | - | <label class="label"> | |
| 76 | - | <span class="label-text">Exposed Ports (Optional)</span> | |
| 77 | - | <span class="label-text-alt">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span> | |
| 78 | - | </label> | |
| 79 | - | <input type="text" name="ports" class="input input-bordered w-full" value="{{.Ports}}" /> | |
| 80 | - | </div> | |
| 81 | - | ||
| 82 | - | <!-- Proxy Port --> | |
| 83 | - | <div class="form-control pb-4"> | |
| 84 | - | <label class="label"> | |
| 85 | - | <span class="label-text">Container Proxy Port (Required)</span> | |
| 86 | - | <span class="label-text-alt">Specify the internal container port that the proxy will route traffic to</span> | |
| 87 | - | </label> | |
| 88 | - | <input type="text" name="container_port" class="input input-bordered w-full" value="{{.ProxyPort}}" required /> | |
| 89 | - | </div> | |
| 90 | - | ||
| 91 | - | <!-- Volumes --> | |
| 92 | - | <div class="form-control pb-4"> | |
| 93 | - | <label class="label"> | |
| 94 | - | <span class="label-text">Volumes</span> | |
| 95 | - | <span class="label-text-alt">e.g. ./data:/container/data</span> | |
| 96 | - | </label> | |
| 97 | - | <input type="text" name="volumes" class="input input-bordered w-full" value="{{.Volumes}}" /> | |
| 98 | - | </div> | |
| 99 | - | ||
| 100 | - | <div class="form-control pb-4"> | |
| 101 | - | <label class="label"> | |
| 102 | - | <span class="label-text">Environment variables</span> | |
| 103 | - | <span class="label-text-alt">e.g. KEY=VALUE</span> | |
| 104 | - | </label> | |
| 105 | - | <textarea name="environment_variables" class="textarea textarea-bordered w-full" | |
| 106 | - | rows="4">{{.EnvironmentVariables}}</textarea> | |
| 107 | - | </div> | |
| 108 | - | ||
| 109 | - | <!-- Restart: always/on-failure/only when stopped/never --> | |
| 110 | - | <div class="form-control pb-4"> | |
| 111 | - | <label class="label"> | |
| 112 | - | <span class="label-text">Restart</span> | |
| 113 | - | </label> | |
| 114 | - | <select name="restart" class="select select-bordered w-full"> | |
| 115 | - | <option value="always">Always</option> | |
| 116 | - | <option value="on-failure">On failure</option> | |
| 117 | - | <option value="unless-stopped">Unless stopped</option> | |
| 118 | - | <option value="never">Never</option> | |
| 119 | - | </select> | |
| 120 | - | </div> | |
| 121 | - | ||
| 122 | - | <!-- Skip Proxy Setup Option --> | |
| 123 | - | <div class="form-control pb-4"> | |
| 124 | - | <label class="label cursor-pointer justify-start gap-4"> | |
| 125 | - | <input type="checkbox" name="skip_proxy_setup" value="true" class="checkbox checkbox-primary" /> | |
| 126 | - | <span>Skip proxy setup (create container only, don't set up domain/certificates)</span> | |
| 127 | - | </label> | |
| 128 | - | <div class="text-sm opacity-70 ml-10"> | |
| 129 | - | Use this option if you only want to create the container without triggering proxy setup, | |
| 130 | - | certificate generation, and other related processes. | |
| 131 | - | </div> | |
| 132 | - | </div> | |
| 133 | - | ||
| 134 | - | <button type="submit" id="create-container-submit" class="btn btn-primary"> | |
| 135 | - | Create Container | |
| 136 | - | </button> | |
| 137 | - | </form> | |
| 138 | - | <div id="error-message-create-container-{{.ShortID}}" class="text-error mt-4"></div> | |
| 139 | - | </div> | |
| 140 | - | </div>⏎̸ | |
| @@ -1,140 +0,0 @@ | |||||
| 1 | - | <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets"> | |||
| 2 | - | <!-- Left Side: Docker Run Options --> | |||
| 3 | - | <div class="flex-grow card p-6"> | |||
| 4 | - | <h2 class="card-title pb-4">Deploy a container based on {{.ImageName}}</h2> | |||
| 5 | - | {{if .ErrorMessage}} | |||
| 6 | - | <div class="alert alert-error"> | |||
| 7 | - | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |||
| 8 | - | <span>{{.ErrorMessage}}</span> | |||
| 9 | - | </div> | |||
| 10 | - | {{ end }} | |||
| 11 | - | <form id="create-container-form-{{.ShortID}}" action="/create-container/{{.ShortID}}" method="post" | |||
| 12 | - | hx-post="/htmx/create-container/{{.ShortID}}" | |||
| 13 | - | hx-swap="innerHTML" | |||
| 14 | - | hx-target="#create-container-{{.ShortID}}" | |||
| 15 | - | hx-target-*="#error-message-create-container-{{.ShortID}}"> | |||
| 16 | - | <!-- Image Name --> | |||
| 17 | - | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4"> | |||
| 18 | - | <div class="md:col-span-1"> | |||
| 19 | - | <label class="label"> | |||
| 20 | - | <span class="label-text">Image name</span> | |||
| 21 | - | </label> | |||
| 22 | - | <input type="text" name="image_name" class="input input-bordered input-disabled w-full" | |||
| 23 | - | value="{{.ImageName}}" readonly /> | |||
| 24 | - | </div> | |||
| 25 | - | ||||
| 26 | - | <div class="md:col-span-2"> | |||
| 27 | - | <label class="label"> | |||
| 28 | - | <span class="label-text">Image ID</span> | |||
| 29 | - | </label> | |||
| 30 | - | <input type="text" name="image_id" class="input input-bordered input-disabled w-full" | |||
| 31 | - | value="{{.ImageID}}" readonly /> | |||
| 32 | - | </div> | |||
| 33 | - | </div> | |||
| 34 | - | ||||
| 35 | - | <!-- Container Name --> | |||
| 36 | - | <div class="form-control pb-4"> | |||
| 37 | - | <label class="label"> | |||
| 38 | - | <span class="label-text">Container name</span> | |||
| 39 | - | </label> | |||
| 40 | - | <input type="text" name="container_name" class="input input-bordered w-full" value="{{.ContainerName}}" /> | |||
| 41 | - | </div> | |||
| 42 | - | ||||
| 43 | - | <!-- Http(s) / Subdomain / Domain --> | |||
| 44 | - | <div class="grid grid-cols-1 md:grid-cols-12 gap-4 pb-4"> | |||
| 45 | - | <!-- Protocol --> | |||
| 46 | - | <div class="md:col-span-3"> | |||
| 47 | - | <label class="label"> | |||
| 48 | - | <span class="label-text">Protocol</span> | |||
| 49 | - | </label> | |||
| 50 | - | <select name="container_protocol" class="select select-bordered w-full"> | |||
| 51 | - | <option value="https">https</option> | |||
| 52 | - | <option value="http">http</option> | |||
| 53 | - | </select> | |||
| 54 | - | </div> | |||
| 55 | - | ||||
| 56 | - | <!-- Subdomain --> | |||
| 57 | - | <div class="md:col-span-4"> | |||
| 58 | - | <label class="label"> | |||
| 59 | - | <span class="label-text">Subdomain</span> | |||
| 60 | - | </label> | |||
| 61 | - | <input type="text" name="container_subdomain" class="input input-bordered w-full" value="{{.ContainerSubdomain}}" /> | |||
| 62 | - | </div> | |||
| 63 | - | ||||
| 64 | - | <!-- Domain --> | |||
| 65 | - | <div class="md:col-span-5"> | |||
| 66 | - | <label class="label"> | |||
| 67 | - | <span class="label-text">Domain</span> | |||
| 68 | - | </label> | |||
| 69 | - | <input type="text" name="container_domain" class="input input-bordered w-full" value="{{.ContainerDomain}}" /> | |||
| 70 | - | </div> | |||
| 71 | - | </div> | |||
| 72 | - | ||||
| 73 | - | <!-- Exposed Ports --> | |||
| 74 | - | <div class="form-control pb-4"> | |||
| 75 | - | <label class="label"> | |||
| 76 | - | <span class="label-text">Exposed Ports (Optional)</span> | |||
| 77 | - | <span class="label-text-alt">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span> | |||
| 78 | - | </label> | |||
| 79 | - | <input type="text" name="ports" class="input input-bordered w-full" value="{{.Ports}}" /> | |||
| 80 | - | </div> | |||
| 81 | - | ||||
| 82 | - | <!-- Proxy Port --> | |||
| 83 | - | <div class="form-control pb-4"> | |||
| 84 | - | <label class="label"> | |||
| 85 | - | <span class="label-text">Container Proxy Port (Required)</span> | |||
| 86 | - | <span class="label-text-alt">Specify the internal container port that the proxy will route traffic to</span> | |||
| 87 | - | </label> | |||
| 88 | - | <input type="text" name="container_port" class="input input-bordered w-full" value="{{.ProxyPort}}" required /> | |||
| 89 | - | </div> | |||
| 90 | - | ||||
| 91 | - | <!-- Volumes --> | |||
| 92 | - | <div class="form-control pb-4"> | |||
| 93 | - | <label class="label"> | |||
| 94 | - | <span class="label-text">Volumes</span> | |||
| 95 | - | <span class="label-text-alt">e.g. ./data:/container/data</span> | |||
| 96 | - | </label> | |||
| 97 | - | <input type="text" name="volumes" class="input input-bordered w-full" value="{{.Volumes}}" /> | |||
| 98 | - | </div> | |||
| 99 | - | ||||
| 100 | - | <div class="form-control pb-4"> | |||
| 101 | - | <label class="label"> | |||
| 102 | - | <span class="label-text">Environment variables</span> | |||
| 103 | - | <span class="label-text-alt">e.g. KEY=VALUE</span> | |||
| 104 | - | </label> | |||
| 105 | - | <textarea name="environment_variables" class="textarea textarea-bordered w-full" | |||
| 106 | - | rows="4">{{.EnvironmentVariables}}</textarea> | |||
| 107 | - | </div> | |||
| 108 | - | ||||
| 109 | - | <!-- Restart: always/on-failure/only when stopped/never --> | |||
| 110 | - | <div class="form-control pb-4"> | |||
| 111 | - | <label class="label"> | |||
| 112 | - | <span class="label-text">Restart</span> | |||
| 113 | - | </label> | |||
| 114 | - | <select name="restart" class="select select-bordered w-full"> | |||
| 115 | - | <option value="always">Always</option> | |||
| 116 | - | <option value="on-failure">On failure</option> | |||
| 117 | - | <option value="unless-stopped">Unless stopped</option> | |||
| 118 | - | <option value="never">Never</option> | |||
| 119 | - | </select> | |||
| 120 | - | </div> | |||
| 121 | - | ||||
| 122 | - | <!-- Skip Proxy Setup Option --> | |||
| 123 | - | <div class="form-control pb-4"> | |||
| 124 | - | <label class="label cursor-pointer justify-start gap-4"> | |||
| 125 | - | <input type="checkbox" name="skip_proxy_setup" value="true" class="checkbox checkbox-primary" /> | |||
| 126 | - | <span>Skip proxy setup (create container only, don't set up domain/certificates)</span> | |||
| 127 | - | </label> | |||
| 128 | - | <div class="text-sm opacity-70 ml-10"> | |||
| 129 | - | Use this option if you only want to create the container without triggering proxy setup, | |||
| 130 | - | certificate generation, and other related processes. | |||
| 131 | - | </div> | |||
| 132 | - | </div> | |||
| 133 | - | ||||
| 134 | - | <button type="submit" id="create-container-submit" class="btn btn-primary"> | |||
| 135 | - | Create Container | |||
| 136 | - | </button> | |||
| 137 | - | </form> | |||
| 138 | - | <div id="error-message-create-container-{{.ShortID}}" class="text-error mt-4"></div> | |||
| 139 | - | </div> | |||
| 140 | - | </div>⏎̸ | |||
D internal/templating/models/html/fragments/editcontainer.gohtml +0 -32
| @@ -1,32 +0,0 @@ | |||
| 1 | - | <div | |
| 2 | - | id="edit-container-{{.ID}}" | |
| 3 | - | class="card bg-base-100 shadow-xl" | |
| 4 | - | hx-ext="response-targets" | |
| 5 | - | > | |
| 6 | - | <div id="editor" class="card-body"> | |
| 7 | - | <form | |
| 8 | - | method="post" | |
| 9 | - | action="/htmx/container-manager/edit/{{.ID}}" | |
| 10 | - | hx-post="/htmx/container-manager/edit/{{.ID}}" | |
| 11 | - | hx-swap="innerHTML" | |
| 12 | - | hx-target-*="#error-message-edit-container-{{.ID}}" | |
| 13 | - | > | |
| 14 | - | <textarea | |
| 15 | - | name="container_config" | |
| 16 | - | id="container_config" | |
| 17 | - | class="textarea textarea-bordered w-full h-96 font-mono JetBrainsMonoNF" | |
| 18 | - | rows="20" | |
| 19 | - | >{{.ContainerInfo}}</textarea | |
| 20 | - | > | |
| 21 | - | <div class="card-actions justify-end mt-4"> | |
| 22 | - | <input | |
| 23 | - | type="submit" | |
| 24 | - | value="Save" | |
| 25 | - | id="save-button-{{.ID}}" | |
| 26 | - | class="btn btn-primary" | |
| 27 | - | /> | |
| 28 | - | </div> | |
| 29 | - | </form> | |
| 30 | - | <div id="error-message-edit-container-{{.ID}}" class="text-error mt-2"></div> | |
| 31 | - | </div> | |
| 32 | - | </div> | |
| @@ -1,32 +0,0 @@ | |||||
| 1 | - | <div | |||
| 2 | - | id="edit-container-{{.ID}}" | |||
| 3 | - | class="card bg-base-100 shadow-xl" | |||
| 4 | - | hx-ext="response-targets" | |||
| 5 | - | > | |||
| 6 | - | <div id="editor" class="card-body"> | |||
| 7 | - | <form | |||
| 8 | - | method="post" | |||
| 9 | - | action="/htmx/container-manager/edit/{{.ID}}" | |||
| 10 | - | hx-post="/htmx/container-manager/edit/{{.ID}}" | |||
| 11 | - | hx-swap="innerHTML" | |||
| 12 | - | hx-target-*="#error-message-edit-container-{{.ID}}" | |||
| 13 | - | > | |||
| 14 | - | <textarea | |||
| 15 | - | name="container_config" | |||
| 16 | - | id="container_config" | |||
| 17 | - | class="textarea textarea-bordered w-full h-96 font-mono JetBrainsMonoNF" | |||
| 18 | - | rows="20" | |||
| 19 | - | >{{.ContainerInfo}}</textarea | |||
| 20 | - | > | |||
| 21 | - | <div class="card-actions justify-end mt-4"> | |||
| 22 | - | <input | |||
| 23 | - | type="submit" | |||
| 24 | - | value="Save" | |||
| 25 | - | id="save-button-{{.ID}}" | |||
| 26 | - | class="btn btn-primary" | |||
| 27 | - | /> | |||
| 28 | - | </div> | |||
| 29 | - | </form> | |||
| 30 | - | <div id="error-message-edit-container-{{.ID}}" class="text-error mt-2"></div> | |||
| 31 | - | </div> | |||
| 32 | - | </div> | |||
D internal/templating/models/html/fragments/footer.gohtml +0 -33
| @@ -1,33 +0,0 @@ | |||
| 1 | - | <footer class="footer p-10 bg-base-200 text-base-content"> | |
| 2 | - | <div> | |
| 3 | - | <span class="footer-title">Gordon</span> | |
| 4 | - | <a class="link link-hover">About us</a> | |
| 5 | - | <a class="link link-hover">Documentation</a> | |
| 6 | - | <a class="link link-hover">Github</a> | |
| 7 | - | </div> | |
| 8 | - | <div> | |
| 9 | - | <span class="footer-title">Container Management</span> | |
| 10 | - | <a class="link link-hover">Dashboard</a> | |
| 11 | - | <a class="link link-hover">Settings</a> | |
| 12 | - | <a class="link link-hover">Stats</a> | |
| 13 | - | </div> | |
| 14 | - | <div> | |
| 15 | - | <span class="footer-title">Legal</span> | |
| 16 | - | <a class="link link-hover">Terms of use</a> | |
| 17 | - | <a class="link link-hover">Privacy policy</a> | |
| 18 | - | <a class="link link-hover">License</a> | |
| 19 | - | </div> | |
| 20 | - | </footer> | |
| 21 | - | <footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300"> | |
| 22 | - | <div class="items-center grid-flow-col"> | |
| 23 | - | <span class="iconfb text-2xl"></span> | |
| 24 | - | <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p> | |
| 25 | - | </div> | |
| 26 | - | <div class="md:place-self-center md:justify-self-end"> | |
| 27 | - | <div class="grid grid-flow-col gap-4"> | |
| 28 | - | <a class="iconfb text-2xl"></a> | |
| 29 | - | <a class="iconfb text-2xl"></a> | |
| 30 | - | <a class="iconfb text-2xl"></a> | |
| 31 | - | </div> | |
| 32 | - | </div> | |
| 33 | - | </footer> | |
| @@ -1,33 +0,0 @@ | |||||
| 1 | - | <footer class="footer p-10 bg-base-200 text-base-content"> | |||
| 2 | - | <div> | |||
| 3 | - | <span class="footer-title">Gordon</span> | |||
| 4 | - | <a class="link link-hover">About us</a> | |||
| 5 | - | <a class="link link-hover">Documentation</a> | |||
| 6 | - | <a class="link link-hover">Github</a> | |||
| 7 | - | </div> | |||
| 8 | - | <div> | |||
| 9 | - | <span class="footer-title">Container Management</span> | |||
| 10 | - | <a class="link link-hover">Dashboard</a> | |||
| 11 | - | <a class="link link-hover">Settings</a> | |||
| 12 | - | <a class="link link-hover">Stats</a> | |||
| 13 | - | </div> | |||
| 14 | - | <div> | |||
| 15 | - | <span class="footer-title">Legal</span> | |||
| 16 | - | <a class="link link-hover">Terms of use</a> | |||
| 17 | - | <a class="link link-hover">Privacy policy</a> | |||
| 18 | - | <a class="link link-hover">License</a> | |||
| 19 | - | </div> | |||
| 20 | - | </footer> | |||
| 21 | - | <footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300"> | |||
| 22 | - | <div class="items-center grid-flow-col"> | |||
| 23 | - | <span class="iconfb text-2xl"></span> | |||
| 24 | - | <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p> | |||
| 25 | - | </div> | |||
| 26 | - | <div class="md:place-self-center md:justify-self-end"> | |||
| 27 | - | <div class="grid grid-flow-col gap-4"> | |||
| 28 | - | <a class="iconfb text-2xl"></a> | |||
| 29 | - | <a class="iconfb text-2xl"></a> | |||
| 30 | - | <a class="iconfb text-2xl"></a> | |||
| 31 | - | </div> | |||
| 32 | - | </div> | |||
| 33 | - | </footer> | |||
D internal/templating/models/html/fragments/header.gohtml +0 -24
| @@ -1,24 +0,0 @@ | |||
| 1 | - | <head> | |
| 2 | - | <meta charset="UTF-8" /> | |
| 3 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 4 | - | <title>{{ .header.meta.title }}</title> | |
| 5 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 6 | - | ||
| 7 | - | <script src="/libs/jquery.min.js"></script> | |
| 8 | - | <script src="/libs/htmx.min.js"></script> | |
| 9 | - | <script src="/libs/htmx-response-targets.js"></script> | |
| 10 | - | <script src="/libs/htmx-debug.js"></script> | |
| 11 | - | <script src="/libs/htmx-sse.js"></script> | |
| 12 | - | <script defer src="/libs/hyperscript.min.js"></script> | |
| 13 | - | <script defer src="/assets/js/custom.js"></script> | |
| 14 | - | ||
| 15 | - | <!-- daisyUI theme configuration --> | |
| 16 | - | <script> | |
| 17 | - | // Check for saved theme preference or use preferred color scheme | |
| 18 | - | const savedTheme = localStorage.getItem('theme') || 'gordon'; | |
| 19 | - | document.documentElement.setAttribute('data-theme', savedTheme); | |
| 20 | - | ||
| 21 | - | // Log theme loading for debugging | |
| 22 | - | console.debug('Theme loaded:', savedTheme); | |
| 23 | - | </script> | |
| 24 | - | </head> | |
| @@ -1,24 +0,0 @@ | |||||
| 1 | - | <head> | |||
| 2 | - | <meta charset="UTF-8" /> | |||
| 3 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 4 | - | <title>{{ .header.meta.title }}</title> | |||
| 5 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |||
| 6 | - | ||||
| 7 | - | <script src="/libs/jquery.min.js"></script> | |||
| 8 | - | <script src="/libs/htmx.min.js"></script> | |||
| 9 | - | <script src="/libs/htmx-response-targets.js"></script> | |||
| 10 | - | <script src="/libs/htmx-debug.js"></script> | |||
| 11 | - | <script src="/libs/htmx-sse.js"></script> | |||
| 12 | - | <script defer src="/libs/hyperscript.min.js"></script> | |||
| 13 | - | <script defer src="/assets/js/custom.js"></script> | |||
| 14 | - | ||||
| 15 | - | <!-- daisyUI theme configuration --> | |||
| 16 | - | <script> | |||
| 17 | - | // Check for saved theme preference or use preferred color scheme | |||
| 18 | - | const savedTheme = localStorage.getItem('theme') || 'gordon'; | |||
| 19 | - | document.documentElement.setAttribute('data-theme', savedTheme); | |||
| 20 | - | ||||
| 21 | - | // Log theme loading for debugging | |||
| 22 | - | console.debug('Theme loaded:', savedTheme); | |||
| 23 | - | </script> | |||
| 24 | - | </head> | |||
D internal/templating/models/html/fragments/imagelist.gohtml +0 -98
| @@ -1,98 +0,0 @@ | |||
| 1 | - | <!-- Image list using daisyUI components --> | |
| 2 | - | <div class="card w-full bg-base-300"> | |
| 3 | - | <div class="card-body p-4"> | |
| 4 | - | ||
| 5 | - | <!-- Action buttons --> | |
| 6 | - | <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4"> | |
| 7 | - | <h3 class="font-semibold text-base-content">Available Images: {{ len .Images }}</h3> | |
| 8 | - | <div class="flex gap-2"> | |
| 9 | - | <button | |
| 10 | - | class="btn btn-sm btn-secondary" | |
| 11 | - | hx-get="/htmx/upload-image" | |
| 12 | - | hx-target="#image-manager" | |
| 13 | - | hx-swap="innerHTML"> | |
| 14 | - | <span class="iconf"></span> Upload | |
| 15 | - | </button> | |
| 16 | - | <button | |
| 17 | - | class="btn btn-sm btn-primary" | |
| 18 | - | hx-get="/htmx/image-manager" | |
| 19 | - | hx-target="#image-manager" | |
| 20 | - | hx-swap="innerHTML"> | |
| 21 | - | <span class="iconf"></span> Refresh | |
| 22 | - | </button> | |
| 23 | - | </div> | |
| 24 | - | </div> | |
| 25 | - | ||
| 26 | - | <!-- Image list --> | |
| 27 | - | <table class="table"> | |
| 28 | - | <!-- head --> | |
| 29 | - | <thead> | |
| 30 | - | <tr> | |
| 31 | - | <th>Repository</th> | |
| 32 | - | <th>Tag</th> | |
| 33 | - | <th>Size</th> | |
| 34 | - | <th>Created</th> | |
| 35 | - | <th>Actions</th> | |
| 36 | - | </tr> | |
| 37 | - | </thead> | |
| 38 | - | <tbody> | |
| 39 | - | {{ if .NoImage }} | |
| 40 | - | <tr> | |
| 41 | - | <td colspan="5" class="text-center py-4"> | |
| 42 | - | <div class="alert alert-info"> | |
| 43 | - | <span class="iconf">ℹ️</span> | |
| 44 | - | <span>No images found. Upload an image first.</span> | |
| 45 | - | </div> | |
| 46 | - | </td> | |
| 47 | - | </tr> | |
| 48 | - | {{ else }} | |
| 49 | - | {{ range .Images }} | |
| 50 | - | <tr class="hover"> | |
| 51 | - | <td> | |
| 52 | - | <div class="flex items-center gap-3"> | |
| 53 | - | <div> | |
| 54 | - | <div class="font-bold">{{ .Repository }}</div> | |
| 55 | - | <div class="text-sm opacity-50">{{ .ID }}</div> | |
| 56 | - | </div> | |
| 57 | - | </div> | |
| 58 | - | </td> | |
| 59 | - | <td>{{ .Tag }}</td> | |
| 60 | - | <td>{{ .Size }}</td> | |
| 61 | - | <td>{{ .Created }}</td> | |
| 62 | - | <th> | |
| 63 | - | <div class="dropdown dropdown-end"> | |
| 64 | - | <div tabindex="0" role="button" class="btn btn-ghost btn-xs"> | |
| 65 | - | <span class="iconf"></span> | |
| 66 | - | </div> | |
| 67 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52"> | |
| 68 | - | <li> | |
| 69 | - | <a | |
| 70 | - | href="/htmx/create-container/{{ if gt (len .ID) 12 }}{{ slice .ID 0 12 }}{{ else }}{{ .ID }}{{ end }}" | |
| 71 | - | class="text-success flex items-center"> | |
| 72 | - | <span class="iconf mr-2"></span> Create Container | |
| 73 | - | </a> | |
| 74 | - | </li> | |
| 75 | - | <li> | |
| 76 | - | <button | |
| 77 | - | class="text-error flex items-center" | |
| 78 | - | hx-delete="/htmx/image-manager/delete/{{ .ID }}" | |
| 79 | - | hx-target="#image-manager" | |
| 80 | - | hx-swap="innerHTML" | |
| 81 | - | hx-confirm="Are you sure you want to delete this image?"> | |
| 82 | - | <span class="iconf mr-2"></span> Delete | |
| 83 | - | </button> | |
| 84 | - | </li> | |
| 85 | - | </ul> | |
| 86 | - | </div> | |
| 87 | - | </th> | |
| 88 | - | </tr> | |
| 89 | - | {{ end }} | |
| 90 | - | {{ end }} | |
| 91 | - | </tbody> | |
| 92 | - | </table> | |
| 93 | - | </div> | |
| 94 | - | </div> | |
| 95 | - | ||
| 96 | - | <script> | |
| 97 | - | console.debug('Image list rendered'); | |
| 98 | - | </script> | |
| @@ -1,98 +0,0 @@ | |||||
| 1 | - | <!-- Image list using daisyUI components --> | |||
| 2 | - | <div class="card w-full bg-base-300"> | |||
| 3 | - | <div class="card-body p-4"> | |||
| 4 | - | ||||
| 5 | - | <!-- Action buttons --> | |||
| 6 | - | <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4"> | |||
| 7 | - | <h3 class="font-semibold text-base-content">Available Images: {{ len .Images }}</h3> | |||
| 8 | - | <div class="flex gap-2"> | |||
| 9 | - | <button | |||
| 10 | - | class="btn btn-sm btn-secondary" | |||
| 11 | - | hx-get="/htmx/upload-image" | |||
| 12 | - | hx-target="#image-manager" | |||
| 13 | - | hx-swap="innerHTML"> | |||
| 14 | - | <span class="iconf"></span> Upload | |||
| 15 | - | </button> | |||
| 16 | - | <button | |||
| 17 | - | class="btn btn-sm btn-primary" | |||
| 18 | - | hx-get="/htmx/image-manager" | |||
| 19 | - | hx-target="#image-manager" | |||
| 20 | - | hx-swap="innerHTML"> | |||
| 21 | - | <span class="iconf"></span> Refresh | |||
| 22 | - | </button> | |||
| 23 | - | </div> | |||
| 24 | - | </div> | |||
| 25 | - | ||||
| 26 | - | <!-- Image list --> | |||
| 27 | - | <table class="table"> | |||
| 28 | - | <!-- head --> | |||
| 29 | - | <thead> | |||
| 30 | - | <tr> | |||
| 31 | - | <th>Repository</th> | |||
| 32 | - | <th>Tag</th> | |||
| 33 | - | <th>Size</th> | |||
| 34 | - | <th>Created</th> | |||
| 35 | - | <th>Actions</th> | |||
| 36 | - | </tr> | |||
| 37 | - | </thead> | |||
| 38 | - | <tbody> | |||
| 39 | - | {{ if .NoImage }} | |||
| 40 | - | <tr> | |||
| 41 | - | <td colspan="5" class="text-center py-4"> | |||
| 42 | - | <div class="alert alert-info"> | |||
| 43 | - | <span class="iconf">ℹ️</span> | |||
| 44 | - | <span>No images found. Upload an image first.</span> | |||
| 45 | - | </div> | |||
| 46 | - | </td> | |||
| 47 | - | </tr> | |||
| 48 | - | {{ else }} | |||
| 49 | - | {{ range .Images }} | |||
| 50 | - | <tr class="hover"> | |||
| 51 | - | <td> | |||
| 52 | - | <div class="flex items-center gap-3"> | |||
| 53 | - | <div> | |||
| 54 | - | <div class="font-bold">{{ .Repository }}</div> | |||
| 55 | - | <div class="text-sm opacity-50">{{ .ID }}</div> | |||
| 56 | - | </div> | |||
| 57 | - | </div> | |||
| 58 | - | </td> | |||
| 59 | - | <td>{{ .Tag }}</td> | |||
| 60 | - | <td>{{ .Size }}</td> | |||
| 61 | - | <td>{{ .Created }}</td> | |||
| 62 | - | <th> | |||
| 63 | - | <div class="dropdown dropdown-end"> | |||
| 64 | - | <div tabindex="0" role="button" class="btn btn-ghost btn-xs"> | |||
| 65 | - | <span class="iconf"></span> | |||
| 66 | - | </div> | |||
| 67 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52"> | |||
| 68 | - | <li> | |||
| 69 | - | <a | |||
| 70 | - | href="/htmx/create-container/{{ if gt (len .ID) 12 }}{{ slice .ID 0 12 }}{{ else }}{{ .ID }}{{ end }}" | |||
| 71 | - | class="text-success flex items-center"> | |||
| 72 | - | <span class="iconf mr-2"></span> Create Container | |||
| 73 | - | </a> | |||
| 74 | - | </li> | |||
| 75 | - | <li> | |||
| 76 | - | <button | |||
| 77 | - | class="text-error flex items-center" | |||
| 78 | - | hx-delete="/htmx/image-manager/delete/{{ .ID }}" | |||
| 79 | - | hx-target="#image-manager" | |||
| 80 | - | hx-swap="innerHTML" | |||
| 81 | - | hx-confirm="Are you sure you want to delete this image?"> | |||
| 82 | - | <span class="iconf mr-2"></span> Delete | |||
| 83 | - | </button> | |||
| 84 | - | </li> | |||
| 85 | - | </ul> | |||
| 86 | - | </div> | |||
| 87 | - | </th> | |||
| 88 | - | </tr> | |||
| 89 | - | {{ end }} | |||
| 90 | - | {{ end }} | |||
| 91 | - | </tbody> | |||
| 92 | - | </table> | |||
| 93 | - | </div> | |||
| 94 | - | </div> | |||
| 95 | - | ||||
| 96 | - | <script> | |||
| 97 | - | console.debug('Image list rendered'); | |||
| 98 | - | </script> | |||
D internal/templating/models/html/fragments/menu.gohtml +0 -67
| @@ -1,67 +0,0 @@ | |||
| 1 | - | <!-- DaisyUI Navbar --> | |
| 2 | - | <div class="navbar bg-base-100 shadow-lg fixed top-0 z-50"> | |
| 3 | - | <div class="navbar-start"> | |
| 4 | - | <div class="flex items-center"> | |
| 5 | - | <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4"> | |
| 6 | - | Gordon | |
| 7 | - | </h1> | |
| 8 | - | <span id="actual-version" class="text-xs md:text-sm pr-1 JetBrainsMonoNF text-base-content/70"> | |
| 9 | - | {{ .BuildVersion }} | |
| 10 | - | </span> | |
| 11 | - | <span hidden id="update-available" class="text-success text-xs md:text-sm JetBrainsMonoNF hidden" title=""> | |
| 12 | - | <a href="https://github.com/bnema/gordon/releases" class="link"></a> | |
| 13 | - | </span> | |
| 14 | - | </div> | |
| 15 | - | </div> | |
| 16 | - | <div class="navbar-center hidden lg:flex"> | |
| 17 | - | <ul class="menu menu-horizontal px-1"> | |
| 18 | - | <li> | |
| 19 | - | <a href="{{ .AdminPath }}/manager" class="font-semibold"> | |
| 20 | - | <span class="JetBrainsMonoBoldNF pr-1"></span> Manager | |
| 21 | - | </a> | |
| 22 | - | </li> | |
| 23 | - | </ul> | |
| 24 | - | </div> | |
| 25 | - | <div class="navbar-end"> | |
| 26 | - | <!-- Theme toggle --> | |
| 27 | - | <div class="dropdown dropdown-end"> | |
| 28 | - | <label tabindex="0" class="btn btn-ghost btn-circle"> | |
| 29 | - | <span class="iconf text-lg md:text-xl"></span> | |
| 30 | - | </label> | |
| 31 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | |
| 32 | - | <li><button onclick="setTheme('gordon')">Gordon Light</button></li> | |
| 33 | - | <li><button onclick="setTheme('gordon-dark')">Gordon Dark</button></li> | |
| 34 | - | <li><button onclick="setTheme('light')">Light</button></li> | |
| 35 | - | <li><button onclick="setTheme('dark')">Dark</button></li> | |
| 36 | - | </ul> | |
| 37 | - | </div> | |
| 38 | - | ||
| 39 | - | <!-- User menu --> | |
| 40 | - | <div class="dropdown dropdown-end"> | |
| 41 | - | <label tabindex="0" class="btn btn-ghost btn-circle"> | |
| 42 | - | <span class="iconf text-lg md:text-xl"></span> | |
| 43 | - | </label> | |
| 44 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | |
| 45 | - | <li> | |
| 46 | - | <a href="{{ .UserSettingsURL }}" title="User settings"> | |
| 47 | - | <span class="iconf"></span> Settings | |
| 48 | - | </a> | |
| 49 | - | </li> | |
| 50 | - | <li> | |
| 51 | - | <a href="{{ .LogoutURL }}" title="Logout"> | |
| 52 | - | <span class="iconf"></span> Logout | |
| 53 | - | </a> | |
| 54 | - | </li> | |
| 55 | - | </ul> | |
| 56 | - | </div> | |
| 57 | - | </div> | |
| 58 | - | </div> | |
| 59 | - | ||
| 60 | - | <!-- Theme toggle script --> | |
| 61 | - | <script> | |
| 62 | - | function setTheme(theme) { | |
| 63 | - | document.documentElement.setAttribute('data-theme', theme); | |
| 64 | - | localStorage.setItem('theme', theme); | |
| 65 | - | console.debug('Theme changed to:', theme); | |
| 66 | - | } | |
| 67 | - | </script> | |
| @@ -1,67 +0,0 @@ | |||||
| 1 | - | <!-- DaisyUI Navbar --> | |||
| 2 | - | <div class="navbar bg-base-100 shadow-lg fixed top-0 z-50"> | |||
| 3 | - | <div class="navbar-start"> | |||
| 4 | - | <div class="flex items-center"> | |||
| 5 | - | <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4"> | |||
| 6 | - | Gordon | |||
| 7 | - | </h1> | |||
| 8 | - | <span id="actual-version" class="text-xs md:text-sm pr-1 JetBrainsMonoNF text-base-content/70"> | |||
| 9 | - | {{ .BuildVersion }} | |||
| 10 | - | </span> | |||
| 11 | - | <span hidden id="update-available" class="text-success text-xs md:text-sm JetBrainsMonoNF hidden" title=""> | |||
| 12 | - | <a href="https://github.com/bnema/gordon/releases" class="link"></a> | |||
| 13 | - | </span> | |||
| 14 | - | </div> | |||
| 15 | - | </div> | |||
| 16 | - | <div class="navbar-center hidden lg:flex"> | |||
| 17 | - | <ul class="menu menu-horizontal px-1"> | |||
| 18 | - | <li> | |||
| 19 | - | <a href="{{ .AdminPath }}/manager" class="font-semibold"> | |||
| 20 | - | <span class="JetBrainsMonoBoldNF pr-1"></span> Manager | |||
| 21 | - | </a> | |||
| 22 | - | </li> | |||
| 23 | - | </ul> | |||
| 24 | - | </div> | |||
| 25 | - | <div class="navbar-end"> | |||
| 26 | - | <!-- Theme toggle --> | |||
| 27 | - | <div class="dropdown dropdown-end"> | |||
| 28 | - | <label tabindex="0" class="btn btn-ghost btn-circle"> | |||
| 29 | - | <span class="iconf text-lg md:text-xl"></span> | |||
| 30 | - | </label> | |||
| 31 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | |||
| 32 | - | <li><button onclick="setTheme('gordon')">Gordon Light</button></li> | |||
| 33 | - | <li><button onclick="setTheme('gordon-dark')">Gordon Dark</button></li> | |||
| 34 | - | <li><button onclick="setTheme('light')">Light</button></li> | |||
| 35 | - | <li><button onclick="setTheme('dark')">Dark</button></li> | |||
| 36 | - | </ul> | |||
| 37 | - | </div> | |||
| 38 | - | ||||
| 39 | - | <!-- User menu --> | |||
| 40 | - | <div class="dropdown dropdown-end"> | |||
| 41 | - | <label tabindex="0" class="btn btn-ghost btn-circle"> | |||
| 42 | - | <span class="iconf text-lg md:text-xl"></span> | |||
| 43 | - | </label> | |||
| 44 | - | <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | |||
| 45 | - | <li> | |||
| 46 | - | <a href="{{ .UserSettingsURL }}" title="User settings"> | |||
| 47 | - | <span class="iconf"></span> Settings | |||
| 48 | - | </a> | |||
| 49 | - | </li> | |||
| 50 | - | <li> | |||
| 51 | - | <a href="{{ .LogoutURL }}" title="Logout"> | |||
| 52 | - | <span class="iconf"></span> Logout | |||
| 53 | - | </a> | |||
| 54 | - | </li> | |||
| 55 | - | </ul> | |||
| 56 | - | </div> | |||
| 57 | - | </div> | |||
| 58 | - | </div> | |||
| 59 | - | ||||
| 60 | - | <!-- Theme toggle script --> | |||
| 61 | - | <script> | |||
| 62 | - | function setTheme(theme) { | |||
| 63 | - | document.documentElement.setAttribute('data-theme', theme); | |||
| 64 | - | localStorage.setItem('theme', theme); | |||
| 65 | - | console.debug('Theme changed to:', theme); | |||
| 66 | - | } | |||
| 67 | - | </script> | |||
D internal/templating/models/html/fragments/success.gohtml +0 -5
| @@ -1,5 +0,0 @@ | |||
| 1 | - | <div | |
| 2 | - | id="success" | |
| 3 | - | class="flex text-indigo-600 content-center justify-center items-center w-full h-full"> | |
| 4 | - | <p class="text-2xl text-center">{{if .Message}}{{.Message}}{{else}}Success!{{end}}</p> | |
| 5 | - | </div> | |
| @@ -1,5 +0,0 @@ | |||||
| 1 | - | <div | |||
| 2 | - | id="success" | |||
| 3 | - | class="flex text-indigo-600 content-center justify-center items-center w-full h-full"> | |||
| 4 | - | <p class="text-2xl text-center">{{if .Message}}{{.Message}}{{else}}Success!{{end}}</p> | |||
| 5 | - | </div> | |||
D internal/templating/models/html/fragments/uploadimage.gohtml +0 -61
| @@ -1,61 +0,0 @@ | |||
| 1 | - | <!-- Upload image form using DaisyUI components --> | |
| 2 | - | <div class="card bg-base-100 shadow"> | |
| 3 | - | <div class="card-body"> | |
| 4 | - | <div class="flex justify-between items-center"> | |
| 5 | - | <h2 class="card-title text-primary">Upload Image</h2> | |
| 6 | - | <button | |
| 7 | - | class="btn btn-sm btn-circle btn-ghost" | |
| 8 | - | hx-get="/htmx/image-manager" | |
| 9 | - | hx-target="#image-manager" | |
| 10 | - | hx-swap="innerHTML"> | |
| 11 | - | <span class="iconf text-lg">✕</span> | |
| 12 | - | </button> | |
| 13 | - | </div> | |
| 14 | - | <div class="divider"></div> | |
| 15 | - | ||
| 16 | - | <form | |
| 17 | - | id="upload-form" | |
| 18 | - | hx-encoding="multipart/form-data" | |
| 19 | - | hx-post="/htmx/upload-image" | |
| 20 | - | hx-target="#image-manager" | |
| 21 | - | hx-swap="innerHTML" | |
| 22 | - | class="space-y-4"> | |
| 23 | - | ||
| 24 | - | <div class="alert alert-info shadow-lg mb-4"> | |
| 25 | - | <span class="iconf">ℹ️</span> | |
| 26 | - | <div> | |
| 27 | - | <p>This form allows you to upload a container image (.tar file) to Gordon.</p> | |
| 28 | - | <p>After uploading, you can create and manage containers from this image.</p> | |
| 29 | - | </div> | |
| 30 | - | </div> | |
| 31 | - | ||
| 32 | - | <div class="form-control w-full"> | |
| 33 | - | <label class="label"> | |
| 34 | - | <span class="label-text">Select a container image file (.tar)</span> | |
| 35 | - | </label> | |
| 36 | - | <input | |
| 37 | - | type="file" | |
| 38 | - | name="imageFile" | |
| 39 | - | class="file-input file-input-bordered w-full" | |
| 40 | - | accept=".tar" | |
| 41 | - | required /> | |
| 42 | - | </div> | |
| 43 | - | ||
| 44 | - | <div class="card-actions justify-end mt-4"> | |
| 45 | - | <button | |
| 46 | - | class="btn btn-ghost" | |
| 47 | - | hx-get="/htmx/image-manager" | |
| 48 | - | hx-target="#image-manager" | |
| 49 | - | hx-swap="innerHTML" | |
| 50 | - | type="button"> | |
| 51 | - | Cancel | |
| 52 | - | </button> | |
| 53 | - | <button | |
| 54 | - | class="btn btn-primary" | |
| 55 | - | type="submit"> | |
| 56 | - | <span class="iconf mr-1"></span> Upload | |
| 57 | - | </button> | |
| 58 | - | </div> | |
| 59 | - | </form> | |
| 60 | - | </div> | |
| 61 | - | </div> | |
| @@ -1,61 +0,0 @@ | |||||
| 1 | - | <!-- Upload image form using DaisyUI components --> | |||
| 2 | - | <div class="card bg-base-100 shadow"> | |||
| 3 | - | <div class="card-body"> | |||
| 4 | - | <div class="flex justify-between items-center"> | |||
| 5 | - | <h2 class="card-title text-primary">Upload Image</h2> | |||
| 6 | - | <button | |||
| 7 | - | class="btn btn-sm btn-circle btn-ghost" | |||
| 8 | - | hx-get="/htmx/image-manager" | |||
| 9 | - | hx-target="#image-manager" | |||
| 10 | - | hx-swap="innerHTML"> | |||
| 11 | - | <span class="iconf text-lg">✕</span> | |||
| 12 | - | </button> | |||
| 13 | - | </div> | |||
| 14 | - | <div class="divider"></div> | |||
| 15 | - | ||||
| 16 | - | <form | |||
| 17 | - | id="upload-form" | |||
| 18 | - | hx-encoding="multipart/form-data" | |||
| 19 | - | hx-post="/htmx/upload-image" | |||
| 20 | - | hx-target="#image-manager" | |||
| 21 | - | hx-swap="innerHTML" | |||
| 22 | - | class="space-y-4"> | |||
| 23 | - | ||||
| 24 | - | <div class="alert alert-info shadow-lg mb-4"> | |||
| 25 | - | <span class="iconf">ℹ️</span> | |||
| 26 | - | <div> | |||
| 27 | - | <p>This form allows you to upload a container image (.tar file) to Gordon.</p> | |||
| 28 | - | <p>After uploading, you can create and manage containers from this image.</p> | |||
| 29 | - | </div> | |||
| 30 | - | </div> | |||
| 31 | - | ||||
| 32 | - | <div class="form-control w-full"> | |||
| 33 | - | <label class="label"> | |||
| 34 | - | <span class="label-text">Select a container image file (.tar)</span> | |||
| 35 | - | </label> | |||
| 36 | - | <input | |||
| 37 | - | type="file" | |||
| 38 | - | name="imageFile" | |||
| 39 | - | class="file-input file-input-bordered w-full" | |||
| 40 | - | accept=".tar" | |||
| 41 | - | required /> | |||
| 42 | - | </div> | |||
| 43 | - | ||||
| 44 | - | <div class="card-actions justify-end mt-4"> | |||
| 45 | - | <button | |||
| 46 | - | class="btn btn-ghost" | |||
| 47 | - | hx-get="/htmx/image-manager" | |||
| 48 | - | hx-target="#image-manager" | |||
| 49 | - | hx-swap="innerHTML" | |||
| 50 | - | type="button"> | |||
| 51 | - | Cancel | |||
| 52 | - | </button> | |||
| 53 | - | <button | |||
| 54 | - | class="btn btn-primary" | |||
| 55 | - | type="submit"> | |||
| 56 | - | <span class="iconf mr-1"></span> Upload | |||
| 57 | - | </button> | |||
| 58 | - | </div> | |||
| 59 | - | </form> | |||
| 60 | - | </div> | |||
| 61 | - | </div> | |||
D internal/templating/models/html/login/index.gohtml +0 -122
| @@ -1,122 +0,0 @@ | |||
| 1 | - | <!doctype html> | |
| 2 | - | <html lang="en" data-theme="corporate"> | |
| 3 | - | <head> | |
| 4 | - | <meta charset="UTF-8" /> | |
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| 6 | - | <title>{{ .header.meta.title }}</title> | |
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |
| 9 | - | <!-- Custom CSS + Tailwind --> | |
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 11 | - | <!-- HTMX min js --> | |
| 12 | - | <script src="/libs/htmx.min.js"></script> | |
| 13 | - | <!-- HTMX SSE --> | |
| 14 | - | <script src="/libs/htmx-sse.js"></script> | |
| 15 | - | <!-- DaisyUI --> | |
| 16 | - | <script src="/libs/daisyui.js"></script> | |
| 17 | - | <!-- Custom JS from assets/js --> | |
| 18 | - | <script src="/assets/js/custom.js"></script> | |
| 19 | - | ||
| 20 | - | <!-- daisyUI theme configuration --> | |
| 21 | - | <script> | |
| 22 | - | // Check for saved theme preference or use preferred color scheme | |
| 23 | - | const savedTheme = localStorage.getItem('theme') || 'corporate'; | |
| 24 | - | document.documentElement.setAttribute('data-theme', savedTheme); | |
| 25 | - | console.debug('Theme loaded:', savedTheme); | |
| 26 | - | </script> | |
| 27 | - | </head> | |
| 28 | - | ||
| 29 | - | <body class="IBMPlexSans bg-base-200 min-h-screen flex flex-col items-center justify-center p-4"> | |
| 30 | - | <!-- Logo and Title --> | |
| 31 | - | <div | |
| 32 | - | id="logo-mascot" | |
| 33 | - | class="flex flex-col justify-center items-center p-4 mb-4"> | |
| 34 | - | <div class="flex justify-center w-100"> | |
| 35 | - | <img | |
| 36 | - | src="/assets/imgs/gordon-mascot-mq-trsp.png" | |
| 37 | - | alt="logo gordon" | |
| 38 | - | class="sm:h-24 h-32 xl:h-48 drop-shadow-xl" /> | |
| 39 | - | </div> | |
| 40 | - | <div class="flex justify-center mt-4"> | |
| 41 | - | <h1 | |
| 42 | - | class="gordon-title text-4xl lg:text-4xl xl:text-5xl text-primary tracking-tighter"> | |
| 43 | - | Gordon | |
| 44 | - | </h1> | |
| 45 | - | <span | |
| 46 | - | class="gordon-v-subtitle text-secondary text-sm ml-2" | |
| 47 | - | >{{ .BuildVersion }}</span | |
| 48 | - | > | |
| 49 | - | </div> | |
| 50 | - | </div> | |
| 51 | - | ||
| 52 | - | <!-- Main Content --> | |
| 53 | - | <div class="card bg-base-100 shadow-xl w-full max-w-md"> | |
| 54 | - | <div class="card-body"> | |
| 55 | - | <!-- Login Section --> | |
| 56 | - | <div id="login-section"> | |
| 57 | - | <div class="flex flex-col items-center"> | |
| 58 | - | <h2 class="card-title text-center mb-4">Login</h2> | |
| 59 | - | ||
| 60 | - | <!-- Error Message Display --> | |
| 61 | - | {{ if .Error }} | |
| 62 | - | <div class="alert alert-error mb-4"> | |
| 63 | - | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| 64 | - | <span> | |
| 65 | - | {{ if eq .Error "empty_token" }} | |
| 66 | - | Please enter a token. | |
| 67 | - | {{ else if eq .Error "invalid_token" }} | |
| 68 | - | Invalid token. Please try again. | |
| 69 | - | {{ else if eq .Error "server_error" }} | |
| 70 | - | A server error occurred. Please try again later. | |
| 71 | - | {{ else }} | |
| 72 | - | An error occurred. Please try again. | |
| 73 | - | {{ end }} | |
| 74 | - | </span> | |
| 75 | - | </div> | |
| 76 | - | {{ end }} | |
| 77 | - | ||
| 78 | - | <!-- Token Input Form --> | |
| 79 | - | <div class="w-full"> | |
| 80 | - | <form action="login/submit-token" method="POST" class="mb-4"> | |
| 81 | - | <div class="form-control mb-4"> | |
| 82 | - | <label for="token" class="label"> | |
| 83 | - | <span class="label-text">Admin Token</span> | |
| 84 | - | </label> | |
| 85 | - | <input | |
| 86 | - | type="text" | |
| 87 | - | id="token" | |
| 88 | - | name="token" | |
| 89 | - | placeholder="Enter your admin token" | |
| 90 | - | class="input input-bordered w-full" | |
| 91 | - | required | |
| 92 | - | /> | |
| 93 | - | </div> | |
| 94 | - | <button | |
| 95 | - | type="submit" | |
| 96 | - | class="btn btn-primary w-full" | |
| 97 | - | > | |
| 98 | - | Continue with Token | |
| 99 | - | </button> | |
| 100 | - | </form> | |
| 101 | - | <div class="divider">OR</div> | |
| 102 | - | </div> | |
| 103 | - | ||
| 104 | - | <!-- GitHub OAuth --> | |
| 105 | - | <div class="w-full"> | |
| 106 | - | <a | |
| 107 | - | href="login/oauth/github" | |
| 108 | - | class="btn btn-secondary w-full" | |
| 109 | - | > | |
| 110 | - | <svg class="mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"> | |
| 111 | - | <path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" /> | |
| 112 | - | </svg> | |
| 113 | - | Sign in with GitHub | |
| 114 | - | </a> | |
| 115 | - | </div> | |
| 116 | - | </div> | |
| 117 | - | </div> | |
| 118 | - | <!-- End Login Section --> | |
| 119 | - | </div> | |
| 120 | - | </div> | |
| 121 | - | </body> | |
| 122 | - | </html> | |
| @@ -1,122 +0,0 @@ | |||||
| 1 | - | <!doctype html> | |||
| 2 | - | <html lang="en" data-theme="corporate"> | |||
| 3 | - | <head> | |||
| 4 | - | <meta charset="UTF-8" /> | |||
| 5 | - | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| 6 | - | <title>{{ .header.meta.title }}</title> | |||
| 7 | - | <meta name="description" content="{{ .Header.Meta.Description }}" /> | |||
| 8 | - | <meta name="keywords" content="{{ .Header.Meta.Keywords }}" /> | |||
| 9 | - | <!-- Custom CSS + Tailwind --> | |||
| 10 | - | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |||
| 11 | - | <!-- HTMX min js --> | |||
| 12 | - | <script src="/libs/htmx.min.js"></script> | |||
| 13 | - | <!-- HTMX SSE --> | |||
| 14 | - | <script src="/libs/htmx-sse.js"></script> | |||
| 15 | - | <!-- DaisyUI --> | |||
| 16 | - | <script src="/libs/daisyui.js"></script> | |||
| 17 | - | <!-- Custom JS from assets/js --> | |||
| 18 | - | <script src="/assets/js/custom.js"></script> | |||
| 19 | - | ||||
| 20 | - | <!-- daisyUI theme configuration --> | |||
| 21 | - | <script> | |||
| 22 | - | // Check for saved theme preference or use preferred color scheme | |||
| 23 | - | const savedTheme = localStorage.getItem('theme') || 'corporate'; | |||
| 24 | - | document.documentElement.setAttribute('data-theme', savedTheme); | |||
| 25 | - | console.debug('Theme loaded:', savedTheme); | |||
| 26 | - | </script> | |||
| 27 | - | </head> | |||
| 28 | - | ||||
| 29 | - | <body class="IBMPlexSans bg-base-200 min-h-screen flex flex-col items-center justify-center p-4"> | |||
| 30 | - | <!-- Logo and Title --> | |||
| 31 | - | <div | |||
| 32 | - | id="logo-mascot" | |||
| 33 | - | class="flex flex-col justify-center items-center p-4 mb-4"> | |||
| 34 | - | <div class="flex justify-center w-100"> | |||
| 35 | - | <img | |||
| 36 | - | src="/assets/imgs/gordon-mascot-mq-trsp.png" | |||
| 37 | - | alt="logo gordon" | |||
| 38 | - | class="sm:h-24 h-32 xl:h-48 drop-shadow-xl" /> | |||
| 39 | - | </div> | |||
| 40 | - | <div class="flex justify-center mt-4"> | |||
| 41 | - | <h1 | |||
| 42 | - | class="gordon-title text-4xl lg:text-4xl xl:text-5xl text-primary tracking-tighter"> | |||
| 43 | - | Gordon | |||
| 44 | - | </h1> | |||
| 45 | - | <span | |||
| 46 | - | class="gordon-v-subtitle text-secondary text-sm ml-2" | |||
| 47 | - | >{{ .BuildVersion }}</span | |||
| 48 | - | > | |||
| 49 | - | </div> | |||
| 50 | - | </div> | |||
| 51 | - | ||||
| 52 | - | <!-- Main Content --> | |||
| 53 | - | <div class="card bg-base-100 shadow-xl w-full max-w-md"> | |||
| 54 | - | <div class="card-body"> | |||
| 55 | - | <!-- Login Section --> | |||
| 56 | - | <div id="login-section"> | |||
| 57 | - | <div class="flex flex-col items-center"> | |||
| 58 | - | <h2 class="card-title text-center mb-4">Login</h2> | |||
| 59 | - | ||||
| 60 | - | <!-- Error Message Display --> | |||
| 61 | - | {{ if .Error }} | |||
| 62 | - | <div class="alert alert-error mb-4"> | |||
| 63 | - | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |||
| 64 | - | <span> | |||
| 65 | - | {{ if eq .Error "empty_token" }} | |||
| 66 | - | Please enter a token. | |||
| 67 | - | {{ else if eq .Error "invalid_token" }} | |||
| 68 | - | Invalid token. Please try again. | |||
| 69 | - | {{ else if eq .Error "server_error" }} | |||
| 70 | - | A server error occurred. Please try again later. | |||
| 71 | - | {{ else }} | |||
| 72 | - | An error occurred. Please try again. | |||
| 73 | - | {{ end }} | |||
| 74 | - | </span> | |||
| 75 | - | </div> | |||
| 76 | - | {{ end }} | |||
| 77 | - | ||||
| 78 | - | <!-- Token Input Form --> | |||
| 79 | - | <div class="w-full"> | |||
| 80 | - | <form action="login/submit-token" method="POST" class="mb-4"> | |||
| 81 | - | <div class="form-control mb-4"> | |||
| 82 | - | <label for="token" class="label"> | |||
| 83 | - | <span class="label-text">Admin Token</span> | |||
| 84 | - | </label> | |||
| 85 | - | <input | |||
| 86 | - | type="text" | |||
| 87 | - | id="token" | |||
| 88 | - | name="token" | |||
| 89 | - | placeholder="Enter your admin token" | |||
| 90 | - | class="input input-bordered w-full" | |||
| 91 | - | required | |||
| 92 | - | /> | |||
| 93 | - | </div> | |||
| 94 | - | <button | |||
| 95 | - | type="submit" | |||
| 96 | - | class="btn btn-primary w-full" | |||
| 97 | - | > | |||
| 98 | - | Continue with Token | |||
| 99 | - | </button> | |||
| 100 | - | </form> | |||
| 101 | - | <div class="divider">OR</div> | |||
| 102 | - | </div> | |||
| 103 | - | ||||
| 104 | - | <!-- GitHub OAuth --> | |||
| 105 | - | <div class="w-full"> | |||
| 106 | - | <a | |||
| 107 | - | href="login/oauth/github" | |||
| 108 | - | class="btn btn-secondary w-full" | |||
| 109 | - | > | |||
| 110 | - | <svg class="mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"> | |||
| 111 | - | <path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" /> | |||
| 112 | - | </svg> | |||
| 113 | - | Sign in with GitHub | |||
| 114 | - | </a> | |||
| 115 | - | </div> | |||
| 116 | - | </div> | |||
| 117 | - | </div> | |||
| 118 | - | <!-- End Login Section --> | |||
| 119 | - | </div> | |||
| 120 | - | </div> | |||
| 121 | - | </body> | |||
| 122 | - | </html> | |||
M internal/templating/models/templ/components/header.templ +1 -0
| @@ -14,6 +14,7 @@ templ Header(title string) { | |||
| 14 | 14 | <script src="/libs/htmx-response-targets.js"></script> | |
| 15 | 15 | <script src="/libs/htmx-debug.js"></script> | |
| 16 | 16 | <script src="/libs/htmx-sse.js"></script> | |
| 17 | + | <script src="/libs/daisyui.js" type="module"></script> | |
| 17 | 18 | <script defer src="/libs/hyperscript.min.js"></script> | |
| 18 | 19 | <script defer src="/assets/js/custom.js"></script> | |
| 19 | 20 | ||
| @@ -14,6 +14,7 @@ templ Header(title string) { | |||||
| 14 | <script src="/libs/htmx-response-targets.js"></script> | 14 | <script src="/libs/htmx-response-targets.js"></script> | ||
| 15 | <script src="/libs/htmx-debug.js"></script> | 15 | <script src="/libs/htmx-debug.js"></script> | ||
| 16 | <script src="/libs/htmx-sse.js"></script> | 16 | <script src="/libs/htmx-sse.js"></script> | ||
| 17 | + | <script src="/libs/daisyui.js" type="module"></script> | |||
| 17 | <script defer src="/libs/hyperscript.min.js"></script> | 18 | <script defer src="/libs/hyperscript.min.js"></script> | ||
| 18 | <script defer src="/assets/js/custom.js"></script> | 19 | <script defer src="/assets/js/custom.js"></script> | ||
| 19 | 20 | ||||
M internal/templating/models/templ/components/header_templ.go +1 -1 generated file: collapsed
M internal/templating/models/templ/pages/login/index.templ +23 -11
| @@ -17,9 +17,11 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||
| 17 | 17 | <!-- HTMX SSE --> | |
| 18 | 18 | <script src="/libs/htmx-sse.js"></script> | |
| 19 | 19 | <!-- DaisyUI --> | |
| 20 | - | <script src="/libs/daisyui.js"></script> | |
| 20 | + | <script src="/libs/daisyui.js" type="module"></script> | |
| 21 | + | <!-- jQuery --> | |
| 22 | + | <script src="/libs/jquery.min.js"></script> | |
| 21 | 23 | <!-- Custom JS from assets/js --> | |
| 22 | - | <script src="/assets/js/custom.js"></script> | |
| 24 | + | <script src="/assets/js/custom.js" defer></script> | |
| 23 | 25 | ||
| 24 | 26 | <!-- daisyUI theme configuration --> | |
| 25 | 27 | <script> | |
| @@ -65,16 +67,26 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||
| 65 | 67 | <div class="alert alert-error mb-4"> | |
| 66 | 68 | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| 67 | 69 | <span> | |
| 68 | - | // Removed token-specific errors. Display generic or GitHub-related errors. | |
| 69 | - | if errorMsg == "server_error" { | |
| 70 | - | A server error occurred during login. Please try again later. | |
| 71 | - | } else if errorMsg == "github_error" { | |
| 72 | - | An error occurred during GitHub authentication. Please try again. | |
| 73 | - | } else if errorMsg == "unauthorized" { | |
| 74 | - | This GitHub account is not authorized. | |
| 75 | - | } else { | |
| 76 | - | An unknown login error occurred. Please try again. | |
| 70 | + | // --- MODIFIED: Handle new error messages --- | |
| 71 | + | switch errorMsg { | |
| 72 | + | case "server_error": | |
| 73 | + | A server error occurred during login. Please try again later or contact support. | |
| 74 | + | case "github_error": // Kept for potential future use | |
| 75 | + | An error occurred during GitHub authentication. Please try again. | |
| 76 | + | case "unauthorized": | |
| 77 | + | This GitHub account is not authorized to access Gordon. | |
| 78 | + | case "email_mismatch": | |
| 79 | + | Login failed: Your GitHub email does not match the configured administrator email. | |
| 80 | + | case "no_email": | |
| 81 | + | Login failed: Your GitHub account must have a public primary email address configured. | |
| 82 | + | case "config_email_missing": | |
| 83 | + | Server Configuration Error: The administrator email is not set up. Please contact support. | |
| 84 | + | case "session_invalid_or_expired": // Added from login page handler | |
| 85 | + | Your session is invalid or has expired. Please log in again. | |
| 86 | + | default: | |
| 87 | + | An unknown login error occurred: { errorMsg } | |
| 77 | 88 | } | |
| 89 | + | // --- END MODIFICATION --- | |
| 78 | 90 | </span> | |
| 79 | 91 | </div> | |
| 80 | 92 | } | |
| @@ -17,9 +17,11 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||||
| 17 | <!-- HTMX SSE --> | 17 | <!-- HTMX SSE --> | ||
| 18 | <script src="/libs/htmx-sse.js"></script> | 18 | <script src="/libs/htmx-sse.js"></script> | ||
| 19 | <!-- DaisyUI --> | 19 | <!-- DaisyUI --> | ||
| 20 | - | <script src="/libs/daisyui.js"></script> | 20 | + | <script src="/libs/daisyui.js" type="module"></script> |
| 21 | + | <!-- jQuery --> | |||
| 22 | + | <script src="/libs/jquery.min.js"></script> | |||
| 21 | <!-- Custom JS from assets/js --> | 23 | <!-- Custom JS from assets/js --> | ||
| 22 | - | <script src="/assets/js/custom.js"></script> | 24 | + | <script src="/assets/js/custom.js" defer></script> |
| 23 | 25 | ||||
| 24 | <!-- daisyUI theme configuration --> | 26 | <!-- daisyUI theme configuration --> | ||
| 25 | <script> | 27 | <script> | ||
| @@ -65,16 +67,26 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||||
| 65 | <div class="alert alert-error mb-4"> | 67 | <div class="alert alert-error mb-4"> | ||
| 66 | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | 68 | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | ||
| 67 | <span> | 69 | <span> | ||
| 68 | - | // Removed token-specific errors. Display generic or GitHub-related errors. | 70 | + | // --- MODIFIED: Handle new error messages --- |
| 69 | - | if errorMsg == "server_error" { | 71 | + | switch errorMsg { |
| 70 | - | A server error occurred during login. Please try again later. | 72 | + | case "server_error": |
| 71 | - | } else if errorMsg == "github_error" { | 73 | + | A server error occurred during login. Please try again later or contact support. |
| 72 | - | An error occurred during GitHub authentication. Please try again. | 74 | + | case "github_error": // Kept for potential future use |
| 73 | - | } else if errorMsg == "unauthorized" { | 75 | + | An error occurred during GitHub authentication. Please try again. |
| 74 | - | This GitHub account is not authorized. | 76 | + | case "unauthorized": |
| 75 | - | } else { | 77 | + | This GitHub account is not authorized to access Gordon. |
| 76 | - | An unknown login error occurred. Please try again. | 78 | + | case "email_mismatch": |
| 79 | + | Login failed: Your GitHub email does not match the configured administrator email. | |||
| 80 | + | case "no_email": | |||
| 81 | + | Login failed: Your GitHub account must have a public primary email address configured. | |||
| 82 | + | case "config_email_missing": | |||
| 83 | + | Server Configuration Error: The administrator email is not set up. Please contact support. | |||
| 84 | + | case "session_invalid_or_expired": // Added from login page handler | |||
| 85 | + | Your session is invalid or has expired. Please log in again. | |||
| 86 | + | default: | |||
| 87 | + | An unknown login error occurred: { errorMsg } | |||
| 77 | } | 88 | } | ||
| 89 | + | // --- END MODIFICATION --- | |||
| 78 | </span> | 90 | </span> | ||
| 79 | </div> | 91 | </div> | ||
| 80 | } | 92 | } | ||
M internal/templating/models/templ/pages/login/index_templ.go +42 -12 generated file: collapsed
D internal/templating/render/html.go +0 -77
| @@ -1,77 +0,0 @@ | |||
| 1 | - | package render | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "fmt" | |
| 6 | - | "html/template" | |
| 7 | - | "io/fs" | |
| 8 | - | "path" | |
| 9 | - | ||
| 10 | - | "github.com/bnema/gordon/internal/server" | |
| 11 | - | ) | |
| 12 | - | ||
| 13 | - | type Renderer struct { | |
| 14 | - | Template *template.Template | |
| 15 | - | ParseError error | |
| 16 | - | BuildVersion string | |
| 17 | - | LatestVersion string | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | // Render function renders the template with the given data | |
| 21 | - | func (r *Renderer) Render(data interface{}, a *server.App) (string, error) { | |
| 22 | - | if r.ParseError != nil { | |
| 23 | - | return "", fmt.Errorf("failed to parse template: %w", r.ParseError) | |
| 24 | - | ||
| 25 | - | } | |
| 26 | - | if r.Template == nil { | |
| 27 | - | return "", fmt.Errorf("template is nil") | |
| 28 | - | } | |
| 29 | - | // Type assert data to map[string]interface{} to add BuildVersion | |
| 30 | - | dataMap, ok := data.(map[string]interface{}) | |
| 31 | - | if !ok { | |
| 32 | - | return "", fmt.Errorf("data is not a map[string]interface{}") | |
| 33 | - | } | |
| 34 | - | ||
| 35 | - | // Automatically add BuildVersion to the data map | |
| 36 | - | dataMap["BuildVersion"] = r.BuildVersion | |
| 37 | - | buf := new(bytes.Buffer) | |
| 38 | - | err := r.Template.Execute(buf, dataMap) | |
| 39 | - | if err != nil { | |
| 40 | - | return "", fmt.Errorf("failed to execute template: %w", err) | |
| 41 | - | } | |
| 42 | - | ||
| 43 | - | return buf.String(), nil | |
| 44 | - | } | |
| 45 | - | ||
| 46 | - | // GetHTMLRenderer function returns a new Renderer instance | |
| 47 | - | func GetHTMLRenderer(mainPath string, filename string, fs fs.FS, a *server.App, fragmentsPath ...string) (*Renderer, error) { | |
| 48 | - | // Full path to the main template | |
| 49 | - | fullPath := path.Join(mainPath, filename) | |
| 50 | - | // Check if the file exists in the provided fs.FS using fs.Open | |
| 51 | - | file, err := fs.Open(fullPath) | |
| 52 | - | if err != nil { | |
| 53 | - | return nil, fmt.Errorf("failed to open %s: %w", filename, err) | |
| 54 | - | } | |
| 55 | - | file.Close() | |
| 56 | - | ||
| 57 | - | // Parse the main template | |
| 58 | - | tmpl, err := template.New(filename).ParseFS(fs, fullPath) | |
| 59 | - | if err != nil { | |
| 60 | - | return nil, fmt.Errorf("failed to parse %s: %w", filename, err) | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | // If a fragments path is provided, parse the fragment templates | |
| 64 | - | if len(fragmentsPath) > 0 && fragmentsPath[0] != "" { | |
| 65 | - | fragmentsGlob := path.Join(fragmentsPath[0], "*.gohtml") | |
| 66 | - | _, err = tmpl.ParseFS(fs, fragmentsGlob) | |
| 67 | - | if err != nil { | |
| 68 | - | return nil, fmt.Errorf("failed to parse fragments: %w", err) | |
| 69 | - | } | |
| 70 | - | } | |
| 71 | - | ||
| 72 | - | // Return the Renderer instance | |
| 73 | - | return &Renderer{ | |
| 74 | - | Template: tmpl, | |
| 75 | - | BuildVersion: a.Config.Build.BuildVersion, | |
| 76 | - | }, nil | |
| 77 | - | } | |
| @@ -1,77 +0,0 @@ | |||||
| 1 | - | package render | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | "fmt" | |||
| 6 | - | "html/template" | |||
| 7 | - | "io/fs" | |||
| 8 | - | "path" | |||
| 9 | - | ||||
| 10 | - | "github.com/bnema/gordon/internal/server" | |||
| 11 | - | ) | |||
| 12 | - | ||||
| 13 | - | type Renderer struct { | |||
| 14 | - | Template *template.Template | |||
| 15 | - | ParseError error | |||
| 16 | - | BuildVersion string | |||
| 17 | - | LatestVersion string | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | // Render function renders the template with the given data | |||
| 21 | - | func (r *Renderer) Render(data interface{}, a *server.App) (string, error) { | |||
| 22 | - | if r.ParseError != nil { | |||
| 23 | - | return "", fmt.Errorf("failed to parse template: %w", r.ParseError) | |||
| 24 | - | ||||
| 25 | - | } | |||
| 26 | - | if r.Template == nil { | |||
| 27 | - | return "", fmt.Errorf("template is nil") | |||
| 28 | - | } | |||
| 29 | - | // Type assert data to map[string]interface{} to add BuildVersion | |||
| 30 | - | dataMap, ok := data.(map[string]interface{}) | |||
| 31 | - | if !ok { | |||
| 32 | - | return "", fmt.Errorf("data is not a map[string]interface{}") | |||
| 33 | - | } | |||
| 34 | - | ||||
| 35 | - | // Automatically add BuildVersion to the data map | |||
| 36 | - | dataMap["BuildVersion"] = r.BuildVersion | |||
| 37 | - | buf := new(bytes.Buffer) | |||
| 38 | - | err := r.Template.Execute(buf, dataMap) | |||
| 39 | - | if err != nil { | |||
| 40 | - | return "", fmt.Errorf("failed to execute template: %w", err) | |||
| 41 | - | } | |||
| 42 | - | ||||
| 43 | - | return buf.String(), nil | |||
| 44 | - | } | |||
| 45 | - | ||||
| 46 | - | // GetHTMLRenderer function returns a new Renderer instance | |||
| 47 | - | func GetHTMLRenderer(mainPath string, filename string, fs fs.FS, a *server.App, fragmentsPath ...string) (*Renderer, error) { | |||
| 48 | - | // Full path to the main template | |||
| 49 | - | fullPath := path.Join(mainPath, filename) | |||
| 50 | - | // Check if the file exists in the provided fs.FS using fs.Open | |||
| 51 | - | file, err := fs.Open(fullPath) | |||
| 52 | - | if err != nil { | |||
| 53 | - | return nil, fmt.Errorf("failed to open %s: %w", filename, err) | |||
| 54 | - | } | |||
| 55 | - | file.Close() | |||
| 56 | - | ||||
| 57 | - | // Parse the main template | |||
| 58 | - | tmpl, err := template.New(filename).ParseFS(fs, fullPath) | |||
| 59 | - | if err != nil { | |||
| 60 | - | return nil, fmt.Errorf("failed to parse %s: %w", filename, err) | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | // If a fragments path is provided, parse the fragment templates | |||
| 64 | - | if len(fragmentsPath) > 0 && fragmentsPath[0] != "" { | |||
| 65 | - | fragmentsGlob := path.Join(fragmentsPath[0], "*.gohtml") | |||
| 66 | - | _, err = tmpl.ParseFS(fs, fragmentsGlob) | |||
| 67 | - | if err != nil { | |||
| 68 | - | return nil, fmt.Errorf("failed to parse fragments: %w", err) | |||
| 69 | - | } | |||
| 70 | - | } | |||
| 71 | - | ||||
| 72 | - | // Return the Renderer instance | |||
| 73 | - | return &Renderer{ | |||
| 74 | - | Template: tmpl, | |||
| 75 | - | BuildVersion: a.Config.Build.BuildVersion, | |||
| 76 | - | }, nil | |||
| 77 | - | } | |||
M internal/webui/public/assets/css/custom.css +10 -10
| @@ -22,8 +22,8 @@ | |||
| 22 | 22 | --color-base-content: oklch(93% 0.034 272.788); | |
| 23 | 23 | --color-primary: oklch(87% 0 0); | |
| 24 | 24 | --color-primary-content: oklch(14% 0 0); | |
| 25 | - | --color-secondary: oklch(83% 0.145 321.434); | |
| 26 | - | --color-secondary-content: oklch(29% 0.136 325.661); | |
| 25 | + | --color-secondary: oklch(80% 0.105 251.813); | |
| 26 | + | --color-secondary-content: oklch(28% 0.091 267.935); | |
| 27 | 27 | --color-accent: oklch(80% 0.105 251.813); | |
| 28 | 28 | --color-accent-content: oklch(28% 0.091 267.935); | |
| 29 | 29 | --color-neutral: oklch(37% 0.034 259.733); | |
| @@ -36,9 +36,9 @@ | |||
| 36 | 36 | --color-warning-content: oklch(98% 0.022 95.277); | |
| 37 | 37 | --color-error: oklch(64% 0.246 16.439); | |
| 38 | 38 | --color-error-content: oklch(96% 0.015 12.422); | |
| 39 | - | --radius-selector: 1rem; | |
| 39 | + | --radius-selector: 0rem; | |
| 40 | 40 | --radius-field: 0rem; | |
| 41 | - | --radius-box: 0.25rem; | |
| 41 | + | --radius-box: 0rem; | |
| 42 | 42 | --size-selector: 0.25rem; | |
| 43 | 43 | --size-field: 0.25rem; | |
| 44 | 44 | --border: 1px; | |
| @@ -51,20 +51,20 @@ | |||
| 51 | 51 | @layer components { | |
| 52 | 52 | /* Button styles */ | |
| 53 | 53 | .btn-primary { | |
| 54 | - | @apply flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | |
| 54 | + | @apply flex w-full justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | .btn-secondary { | |
| 58 | - | @apply flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | |
| 58 | + | @apply flex w-full justify-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | 61 | .btn-danger { | |
| 62 | - | @apply flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2; | |
| 62 | + | @apply flex w-full justify-center border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2; | |
| 63 | 63 | } | |
| 64 | 64 | ||
| 65 | 65 | /* Alert styles */ | |
| 66 | 66 | .alert-error { | |
| 67 | - | @apply rounded-md bg-red-50 p-4 dark:bg-red-900/20; | |
| 67 | + | @apply bg-red-50 p-4 dark:bg-red-900/20; | |
| 68 | 68 | } | |
| 69 | 69 | ||
| 70 | 70 | .alert-error-icon { | |
| @@ -76,7 +76,7 @@ | |||
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | 78 | .alert-success { | |
| 79 | - | @apply rounded-md bg-green-50 p-4 dark:bg-green-900/20; | |
| 79 | + | @apply bg-green-50 p-4 dark:bg-green-900/20; | |
| 80 | 80 | } | |
| 81 | 81 | ||
| 82 | 82 | .alert-success-icon { | |
| @@ -89,7 +89,7 @@ | |||
| 89 | 89 | ||
| 90 | 90 | /* Card styles */ | |
| 91 | 91 | .card { | |
| 92 | - | @apply bg-white p-4 rounded-md shadow mb-4; | |
| 92 | + | @apply bg-white p-4 shadow mb-4; | |
| 93 | 93 | } | |
| 94 | 94 | ||
| 95 | 95 | .card-header { | |
| @@ -22,8 +22,8 @@ | |||||
| 22 | --color-base-content: oklch(93% 0.034 272.788); | 22 | --color-base-content: oklch(93% 0.034 272.788); | ||
| 23 | --color-primary: oklch(87% 0 0); | 23 | --color-primary: oklch(87% 0 0); | ||
| 24 | --color-primary-content: oklch(14% 0 0); | 24 | --color-primary-content: oklch(14% 0 0); | ||
| 25 | - | --color-secondary: oklch(83% 0.145 321.434); | 25 | + | --color-secondary: oklch(80% 0.105 251.813); |
| 26 | - | --color-secondary-content: oklch(29% 0.136 325.661); | 26 | + | --color-secondary-content: oklch(28% 0.091 267.935); |
| 27 | --color-accent: oklch(80% 0.105 251.813); | 27 | --color-accent: oklch(80% 0.105 251.813); | ||
| 28 | --color-accent-content: oklch(28% 0.091 267.935); | 28 | --color-accent-content: oklch(28% 0.091 267.935); | ||
| 29 | --color-neutral: oklch(37% 0.034 259.733); | 29 | --color-neutral: oklch(37% 0.034 259.733); | ||
| @@ -36,9 +36,9 @@ | |||||
| 36 | --color-warning-content: oklch(98% 0.022 95.277); | 36 | --color-warning-content: oklch(98% 0.022 95.277); | ||
| 37 | --color-error: oklch(64% 0.246 16.439); | 37 | --color-error: oklch(64% 0.246 16.439); | ||
| 38 | --color-error-content: oklch(96% 0.015 12.422); | 38 | --color-error-content: oklch(96% 0.015 12.422); | ||
| 39 | - | --radius-selector: 1rem; | 39 | + | --radius-selector: 0rem; |
| 40 | --radius-field: 0rem; | 40 | --radius-field: 0rem; | ||
| 41 | - | --radius-box: 0.25rem; | 41 | + | --radius-box: 0rem; |
| 42 | --size-selector: 0.25rem; | 42 | --size-selector: 0.25rem; | ||
| 43 | --size-field: 0.25rem; | 43 | --size-field: 0.25rem; | ||
| 44 | --border: 1px; | 44 | --border: 1px; | ||
| @@ -51,20 +51,20 @@ | |||||
| 51 | @layer components { | 51 | @layer components { | ||
| 52 | /* Button styles */ | 52 | /* Button styles */ | ||
| 53 | .btn-primary { | 53 | .btn-primary { | ||
| 54 | - | @apply flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | 54 | + | @apply flex w-full justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; |
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | .btn-secondary { | 57 | .btn-secondary { | ||
| 58 | - | @apply flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; | 58 | + | @apply flex w-full justify-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2; |
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | .btn-danger { | 61 | .btn-danger { | ||
| 62 | - | @apply flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2; | 62 | + | @apply flex w-full justify-center border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2; |
| 63 | } | 63 | } | ||
| 64 | 64 | ||||
| 65 | /* Alert styles */ | 65 | /* Alert styles */ | ||
| 66 | .alert-error { | 66 | .alert-error { | ||
| 67 | - | @apply rounded-md bg-red-50 p-4 dark:bg-red-900/20; | 67 | + | @apply bg-red-50 p-4 dark:bg-red-900/20; |
| 68 | } | 68 | } | ||
| 69 | 69 | ||||
| 70 | .alert-error-icon { | 70 | .alert-error-icon { | ||
| @@ -76,7 +76,7 @@ | |||||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | .alert-success { | 78 | .alert-success { | ||
| 79 | - | @apply rounded-md bg-green-50 p-4 dark:bg-green-900/20; | 79 | + | @apply bg-green-50 p-4 dark:bg-green-900/20; |
| 80 | } | 80 | } | ||
| 81 | 81 | ||||
| 82 | .alert-success-icon { | 82 | .alert-success-icon { | ||
| @@ -89,7 +89,7 @@ | |||||
| 89 | 89 | ||||
| 90 | /* Card styles */ | 90 | /* Card styles */ | ||
| 91 | .card { | 91 | .card { | ||
| 92 | - | @apply bg-white p-4 rounded-md shadow mb-4; | 92 | + | @apply bg-white p-4 shadow mb-4; |
| 93 | } | 93 | } | ||
| 94 | 94 | ||||
| 95 | .card-header { | 95 | .card-header { | ||
M internal/webui/public/assets/css/tailwind.css +8 -61
| @@ -4529,9 +4529,6 @@ | |||
| 4529 | 4529 | .mt-8 { | |
| 4530 | 4530 | margin-top: calc(var(--spacing) * 8); | |
| 4531 | 4531 | } | |
| 4532 | - | .mt-16 { | |
| 4533 | - | margin-top: calc(var(--spacing) * 16); | |
| 4534 | - | } | |
| 4535 | 4532 | .breadcrumbs { | |
| 4536 | 4533 | max-width: 100%; | |
| 4537 | 4534 | overflow-x: auto; | |
| @@ -5393,9 +5390,6 @@ | |||
| 5393 | 5390 | .max-w-md { | |
| 5394 | 5391 | max-width: var(--container-md); | |
| 5395 | 5392 | } | |
| 5396 | - | .flex-1 { | |
| 5397 | - | flex: 1; | |
| 5398 | - | } | |
| 5399 | 5393 | .flex-shrink { | |
| 5400 | 5394 | flex-shrink: 1; | |
| 5401 | 5395 | } | |
| @@ -6311,9 +6305,6 @@ | |||
| 6311 | 6305 | font-size: 1.375rem; | |
| 6312 | 6306 | } | |
| 6313 | 6307 | } | |
| 6314 | - | .pt-16 { | |
| 6315 | - | padding-top: calc(var(--spacing) * 16); | |
| 6316 | - | } | |
| 6317 | 6308 | .pt-20 { | |
| 6318 | 6309 | padding-top: calc(var(--spacing) * 20); | |
| 6319 | 6310 | } | |
| @@ -6725,9 +6716,6 @@ | |||
| 6725 | 6716 | .progress-warning { | |
| 6726 | 6717 | color: var(--color-warning); | |
| 6727 | 6718 | } | |
| 6728 | - | .text-accent { | |
| 6729 | - | color: var(--color-accent); | |
| 6730 | - | } | |
| 6731 | 6719 | .text-base-content { | |
| 6732 | 6720 | color: var(--color-base-content); | |
| 6733 | 6721 | } | |
| @@ -7548,11 +7536,6 @@ | |||
| 7548 | 7536 | grid-column: span 5 / span 5; | |
| 7549 | 7537 | } | |
| 7550 | 7538 | } | |
| 7551 | - | .md\:ml-4 { | |
| 7552 | - | @media (width >= 48rem) { | |
| 7553 | - | margin-left: calc(var(--spacing) * 4); | |
| 7554 | - | } | |
| 7555 | - | } | |
| 7556 | 7539 | .md\:w-2\/6 { | |
| 7557 | 7540 | @media (width >= 48rem) { | |
| 7558 | 7541 | width: calc(2/6 * 100%); | |
| @@ -7606,31 +7589,6 @@ | |||
| 7606 | 7589 | justify-self: flex-end; | |
| 7607 | 7590 | } | |
| 7608 | 7591 | } | |
| 7609 | - | .md\:p-0 { | |
| 7610 | - | @media (width >= 48rem) { | |
| 7611 | - | padding: calc(var(--spacing) * 0); | |
| 7612 | - | } | |
| 7613 | - | } | |
| 7614 | - | .md\:p-8 { | |
| 7615 | - | @media (width >= 48rem) { | |
| 7616 | - | padding: calc(var(--spacing) * 8); | |
| 7617 | - | } | |
| 7618 | - | } | |
| 7619 | - | .md\:px-6 { | |
| 7620 | - | @media (width >= 48rem) { | |
| 7621 | - | padding-inline: calc(var(--spacing) * 6); | |
| 7622 | - | } | |
| 7623 | - | } | |
| 7624 | - | .md\:pt-10 { | |
| 7625 | - | @media (width >= 48rem) { | |
| 7626 | - | padding-top: calc(var(--spacing) * 10); | |
| 7627 | - | } | |
| 7628 | - | } | |
| 7629 | - | .md\:pt-32 { | |
| 7630 | - | @media (width >= 48rem) { | |
| 7631 | - | padding-top: calc(var(--spacing) * 32); | |
| 7632 | - | } | |
| 7633 | - | } | |
| 7634 | 7592 | .md\:text-3xl { | |
| 7635 | 7593 | @media (width >= 48rem) { | |
| 7636 | 7594 | font-size: var(--text-3xl); | |
| @@ -7654,11 +7612,6 @@ | |||
| 7654 | 7612 | display: flex; | |
| 7655 | 7613 | } | |
| 7656 | 7614 | } | |
| 7657 | - | .lg\:flex-row { | |
| 7658 | - | @media (width >= 64rem) { | |
| 7659 | - | flex-direction: row; | |
| 7660 | - | } | |
| 7661 | - | } | |
| 7662 | 7615 | .lg\:text-4xl { | |
| 7663 | 7616 | @media (width >= 64rem) { | |
| 7664 | 7617 | font-size: var(--text-4xl); | |
| @@ -7682,7 +7635,6 @@ | |||
| 7682 | 7635 | display: flex; | |
| 7683 | 7636 | width: 100%; | |
| 7684 | 7637 | justify-content: center; | |
| 7685 | - | border-radius: var(--radius-md); | |
| 7686 | 7638 | border-style: var(--tw-border-style); | |
| 7687 | 7639 | border-width: 1px; | |
| 7688 | 7640 | border-color: transparent; | |
| @@ -7719,7 +7671,6 @@ | |||
| 7719 | 7671 | display: flex; | |
| 7720 | 7672 | width: 100%; | |
| 7721 | 7673 | justify-content: center; | |
| 7722 | - | border-radius: var(--radius-md); | |
| 7723 | 7674 | border-style: var(--tw-border-style); | |
| 7724 | 7675 | border-width: 1px; | |
| 7725 | 7676 | border-color: var(--color-gray-300); | |
| @@ -7756,7 +7707,6 @@ | |||
| 7756 | 7707 | display: flex; | |
| 7757 | 7708 | width: 100%; | |
| 7758 | 7709 | justify-content: center; | |
| 7759 | - | border-radius: var(--radius-md); | |
| 7760 | 7710 | border-style: var(--tw-border-style); | |
| 7761 | 7711 | border-width: 1px; | |
| 7762 | 7712 | border-color: transparent; | |
| @@ -7790,7 +7740,6 @@ | |||
| 7790 | 7740 | } | |
| 7791 | 7741 | } | |
| 7792 | 7742 | .alert-error { | |
| 7793 | - | border-radius: var(--radius-md); | |
| 7794 | 7743 | background-color: var(--color-red-50); | |
| 7795 | 7744 | padding: calc(var(--spacing) * 4); | |
| 7796 | 7745 | @media (prefers-color-scheme: dark) { | |
| @@ -7813,7 +7762,6 @@ | |||
| 7813 | 7762 | } | |
| 7814 | 7763 | } | |
| 7815 | 7764 | .alert-success { | |
| 7816 | - | border-radius: var(--radius-md); | |
| 7817 | 7765 | background-color: var(--color-green-50); | |
| 7818 | 7766 | padding: calc(var(--spacing) * 4); | |
| 7819 | 7767 | @media (prefers-color-scheme: dark) { | |
| @@ -7837,7 +7785,6 @@ | |||
| 7837 | 7785 | } | |
| 7838 | 7786 | .card { | |
| 7839 | 7787 | margin-bottom: calc(var(--spacing) * 4); | |
| 7840 | - | border-radius: var(--radius-md); | |
| 7841 | 7788 | background-color: var(--color-white); | |
| 7842 | 7789 | padding: calc(var(--spacing) * 4); | |
| 7843 | 7790 | --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); | |
| @@ -8188,8 +8135,8 @@ | |||
| 8188 | 8135 | --color-base-content: oklch(93% 0.034 272.788); | |
| 8189 | 8136 | --color-primary: oklch(87% 0 0); | |
| 8190 | 8137 | --color-primary-content: oklch(14% 0 0); | |
| 8191 | - | --color-secondary: oklch(83% 0.145 321.434); | |
| 8192 | - | --color-secondary-content: oklch(29% 0.136 325.661); | |
| 8138 | + | --color-secondary: oklch(80% 0.105 251.813); | |
| 8139 | + | --color-secondary-content: oklch(28% 0.091 267.935); | |
| 8193 | 8140 | --color-accent: oklch(80% 0.105 251.813); | |
| 8194 | 8141 | --color-accent-content: oklch(28% 0.091 267.935); | |
| 8195 | 8142 | --color-neutral: oklch(37% 0.034 259.733); | |
| @@ -8202,9 +8149,9 @@ | |||
| 8202 | 8149 | --color-warning-content: oklch(98% 0.022 95.277); | |
| 8203 | 8150 | --color-error: oklch(64% 0.246 16.439); | |
| 8204 | 8151 | --color-error-content: oklch(96% 0.015 12.422); | |
| 8205 | - | --radius-selector: 1rem; | |
| 8152 | + | --radius-selector: 0rem; | |
| 8206 | 8153 | --radius-field: 0rem; | |
| 8207 | - | --radius-box: 0.25rem; | |
| 8154 | + | --radius-box: 0rem; | |
| 8208 | 8155 | --size-selector: 0.25rem; | |
| 8209 | 8156 | --size-field: 0.25rem; | |
| 8210 | 8157 | --border: 1px; | |
| @@ -8222,8 +8169,8 @@ | |||
| 8222 | 8169 | --color-base-content: oklch(93% 0.034 272.788); | |
| 8223 | 8170 | --color-primary: oklch(87% 0 0); | |
| 8224 | 8171 | --color-primary-content: oklch(14% 0 0); | |
| 8225 | - | --color-secondary: oklch(83% 0.145 321.434); | |
| 8226 | - | --color-secondary-content: oklch(29% 0.136 325.661); | |
| 8172 | + | --color-secondary: oklch(80% 0.105 251.813); | |
| 8173 | + | --color-secondary-content: oklch(28% 0.091 267.935); | |
| 8227 | 8174 | --color-accent: oklch(80% 0.105 251.813); | |
| 8228 | 8175 | --color-accent-content: oklch(28% 0.091 267.935); | |
| 8229 | 8176 | --color-neutral: oklch(37% 0.034 259.733); | |
| @@ -8236,9 +8183,9 @@ | |||
| 8236 | 8183 | --color-warning-content: oklch(98% 0.022 95.277); | |
| 8237 | 8184 | --color-error: oklch(64% 0.246 16.439); | |
| 8238 | 8185 | --color-error-content: oklch(96% 0.015 12.422); | |
| 8239 | - | --radius-selector: 1rem; | |
| 8186 | + | --radius-selector: 0rem; | |
| 8240 | 8187 | --radius-field: 0rem; | |
| 8241 | - | --radius-box: 0.25rem; | |
| 8188 | + | --radius-box: 0rem; | |
| 8242 | 8189 | --size-selector: 0.25rem; | |
| 8243 | 8190 | --size-field: 0.25rem; | |
| 8244 | 8191 | --border: 1px; | |
| @@ -4529,9 +4529,6 @@ | |||||
| 4529 | .mt-8 { | 4529 | .mt-8 { | ||
| 4530 | margin-top: calc(var(--spacing) * 8); | 4530 | margin-top: calc(var(--spacing) * 8); | ||
| 4531 | } | 4531 | } | ||
| 4532 | - | .mt-16 { | |||
| 4533 | - | margin-top: calc(var(--spacing) * 16); | |||
| 4534 | - | } | |||
| 4535 | .breadcrumbs { | 4532 | .breadcrumbs { | ||
| 4536 | max-width: 100%; | 4533 | max-width: 100%; | ||
| 4537 | overflow-x: auto; | 4534 | overflow-x: auto; | ||
| @@ -5393,9 +5390,6 @@ | |||||
| 5393 | .max-w-md { | 5390 | .max-w-md { | ||
| 5394 | max-width: var(--container-md); | 5391 | max-width: var(--container-md); | ||
| 5395 | } | 5392 | } | ||
| 5396 | - | .flex-1 { | |||
| 5397 | - | flex: 1; | |||
| 5398 | - | } | |||
| 5399 | .flex-shrink { | 5393 | .flex-shrink { | ||
| 5400 | flex-shrink: 1; | 5394 | flex-shrink: 1; | ||
| 5401 | } | 5395 | } | ||
| @@ -6311,9 +6305,6 @@ | |||||
| 6311 | font-size: 1.375rem; | 6305 | font-size: 1.375rem; | ||
| 6312 | } | 6306 | } | ||
| 6313 | } | 6307 | } | ||
| 6314 | - | .pt-16 { | |||
| 6315 | - | padding-top: calc(var(--spacing) * 16); | |||
| 6316 | - | } | |||
| 6317 | .pt-20 { | 6308 | .pt-20 { | ||
| 6318 | padding-top: calc(var(--spacing) * 20); | 6309 | padding-top: calc(var(--spacing) * 20); | ||
| 6319 | } | 6310 | } | ||
| @@ -6725,9 +6716,6 @@ | |||||
| 6725 | .progress-warning { | 6716 | .progress-warning { | ||
| 6726 | color: var(--color-warning); | 6717 | color: var(--color-warning); | ||
| 6727 | } | 6718 | } | ||
| 6728 | - | .text-accent { | |||
| 6729 | - | color: var(--color-accent); | |||
| 6730 | - | } | |||
| 6731 | .text-base-content { | 6719 | .text-base-content { | ||
| 6732 | color: var(--color-base-content); | 6720 | color: var(--color-base-content); | ||
| 6733 | } | 6721 | } | ||
| @@ -7548,11 +7536,6 @@ | |||||
| 7548 | grid-column: span 5 / span 5; | 7536 | grid-column: span 5 / span 5; | ||
| 7549 | } | 7537 | } | ||
| 7550 | } | 7538 | } | ||
| 7551 | - | .md\:ml-4 { | |||
| 7552 | - | @media (width >= 48rem) { | |||
| 7553 | - | margin-left: calc(var(--spacing) * 4); | |||
| 7554 | - | } | |||
| 7555 | - | } | |||
| 7556 | .md\:w-2\/6 { | 7539 | .md\:w-2\/6 { | ||
| 7557 | @media (width >= 48rem) { | 7540 | @media (width >= 48rem) { | ||
| 7558 | width: calc(2/6 * 100%); | 7541 | width: calc(2/6 * 100%); | ||
| @@ -7606,31 +7589,6 @@ | |||||
| 7606 | justify-self: flex-end; | 7589 | justify-self: flex-end; | ||
| 7607 | } | 7590 | } | ||
| 7608 | } | 7591 | } | ||
| 7609 | - | .md\:p-0 { | |||
| 7610 | - | @media (width >= 48rem) { | |||
| 7611 | - | padding: calc(var(--spacing) * 0); | |||
| 7612 | - | } | |||
| 7613 | - | } | |||
| 7614 | - | .md\:p-8 { | |||
| 7615 | - | @media (width >= 48rem) { | |||
| 7616 | - | padding: calc(var(--spacing) * 8); | |||
| 7617 | - | } | |||
| 7618 | - | } | |||
| 7619 | - | .md\:px-6 { | |||
| 7620 | - | @media (width >= 48rem) { | |||
| 7621 | - | padding-inline: calc(var(--spacing) * 6); | |||
| 7622 | - | } | |||
| 7623 | - | } | |||
| 7624 | - | .md\:pt-10 { | |||
| 7625 | - | @media (width >= 48rem) { | |||
| 7626 | - | padding-top: calc(var(--spacing) * 10); | |||
| 7627 | - | } | |||
| 7628 | - | } | |||
| 7629 | - | .md\:pt-32 { | |||
| 7630 | - | @media (width >= 48rem) { | |||
| 7631 | - | padding-top: calc(var(--spacing) * 32); | |||
| 7632 | - | } | |||
| 7633 | - | } | |||
| 7634 | .md\:text-3xl { | 7592 | .md\:text-3xl { | ||
| 7635 | @media (width >= 48rem) { | 7593 | @media (width >= 48rem) { | ||
| 7636 | font-size: var(--text-3xl); | 7594 | font-size: var(--text-3xl); | ||
| @@ -7654,11 +7612,6 @@ | |||||
| 7654 | display: flex; | 7612 | display: flex; | ||
| 7655 | } | 7613 | } | ||
| 7656 | } | 7614 | } | ||
| 7657 | - | .lg\:flex-row { | |||
| 7658 | - | @media (width >= 64rem) { | |||
| 7659 | - | flex-direction: row; | |||
| 7660 | - | } | |||
| 7661 | - | } | |||
| 7662 | .lg\:text-4xl { | 7615 | .lg\:text-4xl { | ||
| 7663 | @media (width >= 64rem) { | 7616 | @media (width >= 64rem) { | ||
| 7664 | font-size: var(--text-4xl); | 7617 | font-size: var(--text-4xl); | ||
| @@ -7682,7 +7635,6 @@ | |||||
| 7682 | display: flex; | 7635 | display: flex; | ||
| 7683 | width: 100%; | 7636 | width: 100%; | ||
| 7684 | justify-content: center; | 7637 | justify-content: center; | ||
| 7685 | - | border-radius: var(--radius-md); | |||
| 7686 | border-style: var(--tw-border-style); | 7638 | border-style: var(--tw-border-style); | ||
| 7687 | border-width: 1px; | 7639 | border-width: 1px; | ||
| 7688 | border-color: transparent; | 7640 | border-color: transparent; | ||
| @@ -7719,7 +7671,6 @@ | |||||
| 7719 | display: flex; | 7671 | display: flex; | ||
| 7720 | width: 100%; | 7672 | width: 100%; | ||
| 7721 | justify-content: center; | 7673 | justify-content: center; | ||
| 7722 | - | border-radius: var(--radius-md); | |||
| 7723 | border-style: var(--tw-border-style); | 7674 | border-style: var(--tw-border-style); | ||
| 7724 | border-width: 1px; | 7675 | border-width: 1px; | ||
| 7725 | border-color: var(--color-gray-300); | 7676 | border-color: var(--color-gray-300); | ||
| @@ -7756,7 +7707,6 @@ | |||||
| 7756 | display: flex; | 7707 | display: flex; | ||
| 7757 | width: 100%; | 7708 | width: 100%; | ||
| 7758 | justify-content: center; | 7709 | justify-content: center; | ||
| 7759 | - | border-radius: var(--radius-md); | |||
| 7760 | border-style: var(--tw-border-style); | 7710 | border-style: var(--tw-border-style); | ||
| 7761 | border-width: 1px; | 7711 | border-width: 1px; | ||
| 7762 | border-color: transparent; | 7712 | border-color: transparent; | ||
| @@ -7790,7 +7740,6 @@ | |||||
| 7790 | } | 7740 | } | ||
| 7791 | } | 7741 | } | ||
| 7792 | .alert-error { | 7742 | .alert-error { | ||
| 7793 | - | border-radius: var(--radius-md); | |||
| 7794 | background-color: var(--color-red-50); | 7743 | background-color: var(--color-red-50); | ||
| 7795 | padding: calc(var(--spacing) * 4); | 7744 | padding: calc(var(--spacing) * 4); | ||
| 7796 | @media (prefers-color-scheme: dark) { | 7745 | @media (prefers-color-scheme: dark) { | ||
| @@ -7813,7 +7762,6 @@ | |||||
| 7813 | } | 7762 | } | ||
| 7814 | } | 7763 | } | ||
| 7815 | .alert-success { | 7764 | .alert-success { | ||
| 7816 | - | border-radius: var(--radius-md); | |||
| 7817 | background-color: var(--color-green-50); | 7765 | background-color: var(--color-green-50); | ||
| 7818 | padding: calc(var(--spacing) * 4); | 7766 | padding: calc(var(--spacing) * 4); | ||
| 7819 | @media (prefers-color-scheme: dark) { | 7767 | @media (prefers-color-scheme: dark) { | ||
| @@ -7837,7 +7785,6 @@ | |||||
| 7837 | } | 7785 | } | ||
| 7838 | .card { | 7786 | .card { | ||
| 7839 | margin-bottom: calc(var(--spacing) * 4); | 7787 | margin-bottom: calc(var(--spacing) * 4); | ||
| 7840 | - | border-radius: var(--radius-md); | |||
| 7841 | background-color: var(--color-white); | 7788 | background-color: var(--color-white); | ||
| 7842 | padding: calc(var(--spacing) * 4); | 7789 | padding: calc(var(--spacing) * 4); | ||
| 7843 | --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); | 7790 | --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); | ||
| @@ -8188,8 +8135,8 @@ | |||||
| 8188 | --color-base-content: oklch(93% 0.034 272.788); | 8135 | --color-base-content: oklch(93% 0.034 272.788); | ||
| 8189 | --color-primary: oklch(87% 0 0); | 8136 | --color-primary: oklch(87% 0 0); | ||
| 8190 | --color-primary-content: oklch(14% 0 0); | 8137 | --color-primary-content: oklch(14% 0 0); | ||
| 8191 | - | --color-secondary: oklch(83% 0.145 321.434); | 8138 | + | --color-secondary: oklch(80% 0.105 251.813); |
| 8192 | - | --color-secondary-content: oklch(29% 0.136 325.661); | 8139 | + | --color-secondary-content: oklch(28% 0.091 267.935); |
| 8193 | --color-accent: oklch(80% 0.105 251.813); | 8140 | --color-accent: oklch(80% 0.105 251.813); | ||
| 8194 | --color-accent-content: oklch(28% 0.091 267.935); | 8141 | --color-accent-content: oklch(28% 0.091 267.935); | ||
| 8195 | --color-neutral: oklch(37% 0.034 259.733); | 8142 | --color-neutral: oklch(37% 0.034 259.733); | ||
| @@ -8202,9 +8149,9 @@ | |||||
| 8202 | --color-warning-content: oklch(98% 0.022 95.277); | 8149 | --color-warning-content: oklch(98% 0.022 95.277); | ||
| 8203 | --color-error: oklch(64% 0.246 16.439); | 8150 | --color-error: oklch(64% 0.246 16.439); | ||
| 8204 | --color-error-content: oklch(96% 0.015 12.422); | 8151 | --color-error-content: oklch(96% 0.015 12.422); | ||
| 8205 | - | --radius-selector: 1rem; | 8152 | + | --radius-selector: 0rem; |
| 8206 | --radius-field: 0rem; | 8153 | --radius-field: 0rem; | ||
| 8207 | - | --radius-box: 0.25rem; | 8154 | + | --radius-box: 0rem; |
| 8208 | --size-selector: 0.25rem; | 8155 | --size-selector: 0.25rem; | ||
| 8209 | --size-field: 0.25rem; | 8156 | --size-field: 0.25rem; | ||
| 8210 | --border: 1px; | 8157 | --border: 1px; | ||
| @@ -8222,8 +8169,8 @@ | |||||
| 8222 | --color-base-content: oklch(93% 0.034 272.788); | 8169 | --color-base-content: oklch(93% 0.034 272.788); | ||
| 8223 | --color-primary: oklch(87% 0 0); | 8170 | --color-primary: oklch(87% 0 0); | ||
| 8224 | --color-primary-content: oklch(14% 0 0); | 8171 | --color-primary-content: oklch(14% 0 0); | ||
| 8225 | - | --color-secondary: oklch(83% 0.145 321.434); | 8172 | + | --color-secondary: oklch(80% 0.105 251.813); |
| 8226 | - | --color-secondary-content: oklch(29% 0.136 325.661); | 8173 | + | --color-secondary-content: oklch(28% 0.091 267.935); |
| 8227 | --color-accent: oklch(80% 0.105 251.813); | 8174 | --color-accent: oklch(80% 0.105 251.813); | ||
| 8228 | --color-accent-content: oklch(28% 0.091 267.935); | 8175 | --color-accent-content: oklch(28% 0.091 267.935); | ||
| 8229 | --color-neutral: oklch(37% 0.034 259.733); | 8176 | --color-neutral: oklch(37% 0.034 259.733); | ||
| @@ -8236,9 +8183,9 @@ | |||||
| 8236 | --color-warning-content: oklch(98% 0.022 95.277); | 8183 | --color-warning-content: oklch(98% 0.022 95.277); | ||
| 8237 | --color-error: oklch(64% 0.246 16.439); | 8184 | --color-error: oklch(64% 0.246 16.439); | ||
| 8238 | --color-error-content: oklch(96% 0.015 12.422); | 8185 | --color-error-content: oklch(96% 0.015 12.422); | ||
| 8239 | - | --radius-selector: 1rem; | 8186 | + | --radius-selector: 0rem; |
| 8240 | --radius-field: 0rem; | 8187 | --radius-field: 0rem; | ||
| 8241 | - | --radius-box: 0.25rem; | 8188 | + | --radius-box: 0rem; |
| 8242 | --size-selector: 0.25rem; | 8189 | --size-selector: 0.25rem; | ||
| 8243 | --size-field: 0.25rem; | 8190 | --size-field: 0.25rem; | ||
| 8244 | --border: 1px; | 8191 | --border: 1px; | ||
M internal/webui/public/assets/js/custom.js +12 -6
| @@ -36,18 +36,19 @@ function initializeVersionCheck() { | |||
| 36 | 36 | const init = () => { | |
| 37 | 37 | const currentVersionElement = document.getElementById("actual-version"); | |
| 38 | 38 | if (!currentVersionElement) { | |
| 39 | - | console.debug("Error: #actual-version element not found."); | |
| 40 | - | return; | |
| 39 | + | console.debug("Error: #actual-version element not found. Assuming not on admin page."); | |
| 40 | + | return false; // Indicate element not found | |
| 41 | 41 | } | |
| 42 | 42 | const currentVersion = currentVersionElement.textContent.trim(); | |
| 43 | 43 | if (!currentVersion) { | |
| 44 | 44 | console.debug("Dev mode detected, skipping version check."); | |
| 45 | - | return; | |
| 45 | + | return true; // Element found, but dev mode | |
| 46 | 46 | } | |
| 47 | 47 | fetchVersionInfo(currentVersion); | |
| 48 | + | return true; // Indicate element found | |
| 48 | 49 | }; | |
| 49 | 50 | ||
| 50 | - | init(); | |
| 51 | + | return init(); // Return the result of init | |
| 51 | 52 | } | |
| 52 | 53 | ||
| 53 | 54 | // Initialize jQuery functionality for the admin dashboard | |
| @@ -391,6 +392,11 @@ document.addEventListener("DOMContentLoaded", function() { | |||
| 391 | 392 | console.debug('jQuery is loaded successfully'); | |
| 392 | 393 | } | |
| 393 | 394 | ||
| 394 | - | initializeVersionCheck(); | |
| 395 | - | initializeAdminDashboard(); | |
| 395 | + | // Run version check (which also checks if we're on an admin page) | |
| 396 | + | const isAdminPage = initializeVersionCheck(); | |
| 397 | + | ||
| 398 | + | // Only initialize admin dashboard functions if jQuery is loaded AND we are on an admin page | |
| 399 | + | if (typeof $ !== 'undefined' && isAdminPage) { | |
| 400 | + | initializeAdminDashboard(); | |
| 401 | + | } | |
| 396 | 402 | }); | |
| @@ -36,18 +36,19 @@ function initializeVersionCheck() { | |||||
| 36 | const init = () => { | 36 | const init = () => { | ||
| 37 | const currentVersionElement = document.getElementById("actual-version"); | 37 | const currentVersionElement = document.getElementById("actual-version"); | ||
| 38 | if (!currentVersionElement) { | 38 | if (!currentVersionElement) { | ||
| 39 | - | console.debug("Error: #actual-version element not found."); | 39 | + | console.debug("Error: #actual-version element not found. Assuming not on admin page."); |
| 40 | - | return; | 40 | + | return false; // Indicate element not found |
| 41 | } | 41 | } | ||
| 42 | const currentVersion = currentVersionElement.textContent.trim(); | 42 | const currentVersion = currentVersionElement.textContent.trim(); | ||
| 43 | if (!currentVersion) { | 43 | if (!currentVersion) { | ||
| 44 | console.debug("Dev mode detected, skipping version check."); | 44 | console.debug("Dev mode detected, skipping version check."); | ||
| 45 | - | return; | 45 | + | return true; // Element found, but dev mode |
| 46 | } | 46 | } | ||
| 47 | fetchVersionInfo(currentVersion); | 47 | fetchVersionInfo(currentVersion); | ||
| 48 | + | return true; // Indicate element found | |||
| 48 | }; | 49 | }; | ||
| 49 | 50 | ||||
| 50 | - | init(); | 51 | + | return init(); // Return the result of init |
| 51 | } | 52 | } | ||
| 52 | 53 | ||||
| 53 | // Initialize jQuery functionality for the admin dashboard | 54 | // Initialize jQuery functionality for the admin dashboard | ||
| @@ -391,6 +392,11 @@ document.addEventListener("DOMContentLoaded", function() { | |||||
| 391 | console.debug('jQuery is loaded successfully'); | 392 | console.debug('jQuery is loaded successfully'); | ||
| 392 | } | 393 | } | ||
| 393 | 394 | ||||
| 394 | - | initializeVersionCheck(); | 395 | + | // Run version check (which also checks if we're on an admin page) |
| 395 | - | initializeAdminDashboard(); | 396 | + | const isAdminPage = initializeVersionCheck(); |
| 397 | + | ||||
| 398 | + | // Only initialize admin dashboard functions if jQuery is loaded AND we are on an admin page | |||
| 399 | + | if (typeof $ !== 'undefined' && isAdminPage) { | |||
| 400 | + | initializeAdminDashboard(); | |||
| 401 | + | } | |||
| 396 | }); | 402 | }); | ||
D pkg/htmx/htmlfragments.go +0 -42
| @@ -1,42 +0,0 @@ | |||
| 1 | - | package htmx | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "bytes" | |
| 5 | - | "fmt" | |
| 6 | - | ||
| 7 | - | "github.com/PuerkitoBio/goquery" | |
| 8 | - | "github.com/bnema/gordon/internal/server" | |
| 9 | - | "github.com/bnema/gordon/internal/templating/render" | |
| 10 | - | ) | |
| 11 | - | ||
| 12 | - | // GetHTMLFragmentByID returns the HTML fragment with the specified id | |
| 13 | - | func GetHTMLFragmentByID(id string, data interface{}, a *server.App) (string, error) { | |
| 14 | - | // 1. Render the template | |
| 15 | - | renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a) | |
| 16 | - | if err != nil { | |
| 17 | - | return "", fmt.Errorf("failed to get renderer: %w", err) | |
| 18 | - | } | |
| 19 | - | ||
| 20 | - | renderedHTML, err := renderer.Render(data, a) | |
| 21 | - | if err != nil { | |
| 22 | - | return "", fmt.Errorf("failed to render template: %w", err) | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | // 2. Parse the rendered HTML with goquery | |
| 26 | - | doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML))) | |
| 27 | - | if err != nil { | |
| 28 | - | return "", fmt.Errorf("failed to parse rendered HTML: %w", err) | |
| 29 | - | } | |
| 30 | - | ||
| 31 | - | // 3. Find the div with the specified id | |
| 32 | - | fragmentContent, err := doc.Find("#" + id).Html() | |
| 33 | - | if err != nil { | |
| 34 | - | return "", fmt.Errorf("failed to find fragment with id %s: %w", id, err) | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | if fragmentContent == "" { | |
| 38 | - | return "", fmt.Errorf("failed to find fragment with id %s: %w", id, err) | |
| 39 | - | } | |
| 40 | - | ||
| 41 | - | return fragmentContent, nil | |
| 42 | - | } | |
| @@ -1,42 +0,0 @@ | |||||
| 1 | - | package htmx | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "bytes" | |||
| 5 | - | "fmt" | |||
| 6 | - | ||||
| 7 | - | "github.com/PuerkitoBio/goquery" | |||
| 8 | - | "github.com/bnema/gordon/internal/server" | |||
| 9 | - | "github.com/bnema/gordon/internal/templating/render" | |||
| 10 | - | ) | |||
| 11 | - | ||||
| 12 | - | // GetHTMLFragmentByID returns the HTML fragment with the specified id | |||
| 13 | - | func GetHTMLFragmentByID(id string, data interface{}, a *server.App) (string, error) { | |||
| 14 | - | // 1. Render the template | |||
| 15 | - | renderer, err := render.GetHTMLRenderer("html/fragments", "components.gohtml", a.TemplateFS, a) | |||
| 16 | - | if err != nil { | |||
| 17 | - | return "", fmt.Errorf("failed to get renderer: %w", err) | |||
| 18 | - | } | |||
| 19 | - | ||||
| 20 | - | renderedHTML, err := renderer.Render(data, a) | |||
| 21 | - | if err != nil { | |||
| 22 | - | return "", fmt.Errorf("failed to render template: %w", err) | |||
| 23 | - | } | |||
| 24 | - | ||||
| 25 | - | // 2. Parse the rendered HTML with goquery | |||
| 26 | - | doc, err := goquery.NewDocumentFromReader(bytes.NewReader([]byte(renderedHTML))) | |||
| 27 | - | if err != nil { | |||
| 28 | - | return "", fmt.Errorf("failed to parse rendered HTML: %w", err) | |||
| 29 | - | } | |||
| 30 | - | ||||
| 31 | - | // 3. Find the div with the specified id | |||
| 32 | - | fragmentContent, err := doc.Find("#" + id).Html() | |||
| 33 | - | if err != nil { | |||
| 34 | - | return "", fmt.Errorf("failed to find fragment with id %s: %w", id, err) | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | if fragmentContent == "" { | |||
| 38 | - | return "", fmt.Errorf("failed to find fragment with id %s: %w", id, err) | |||
| 39 | - | } | |||
| 40 | - | ||||
| 41 | - | return fragmentContent, nil | |||
| 42 | - | } | |||