package server import ( "bufio" "embed" "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "odoo-go/pkg/tools" ) //go:embed assets_js.txt var assetsJSFile embed.FS //go:embed assets_css.txt var assetsCSSFile embed.FS //go:embed assets_xml.txt var assetsXMLFile embed.FS var jsFiles []string var cssFiles []string var xmlFiles []string func init() { jsFiles = loadAssetList("assets_js.txt", assetsJSFile) cssFiles = loadAssetList("assets_css.txt", assetsCSSFile) xmlFiles = loadAssetList("assets_xml.txt", assetsXMLFile) } // loadXMLTemplate reads an XML template file from the Odoo addons paths. func loadXMLTemplate(cfg *tools.Config, urlPath string) string { rel := strings.TrimPrefix(urlPath, "/") for _, addonsDir := range cfg.OdooAddonsPath { fullPath := filepath.Join(addonsDir, rel) data, err := os.ReadFile(fullPath) if err == nil { return string(data) } } return "" } func loadAssetList(name string, fs embed.FS) []string { data, err := fs.ReadFile(name) if err != nil { return nil } var files []string scanner := bufio.NewScanner(strings.NewReader(string(data))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line != "" && !strings.HasPrefix(line, "#") { files = append(files, line) } } return files } // handleWebClient serves the Odoo webclient HTML shell. // Mirrors: odoo/addons/web/controllers/home.py Home.web_client() func (s *Server) handleWebClient(w http.ResponseWriter, r *http.Request) { // Check if setup is needed if s.isSetupNeeded() { http.Redirect(w, r, "/web/setup", http.StatusFound) return } // Check authentication sess := GetSession(r) if sess == nil { // Try cookie directly cookie, err := r.Cookie("session_id") if err != nil || cookie.Value == "" { http.Redirect(w, r, "/web/login", http.StatusFound) return } sess = s.sessions.Get(cookie.Value) if sess == nil { http.Redirect(w, r, "/web/login", http.StatusFound) return } } sessionInfo := s.buildSessionInfo(sess) sessionInfoJSON, _ := json.Marshal(sessionInfo) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") // Build script tags for all JS files (with cache buster) var scriptTags strings.Builder cacheBuster := "?v=odoo-go-1" for _, src := range jsFiles { if strings.HasSuffix(src, ".scss") { continue } scriptTags.WriteString(fmt.Sprintf(" \n", src, cacheBuster)) } // Build link tags for CSS: compiled SCSS bundle + individual CSS files var linkTags strings.Builder // Main compiled SCSS bundle (Bootstrap + Odoo core styles) linkTags.WriteString(fmt.Sprintf(" \n", cacheBuster)) // Additional plain CSS files for _, src := range cssFiles { if strings.HasSuffix(src, ".css") { linkTags.WriteString(fmt.Sprintf(" \n", src, cacheBuster)) } } // XML templates are compiled to JS (registerTemplate calls) and included // in the JS bundle as xml_templates_bundle.js — no inline XML needed. fmt.Fprintf(w, ` Odoo %s %s `, linkTags.String(), sessionInfoJSON, scriptTags.String()) } // buildSessionInfo constructs the session_info JSON object expected by the webclient. // Mirrors: odoo/addons/web/models/ir_http.py session_info() func (s *Server) buildSessionInfo(sess *Session) map[string]interface{} { return map[string]interface{}{ "uid": sess.UID, "is_system": sess.UID == 1, "is_admin": sess.UID == 1, "is_public": false, "is_internal_user": true, "user_context": map[string]interface{}{ "lang": "en_US", "tz": "UTC", "allowed_company_ids": []int64{sess.CompanyID}, }, "db": s.config.DBName, "registry_hash": "odoo-go-static", "server_version": "19.0-go", "server_version_info": []interface{}{19, 0, 0, "final", 0, "g"}, "name": sess.Login, "username": sess.Login, "partner_id": sess.UID + 1, // Simplified mapping "partner_display_name": sess.Login, "partner_write_date": "2026-01-01 00:00:00", "quick_login": true, "web.base.url": fmt.Sprintf("http://localhost:%d", s.config.HTTPPort), "active_ids_limit": 20000, "max_file_upload_size": 134217728, "home_action_id": false, "support_url": "", "test_mode": false, "show_effect": true, "currencies": map[string]interface{}{ "1": map[string]interface{}{ "id": 1, "name": "EUR", "symbol": "€", "position": "after", "digits": []int{69, 2}, }, }, "bundle_params": map[string]interface{}{ "lang": "en_US", "debug": "assets", }, "user_companies": map[string]interface{}{ "current_company": sess.CompanyID, "allowed_companies": map[string]interface{}{ fmt.Sprintf("%d", sess.CompanyID): map[string]interface{}{ "id": sess.CompanyID, "name": "My Company", "sequence": 10, "child_ids": []int64{}, "parent_id": false, "currency_id": 1, }, }, "disallowed_ancestor_companies": map[string]interface{}{}, }, "user_settings": map[string]interface{}{ "id": 1, "user_id": map[string]interface{}{"id": sess.UID, "display_name": sess.Login}, }, "view_info": map[string]interface{}{ "list": map[string]interface{}{"display_name": "List", "icon": "oi oi-view-list", "multi_record": true}, "form": map[string]interface{}{"display_name": "Form", "icon": "fa fa-address-card", "multi_record": false}, "kanban": map[string]interface{}{"display_name": "Kanban", "icon": "oi oi-view-kanban", "multi_record": true}, "graph": map[string]interface{}{"display_name": "Graph", "icon": "fa fa-area-chart", "multi_record": true}, "pivot": map[string]interface{}{"display_name": "Pivot", "icon": "oi oi-view-pivot", "multi_record": true}, "calendar": map[string]interface{}{"display_name": "Calendar", "icon": "fa fa-calendar", "multi_record": true}, "search": map[string]interface{}{"display_name": "Search", "icon": "oi oi-search", "multi_record": true}, }, "groups": map[string]interface{}{ "base.group_allow_export": true, "base.group_user": true, "base.group_system": true, }, } } // handleTranslations returns empty English translations. // Mirrors: odoo/addons/web/controllers/webclient.py translations() func (s *Server) handleTranslations(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "public, max-age=3600") json.NewEncoder(w).Encode(map[string]interface{}{ "lang": "en_US", "hash": "odoo-go-empty", "lang_parameters": map[string]interface{}{ "direction": "ltr", "date_format": "%%m/%%d/%%Y", "time_format": "%%H:%%M:%%S", "grouping": "[3,0]", "decimal_point": ".", "thousands_sep": ",", "week_start": 1, }, "modules": map[string]interface{}{}, "multi_lang": false, }) }