From c4c2d422349f43e200d8c43158a4cfc37a58bc1e Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Thu, 22 May 2025 14:26:06 +0200
Subject: [PATCH 1/9] Add lite webpage

---
 backend/lite.gohtml | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 backend/lite.gohtml

diff --git a/backend/lite.gohtml b/backend/lite.gohtml
new file mode 100644
index 0000000..e69de29
-- 
2.47.2


From adff57bf29aa9e70a74f5eabb097f3f4c4a25b50 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Thu, 22 May 2025 14:32:13 +0200
Subject: [PATCH 2/9] Add executable to gitignore

---
 backend/.gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/backend/.gitignore b/backend/.gitignore
index 0f580ed..23487f5 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -1,2 +1,3 @@
 *.db3
 *.db3-*
+/allowance_planner
-- 
2.47.2


From 6a415ce878049c6cf00cf94cca7ead1d00c3de9d Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Thu, 22 May 2025 14:32:43 +0200
Subject: [PATCH 3/9] Tidied models

---
 backend/go.sum | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/backend/go.sum b/backend/go.sum
index 964d5a6..0a43417 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -1,7 +1,3 @@
-gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0 h1:kl0VFgvm52UKxJhZpf1hvucxZdOoXY50g/VmzsWH+/8=
-gitea.seeseepuff.be/seeseemelk/mysqlite v0.12.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
-gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0 h1:nqSXu5i5fHB1rrx/kfi8Phn/J6eFa2yh02FiGc9U1yg=
-gitea.seeseepuff.be/seeseemelk/mysqlite v0.13.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0 h1:aRItVfUj48fBmuec7rm/jY9KCfvHW2VzJfItVk4t8sw=
 gitea.seeseepuff.be/seeseemelk/mysqlite v0.14.0/go.mod h1:cgswydOxJjMlNwfcBIXnKjr47LwXnMT9BInkiHb0tXE=
 github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4=
@@ -218,14 +214,10 @@ modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
 modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
 modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
-modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
 modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
 modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
-modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
-modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
 modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
-- 
2.47.2


From 19292ec746c28ef121e391f2e0fc061224da0720 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Thu, 22 May 2025 14:33:02 +0200
Subject: [PATCH 4/9] fixup! Add lite webpage

---
 backend/lite.gohtml |  9 +++++++++
 backend/main.go     | 10 +++++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/backend/lite.gohtml b/backend/lite.gohtml
index e69de29..9bb9e0d 100644
--- a/backend/lite.gohtml
+++ b/backend/lite.gohtml
@@ -0,0 +1,9 @@
+<html lang="en">
+<head>
+	<title>Allowance Planner 2000</title>
+</head>
+<body>
+<h1>Allowance Planner 2000</h1>
+<h2>Users</h2>
+</body>
+</html>
diff --git a/backend/main.go b/backend/main.go
index 49a8a0d..f496614 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -578,6 +578,10 @@ func getHistory(c *gin.Context) {
 	c.IndentedJSON(http.StatusOK, history)
 }
 
