Skip to content

Commit 6f1a1df

Browse files
authored
Merge pull request #13 from Tanq16/relnew
code quality, remove cli client, logging, curr/categ edit funcs
2 parents d8fbaeb + 704ad39 commit 6f1a1df

File tree

7 files changed

+216
-313
lines changed

7 files changed

+216
-313
lines changed

README.md

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@
2222

2323
# Why Create This?
2424

25-
There are a ton of amazing projects for expense tracking across GitHub ([Actual](https://github.com/actualbudget/actual), [Firefly III](https://github.com/firefly-iii/firefly-iii), etc.). They're all incredible, but they aren't the *fastest* when trying to add expenses and offer many features I don't use. Some use varying formats of data or complex APIs. *Don't get me wrong*, they're great when needed, but I wanted something dead simple that only gives me a monthly pie chart and a tabular representation. NOTHING else!
25+
There are a ton of amazing projects for expense tracking across GitHub ([Actual](https://github.com/actualbudget/actual), [Firefly III](https://github.com/firefly-iii/firefly-iii), etc.). They're all incredible, but they aren't the *fastest* when trying to add expenses and offer many features I don't use. Some use varying formats of data or complex budgeting or complex APIs. *Don't get me wrong*, they're incredible when fully utilized, but I wanted something dead simple that only gives me a monthly pie chart and a tabular representation. NOTHING else!
2626

27-
Hence, I created this project, which I use in my home lab to track my expenses. The data is just JSON, so I can do whatever I want with it, including using `jq` to convert to CSV. The UI is elegant and mobile-friendly.
28-
29-
This app's intention is to track spending across your categories in a simplistic manner. There is no complicated searching or editing - just add, delete, and view! This intention will not change throughout the project's lifecycle. This is not an app for budgeting; it's for tracking.
27+
So, I created this project, which I use in my home lab to track my expenses. This app's intention is to track spending across your categories (custom or pre-defined) in a simplistic manner. There is no complicated searching or editing - just `add`, `delete`, and `view`! This intention will not change throughout the project's lifecycle. This is *not* an app for budgeting; it's for straightforward tracking.
3028

3129
# Features
3230

@@ -39,7 +37,6 @@ This app's intention is to track spending across your categories in a simplistic
3937
- REST API for expense management
4038
- Single-user focused (mainly for a home lab deployment)
4139
- CSV export of all expense data from the UI
42-
- CLI for both server and client (if needed) operations
4340
- Custom categories via environment variable (`EXPENSE_CATEGORIES`) with sensible defaults
4441
- Custom currency symbol in the frontend via environment variable (`CURRENCY`)
4542

@@ -68,9 +65,9 @@ I reiterate that you should use this to add expenses quickly. The default name f
6865

6966
In the ideal case, `enter the amount and choose the category` - that's it!
7067

71-
For a bit more involved case, `enter the amount and name, choose the category, and select the date` - still very simple!
68+
For a bit more involved case, `enter the name, choose the category, enter the amount, and select the date` - still very simple!
7269

73-
The application only allows addition and deletion; there's no need for editing. There are no tags, wallet info, budgeting, or anything else! Plain and simple for the win.
70+
The application only allows addition and deletion; there's no need for editing (if needed, just delete and re-add). There are no tags, wallet info, budgeting, or anything else! Plain and simple for the win.
7471

7572
# Screenshots
7673

@@ -148,30 +145,20 @@ Ideally, once deployed, use the web interface and you're good to go. Access the
148145
149146
If command-line automations are required for use with the REST API, read on!
150147

151-
### CLI Mode
152-
153-
The application binary can run in either server or client mode:
148+
### Executable
154149

155-
Server Mode (Default):
150+
The application binary can be run directly within CLI for any common OS and architecture:
156151

157152
```bash
158153
./expenseowl
159-
# or explicitly
160-
./expenseowl -serve
161154
# or from a custom directory
162155
./expenseowl -data /custom/path
163156
```
164157

165-
Client Mode:
166-
167-
```bash
168-
./expenseowl -client -addr localhost:8080
169-
```
170-
171-
In client mode, you'll be prompted to enter the expense name, category (select from a list), amount, and date (in YYYY-MM-DD; optional, sets to the current date when not provided).
172-
173158
### REST API
174159

160+
ExpenseOwl provides an API to allow adding expenses via automations or simply via cURL, Siri Shortcuts, or other automations.
161+
175162
Add Expense:
176163

177164
```bash

cmd/expenseowl/main.go

Lines changed: 7 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,28 @@
11
package main
22

33
import (
4-
"bufio"
5-
"bytes"
6-
"encoding/json"
74
"flag"
8-
"fmt"
95
"log"
106
"net/http"
11-
"os"
127
"path/filepath"
13-
"strconv"
14-
"strings"
15-
"time"
168

179
"github.com/tanq16/expenseowl/internal/api"
1810
"github.com/tanq16/expenseowl/internal/config"
19-
"github.com/tanq16/expenseowl/internal/storage/jsonfile"
11+
"github.com/tanq16/expenseowl/internal/storage"
2012
"github.com/tanq16/expenseowl/internal/web"
2113
)
2214

2315
func runServer(dataPath string) {
2416
cfg := config.NewConfig(dataPath)
25-
if err := os.MkdirAll(cfg.StoragePath, 0755); err != nil {
26-
log.Fatalf("Failed to create data directory: %v", err)
27-
}
28-
storage, err := jsonfile.New(filepath.Join(cfg.StoragePath, "expenses.json"))
17+
storage, err := storage.New(filepath.Join(cfg.StoragePath, "expenses.json"))
2918
if err != nil {
3019
log.Fatalf("Failed to initialize storage: %v", err)
3120
}
3221

3322
handler := api.NewHandler(storage, cfg)
3423
http.HandleFunc("/categories", handler.GetCategories)
24+
http.HandleFunc("/categories/edit", handler.EditCategories)
25+
http.HandleFunc("/currency", handler.EditCurrency)
3526
http.HandleFunc("/expense", handler.AddExpense)
3627
http.HandleFunc("/expenses", handler.GetExpenses)
3728
http.HandleFunc("/table", handler.ServeTableView)
@@ -49,123 +40,19 @@ func runServer(dataPath string) {
4940
}
5041
w.Header().Set("Content-Type", "text/html")
5142
if err := web.ServeTemplate(w, "index.html"); err != nil {
43+
log.Printf("HTTP ERROR: Failed to serve template: %v", err)
5244
http.Error(w, "Failed to serve template", http.StatusInternalServerError)
5345
return
5446
}
5547
})
56-
log.Printf("Starting server on port %s...", cfg.ServerPort)
48+
log.Printf("Starting server on port %s...\n", cfg.ServerPort)
5749
if err := http.ListenAndServe(":"+cfg.ServerPort, nil); err != nil {
5850
log.Fatalf("Server failed to start: %v", err)
5951
}
6052
}
6153

62-
func readNonEmptyInput(prompt string) string {
63-
reader := bufio.NewReader(os.Stdin)
64-
for {
65-
fmt.Print(prompt)
66-
input, _ := reader.ReadString('\n')
67-
input = strings.TrimSpace(input)
68-
if input != "" {
69-
return input
70-
}
71-
fmt.Println("This field is required. Please try again.")
72-
}
73-
}
74-
75-
func getCategory(cfg *config.Config) string {
76-
fmt.Println("\nAvailable categories:")
77-
for i, category := range cfg.Categories {
78-
fmt.Printf("%d. %s\n", i+1, category)
79-
}
80-
for {
81-
fmt.Print("\nSelect category: ")
82-
reader := bufio.NewReader(os.Stdin)
83-
input, _ := reader.ReadString('\n')
84-
input = strings.TrimSpace(input)
85-
if num, err := strconv.Atoi(input); err == nil && num >= 1 && num <= len(cfg.Categories) {
86-
return cfg.Categories[num-1]
87-
}
88-
fmt.Println("Invalid selection. Please try again.")
89-
}
90-
}
91-
92-
func getAmount() float64 {
93-
for {
94-
input := readNonEmptyInput("Enter amount: ")
95-
amount, err := strconv.ParseFloat(input, 64)
96-
if err == nil && amount > 0 {
97-
return amount
98-
}
99-
fmt.Println("Invalid amount. Please enter a positive number.")
100-
}
101-
}
102-
103-
func getDate() time.Time {
104-
reader := bufio.NewReader(os.Stdin)
105-
fmt.Print("Enter date (YYYY-MM-DD, press Enter for today): ")
106-
input, _ := reader.ReadString('\n')
107-
input = strings.TrimSpace(input)
108-
if input == "" {
109-
return time.Now()
110-
}
111-
currTime := time.Now()
112-
if date, err := time.ParseInLocation("2006-01-02", input, time.Local); err == nil {
113-
return time.Date(
114-
date.Year(), date.Month(), date.Day(),
115-
currTime.Hour(), currTime.Minute(), currTime.Second(), currTime.Nanosecond(),
116-
time.Local,
117-
)
118-
}
119-
fmt.Println("Invalid date format, using current time.")
120-
return time.Now()
121-
}
122-
123-
func runClient(serverAddr string) {
124-
cfg := config.NewConfig("data") // Default data path hardcoded for client
125-
name := readNonEmptyInput("Enter expense name: ")
126-
category := getCategory(cfg)
127-
amount := getAmount()
128-
date := getDate()
129-
expense := api.ExpenseRequest{
130-
Name: name,
131-
Category: category,
132-
Amount: amount,
133-
Date: date,
134-
}
135-
136-
jsonData, err := json.Marshal(expense)
137-
if err != nil {
138-
log.Fatalf("Failed to marshal expense data: %v", err)
139-
}
140-
url := fmt.Sprintf("http://%s/expense", serverAddr)
141-
resp, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(jsonData))
142-
if err != nil {
143-
log.Fatalf("Failed to create request: %v", err)
144-
}
145-
resp.Header.Set("Content-Type", "application/json")
146-
147-
client := &http.Client{}
148-
response, err := client.Do(resp)
149-
if err != nil {
150-
log.Fatalf("Failed to send request: %v", err)
151-
}
152-
defer response.Body.Close()
153-
if response.StatusCode != http.StatusOK {
154-
log.Fatalf("Server returned error: %s", response.Status)
155-
}
156-
fmt.Println("Expense added successfully!")
157-
}
158-
15954
func main() {
160-
isServer := flag.Bool("serve", true, "Run as server (default true)")
161-
isClient := flag.Bool("client", false, "Run as client")
162-
serverAddr := flag.String("addr", "localhost:8080", "Server address (for client mode)")
16355
dataPath := flag.String("data", "data", "Path to data directory")
16456
flag.Parse()
165-
// If both flags are provided, prefer client mode
166-
if *isClient {
167-
runClient(*serverAddr)
168-
} else if *isServer {
169-
runServer(*dataPath)
170-
}
57+
runServer(*dataPath)
17158
}

0 commit comments

Comments
 (0)