package server import ( "bufio" "embed" "encoding/json" "fmt" "net/http" "os" "path/filepath" "strings" "time" "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 frontend directory. func loadXMLTemplate(cfg *tools.Config, urlPath string) string { if cfg.FrontendDir == "" { return "" } rel := strings.TrimPrefix(urlPath, "/") fullPath := filepath.Join(cfg.FrontendDir, 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 database needs initialization // Mirrors: odoo/addons/web/controllers/home.py ensure_db() if s.isSetupNeeded() { http.Redirect(w, r, "/web/database/manager", 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: module_loader must load first (defines odoo.loader), // then the concatenated bundle serves everything else in one request. // We suppress transient missing-dependency errors during loading by // temporarily replacing reportErrors with a no-op, then restore it // after the bundle has loaded. var scriptTags strings.Builder cacheBuster := fmt.Sprintf("?v=%d", time.Now().Unix()) // 1) module_loader.js — must run first to define odoo.define/odoo.loader scriptTags.WriteString(fmt.Sprintf(" \n", cacheBuster)) // 2) Suppress transient reportErrors while the bundle loads scriptTags.WriteString(" \n") // 3) The concatenated JS bundle (all other modules + XML templates) scriptTags.WriteString(fmt.Sprintf(" \n", cacheBuster)) // 4) Restore reportErrors and run a final check for genuine errors scriptTags.WriteString(" \n") // 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, `