+func renderLite(c *gin.Context) {
+	c.HTML(http.StatusOK, "lite.gohtml", nil)
+}
+
 /*
 Initialises the database, and then starts the server.
 If the context gets cancelled, the server is shutdown and the database is closed.
@@ -592,6 +596,10 @@ func start(ctx context.Context, config *ServerConfig) {
 	corsConfig.AllowAllOrigins = true
 	router.Use(cors.New(corsConfig))
 
+	// Web endpoints
+	router.LoadHTMLFiles("lite.gohtml")
+	router.GET("/", renderLite)
+	// API endpoints
 	router.GET("/api/users", getUsers)
 	router.GET("/api/user/:userId", getUser)
 	router.POST("/api/user/:userId/history", postHistory)
@@ -641,7 +649,7 @@ func start(ctx context.Context, config *ServerConfig) {
 func main() {
 	config := ServerConfig{
 		Datasource: os.Getenv("DB_PATH"),
-		Addr:       ":8080",
+		Addr:       ":8081",
 	}
 	if config.Datasource == "" {
 		config.Datasource = "allowance_planner.db3"
-- 
2.47.2


From 5330cdd98888e91cadb98b4d2fc43cecaa93436c Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Thu, 22 May 2025 15:09:52 +0200
Subject: [PATCH 5/9] Somewhat working first page

---
 backend/lite.go     | 107 ++++++++++++++++++++++++++++++++++++++++++++
 backend/lite.gohtml |  79 ++++++++++++++++++++++++++++++++
 backend/main.go     |   4 --
 3 files changed, 186 insertions(+), 4 deletions(-)
 create mode 100644 backend/lite.go

diff --git a/backend/lite.go b/backend/lite.go
new file mode 100644
index 0000000..050c542
--- /dev/null
+++ b/backend/lite.go
@@ -0,0 +1,107 @@
+package main
+
+import (
+	"errors"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"strconv"
+)
+
+type ViewModel struct {
+	Users       []User
+	CurrentUser int
+	Allowances  []Allowance
+	Tasks       []Task
+	History     []History
+}
+
+func renderLite(c *gin.Context) {
+	if c.Query("user") != "" {
+		c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
+		c.Redirect(http.StatusFound, "/")
+		return
+	}
+
+	currentUserStr, err := c.Cookie("user")
+	if errors.Is(err, http.ErrNoCookie) {
+		renderNoUser(c)
+		return
+	}
+
+	if err != nil {
+		unsetUserCookie(c)
+		return
+	}
+	currentUser, err := strconv.Atoi(currentUserStr)
+	if err != nil {
+		unsetUserCookie(c)
+		return
+	}
+	userExists, err := db.UserExists(currentUser)
+	if !userExists || err != nil {
+		unsetUserCookie(c)
+		return
+	}
+	renderWithUser(c, currentUser)
+}
+
+func unsetUserCookie(c *gin.Context) {
+	c.SetCookie("user", "", -1, "/", "localhost", false, true)
+	c.Redirect(http.StatusFound, "/")
+}
+
+func renderNoUser(c *gin.Context) {
+	users, err := db.GetUsers()
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	c.HTML(http.StatusOK, "lite.gohtml", ViewModel{
+		Users: users,
+	})
+}
+
+func renderWithUser(c *gin.Context, currentUser int) {
+	users, err := db.GetUsers()
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	allowances, err := db.GetUserAllowances(currentUser)
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	tasks, err := db.GetTasks()
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	history, err := db.GetHistory(currentUser)
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	c.HTML(http.StatusOK, "lite.gohtml", ViewModel{
+		Users:       users,
+		CurrentUser: currentUser,
+		Allowances:  allowances,
+		Tasks:       tasks,
+		History:     history,
+	})
+}
diff --git a/backend/lite.gohtml b/backend/lite.gohtml
index 9bb9e0d..9d764b6 100644
--- a/backend/lite.gohtml
+++ b/backend/lite.gohtml
@@ -1,3 +1,4 @@
+{{- /*gotype: allowance_planner.ViewModel*/}}
 <html lang="en">
 <head>
 	<title>Allowance Planner 2000</title>
@@ -5,5 +6,83 @@
 <body>
 <h1>Allowance Planner 2000</h1>
 <h2>Users</h2>
+{{range .Users}}
+	{{if eq $.CurrentUser .ID}}
+		<strong>{{.Name}}</strong>
+	{{else}}
+		<a href="?user={{.ID}}">{{.Name}}</a>
+	{{end}}
+{{end}}
+
+{{if ne .CurrentUser 0}}
+	<h2>Allowances</h2>
+	<table border="1">
+		<thead>
+		<tr>
+			<th>Name</th>
+			<th>Progress</th>
+			<th>Target</th>
+			<th>Weight</th>
+		</tr>
+		</thead>
+		<tbody>
+		{{range .Allowances}}
+			{{if eq .ID 0}}
+				<tr>
+					<td>Total</td>
+					<td>{{.Progress}}</td>
+					<td></td>
+					<td>{{.Weight}}</td>
+				</tr>
+			{{else}}
+				<tr>
+					<td>{{.Name}}</td>
+					<td>{{.Progress}}</td>
+					<td>{{.Target}}</td>
+					<td>{{.Weight}}</td>
+				</tr>
+			{{end}}
+		{{end}}
+		</tbody>
+	</table>
+
+	<h2>Tasks</h2>
+	<table border="1">
+		<thead>
+		<tr>
+			<th>Name</th>
+			<th>Assigned</th>
+			<th>Reward</th>
+		</tr>
+		</thead>
+		<tbody>
+		{{range .Tasks}}
+			<tr>
+				<td>{{.Name}}</td>
+				<td>{{.Assigned}}</td>
+				<td>{{.Reward}}</td>
+			</tr>
+		{{end}}
+		</tbody>
+	</table>
+
+	<h2>History</h2>
+	<table border="1">
+		<thead>
+		<tr>
+			<th>Timestamp</th>
+			<th>Allowance</th>
+		</tr>
+		</thead>
+		<tbody>
+		{{range .History}}
+			<tr>
+				<td>{{.Timestamp}}</td>
+				<td>{{.Allowance}}</td>
+			</tr>
+		{{end}}
+		</tbody>
+	</table>
+{{end}}
 </body>
 </html>
