Skip to content

Commit 1fa4c4a

Browse files
committed
feat: webapp GUI for testing
1 parent a72298f commit 1fa4c4a

4 files changed

Lines changed: 778 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,10 @@ ERROR.md
2323
# Test outputs
2424
/testing_output
2525

26+
# Webapp runtime data
27+
/webapp/uploads
28+
/webapp/outputs
29+
/webapp/logs
30+
2631
# Debug
2732
*.pdb

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: all build debug release debug-video release-video test test-unit test-integration test-video test-release sanity clean fmt lint check help
1+
.PHONY: all build debug release debug-video release-video test test-unit test-integration test-video test-release sanity clean fmt lint check help build-webapp webapp
22

33
# Default target
44
all: debug
@@ -59,6 +59,15 @@ sanity: fmt-check lint debug test-unit test-integration release test-release deb
5959
@echo " All sanity checks passed."
6060
@echo "========================================="
6161

62+
# ── Web App ───────────────────────────────────────────────────────────────
63+
64+
build-webapp: release ## Build webapp server binary + infinishield release binary
65+
go build -o target/release/infinishield-webapp webapp/main.go
66+
67+
webapp: build-webapp ## Run webapp on port 1983
68+
@echo "Starting infinishield webapp on http://localhost:1983"
69+
cd $(CURDIR) && target/release/infinishield-webapp
70+
6271
# ── Clean ─────────────────────────────────────────────────────────────────
6372

6473
clean: ## Remove build artifacts and test outputs

