diff --git a/handlers/admin/manualtriggers.go b/handlers/admin/manualtriggers.go new file mode 100644 index 0000000..6b24a88 --- /dev/null +++ b/handlers/admin/manualtriggers.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "database/sql" + "html/template" + "net/http" + "strconv" + "synlotto-website/helpers" + "synlotto-website/services" +) + +func AdminTriggersHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + context := helpers.TemplateContext(w, r) + + if r.Method == http.MethodPost { + stats, err := services.RunTicketMatching(db, "manual") + if err != nil { + http.Error(w, "Matching failed: "+err.Error(), http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/admin/triggers?flash=Matched "+ + strconv.Itoa(stats.TicketsMatched)+" tickets, "+ + strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther) + return + } + + // Render admin trigger page + tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles( + "templates/layout.html", + "templates/admin/triggers.html", + )) + err := tmpl.ExecuteTemplate(w, "layout", context) + if err != nil { + http.Error(w, "Failed to load page", http.StatusInternalServerError) + } + } +} diff --git a/handlers/common.go b/handlers/common.go index eca3fbf..9b44999 100644 --- a/handlers/common.go +++ b/handlers/common.go @@ -5,4 +5,4 @@ import ( ) var Draws []models.ThunderballResult -var MyTickets []models.MyTicket +var MyTickets []models.Ticket diff --git a/handlers/draw_handler.go b/handlers/draw_handler.go index 05eb615..99c3221 100644 --- a/handlers/draw_handler.go +++ b/handlers/draw_handler.go @@ -14,8 +14,6 @@ import ( func Home(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Println("✅ Home hit") - rows, err := db.Query(` SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball FROM results_thunderball diff --git a/handlers/ticket_handler.go b/handlers/ticket_handler.go index db447cb..ff5120b 100644 --- a/handlers/ticket_handler.go +++ b/handlers/ticket_handler.go @@ -170,7 +170,7 @@ func AddTicket(db *sql.DB) http.HandlerFunc { if err != nil { log.Println("❌ Failed to insert ticket line:", err) } else { - log.Printf("✅ Ticket line %d saved", i+1) + log.Printf("✅ Ticket line %d saved", i+1) // ToDo create audit } } @@ -270,23 +270,20 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc { return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) { userID, ok := helpers.GetCurrentUserID(r) if !ok { - log.Println("🚫 Not logged in, redirecting") http.Redirect(w, r, "/login", http.StatusSeeOther) return } - log.Printf("👤 Fetching tickets for user ID: %d\n", userID) - rows, err := db.Query(` SELECT id, game_type, draw_date, - ball1, ball2, ball3, ball4, ball5, ball6, - bonus1, bonus2, - purchase_method, purchase_date, image_path + ball1, ball2, ball3, ball4, ball5, ball6, + bonus1, bonus2, + purchase_method, purchase_date, image_path, duplicate, + matched_main, matched_bonus, prize_tier, is_winner FROM my_tickets - WHERE userId = ? + WHERE userid = ? ORDER BY draw_date DESC, created_at DESC `, userID) - if err != nil { log.Println("❌ Query failed:", err) http.Error(w, "Could not load tickets", http.StatusInternalServerError) @@ -294,25 +291,51 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc { } defer rows.Close() - var tickets []models.MyTicket + var tickets []models.Ticket for rows.Next() { - var t models.MyTicket + var t models.Ticket + var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64 + var matchedMain, matchedBonus sql.NullInt64 + var prizeTier sql.NullString + var isWinner sql.NullBool + err := rows.Scan( &t.Id, &t.GameType, &t.DrawDate, - &t.Ball1, &t.Ball2, &t.Ball3, &t.Ball4, &t.Ball5, &t.Ball6, - &t.Bonus1, &t.Bonus2, - &t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, + &b1, &b2, &b3, &b4, &b5, &b6, + &bo1, &bo2, + &t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate, + &matchedMain, &matchedBonus, &prizeTier, &isWinner, ) if err != nil { log.Println("⚠️ Failed to scan ticket row:", err) continue } + + t.Ball1, t.Ball2, t.Ball3 = int(b1.Int64), int(b2.Int64), int(b3.Int64) + t.Ball4, t.Ball5, t.Ball6 = int(b4.Int64), int(b5.Int64), int(b6.Int64) + t.Bonus1 = helpers.IntPtrIfValid(bo1) + t.Bonus2 = helpers.IntPtrIfValid(bo2) + + if matchedMain.Valid { + t.MatchedMain = int(matchedMain.Int64) + } + if matchedBonus.Valid { + t.MatchedBonus = int(matchedBonus.Int64) + } + if prizeTier.Valid { + t.PrizeTier = prizeTier.String + } + if isWinner.Valid { + t.IsWinner = isWinner.Bool + } + + t.Balls = helpers.BuildBallsSlice(t) + t.BonusBalls = helpers.BuildBonusSlice(t) + tickets = append(tickets, t) } - log.Printf("📄 Loaded %d tickets\n", len(tickets)) - context := helpers.TemplateContext(w, r) context["Tickets"] = tickets diff --git a/handlers/ticket_matcher.go b/handlers/ticket_matcher.go index fb8e22a..8230add 100644 --- a/handlers/ticket_matcher.go +++ b/handlers/ticket_matcher.go @@ -4,7 +4,7 @@ import ( "synlotto-website/models" ) -func MatchTicketToDraw(ticket models.Ticket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult { +func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules []models.PrizeRule) models.MatchResult { mainMatches := countMatches(ticket.Balls, draw.Balls) bonusMatches := countMatches(ticket.BonusBalls, draw.BonusBalls) diff --git a/helpers/ballslice.go b/helpers/ballslice.go new file mode 100644 index 0000000..7c4b54c --- /dev/null +++ b/helpers/ballslice.go @@ -0,0 +1,22 @@ +package helpers + +import "synlotto-website/models" + +func BuildBallsSlice(t models.Ticket) []int { + balls := []int{t.Ball1, t.Ball2, t.Ball3, t.Ball4, t.Ball5} + if t.GameType == "Lotto" && t.Ball6 > 0 { + balls = append(balls, t.Ball6) + } + return balls +} + +func BuildBonusSlice(t models.Ticket) []int { + var bonuses []int + if t.Bonus1 != nil { + bonuses = append(bonuses, *t.Bonus1) + } + if t.Bonus2 != nil { + bonuses = append(bonuses, *t.Bonus2) + } + return bonuses +} diff --git a/helpers/intptr.go b/helpers/intptr.go new file mode 100644 index 0000000..ad282f3 --- /dev/null +++ b/helpers/intptr.go @@ -0,0 +1,13 @@ +package helpers + +import ( + "database/sql" +) + +func IntPtrIfValid(val sql.NullInt64) *int { + if val.Valid { + n := int(val.Int64) + return &n + } + return nil +} diff --git a/helpers/template.go b/helpers/template.go index a6b2817..243933a 100644 --- a/helpers/template.go +++ b/helpers/template.go @@ -4,6 +4,8 @@ import ( "html/template" "net/http" "synlotto-website/models" + + "github.com/gorilla/csrf" ) func TemplateFuncs() template.FuncMap { @@ -29,6 +31,7 @@ func TemplateFuncs() template.FuncMap { } return *p }, + "inSlice": InSlice, } } @@ -54,7 +57,17 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac } return map[string]interface{}{ - "Flash": flash, - "User": currentUser, + "CSRFField": csrf.TemplateField(r), + "Flash": flash, + "User": currentUser, } } + +func InSlice(n int, list []int) bool { + for _, v := range list { + if v == n { + return true + } + } + return false +} diff --git a/main.go b/main.go index 4f7cdcb..d6ba515 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,10 @@ import ( "log" "net/http" "synlotto-website/handlers" + services "synlotto-website/handlers/admin" "synlotto-website/helpers" "synlotto-website/middleware" "synlotto-website/models" - services "synlotto-website/services/admin" "synlotto-website/storage" "github.com/gorilla/csrf" @@ -25,6 +25,9 @@ func main() { mux := http.NewServeMux() + // Styling + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + mux.HandleFunc("/", handlers.Home(db)) mux.HandleFunc("/new", handlers.NewDraw) // ToDo: needs to be wrapped in admin auth mux.HandleFunc("/submit", handlers.Submit) @@ -40,7 +43,7 @@ func main() { mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) // Admin Pages - mux.HandleFunc("/admin/match", services.AdminMatchHandler) + mux.HandleFunc("/admin/triggers", services.AdminTriggersHandler(db)) log.Println("🌐 Running on http://localhost:8080") http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux))) diff --git a/matcher/engine.go b/matcher/engine.go new file mode 100644 index 0000000..e69de29 diff --git a/models/match.go b/models/match.go index 49389f6..5a5290a 100644 --- a/models/match.go +++ b/models/match.go @@ -1,6 +1,6 @@ package models -type Ticket struct { +type MatchTicket struct { ID int GameType string DrawDate string diff --git a/models/myticket.go b/models/ticket.go similarity index 56% rename from models/myticket.go rename to models/ticket.go index 455a730..46ed80d 100644 --- a/models/myticket.go +++ b/models/ticket.go @@ -1,6 +1,6 @@ package models -type MyTicket struct { +type Ticket struct { Id int GameType string DrawDate string @@ -16,4 +16,12 @@ type MyTicket struct { PurchaseDate string ImagePath string Duplicate bool + MatchedMain int + MatchedBonus int + PrizeTier string + IsWinner bool + + // Used only for display these are not stored in the DB, they mirror MatchTicket structure but are populated on read. + Balls []int + BonusBalls []int } diff --git a/services/admin/manualtriggers.go b/services/admin/manualtriggers.go deleted file mode 100644 index 8af7f16..0000000 --- a/services/admin/manualtriggers.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "database/sql" - "net/http" - "strconv" - "synlotto-website/services" -) - -var db *sql.DB - -func AdminMatchHandler(w http.ResponseWriter, r *http.Request) { - stats, err := services.RunTicketMatching(db, "manual") - if err != nil { - http.Error(w, "Matching failed: "+err.Error(), 500) - return - } - http.Redirect(w, r, "/admin?flash=Matched "+ - strconv.Itoa(stats.TicketsMatched)+" tickets, "+ - strconv.Itoa(stats.WinnersFound)+" winners.", http.StatusSeeOther) -} diff --git a/services/draws/drawlookup.go b/services/draws/drawlookup.go new file mode 100644 index 0000000..ac66fde --- /dev/null +++ b/services/draws/drawlookup.go @@ -0,0 +1,29 @@ +package services + +import ( + "database/sql" + "log" + "synlotto-website/models" +) + +func GetDrawResultForTicket(db *sql.DB, game string, drawDate string) models.DrawResult { + var result models.DrawResult + query := ` + SELECT id, ball1, ball2, ball3, ball4, ball5, bonus1 + FROM results_thunderball + WHERE draw_date = ? + ` + var b1, b2, b3, b4, b5, bonus sql.NullInt64 + err := db.QueryRow(query, drawDate).Scan(&result.DrawID, &b1, &b2, &b3, &b4, &b5, &bonus) + if err != nil { + log.Printf("No draw found for %s %s: %v", game, drawDate, err) + return result + } + result.GameType = game + result.DrawDate = drawDate + result.Balls = []int{int(b1.Int64), int(b2.Int64), int(b3.Int64), int(b4.Int64), int(b5.Int64)} + if bonus.Valid { + result.BonusBalls = []int{int(bonus.Int64)} + } + return result +} diff --git a/services/ticketchecker.go b/services/ticketchecker.go deleted file mode 100644 index 9c1af4e..0000000 --- a/services/ticketchecker.go +++ /dev/null @@ -1,90 +0,0 @@ -package services - -import ( - "database/sql" - "log" - "synlotto-website/handlers" - "synlotto-website/models" - "synlotto-website/rules" -) - -func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { - stats := models.MatchRunStats{} - - rows, err := db.Query(` - SELECT id, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, bonus1 - FROM my_tickets - WHERE matched_draw_id IS NULL - `) - if err != nil { - return stats, err - } - defer rows.Close() - - for rows.Next() { - var t models.Ticket - var b1, b2, b3, b4, b5, bonus sql.NullInt64 - if err := rows.Scan(&t.ID, &t.GameType, &t.DrawDate, &b1, &b2, &b3, &b4, &b5, &bonus); err != nil { - continue - } - - t.Balls = []int{int(b1.Int64), int(b2.Int64), int(b3.Int64), int(b4.Int64), int(b5.Int64)} - if bonus.Valid { - t.BonusBalls = []int{int(bonus.Int64)} - } else { - t.BonusBalls = nil - } - - draw := GetDrawResultForTicket(db, t.GameType, t.DrawDate) - result := handlers.MatchTicketToDraw(t, draw, rules.ThunderballPrizeRules) - - if result.MatchedDrawID == 0 { - continue // no draw found - } - - _, err := db.Exec(` - UPDATE my_tickets SET - matched_draw_id = ?, matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ? - WHERE id = ?`, - result.MatchedDrawID, result.MatchedMain, result.MatchedBonus, result.PrizeTier, result.IsWinner, t.ID, - ) - if err != nil { - continue - } - - stats.TicketsMatched++ - if result.IsWinner { - stats.WinnersFound++ - } - } - - _, _ = db.Exec(` - INSERT INTO matching_log (triggered_by, tickets_matched, winners_found) - VALUES (?, ?, ?)`, - triggeredBy, stats.TicketsMatched, stats.WinnersFound, - ) - - return stats, nil -} - -func GetDrawResultForTicket(db *sql.DB, game string, drawDate string) models.DrawResult { - var result models.DrawResult - query := ` - SELECT id, ball1, ball2, ball3, ball4, ball5, bonus1 - FROM results_thunderball - WHERE draw_date = ? - ` - var b1, b2, b3, b4, b5, bonus sql.NullInt64 - err := db.QueryRow(query, drawDate).Scan(&result.DrawID, &b1, &b2, &b3, &b4, &b5, &bonus) - if err != nil { - log.Printf("No draw found for %s %s: %v", game, drawDate, err) - return result - } - result.GameType = game - result.DrawDate = drawDate - result.Balls = []int{int(b1.Int64), int(b2.Int64), int(b3.Int64), int(b4.Int64), int(b5.Int64)} - if bonus.Valid { - result.BonusBalls = []int{int(bonus.Int64)} - } - return result -} diff --git a/services/tickets/ticketmatching.go b/services/tickets/ticketmatching.go new file mode 100644 index 0000000..4ee7ed8 --- /dev/null +++ b/services/tickets/ticketmatching.go @@ -0,0 +1,85 @@ +package services + +import ( + "database/sql" + "log" + "synlotto-website/handlers" + "synlotto-website/helpers" + "synlotto-website/models" + "synlotto-website/rules" +) + +func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { + stats := models.MatchRunStats{} + + rows, err := db.Query(` + SELECT id, game_type, draw_date, + ball1, ball2, ball3, ball4, ball5, ball6, + bonus1, bonus2 + FROM my_tickets + WHERE matched_main IS NULL + `) + if err != nil { + return stats, err + } + defer rows.Close() + + for rows.Next() { + var t models.Ticket + var b1, b2, b3, b4, b5, b6, bo1, bo2 sql.NullInt64 + + if err := rows.Scan( + &t.Id, &t.GameType, &t.DrawDate, + &b1, &b2, &b3, &b4, &b5, &b6, + &bo1, &bo2, + ); err != nil { + continue + } + + t.Ball1 = int(b1.Int64) + t.Ball2 = int(b2.Int64) + t.Ball3 = int(b3.Int64) + t.Ball4 = int(b4.Int64) + t.Ball5 = int(b5.Int64) + t.Ball6 = int(b6.Int64) + t.Bonus1 = helpers.IntPtrIfValid(bo1) + t.Bonus2 = helpers.IntPtrIfValid(bo2) + + matchTicket := models.MatchTicket{ + ID: t.Id, + GameType: t.GameType, + DrawDate: t.DrawDate, + Balls: helpers.BuildBallsSlice(t), + BonusBalls: helpers.BuildBonusSlice(t), + } + + draw := GetDrawResultForTicket(db, t.GameType, t.DrawDate) + result := handlers.MatchTicketToDraw(matchTicket, draw, rules.ThunderballPrizeRules) + + if result.MatchedDrawID == 0 { + continue + } + + _, err = db.Exec(` + UPDATE my_tickets + SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ? + WHERE id = ? + `, result.MatchedMain, result.MatchedBonus, result.PrizeTier, result.IsWinner, t.Id) + if err != nil { + log.Println("⚠️ Failed to update ticket match:", err) + continue + } + + stats.TicketsMatched++ + if result.IsWinner { + stats.WinnersFound++ + } + } + + _, _ = db.Exec(` + INSERT INTO log_ticket_matching (triggered_by, tickets_matched, winners_found) + VALUES (?, ?, ?) + `, triggeredBy, stats.TicketsMatched, stats.WinnersFound) + + return stats, nil +} diff --git a/static/css/site.css b/static/css/site.css new file mode 100644 index 0000000..3436477 --- /dev/null +++ b/static/css/site.css @@ -0,0 +1,60 @@ +.ball { + display: inline-flex; + justify-content: center; + align-items: center; + background-color: #eee; + border-radius: 50%; + min-width: 32px; + height: 32px; + padding: 6px; + margin-right: 6px; + font-weight: bold; + text-align: center; + } + + .ball-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .matched { + background-color: #22c55e; + color: white; + font-weight: bold; + } + + .bonus { + border: 2px solid #facc15; + } + + .matched-bonus { + background-color: #facc15; + color: black; + font-weight: bold; + animation: pulse 1s infinite; + } + + .prize-badge { + background-color: #9333ea; + color: white; + border-radius: 9999px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + box-shadow: 0 0 6px rgba(0,0,0,0.2); + } + + .ball.bonus { + background-color: #d8b4fe; /* Light purple */ + } + + .ball.matched { + background-color: #4ade80; /* Green */ + animation: pulse 1s ease-in-out infinite; + } + + @keyframes pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.1); } + 100% { transform: scale(1); } + } \ No newline at end of file diff --git a/storage/db.go b/storage/db.go index a109bde..e4a43bd 100644 --- a/storage/db.go +++ b/storage/db.go @@ -31,6 +31,57 @@ func InitDB(filepath string) *sql.DB { log.Fatal("❌ Failed to create Thunderball table:", err) } + createThunderballPrizeTable := ` + CREATE TABLE IF NOT EXISTS prizes_thunderball ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_id INTEGER NOT NULL, + draw_date TEXT, + prize1 TEXT, + prize1_winners INTEGER, + prize1_per_winner INTEGER, + prize1_fund INTEGER, + prize2 TEXT, + prize2_winners INTEGER, + prize2_per_winner INTEGER, + prize2_fund INTEGER, + prize3 TEXT, + prize3_winners INTEGER, + prize3_per_winner INTEGER, + prize3_fund INTEGER, + prize4 TEXT, + prize4_winners INTEGER, + prize4_per_winner INTEGER, + prize4_fund INTEGER, + prize5 TEXT, + prize5_winners INTEGER, + prize5_per_winner INTEGER, + prize5_fund INTEGER, + prize6 TEXT, + prize6_winners INTEGER, + prize6_per_winner INTEGER, + prize6_fund INTEGER, + prize7 TEXT, + prize7_winners INTEGER, + prize7_per_winner INTEGER, + prize7_fund INTEGER, + prize8 TEXT, + prize8_winners INTEGER, + prize8_per_winner INTEGER, + prize8_fund INTEGER, + prize9 TEXT, + prize9_winners INTEGER, + prize9_per_winner INTEGER, + prize9_fund INTEGER, + total_winners INTEGER, + total_prize_fund INTEGER, + FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date) + );` + + _, err = db.Exec(createThunderballPrizeTable) + if err != nil { + log.Fatal("❌ Failed to create Thunderball prize table:", err) + } + createLottoResultsTable := ` CREATE TABLE IF NOT EXISTS results_lotto ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -69,6 +120,10 @@ func InitDB(filepath string) *sql.DB { purchase_method TEXT, image_path TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + matched_main INTEGER, + matched_bonus INTEGER, + prize_tier TEXT, + is_winner BOOLEAN, FOREIGN KEY (userId) REFERENCES users(id) );` diff --git a/storage/insert.go b/storage/insert.go index 617bf20..520fd2e 100644 --- a/storage/insert.go +++ b/storage/insert.go @@ -29,7 +29,7 @@ func InsertThunderballResult(db *sql.DB, res models.ThunderballResult) error { return err } -func InsertTicket(db *sql.DB, ticket models.MyTicket) error { +func InsertTicket(db *sql.DB, ticket models.Ticket) error { var bonus1Val interface{} var bonus2Val interface{} diff --git a/templates/account/tickets/my_tickets.html b/templates/account/tickets/my_tickets.html index 3e1d7ad..91f97de 100644 --- a/templates/account/tickets/my_tickets.html +++ b/templates/account/tickets/my_tickets.html @@ -23,17 +23,28 @@ {{ .GameType }} {{ .DrawDate }} - {{ .Ball1 }} {{ .Ball2 }} {{ .Ball3 }} {{ .Ball4 }} {{ .Ball5 }} - {{ if ne .Ball6 0 }} {{ .Ball6 }} {{ end }} +
+ {{ range .Balls }} +
{{ . }}
+ {{ end }} +
+ +
+ MatchedMain: {{ .MatchedMain }} | MatchedBonus: {{ .MatchedBonus }} +
- {{ if eq .GameType "Lotto" }} + {{ if eq .GameType "Lotto" }} {{ else if gt (intVal .Bonus2) 0 }} - {{ if gt (intVal .Bonus1) 0 }}{{ intVal .Bonus1 }}, {{ end }} - {{ intVal .Bonus2 }} +
+ {{ if gt (intVal .Bonus1) 0 }} +
{{ intVal .Bonus1 }}
+ {{ end }} +
{{ intVal .Bonus2 }}
+
{{ else if gt (intVal .Bonus1) 0 }} - {{ intVal .Bonus1 }} +
{{ intVal .Bonus1 }}
{{ else }} {{ end }} diff --git a/templates/admin/triggers.html b/templates/admin/triggers.html index 4ccfe3d..1643780 100644 --- a/templates/admin/triggers.html +++ b/templates/admin/triggers.html @@ -1,4 +1,16 @@ -
- {{ .CSRFField }} - -
\ No newline at end of file +{{ define "content" }} +

Manual Admin Triggers

+ +

This page lets you manually run backend processes like result matching.

+ +
+ {{ .CSRFField }} + +
+ +{{ if .Flash }} +

{{ .Flash }}

+{{ end }} +{{ end }} diff --git a/templates/layout.html b/templates/layout.html index 5a94285..df53c4d 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,5 +1,7 @@ {{ define "layout" }} + +