Compare commits
2 Commits
9edb9d6aca
...
ddba2dc3e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddba2dc3e1 | ||
|
|
730d453371 |
BIN
protoc-frontend.exe
LFS
BIN
protoc-frontend.exe
LFS
Binary file not shown.
@@ -1,3 +1,17 @@
|
|||||||
module protoc-frontend-server
|
module protoc-frontend-server
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||||
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
22
server/go.sum
Normal file
22
server/go.sum
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -31,7 +33,6 @@ func main() {
|
|||||||
originalDirector := proxy.Director
|
originalDirector := proxy.Director
|
||||||
proxy.Director = func(r *http.Request) {
|
proxy.Director = func(r *http.Request) {
|
||||||
originalDirector(r)
|
originalDirector(r)
|
||||||
// Rewrite /api/... -> /...
|
|
||||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/api")
|
||||||
if r.URL.Path == "" {
|
if r.URL.Path == "" {
|
||||||
r.URL.Path = "/"
|
r.URL.Path = "/"
|
||||||
@@ -46,31 +47,34 @@ func main() {
|
|||||||
}
|
}
|
||||||
fileServer := http.FileServer(http.FS(distSub))
|
fileServer := http.FileServer(http.FS(distSub))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
// API proxy
|
// API routes
|
||||||
mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) {
|
r.Route("/api", func(api chi.Router) {
|
||||||
proxy.ServeHTTP(w, r)
|
// Protocol generation handled locally (xlsx fill via excelize)
|
||||||
|
api.Post("/protocols/", protocolHandler(*apiTarget))
|
||||||
|
|
||||||
|
// All other /api/* requests proxied to Python backend
|
||||||
|
api.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// SPA: serve static files, fallback to index.html
|
// SPA: serve static files, fallback to index.html
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
// Try to serve the file directly
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/")
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
||||||
if path == "" {
|
if path == "" {
|
||||||
path = "index.html"
|
path = "index.html"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file exists in embedded FS
|
|
||||||
if f, err := distSub.Open(path); err == nil {
|
if f, err := distSub.Open(path); err == nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to index.html for SPA routing
|
|
||||||
indexFile, err := fs.ReadFile(distSub, "index.html")
|
indexFile, err := fs.ReadFile(distSub, "index.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "index.html not found", http.StatusInternalServerError)
|
http.Error(w, "index.html not found", http.StatusInternalServerError)
|
||||||
@@ -84,7 +88,7 @@ func main() {
|
|||||||
log.Printf("Serving on http://localhost%s", addr)
|
log.Printf("Serving on http://localhost%s", addr)
|
||||||
log.Printf("API proxy -> %s", *apiTarget)
|
log.Printf("API proxy -> %s", *apiTarget)
|
||||||
|
|
||||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
if err := http.ListenAndServe(addr, r); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
107
server/protocol.go
Normal file
107
server/protocol.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateProtocolRequest struct {
|
||||||
|
TemplateID string `json:"template_id"`
|
||||||
|
FileID string `json:"file_id"`
|
||||||
|
CellsToUpdate map[string]any `json:"cells_to_update"`
|
||||||
|
PrintArea string `json:"print_area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolHandler(backendURL string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse request body
|
||||||
|
var req CreateProtocolRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch file binary from Python backend
|
||||||
|
fileURL := fmt.Sprintf("%s/files/%s/content", backendURL, req.FileID)
|
||||||
|
resp, err := http.Get(fileURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to fetch file: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
http.Error(w, fmt.Sprintf("backend returned %d when fetching file", resp.StatusCode), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to read file content: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open xlsx with excelize
|
||||||
|
f, err := excelize.OpenReader(bytes.NewReader(fileBytes))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to open xlsx: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Get the first sheet
|
||||||
|
sheetName := f.GetSheetName(0)
|
||||||
|
if sheetName == "" {
|
||||||
|
http.Error(w, "no sheets found in xlsx", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill cells
|
||||||
|
for cellAddr, value := range req.CellsToUpdate {
|
||||||
|
if err := setCellValue(f, sheetName, cellAddr, value); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to set cell %s: %v", cellAddr, err), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write result to buffer
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := f.Write(&buf); err != nil {
|
||||||
|
http.Error(w, "failed to write xlsx: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
protocolID := uuid.New().String()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="protocol_%s.xlsx"`, protocolID))
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||||
|
w.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCellValue(f *excelize.File, sheet, cell string, value any) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
// JSON numbers are float64; check if it's actually an integer
|
||||||
|
if v == float64(int64(v)) {
|
||||||
|
return f.SetCellInt(sheet, cell, int64(v))
|
||||||
|
}
|
||||||
|
return f.SetCellFloat(sheet, cell, v, -1, 64)
|
||||||
|
case string:
|
||||||
|
return f.SetCellStr(sheet, cell, v)
|
||||||
|
case bool:
|
||||||
|
return f.SetCellBool(sheet, cell, v)
|
||||||
|
case nil:
|
||||||
|
return f.SetCellStr(sheet, cell, "")
|
||||||
|
default:
|
||||||
|
return f.SetCellStr(sheet, cell, fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,16 +14,24 @@
|
|||||||
"outDir": "./dist/",
|
"outDir": "./dist/",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": [
|
||||||
"shared/*": ["./shared/*"]
|
"./src/*"
|
||||||
|
],
|
||||||
|
"shared/*": [
|
||||||
|
"./shared/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
|
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "CommonJS"
|
"module": "CommonJS"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist", "build", "coverage"]
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
"coverage"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user