webapp/main.go

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"log"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"strings"
13+
"time"
14+
)
15+
16+
const (
17+
port = ":1983"
18+
uploadDir = "webapp/uploads"
19+
outputDir = "webapp/outputs"
20+
logDir = "webapp/logs"
21+
binaryName = "infinishield"
22+
)
23+
24+
var eventLog *log.Logger
25+
26+
func main() {
27+
os.MkdirAll(uploadDir, 0755)
28+
os.MkdirAll(outputDir, 0755)
29+
os.MkdirAll(logDir, 0755)
30+
31+
// Setup event log file
32+
logFile, err := os.OpenFile(
33+
filepath.Join(logDir, time.Now().Format("2006-01-02")+".log"),
34+
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644,
35+
)
36+
if err != nil {
37+
log.Fatalf("Failed to open log file: %v", err)
38+
}
39+
defer logFile.Close()
40+
eventLog = log.New(logFile, "", 0)
41+
42+
binary := findBinary()
43+
if binary == "" {
44+
log.Fatal("infinishield release binary not found at target/release/infinishield. Run 'make release' or 'make release-video' first.")
45+
}
46+
log.Printf("Using binary: %s", binary)
47+
logEvent("SERVER_START", nil, map[string]string{"binary": binary})
48+
49+
http.HandleFunc("/", serveIndex)
50+
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("webapp/static"))))
51+
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir(uploadDir))))
52+
http.Handle("/outputs/", http.StripPrefix("/outputs/", http.FileServer(http.Dir(outputDir))))
53+
54+
http.HandleFunc("/api/upload", handleUpload)
55+
http.HandleFunc("/api/dryrun", makeDryRunHandler(binary))
56+
http.HandleFunc("/api/embed", makeEmbedHandler(binary))
57+
http.HandleFunc("/api/verify", makeVerifyHandler(binary))
58+
59+
log.Printf("infinishield webapp running on http://localhost%s", port)
60+
log.Fatal(http.ListenAndServe(port, nil))
61+
}
62+
63+
// ── Structured Event Logging ─────────────────────────────────────────────
64+
65+
func logEvent(event string, r *http.Request, data map[string]string) {
66+
entry := map[string]interface{}{
67+
"time": time.Now().UTC().Format(time.RFC3339),
68+
"event": event,
69+
}
70+
71+
if r != nil {
72+
entry["remote_addr"] = r.RemoteAddr
73+
entry["user_agent"] = r.UserAgent()
74+
entry["referer"] = r.Referer()
75+
entry["method"] = r.Method
76+
entry["path"] = r.URL.Path
77+
}
78+
79+
for k, v := range data {
80+
entry[k] = v
81+
}
82+
83+
jsonBytes, _ := json.Marshal(entry)
84+
eventLog.Println(string(jsonBytes))
85+
}
86+
87+
func logEventR(event string, r *http.Request, data map[string]string) {
88+
logEvent(event, r, data)
89+
}
90+
91+
// ── Binary Finder ────────────────────────────────────────────────────────
92+
93+
func findBinary() string {
94+
candidates := []string{
95+
"target/release/infinishield",
96+
}
97+
for _, c := range candidates {
98+
if _, err := os.Stat(c); err == nil {
99+
return c
100+
}
101+
}
102+
if p, err := exec.LookPath(binaryName); err == nil {
103+
return p
104+
}
105+
return ""
106+
}
107+
108+
// ── Handlers ─────────────────────────────────────────────────────────────
109+
110+
func serveIndex(w http.ResponseWriter, r *http.Request) {
111+
if r.URL.Path != "/" {
112+
http.NotFound(w, r)
113+
return
114+
}
115+
logEventR("PAGE_VIEW", r, nil)
116+
http.ServeFile(w, r, "webapp/static/index.html")
117+
}
118+
119+
type UploadResponse struct {
120+
Filename string `json:"filename"`
121+
Path string `json:"path"`
122+
Size int64 `json:"size"`
123+
Type string `json:"type"`
124+
}
125+
126+
func handleUpload(w http.ResponseWriter, r *http.Request) {
127+
if r.Method != http.MethodPost {
128+
http.Error(w, "POST only", http.StatusMethodNotAllowed)
129+
return
130+
}
131+
132+
r.ParseMultipartForm(100 << 20)
133+
file, header, err := r.FormFile("file")
134+
if err != nil {
135+
logEventR("UPLOAD_ERROR", r, map[string]string{"error": err.Error()})
136+
jsonError(w, "No file uploaded", http.StatusBadRequest)
137+
return
138+
}
139+
defer file.Close()
140+
141+
savePath := filepath.Join(uploadDir, header.Filename)
142+
dst, err := os.Create(savePath)
143+
if err != nil {
144+
logEventR("UPLOAD_ERROR", r, map[string]string{"error": err.Error()})
145+
jsonError(w, "Failed to save file", http.StatusInternalServerError)
146+
return
147+
}
148+
defer dst.Close()
149+
io.Copy(dst, file)
150+
151+
ext := filepath.Ext(header.Filename)
152+
fileType := detectType(ext)
153+
154+
logEventR("UPLOAD", r, map[string]string{
155+
"filename": header.Filename,
156+
"size": fmt.Sprintf("%d", header.Size),
157+
"type": fileType,
158+
})
159+
160+
resp := UploadResponse{
161+
Filename: header.Filename,
162+
Path: savePath,
163+
Size: header.Size,
164+
Type: fileType,
165+
}
166+
jsonResponse(w, resp)
167+
}
168+
169+
func detectType(ext string) string {
170+
ext = strings.ToLower(ext)
171+
switch ext {
172+
case ".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif", ".gif":
173+
return "image"
174+
case ".svg":
175+
return "svg"
176+
case ".mp4", ".webm", ".mov", ".avi", ".mkv":
177+
return "video"
178+
default:
179+
return "unknown"
180+
}
181+
}
182+
183+
type APIResponse struct {
184+
Success bool `json:"success"`
185+
Output string `json:"output"`
186+
Error string `json:"error,omitempty"`
187+
}
188+
189+
func makeDryRunHandler(binary string) http.HandlerFunc {
190+
return func(w http.ResponseWriter, r *http.Request) {
191+
if r.Method != http.MethodPost {
192+
http.Error(w, "POST only", http.StatusMethodNotAllowed)
193+
return
194+
}
195+
196+
var req struct {
197+
InputPath string `json:"input_path"`
198+
Message string `json:"message"`
199+
Password string `json:"password"`
200+
Intensity string `json:"intensity"`
201+
}
202+
json.NewDecoder(r.Body).Decode(&req)
203+
204+
start := time.Now()
205+
206+
args := []string{"embed", "-i", req.InputPath, "-o", "/dev/null", "--dry-run"}
207+
if req.Message != "" {
208+
args = append(args, "-m", req.Message)
209+
}
210+
if req.Password != "" {
211+
args = append(args, "-p", req.Password)
212+
}
213+
if req.Intensity != "" && req.Intensity != "auto" {
214+
args = append(args, "--intensity", req.Intensity)
215+
}
216+
217+
out, err := exec.Command(binary, args...).CombinedOutput()
218+
elapsed := time.Since(start)
219+
success := err == nil
220+
221+
logEventR("DRYRUN", r, map[string]string{
222+
"input": req.InputPath,
223+
"message": req.Message,
224+
"intensity": req.Intensity,
225+
"success": fmt.Sprintf("%v", success),
226+
"elapsed": elapsed.String(),
227+
})
228+
229+
if !success {
230+
jsonResponse(w, APIResponse{Success: false, Output: string(out), Error: err.Error()})
231+
return
232+
}
233+
jsonResponse(w, APIResponse{Success: true, Output: string(out)})
234+
}
235+
}
236+
237+
func makeEmbedHandler(binary string) http.HandlerFunc {
238+
return func(w http.ResponseWriter, r *http.Request) {
239+
if r.Method != http.MethodPost {
240+
http.Error(w, "POST only", http.StatusMethodNotAllowed)
241+
return
242+
}
243+
244+
var req struct {
245+
InputPath string `json:"input_path"`
246+
Message string `json:"message"`
247+
Password string `json:"password"`
248+
Intensity string `json:"intensity"`
249+
OutputName string `json:"output_name"`
250+
}
251+
json.NewDecoder(r.Body).Decode(&req)
252+
253+
start := time.Now()
254+
outputPath := filepath.Join(outputDir, req.OutputName)
255+
args := []string{"embed", "-i", req.InputPath, "-o", outputPath}
256+
if req.Message != "" {
257+
args = append(args, "-m", req.Message)
258+
}
259+
if req.Password != "" {
260+
args = append(args, "-p", req.Password)
261+
}
262+
if req.Intensity != "" && req.Intensity != "auto" {
263+
args = append(args, "--intensity", req.Intensity)
264+
}
265+
266+
out, err := exec.Command(binary, args...).CombinedOutput()
267+
embedElapsed := time.Since(start)
268+
269+
resp := struct {
270+
Success bool `json:"success"`
271+
Output string `json:"output"`
272+
Error string `json:"error,omitempty"`
273+
OutputPath string `json:"output_path,omitempty"`
274+
OutputURL string `json:"output_url,omitempty"`
275+
}{
276+
Success: err == nil,
277+
Output: string(out),
278+
OutputPath: outputPath,
279+
OutputURL: "/outputs/" + req.OutputName,
280+
}
281+
if err != nil {
282+
resp.Error = err.Error()
283+
}
284+
285+
// Auto-verify
286+
verifySuccess := false
287+
if err == nil {
288+
verifyArgs := []string{"verify", "-i", outputPath}
289+
if req.Password != "" {
290+
verifyArgs = append(verifyArgs, "-p", req.Password)
291+
}
292+
verifyOut, verifyErr := exec.Command(binary, verifyArgs...).CombinedOutput()
293+
verifySuccess = verifyErr == nil
294+
resp.Output += "\n--- Verification ---\n" + string(verifyOut)
295+
}
296+
297+
totalElapsed := time.Since(start)
298+
299+
logEventR("EMBED", r, map[string]string{
300+
"input": req.InputPath,
301+
"output": outputPath,
302+
"message": req.Message,
303+
"intensity": req.Intensity,
304+
"embed_success": fmt.Sprintf("%v", err == nil),
305+
"verify_success": fmt.Sprintf("%v", verifySuccess),
306+
"embed_elapsed": embedElapsed.String(),
307+
"total_elapsed": totalElapsed.String(),
308+
})
309+
310+
jsonResponse(w, resp)
311+
}
312+
}
313+
314+
func makeVerifyHandler(binary string) http.HandlerFunc {
315+
return func(w http.ResponseWriter, r *http.Request) {
316+
if r.Method != http.MethodPost {
317+
http.Error(w, "POST only", http.StatusMethodNotAllowed)
318+
return
319+
}
320+
321+
var req struct {
322+
InputPath string `json:"input_path"`
323+
Password string `json:"password"`
324+
}
325+
json.NewDecoder(r.Body).Decode(&req)
326+
327+
start := time.Now()
328+
329+
args := []string{"verify", "-i", req.InputPath}
330+
if req.Password != "" {
331+
args = append(args, "-p", req.Password)
332+
}
333+
334+
out, err := exec.Command(binary, args...).CombinedOutput()
335+
elapsed := time.Since(start)
336+
success := err == nil
337+
338+
logEventR("VERIFY", r, map[string]string{
339+
"input": req.InputPath,
340+
"success": fmt.Sprintf("%v", success),
341+
"elapsed": elapsed.String(),
342+
})
343+
344+
if !success {
345+
jsonResponse(w, APIResponse{Success: false, Output: string(out), Error: err.Error()})
346+
return
347+
}
348+
jsonResponse(w, APIResponse{Success: true, Output: string(out)})
349+
}
350+
}
351+
352+
// ── JSON helpers ─────────────────────────────────────────────────────────
353+
354+
func jsonResponse(w http.ResponseWriter, data interface{}) {
355+
w.Header().Set("Content-Type", "application/json")
356+
json.NewEncoder(w).Encode(data)
357+
}
358+
359+
func jsonError(w http.ResponseWriter, msg string, code int) {
360+
w.Header().Set("Content-Type", "application/json")
361+
w.WriteHeader(code)
362+
json.NewEncoder(w).Encode(map[string]string{"error": msg})
363+
}

0 commit comments

Comments
 (0)