diff --git a/backend/main.go b/backend/main.go
index f496614..0e84a94 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -578,10 +578,6 @@ func getHistory(c *gin.Context) {
 	c.IndentedJSON(http.StatusOK, history)
 }
 
-func renderLite(c *gin.Context) {
-	c.HTML(http.StatusOK, "lite.gohtml", nil)
-}
-
 /*
 Initialises the database, and then starts the server.
 If the context gets cancelled, the server is shutdown and the database is closed.
-- 
2.47.2


From 885455454c5bfa094f33e65a311b8399734b1a11 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Fri, 23 May 2025 18:37:44 +0200
Subject: [PATCH 6/9] Fix compile error

---
 backend/api_test.go | 32 --------------------------------
 1 file changed, 32 deletions(-)

diff --git a/backend/api_test.go b/backend/api_test.go
index f1b6f46..e21c0e7 100644
--- a/backend/api_test.go
+++ b/backend/api_test.go
@@ -623,38 +623,6 @@ func TestCompleteTaskInvalidId(t *testing.T) {
 	e.POST("/task/999/complete").Expect().Status(404)
 }
 
-func TestCompleteTaskAllowanceWeightsSumTo0(t *testing.T) {
-	e := startServer(t)
-	taskId := createTestTaskWithAmount(e, 101)
-
-	e.GET("/tasks").Expect().Status(200).JSON().Array().Length().IsEqual(1)
-
-	// Update rest allowance
-	e.PUT("/user/1/allowance/0").WithJSON(UpdateAllowanceRequest{
-		Weight: 0,
-	}).Expect().Status(200)
-	// Create two allowance goals
-	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
-		Name:   "Test Allowance 1",
-		Target: 1000,
-		Weight: 0,
-	}).Expect().Status(201)
-
-	// Complete the task
-	e.POST("/task/" + strconv.Itoa(taskId) + "/complete").Expect().Status(200)
-
-	// Verify the task is marked as completed
-	e.GET("/task/" + strconv.Itoa(taskId)).Expect().Status(404)
-
-	// Verify the allowances are updated for user 1
-	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
-	allowances.Length().IsEqual(2)
-	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
-	allowances.Value(0).Object().Value("progress").Number().IsEqual(101)
-	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
-	allowances.Value(1).Object().Value("progress").Number().IsEqual(0)
-}
-
 func TestCompleteAllowance(t *testing.T) {
 	e := startServer(t)
 	createTestTaskWithAmount(e, 100)
-- 
2.47.2


From 2b6dad709b56af625c5bc478a2225fd779b04a3b Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Sat, 24 May 2025 06:11:30 +0200
Subject: [PATCH 7/9] Working on it

---
 backend/main.go                     |  3 +-
 backend/{lite.go => web.go}         | 71 +++++++++++++++++++++++++++--
 backend/{lite.gohtml => web.gohtml} | 44 +++++++++++-------
 3 files changed, 94 insertions(+), 24 deletions(-)
 rename backend/{lite.go => web.go} (56%)
 rename backend/{lite.gohtml => web.gohtml} (65%)

diff --git a/backend/main.go b/backend/main.go
index 0e84a94..1375cd1 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -593,8 +593,7 @@ func start(ctx context.Context, config *ServerConfig) {
 	router.Use(cors.New(corsConfig))
 
 	// Web endpoints
-	router.LoadHTMLFiles("lite.gohtml")
-	router.GET("/", renderLite)
+	loadWebEndpoints(router)
 	// API endpoints
 	router.GET("/api/users", getUsers)
 	router.GET("/api/user/:userId", getUser)
diff --git a/backend/lite.go b/backend/web.go
similarity index 56%
rename from backend/lite.go
rename to backend/web.go
index 050c542..97b906a 100644
--- a/backend/lite.go
+++ b/backend/web.go
@@ -15,13 +15,21 @@ type ViewModel struct {
 	History     []History
 }
 
-func renderLite(c *gin.Context) {
+func loadWebEndpoints(router *gin.Engine) {
+	router.LoadHTMLFiles("web.gohtml")
+	router.GET("/", renderIndex)
+	router.GET("/login", renderLogin)
+	router.POST("/createTask", renderCreateTask)
+}
+
+func renderLogin(c *gin.Context) {
 	if c.Query("user") != "" {
 		c.SetCookie("user", c.Query("user"), 3600, "/", "localhost", false, true)
-		c.Redirect(http.StatusFound, "/")
-		return
 	}
+	c.Redirect(http.StatusFound, "/")
+}
 
+func renderIndex(c *gin.Context) {
 	currentUserStr, err := c.Cookie("user")
 	if errors.Is(err, http.ErrNoCookie) {
 		renderNoUser(c)
@@ -45,6 +53,59 @@ func renderLite(c *gin.Context) {
 	renderWithUser(c, currentUser)
 }
 
+func renderCreateTask(c *gin.Context) {
+	currentUserStr, err := c.Cookie("user")
+	if errors.Is(err, http.ErrNoCookie) {
+		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
+			"error": "User not logged in",
+		})
+		return
+	}
+	if err != nil {
+		unsetUserCookie(c)
+		return
+	}
+	currentUser, err := strconv.Atoi(currentUserStr)
+	if err != nil {
+		unsetUserCookie(c)
+		return
+	}
+	userExists, err := db.UserExists(currentUser)
+	if !userExists || err != nil {
+		unsetUserCookie(c)
+		return
+	}
+
+	name := c.PostForm("name")
+	rewardStr := c.PostForm("reward")
+	reward, err := strconv.Atoi(rewardStr)
+	if err != nil {
+		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
+			"error": "Invalid reward value",
+		})
+		return
+	}
+	if name == "" || reward <= 0 {
+		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
+			"error": "Name and reward must be provided",
+		})
+		return
+	}
+
+	_, err = db.CreateTask(&CreateTaskRequest{
+		Name:   name,
+		Reward: reward,
+	})
+	if err != nil {
+		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	c.Redirect(http.StatusFound, "/")
+}
+
 func unsetUserCookie(c *gin.Context) {
 	c.SetCookie("user", "", -1, "/", "localhost", false, true)
 	c.Redirect(http.StatusFound, "/")
@@ -59,7 +120,7 @@ func renderNoUser(c *gin.Context) {
 		return
 	}
 
-	c.HTML(http.StatusOK, "lite.gohtml", ViewModel{
+	c.HTML(http.StatusOK, "web.gohtml", ViewModel{
 		Users: users,
 	})
 }
@@ -97,7 +158,7 @@ func renderWithUser(c *gin.Context, currentUser int) {
 		return
 	}
 
-	c.HTML(http.StatusOK, "lite.gohtml", ViewModel{
+	c.HTML(http.StatusOK, "web.gohtml", ViewModel{
 		Users:       users,
 		CurrentUser: currentUser,
 		Allowances:  allowances,
diff --git a/backend/lite.gohtml b/backend/web.gohtml
similarity index 65%
rename from backend/lite.gohtml
rename to backend/web.gohtml
index 9d764b6..11272a5 100644
--- a/backend/lite.gohtml
+++ b/backend/web.gohtml
@@ -10,7 +10,7 @@
 	{{if eq $.CurrentUser .ID}}
 		<strong>{{.Name}}</strong>
 	{{else}}
-		<a href="?user={{.ID}}">{{.Name}}</a>
+		<a href="/login?user={{.ID}}">{{.Name}}</a>
 	{{end}}
 {{end}}
 
@@ -47,24 +47,34 @@
 	</table>
 
 	<h2>Tasks</h2>
-	<table border="1">
-		<thead>
-		<tr>
-			<th>Name</th>
-			<th>Assigned</th>
-			<th>Reward</th>
-		</tr>
-		</thead>
-		<tbody>
-		{{range .Tasks}}
+	<form method="post" action="/createTask">
+		<table border="1">
+			<thead>
 			<tr>
-				<td>{{.Name}}</td>
-				<td>{{.Assigned}}</td>
-				<td>{{.Reward}}</td>
+				<th>Name</th>
+				<th>Assigned</th>
+				<th>Reward</th>
 			</tr>
-		{{end}}
-		</tbody>
-	</table>
+			</thead>
+			<tbody>
+			{{range .Tasks}}
+				<tr>
+					<td>{{.Name}}</td>
+					<td>{{.Assigned}}</td>
+					<td>{{.Reward}}</td>
+				</tr>
+			{{end}}
+					<tr>
+						<td><label><input type="text" placeholder="Name"></label></td>
+						<td></td>
+						<td>
+							<label><input type="number" placeholder="Reward"></label>
+							<button>Create</button>
+						</td>
+					</tr>
+			</tbody>
+		</table>
+	</form>
 
 	<h2>History</h2>
 	<table border="1">
-- 
2.47.2


From 44e148fbb8e096ba02248da2d0b4033ae49c1b7e Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Sat, 24 May 2025 06:11:35 +0200
Subject: [PATCH 8/9] First functional lite web thingy

---
 backend/api_test.go |  10 +--
 backend/db.go       |  10 ++-
 backend/web.go      | 184 +++++++++++++++++++++++++++++---------------
 backend/web.gohtml  | 182 +++++++++++++++++++++++++------------------
 4 files changed, 242 insertions(+), 144 deletions(-)

diff --git a/backend/api_test.go b/backend/api_test.go
index e21c0e7..4bf31d4 100644
--- a/backend/api_test.go
+++ b/backend/api_test.go
@@ -549,12 +549,12 @@ func TestCompleteTask(t *testing.T) {
 	// Create two allowance goals
 	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
 		Name:   "Test Allowance 1",
-		Target: 1000,
+		Target: 100,
 		Weight: 50,
 	}).Expect().Status(201)
 	e.POST("/user/1/allowance").WithJSON(CreateAllowanceRequest{
 		Name:   "Test Allowance 1",
-		Target: 1000,
+		Target: 10,
 		Weight: 25,
 	}).Expect().Status(201)
 
@@ -568,11 +568,11 @@ func TestCompleteTask(t *testing.T) {
 	allowances := e.GET("/user/1/allowance").Expect().Status(200).JSON().Array()
 	allowances.Length().IsEqual(3)
 	allowances.Value(0).Object().Value("id").Number().IsEqual(0)
-	allowances.Value(0).Object().Value("progress").Number().IsEqual(26)
+	allowances.Value(0).Object().Value("progress").Number().IsEqual(31)
 	allowances.Value(1).Object().Value("id").Number().IsEqual(1)
-	allowances.Value(1).Object().Value("progress").Number().IsEqual(50)
+	allowances.Value(1).Object().Value("progress").Number().IsEqual(60)
 	allowances.Value(2).Object().Value("id").Number().IsEqual(2)
-	allowances.Value(2).Object().Value("progress").Number().IsEqual(25)
+	allowances.Value(2).Object().Value("progress").Number().IsEqual(10)
 
 	// And also for user 2
 	allowances = e.GET("/user/2/allowance").Expect().Status(200).JSON().Array()
diff --git a/backend/db.go b/backend/db.go
index fe02ac2..0aa5570 100644
--- a/backend/db.go
+++ b/backend/db.go
@@ -416,16 +416,20 @@ func (db *Db) CompleteTask(taskId int) error {
 
 		if sumOfWeights > 0 {
 			// Distribute the reward to the allowances
-			for allowanceRow := range tx.Query("select id, weight from allowances where user_id = ? and weight > 0").Bind(userId).Range(&err) {
-				var allowanceId int
+			for allowanceRow := range tx.Query("select id, weight, target, balance from allowances where user_id = ? and weight > 0 order by (target - balance) asc").Bind(userId).Range(&err) {
+				var allowanceId, allowanceTarget, allowanceBalance int
 				var allowanceWeight float64
-				err = allowanceRow.Scan(&allowanceId, &allowanceWeight)
+				err = allowanceRow.Scan(&allowanceId, &allowanceWeight, &allowanceTarget, &allowanceBalance)
 				if err != nil {
 					return err
 				}
 
 				// Calculate the amount to add to the allowance
 				amount := int((allowanceWeight / sumOfWeights) * float64(remainingReward))
+				if allowanceBalance+amount > allowanceTarget {
+					// If the amount reaches past the target, set it to the target
+					amount = allowanceTarget - allowanceBalance
+				}
 				sumOfWeights -= allowanceWeight
 				err = tx.Query("update allowances set balance = balance + ? where id = ? and user_id = ?").
 					Bind(amount, allowanceId, userId).Exec()
diff --git a/backend/web.go b/backend/web.go
index 97b906a..12abbd0 100644
--- a/backend/web.go
+++ b/backend/web.go
@@ -13,6 +13,7 @@ type ViewModel struct {
 	Allowances  []Allowance
 	Tasks       []Task
 	History     []History
+	Error       string
 }
 
 func loadWebEndpoints(router *gin.Engine) {
@@ -20,6 +21,9 @@ func loadWebEndpoints(router *gin.Engine) {
 	router.GET("/", renderIndex)
 	router.GET("/login", renderLogin)
 	router.POST("/createTask", renderCreateTask)
+	router.GET("/completeTask", renderCompleteTask)
+	router.POST("/createAllowance", renderCreateAllowance)
+	router.GET("/completeAllowance", renderCompleteAllowance)
 }
 
 func renderLogin(c *gin.Context) {
@@ -30,49 +34,16 @@ func renderLogin(c *gin.Context) {
 }
 
 func renderIndex(c *gin.Context) {
-	currentUserStr, err := c.Cookie("user")
-	if errors.Is(err, http.ErrNoCookie) {
-		renderNoUser(c)
+	currentUser := getCurrentUser(c)
+	if currentUser == nil {
 		return
 	}
-
-	if err != nil {
-		unsetUserCookie(c)
-		return
-	}
-	currentUser, err := strconv.Atoi(currentUserStr)
-	if err != nil {
-		unsetUserCookie(c)
-		return
-	}
-	userExists, err := db.UserExists(currentUser)
-	if !userExists || err != nil {
-		unsetUserCookie(c)
-		return
-	}
-	renderWithUser(c, currentUser)
+	renderWithUser(c, *currentUser)
 }
 
 func renderCreateTask(c *gin.Context) {
-	currentUserStr, err := c.Cookie("user")
-	if errors.Is(err, http.ErrNoCookie) {
-		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
-			"error": "User not logged in",
-		})
-		return
-	}
-	if err != nil {
-		unsetUserCookie(c)
-		return
-	}
-	currentUser, err := strconv.Atoi(currentUserStr)
-	if err != nil {
-		unsetUserCookie(c)
-		return
-	}
-	userExists, err := db.UserExists(currentUser)
-	if !userExists || err != nil {
-		unsetUserCookie(c)
+	currentUser := getCurrentUser(c)
+	if currentUser == nil {
 		return
 	}
 
@@ -80,15 +51,11 @@ func renderCreateTask(c *gin.Context) {
 	rewardStr := c.PostForm("reward")
 	reward, err := strconv.Atoi(rewardStr)
 	if err != nil {
-		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
-			"error": "Invalid reward value",
-		})
+		renderError(c, http.StatusBadRequest, err)
 		return
 	}
 	if name == "" || reward <= 0 {
-		c.HTML(http.StatusBadRequest, "error.gohtml", gin.H{
-			"error": "Name and reward must be provided",
-		})
+		renderError(c, http.StatusBadRequest, err)
 		return
 	}
 
@@ -97,15 +64,112 @@ func renderCreateTask(c *gin.Context) {
 		Reward: reward,
 	})
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
 	c.Redirect(http.StatusFound, "/")
 }
 
+func renderCompleteTask(c *gin.Context) {
+	taskIDStr := c.Query("task")
+	taskID, err := strconv.Atoi(taskIDStr)
+	if err != nil {
+		renderError(c, http.StatusBadRequest, err)
+		return
+	}
+
+	err = db.CompleteTask(taskID)
+	if err != nil {
+		renderError(c, http.StatusInternalServerError, err)
+		return
+	}
+
+	c.Redirect(http.StatusFound, "/")
+}
+
+func renderCreateAllowance(c *gin.Context) {
+	currentUser := getCurrentUser(c)
+	if currentUser == nil {
+		return
+	}
+
+	name := c.PostForm("name")
+	targetStr := c.PostForm("target")
+	target, err := strconv.Atoi(targetStr)
+	if err != nil {
+		renderError(c, http.StatusBadRequest, err)
+		return
+	}
+	weightStr := c.PostForm("weight")
+	weight, err := strconv.ParseFloat(weightStr, 64)
+	if err != nil {
+		renderError(c, http.StatusBadRequest, err)
+		return
+	}
+	if name == "" || target <= 0 || weight <= 0 {
+		renderError(c, http.StatusBadRequest, err)
+		return
+	}
+
+	_, err = db.CreateAllowance(*currentUser, &CreateAllowanceRequest{
+		Name:   name,
+		Target: target,
+		Weight: weight,
+	})
+	if err != nil {
+		renderError(c, http.StatusInternalServerError, err)
+		return
+	}
+
+	c.Redirect(http.StatusFound, "/")
+}
+
+func renderCompleteAllowance(c *gin.Context) {
+	currentUser := getCurrentUser(c)
+	if currentUser == nil {
+		return
+	}
+
+	allowanceIDStr := c.Query("allowance")
+	allowanceID, err := strconv.Atoi(allowanceIDStr)
+	if err != nil {
+		renderError(c, http.StatusBadRequest, err)
+		return
+	}
+
+	err = db.CompleteAllowance(*currentUser, allowanceID)
+	if err != nil {
+		renderError(c, http.StatusInternalServerError, err)
+		return
+	}
+
+	c.Redirect(http.StatusFound, "/")
+}
+
+func getCurrentUser(c *gin.Context) *int {
+	currentUserStr, err := c.Cookie("user")
+	if errors.Is(err, http.ErrNoCookie) {
+		renderNoUser(c)
+		return nil
+	}
+	if err != nil {
+		unsetUserCookie(c)
+		return nil
+	}
+	currentUser, err := strconv.Atoi(currentUserStr)
+	if err != nil {
+		unsetUserCookie(c)
+		return nil
+	}
+	userExists, err := db.UserExists(currentUser)
+	if !userExists || err != nil {
+		unsetUserCookie(c)
+		return nil
+	}
+	return &currentUser
+}
+
 func unsetUserCookie(c *gin.Context) {
 	c.SetCookie("user", "", -1, "/", "localhost", false, true)
 	c.Redirect(http.StatusFound, "/")
@@ -114,9 +178,7 @@ func unsetUserCookie(c *gin.Context) {
 func renderNoUser(c *gin.Context) {
 	users, err := db.GetUsers()
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
@@ -128,33 +190,25 @@ func renderNoUser(c *gin.Context) {
 func renderWithUser(c *gin.Context, currentUser int) {
 	users, err := db.GetUsers()
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
 	allowances, err := db.GetUserAllowances(currentUser)
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
 	tasks, err := db.GetTasks()
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
 	history, err := db.GetHistory(currentUser)
 	if err != nil {
-		c.HTML(http.StatusInternalServerError, "error.gohtml", gin.H{
-			"error": err.Error(),
-		})
+		renderError(c, http.StatusInternalServerError, err)
 		return
 	}
 
@@ -166,3 +220,9 @@ func renderWithUser(c *gin.Context, currentUser int) {
 		History:     history,
 	})
 }
+
+func renderError(c *gin.Context, statusCode int, err error) {
+	c.HTML(statusCode, "web.gohtml", ViewModel{
+		Error: err.Error(),
+	})
+}
diff --git a/backend/web.gohtml b/backend/web.gohtml
index 11272a5..1ed49ee 100644
--- a/backend/web.gohtml
+++ b/backend/web.gohtml
@@ -2,97 +2,131 @@
 <html lang="en">
 <head>
 	<title>Allowance Planner 2000</title>
+	<style>
+		tr:hover {
+			background-color: #f0f0f0;
+		}
+	</style>
 </head>
 <body>
 <h1>Allowance Planner 2000</h1>
-<h2>Users</h2>
-{{range .Users}}
-	{{if eq $.CurrentUser .ID}}
-		<strong>{{.Name}}</strong>
-	{{else}}
-		<a href="/login?user={{.ID}}">{{.Name}}</a>
-	{{end}}
-{{end}}
 
-{{if ne .CurrentUser 0}}
-	<h2>Allowances</h2>
-	<table border="1">
-		<thead>
-		<tr>
-			<th>Name</th>
-			<th>Progress</th>
-			<th>Target</th>
-			<th>Weight</th>
-		</tr>
-		</thead>
-		<tbody>
-		{{range .Allowances}}
-			{{if eq .ID 0}}
-				<tr>
-					<td>Total</td>
-					<td>{{.Progress}}</td>
-					<td></td>
-					<td>{{.Weight}}</td>
-				</tr>
-			{{else}}
-				<tr>
-					<td>{{.Name}}</td>
-					<td>{{.Progress}}</td>
-					<td>{{.Target}}</td>
-					<td>{{.Weight}}</td>
-				</tr>
-			{{end}}
+{{if ne .Error ""}}
+	<h2>Error</h2>
+	<p>{{.Error}}</p>
+{{else}}
+	<h2>Users</h2>
+	{{range .Users}}
+		{{if eq $.CurrentUser .ID}}
+			<strong>{{.Name}}</strong>
+		{{else}}
+			<a href="/login?user={{.ID}}">{{.Name}}</a>
 		{{end}}
-		</tbody>
-	</table>
+	{{end}}
 
-	<h2>Tasks</h2>
-	<form method="post" action="/createTask">
+	{{if ne .CurrentUser 0}}
+		<h2>Allowances</h2>
+		<form action="/createAllowance" method="post">
+			<table border="1">
+				<thead>
+				<tr>
+					<th>Name</th>
+					<th>Progress</th>
+					<th>Target</th>
+					<th>Weight</th>
+					<th>Actions</th>
+				</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td><label><input type="text" name="name" placeholder="Name"></label></td>
+						<td></td>
+						<td><label><input type="number" name="target" placeholder="Target"></label></td>
+						<td><label><input type="number" name="weight" placeholder="Weight"></label></td>
+						<td><button>Create</button></td>
+					</tr>
+				{{range .Allowances}}
+					{{if eq .ID 0}}
+						<tr>
+							<td>Total</td>
+							<td>{{.Progress}}</td>
+							<td></td>
+							<td>{{.Weight}}</td>
+						</tr>
+					{{else}}
+						<tr>
+							<td>{{.Name}}</td>
+							<td><progress max="{{.Target}}" value="{{.Progress}}"></progress> ({{.Progress}})</td>
+							<td>{{.Target}}</td>
+							<td>{{.Weight}}</td>
+                            {{if ge .Progress .Target}}
+								<td>
+									<a href="/completeAllowance?allowance={{.ID}}">Mark as completed</a>
+								</td>
+                            {{end}}
+						</tr>
+					{{end}}
+				{{end}}
+				</tbody>
+			</table>
+		</form>
+
+		<h2>Tasks</h2>
+		<form method="post" action="/createTask">
+			<table border="1">
+				<thead>
+				<tr>
+					<th>Name</th>
+					<th>Assigned</th>
+					<th>Reward</th>
+					<th>Actions</th>
+				</tr>
+				</thead>
+				<tbody>
+				{{range .Tasks}}
+					<tr>
+						<td>{{.Name}}</td>
+						<td>
+							{{if eq .Assigned nil}}
+								None
+							{{else}}
+								{{.Assigned}}
+							{{end}}
+						</td>
+						<td>{{.Reward}}</td>
+						<td>
+							<a href="/completeTask?task={{.ID}}">Mark as completed</a>
+						</td>
+					</tr>
+				{{end}}
+						<tr>
+							<td><label><input type="text" name="name" placeholder="Name"></label></td>
+							<td></td>
+							<td><label><input type="number" name="reward" placeholder="Reward"></label></td>
+							<td><button>Create</button></td>
+						</tr>
+				</tbody>
+			</table>
+		</form>
+
+		<h2>History</h2>
 		<table border="1">
 			<thead>
 			<tr>
-				<th>Name</th>
-				<th>Assigned</th>
-				<th>Reward</th>
+				<th>Timestamp</th>
+				<th>Allowance</th>
 			</tr>
 			</thead>
 			<tbody>
-			{{range .Tasks}}
+			{{range .History}}
 				<tr>
-					<td>{{.Name}}</td>
-					<td>{{.Assigned}}</td>
-					<td>{{.Reward}}</td>
+					<td>{{.Timestamp}}</td>
+					<td>{{.Allowance}}</td>
 				</tr>
 			{{end}}
-					<tr>
-						<td><label><input type="text" placeholder="Name"></label></td>
-						<td></td>
-						<td>
-							<label><input type="number" placeholder="Reward"></label>
-							<button>Create</button>
-						</td>
-					</tr>
 			</tbody>
 		</table>
-	</form>
-
-	<h2>History</h2>
-	<table border="1">
-		<thead>
-		<tr>
-			<th>Timestamp</th>
-			<th>Allowance</th>
-		</tr>
-		</thead>
-		<tbody>
-		{{range .History}}
-			<tr>
-				<td>{{.Timestamp}}</td>
-				<td>{{.Allowance}}</td>
-			</tr>
-		{{end}}
-		</tbody>
-	</table>
+	{{end}}
 {{end}}
 </body>
 </html>
-- 
2.47.2


From 2e28ac0dae832c4a7d736350b839230f5dc2ddc2 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Sat, 24 May 2025 06:11:37 +0200
Subject: [PATCH 9/9] Revert port

---
 backend/main.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/main.go b/backend/main.go
index 1375cd1..e584eb6 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -644,7 +644,7 @@ func start(ctx context.Context, config *ServerConfig) {
 func main() {
 	config := ServerConfig{
 		Datasource: os.Getenv("DB_PATH"),
-		Addr:       ":8081",
+		Addr:       ":8080",
 	}
 	if config.Datasource == "" {
 		config.Datasource = "allowance_planner.db3"
-- 
2.47.2