Merge pull request 'ci: add lint, coverage check and proper deploy workflow' (#2) from dev into main
Some checks failed
Deploy / deploy (push) Failing after 1s

This commit was merged in pull request #2.
This commit is contained in:
2026-03-26 18:34:37 +00:00
6 changed files with 76 additions and 29 deletions

View File

@@ -2,10 +2,12 @@ name: CI
on:
push:
branches: [dev]
branches-ignore: [main]
pull_request:
branches: [main]
jobs:
ci:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -17,11 +19,19 @@ jobs:
- name: Tidy
run: go mod tidy
- name: Vet
run: go vet ./...
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
- name: Test
run: go test ./... -v
run: go test ./... -coverprofile=coverage.out -covermode=atomic
- name: Build
run: CGO_ENABLED=0 go build -o main ./cmd/api
- name: Coverage Check
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | tr -d '%')
echo "Coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 85" | bc -l) )); then
echo "::error::Coverage ${COVERAGE}% is below 85%"
exit 1
fi

View File

@@ -1,4 +1,4 @@
name: Deploy Production
name: Deploy
on:
push:
@@ -8,11 +8,18 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build
run: CGO_ENABLED=0 go build -o main ./cmd/api
- name: Deploy pulse-api
run: |
cd /opt/digital-home/homelab-api
docker compose pull
docker compose up -d --build
echo "Waiting for healthcheck..."
sleep 5
STATUS=$(docker inspect --format='{{.State.Health.Status}}' homelab-api 2>/dev/null || echo "no-healthcheck")
echo "Container status: $STATUS"
if [ "$STATUS" = "unhealthy" ]; then
echo "::error::Container is unhealthy after deploy"
docker logs homelab-api --tail=20
exit 1
fi
echo "Deploy successful!"

View File

@@ -253,11 +253,21 @@ func (h *FinanceHandler) Analytics(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
months, _ := strconv.Atoi(r.URL.Query().Get("months"))
q := r.URL.Query()
months, _ := strconv.Atoi(q.Get("months"))
if months <= 0 {
months = 6
}
analytics, err := h.svc.GetAnalytics(userID, months)
now := time.Now()
month, _ := strconv.Atoi(q.Get("month"))
year, _ := strconv.Atoi(q.Get("year"))
if month <= 0 {
month = int(now.Month())
}
if year <= 0 {
year = now.Year()
}
analytics, err := h.svc.GetAnalytics(userID, months, month, year)
if err != nil {
writeError(w, "failed to get analytics", http.StatusInternalServerError)
return

View File

@@ -73,6 +73,7 @@ type UpdateFinanceTransactionRequest struct {
}
type FinanceSummary struct {
CarriedOver float64 `json:"carried_over"`
Balance float64 `json:"balance"`
TotalIncome float64 `json:"total_income"`
TotalExpense float64 `json:"total_expense"`

View File

@@ -237,15 +237,34 @@ func (r *FinanceRepository) DeleteTransaction(id, userID int64) error {
func (r *FinanceRepository) GetSummary(userID int64, month, year int) (*model.FinanceSummary, error) {
summary := &model.FinanceSummary{}
// First day of selected month and last day of selected month
firstDay := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
lastDay := firstDay.AddDate(0, 1, -1)
// Carried over: balance of all transactions BEFORE the selected month
err := r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0)
FROM finance_transactions WHERE user_id=$1 AND date < $2`,
userID, firstDay).Scan(&summary.CarriedOver)
if err != nil {
return nil, err
}
// Total income & expense for the month
err := r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0),
err = r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE 0 END),0),
COALESCE(SUM(CASE WHEN type='expense' THEN amount ELSE 0 END),0)
FROM finance_transactions WHERE user_id=$1 AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`,
userID, month, year).Scan(&summary.TotalIncome, &summary.TotalExpense)
if err != nil {
return nil, err
}
summary.Balance = summary.TotalIncome - summary.TotalExpense
// Cumulative balance: all transactions up to end of selected month
err = r.db.QueryRow(`SELECT COALESCE(SUM(CASE WHEN type='income' THEN amount ELSE -amount END), 0)
FROM finance_transactions WHERE user_id=$1 AND date <= $2`,
userID, lastDay).Scan(&summary.Balance)
if err != nil {
return nil, err
}
// By category
rows, err := r.db.Query(`SELECT c.id, c.name, c.emoji, t.type, SUM(t.amount) as amount
@@ -298,7 +317,7 @@ func (r *FinanceRepository) GetSummary(userID int64, month, year int) (*model.Fi
return summary, nil
}
func (r *FinanceRepository) GetAnalytics(userID int64, months int) (*model.FinanceAnalytics, error) {
func (r *FinanceRepository) GetAnalytics(userID int64, months, month, year int) (*model.FinanceAnalytics, error) {
analytics := &model.FinanceAnalytics{}
// Monthly trend
@@ -323,27 +342,27 @@ func (r *FinanceRepository) GetAnalytics(userID int64, months int) (*model.Finan
analytics.MonthlyTrend = []model.MonthlyTrend{}
}
// Avg daily expense for current month
now := time.Now()
// Avg daily expense for selected month
var totalExpense float64
var dayCount int
r.db.QueryRow(`SELECT COALESCE(SUM(amount),0), COUNT(DISTINCT date)
FROM finance_transactions WHERE user_id=$1 AND type='expense'
AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`,
userID, int(now.Month()), now.Year()).Scan(&totalExpense, &dayCount)
userID, month, year).Scan(&totalExpense, &dayCount)
if dayCount > 0 {
analytics.AvgDailyExpense = totalExpense / float64(dayCount)
}
// Comparison with previous month
prevMonth := now.AddDate(0, -1, 0)
selectedMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
prevMonthTime := selectedMonth.AddDate(0, -1, 0)
var currentMonthExp, prevMonthExp float64
r.db.QueryRow(`SELECT COALESCE(SUM(amount),0) FROM finance_transactions WHERE user_id=$1 AND type='expense'
AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`,
userID, int(now.Month()), now.Year()).Scan(&currentMonthExp)
userID, month, year).Scan(&currentMonthExp)
r.db.QueryRow(`SELECT COALESCE(SUM(amount),0) FROM finance_transactions WHERE user_id=$1 AND type='expense'
AND EXTRACT(MONTH FROM date)=$2 AND EXTRACT(YEAR FROM date)=$3`,
userID, int(prevMonth.Month()), prevMonth.Year()).Scan(&prevMonthExp)
userID, int(prevMonthTime.Month()), prevMonthTime.Year()).Scan(&prevMonthExp)
analytics.ComparisonPrevMonth = model.Comparison{
Current: currentMonthExp,

View File

@@ -162,6 +162,6 @@ func (s *FinanceService) GetSummary(userID int64, month, year int) (*model.Finan
return s.repo.GetSummary(userID, month, year)
}
func (s *FinanceService) GetAnalytics(userID int64, months int) (*model.FinanceAnalytics, error) {
return s.repo.GetAnalytics(userID, months)
func (s *FinanceService) GetAnalytics(userID int64, months, month, year int) (*model.FinanceAnalytics, error) {
return s.repo.GetAnalytics(userID, months, month, year)
}