diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c75f400
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,86 @@
+name: CI Build and Publish
+
+on:
+ push: {}
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ lint-frontend:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: frontend/package-lock.json
+
+ - name: Install dependencies
+ run: |
+ cd frontend
+ npm ci
+
+ - name: Run Linting
+ run: |
+ cd frontend
+ npm run lint
+
+ - name: Check Type Build (Dry Run)
+ # Runs the build to verify no type/compilation errors, but doesn't keep artifacts
+ run: |
+ cd frontend
+ npm run build
+
+ build-and-push:
+ needs: [lint-frontend]
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Create short SHA output
+ id: sha
+ run: |
+ echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push backend image
+ uses: docker/build-push-action@v4
+ with:
+ context: ./backend
+ file: ./backend/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository_owner }}/finance-manager-backend:latest
+ ghcr.io/${{ github.repository_owner }}/finance-manager-backend:${{ steps.sha.outputs.short_sha }}
+
+ - name: Build and push frontend image
+ uses: docker/build-push-action@v4
+ with:
+ context: ./frontend
+ file: ./frontend/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository_owner }}/finance-manager-frontend:latest
+ ghcr.io/${{ github.repository_owner }}/finance-manager-frontend:${{ steps.sha.outputs.short_sha }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..192b921
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,67 @@
+name: Release Build and Publish
+
+on:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+ packages: write
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Read backend pyproject version
+ id: backend_ver
+ run: |
+ ver=$(python - <<'PY'
+ import re,sys
+ txt=open('backend/pyproject.toml').read()
+ m=re.search(r"version\s*=\s*[\"']([^\"']+)[\"']", txt)
+ if not m:
+ sys.exit(1)
+ print(m.group(1))
+ PY
+ )
+ echo "version=$ver" >> $GITHUB_OUTPUT
+
+ - name: Build and push backend image
+ uses: docker/build-push-action@v4
+ with:
+ context: ./backend
+ file: ./backend/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository_owner }}/finance-manager-backend:${{ steps.backend_ver.outputs.version }}
+ ghcr.io/${{ github.repository_owner }}/finance-manager-backend:latest
+
+ - name: Build and push frontend image
+ uses: docker/build-push-action@v4
+ with:
+ context: ./frontend
+ file: ./frontend/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ghcr.io/${{ github.repository_owner }}/finance-manager-frontend:${{ steps.backend_ver.outputs.version }}
+ ghcr.io/${{ github.repository_owner }}/finance-manager-frontend:latest
diff --git a/.gitignore b/.gitignore
index 0da3447..2d3affa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,8 @@ dist/
downloads/
eggs/
.eggs/
-lib/
-lib64/
+# lib/
+# lib64/
parts/
sdist/
var/
diff --git a/backend/app/api/v1/endpoints/login.py b/backend/app/api/v1/endpoints/login.py
index b728086..12ff6c4 100644
--- a/backend/app/api/v1/endpoints/login.py
+++ b/backend/app/api/v1/endpoints/login.py
@@ -38,4 +38,6 @@ async def login_access_token(
except HTTPException:
raise
except Exception as e:
+ import traceback
+ traceback.print_exc()
raise HTTPException(status_code=500, detail=str(e))
diff --git a/frontend/src/app/(dashboard)/accounts/[id]/page.tsx b/frontend/src/app/(dashboard)/accounts/[id]/page.tsx
index 2dd9dd8..2e23f6b 100644
--- a/frontend/src/app/(dashboard)/accounts/[id]/page.tsx
+++ b/frontend/src/app/(dashboard)/accounts/[id]/page.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useEffect, useState } from "react"
+import { useEffect, useState, useCallback } from "react"
import { useParams } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
@@ -12,7 +12,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table"
-import { Loader2, Plus } from "lucide-react"
+import { Loader2 } from "lucide-react"
import { accountService, Account } from "@/services/accounts"
import { transactionService, Transaction } from "@/services/transactions"
import { categoryService } from "@/services/categories"
@@ -95,7 +95,7 @@ export default function AccountPage() {
}
}
- const fetchAccount = async () => {
+ const fetchAccount = useCallback(async () => {
if (accountId && authService.isAuthenticated()) {
try {
const endDate = date?.to ? format(date.to, "yyyy-MM-dd") : undefined
@@ -105,9 +105,9 @@ export default function AccountPage() {
console.error("Failed to fetch account", error)
}
}
- }
+ }, [accountId, date])
- const fetchTransactions = async () => {
+ const fetchTransactions = useCallback(async () => {
if (accountId && authService.isAuthenticated() && date?.from && date?.to) {
try {
const data = await transactionService.getTransactions({
@@ -122,9 +122,9 @@ export default function AccountPage() {
console.error("Failed to fetch transactions", error)
}
}
- }
+ }, [accountId, date, page, pageSize])
- const fetchTargetAccountExpenses = async () => {
+ const fetchTargetAccountExpenses = useCallback(async () => {
if (!accountId || !authService.isAuthenticated() || !date?.from || !date?.to) {
return
}
@@ -188,9 +188,9 @@ export default function AccountPage() {
} finally {
setLoadingTargetAccounts(false)
}
- }
+ }, [accountId, date, accountsMap])
- const fetchCategoryBreakdown = async () => {
+ const fetchCategoryBreakdown = useCallback(async () => {
if (!accountId || !authService.isAuthenticated() || !date?.from || !date?.to) {
return
}
@@ -263,9 +263,9 @@ export default function AccountPage() {
} finally {
setLoadingCategoryChart(false)
}
- }
+ }, [accountId, date, categoriesMap])
- const fetchIncomeExpenseSplit = async () => {
+ const fetchIncomeExpenseSplit = useCallback(async () => {
if (!accountId || !authService.isAuthenticated() || !date?.from || !date?.to) {
return
}
@@ -309,11 +309,11 @@ export default function AccountPage() {
} finally {
setLoadingIncomeExpense(false)
}
- }
+ }, [accountId, date])
useEffect(() => {
fetchAccount()
- }, [accountId, date])
+ }, [fetchAccount])
// Reset page when date range changes
useEffect(() => {
@@ -322,19 +322,19 @@ export default function AccountPage() {
useEffect(() => {
fetchTransactions()
- }, [accountId, date, page])
+ }, [fetchTransactions])
useEffect(() => {
fetchTargetAccountExpenses()
- }, [accountId, date, accountsMap])
+ }, [fetchTargetAccountExpenses])
useEffect(() => {
fetchCategoryBreakdown()
- }, [accountId, date, categoriesMap])
+ }, [fetchCategoryBreakdown])
useEffect(() => {
fetchIncomeExpenseSplit()
- }, [accountId, date])
+ }, [fetchIncomeExpenseSplit])
if (!account) {
return
Loading...
diff --git a/frontend/src/app/(dashboard)/categories/page.tsx b/frontend/src/app/(dashboard)/categories/page.tsx
index 1d30262..2b7643d 100644
--- a/frontend/src/app/(dashboard)/categories/page.tsx
+++ b/frontend/src/app/(dashboard)/categories/page.tsx
@@ -14,7 +14,7 @@ import { Button } from "@/components/ui/button"
import { categoryService, Category } from "@/services/categories"
import { CategoryDialog } from "@/components/categories/CategoryDialog"
import { ImportCategoriesDialog } from "@/components/categories/ImportCategoriesDialog"
-import { Loader2, Trash2 } from "lucide-react"
+import { Trash2 } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
diff --git a/frontend/src/app/(dashboard)/settings/page.tsx b/frontend/src/app/(dashboard)/settings/page.tsx
index b4dda3e..83752fc 100644
--- a/frontend/src/app/(dashboard)/settings/page.tsx
+++ b/frontend/src/app/(dashboard)/settings/page.tsx
@@ -91,9 +91,10 @@ export default function SettingsPage() {
setPasswordMessage({ type: 'success', text: 'Password updated successfully.' })
setPassword("")
setConfirmPassword("")
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Failed to update password", error)
- const msg = error.response?.data?.detail || "Failed to update password.";
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const msg = (error as any).response?.data?.detail || "Failed to update password.";
setPasswordMessage({ type: 'error', text: msg })
} finally {
setUpdatingPassword(false)
@@ -130,9 +131,10 @@ export default function SettingsPage() {
text: `Restore successful! Processed ${result.counts.categories} categories, ${result.counts.accounts} accounts, and ${result.counts.transactions} transactions.`
})
if (fileInputRef.current) fileInputRef.current.value = ''
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Failed to restore backup", error)
- const msg = error.response?.data?.detail || "Failed to restore backup. Please check the file format.";
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const msg = (error as any).response?.data?.detail || "Failed to restore backup. Please check the file format.";
setRestoreMessage({ type: 'error', text: msg })
} finally {
setRestoring(false)
@@ -287,7 +289,7 @@ export default function SettingsPage() {
Restore Data
Restore your data from a backup ZIP file.
- Warning: This will add missing records and update existing ones. It does NOT delete existing data that isn't in the backup.
+ Warning: This will add missing records and update existing ones. It does NOT delete existing data that isn't in the backup.
diff --git a/frontend/src/components/accounts/AccountSettingsDialog.tsx b/frontend/src/components/accounts/AccountSettingsDialog.tsx
index da8d169..f284933 100644
--- a/frontend/src/components/accounts/AccountSettingsDialog.tsx
+++ b/frontend/src/components/accounts/AccountSettingsDialog.tsx
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
-import { Loader2, Settings, Pencil } from "lucide-react"
+import { Loader2, Pencil } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
diff --git a/frontend/src/components/accounts/CreateAccountDialog.tsx b/frontend/src/components/accounts/CreateAccountDialog.tsx
index 795f9ab..411840f 100644
--- a/frontend/src/components/accounts/CreateAccountDialog.tsx
+++ b/frontend/src/components/accounts/CreateAccountDialog.tsx
@@ -91,8 +91,9 @@ export function CreateAccountDialog({ onSuccess }: CreateAccountDialogProps) {
setOpen(false)
form.reset()
onSuccess()
- } catch (error) {
+ } catch (error: unknown) {
console.error("Failed to create account", error)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const message = (error as any)?.response?.data?.detail || "Failed to create account. Please try again."
setSubmitError(message)
} finally {
diff --git a/frontend/src/components/accounts/DeleteDestinationAccountDialog.tsx b/frontend/src/components/accounts/DeleteDestinationAccountDialog.tsx
index f963390..42e22d2 100644
--- a/frontend/src/components/accounts/DeleteDestinationAccountDialog.tsx
+++ b/frontend/src/components/accounts/DeleteDestinationAccountDialog.tsx
@@ -37,9 +37,11 @@ export function DeleteDestinationAccountDialog({
await accountService.deleteDestinationAccount(account.id)
setOpen(false)
onSuccess()
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Failed to delete account", error)
- setError(error.response?.data?.detail || "Failed to delete account")
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const msg = (error as any).response?.data?.detail || "Failed to delete account"
+ setError(msg)
} finally {
setLoading(false)
}
@@ -60,7 +62,7 @@ export function DeleteDestinationAccountDialog({
Are you sure?
This action cannot be undone. This will permanently delete the account
- "{account.name}" and all its associated data.
+ "{account.name}" and all its associated data.
{error && (
diff --git a/frontend/src/components/accounts/DestinationAccountDialog.tsx b/frontend/src/components/accounts/DestinationAccountDialog.tsx
index b058022..9672803 100644
--- a/frontend/src/components/accounts/DestinationAccountDialog.tsx
+++ b/frontend/src/components/accounts/DestinationAccountDialog.tsx
@@ -124,9 +124,10 @@ export function DestinationAccountDialog({
}
setOpen(false)
onSuccess()
- } catch (error: any) {
+ } catch (error: unknown) {
console.error("Failed to save account", error)
- const errorMessage = error.response?.data?.detail || "Failed to save account. Please try again."
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const errorMessage = (error as any).response?.data?.detail || "Failed to save account. Please try again."
setError(errorMessage)
} finally {
setLoading(false)
diff --git a/frontend/src/components/accounts/ImportDestinationAccountsDialog.tsx b/frontend/src/components/accounts/ImportDestinationAccountsDialog.tsx
index 5137e5d..766039f 100644
--- a/frontend/src/components/accounts/ImportDestinationAccountsDialog.tsx
+++ b/frontend/src/components/accounts/ImportDestinationAccountsDialog.tsx
@@ -69,8 +69,8 @@ export function ImportDestinationAccountsDialog({
setSuccess(null)
}, 1500)
}
- } catch (err: any) {
- setError(err.message || "Failed to import destination accounts")
+ } catch (err: unknown) {
+ setError((err as Error).message || "Failed to import destination accounts")
} finally {
setLoading(false)
}
@@ -100,7 +100,8 @@ export function ImportDestinationAccountsDialog({
(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ render={({ field: { value: _value, onChange, ...fieldProps } }) => (
CSV File
diff --git a/frontend/src/components/auth/AuthGuard.tsx b/frontend/src/components/auth/AuthGuard.tsx
index 6803fcc..dcd5952 100644
--- a/frontend/src/components/auth/AuthGuard.tsx
+++ b/frontend/src/components/auth/AuthGuard.tsx
@@ -12,7 +12,8 @@ export function AuthGuard({ children }: { children: React.ReactNode }) {
if (!authService.isAuthenticated()) {
router.push("/login")
} else {
- setAuthorized(true)
+ // Avoid calling setState synchronously in effect
+ setTimeout(() => setAuthorized(true), 0)
}
}, [router])
diff --git a/frontend/src/components/categories/ImportCategoriesDialog.tsx b/frontend/src/components/categories/ImportCategoriesDialog.tsx
index 39910cf..ae17d9c 100644
--- a/frontend/src/components/categories/ImportCategoriesDialog.tsx
+++ b/frontend/src/components/categories/ImportCategoriesDialog.tsx
@@ -69,8 +69,8 @@ export function ImportCategoriesDialog({
setSuccess(null)
}, 1500)
}
- } catch (err: any) {
- setError(err.message || "Failed to import categories")
+ } catch (err: unknown) {
+ setError((err as Error).message || "Failed to import categories")
} finally {
setLoading(false)
}
@@ -100,7 +100,8 @@ export function ImportCategoriesDialog({
(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ render={({ field: { value: _value, onChange, ...fieldProps } }) => (
CSV File
diff --git a/frontend/src/components/dashboard/DateRangePicker.tsx b/frontend/src/components/dashboard/DateRangePicker.tsx
index c176c26..d0de8d0 100644
--- a/frontend/src/components/dashboard/DateRangePicker.tsx
+++ b/frontend/src/components/dashboard/DateRangePicker.tsx
@@ -1,9 +1,8 @@
"use client"
import * as React from "react"
-import { addDays, format, subDays } from "date-fns"
+import { format } from "date-fns"
import { Calendar as CalendarIcon } from "lucide-react"
-import { DateRange } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
diff --git a/frontend/src/components/dashboard/Sidebar.tsx b/frontend/src/components/dashboard/Sidebar.tsx
index 9f4897b..90693b5 100644
--- a/frontend/src/components/dashboard/Sidebar.tsx
+++ b/frontend/src/components/dashboard/Sidebar.tsx
@@ -15,7 +15,6 @@ import {
BarChart3
} from "lucide-react"
-import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
diff --git a/frontend/src/components/reports/ReportView.tsx b/frontend/src/components/reports/ReportView.tsx
index f12d2ea..b8d94ae 100644
--- a/frontend/src/components/reports/ReportView.tsx
+++ b/frontend/src/components/reports/ReportView.tsx
@@ -1,6 +1,6 @@
"use client"
-import { useState, useEffect } from "react"
+import { useState, useEffect, useCallback } from "react"
import { format } from "date-fns"
import { Loader2, ChevronDown } from "lucide-react"
@@ -71,9 +71,10 @@ export function ReportView({ type }: ReportViewProps) {
fetchData()
}, [type])
- const fetchReport = async () => {
+ const fetchReport = useCallback(async () => {
setLoading(true)
try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
const params: any = {
start_date: date?.from ? format(date.from, 'yyyy-MM-dd') : undefined,
end_date: date?.to ? format(date.to, 'yyyy-MM-dd') : undefined,
@@ -98,11 +99,11 @@ export function ReportView({ type }: ReportViewProps) {
} finally {
setLoading(false)
}
- }
+ }, [date, selectedAccount, selectedItems, type])
useEffect(() => {
fetchReport()
- }, [date, selectedAccount, selectedItems, type])
+ }, [fetchReport])
const toggleItem = (id: string) => {
setSelectedItems(prev =>
diff --git a/frontend/src/components/transactions/EditTransactionDialog.tsx b/frontend/src/components/transactions/EditTransactionDialog.tsx
index de9f7d0..76ccdbd 100644
--- a/frontend/src/components/transactions/EditTransactionDialog.tsx
+++ b/frontend/src/components/transactions/EditTransactionDialog.tsx
@@ -74,7 +74,7 @@ export function EditTransactionDialog({
resolver: zodResolver(formSchema),
defaultValues: {
name: transaction.name,
- type: transaction.type as any,
+ type: transaction.type as "income" | "expense",
amount: Math.abs(transaction.amount).toString(),
target_account_id: transaction.target_account_id || "",
account_id: transaction.account_id,
@@ -106,7 +106,7 @@ export function EditTransactionDialog({
useEffect(() => {
form.reset({
name: transaction.name,
- type: transaction.type as any,
+ type: transaction.type as "income" | "expense",
amount: Math.abs(transaction.amount).toString(),
target_account_id: transaction.target_account_id || "",
account_id: transaction.account_id,
diff --git a/frontend/src/components/transactions/ImportTransactionsDialog.tsx b/frontend/src/components/transactions/ImportTransactionsDialog.tsx
index 0d977e9..78e4c0c 100644
--- a/frontend/src/components/transactions/ImportTransactionsDialog.tsx
+++ b/frontend/src/components/transactions/ImportTransactionsDialog.tsx
@@ -4,7 +4,7 @@ import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
-import { Loader2, Upload, FileUp } from "lucide-react"
+import { Loader2, Upload } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
@@ -73,8 +73,8 @@ export function ImportTransactionsDialog({
setSuccess(null)
}, 1500)
}
- } catch (err: any) {
- setError(err.message || "Failed to import transactions")
+ } catch (err: unknown) {
+ setError((err as Error).message || "Failed to import transactions")
} finally {
setLoading(false)
}
@@ -101,7 +101,8 @@ export function ImportTransactionsDialog({
(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ render={({ field: { onChange, value: _value, ...field } }) => (
CSV File
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
new file mode 100644
index 0000000..32653d4
--- /dev/null
+++ b/frontend/src/lib/api.ts
@@ -0,0 +1,39 @@
+import axios from 'axios';
+
+const API_URL = process.env.NEXT_PUBLIC_API_URL || '/api/v1';
+
+export const api = axios.create({
+ baseURL: API_URL,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Add a request interceptor to include the auth token
+api.interceptors.request.use(
+ (config) => {
+ const token = localStorage.getItem('token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+// Add a response interceptor to handle 401 errors
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response && error.response.status === 401) {
+ // Redirect to login if unauthorized
+ if (typeof window !== 'undefined' && !window.location.pathname.includes('/login')) {
+ localStorage.removeItem('token');
+ window.location.href = '/login';
+ }
+ }
+ return Promise.reject(error);
+ }
+);
diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/frontend/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/frontend/src/services/transactions.ts b/frontend/src/services/transactions.ts
index 3e91e2b..0951b42 100644
--- a/frontend/src/services/transactions.ts
+++ b/frontend/src/services/transactions.ts
@@ -9,7 +9,7 @@ export interface Transaction {
account_id: string;
date: string;
category_id?: string;
- recurrency?: any;
+ recurrency?: Record;
}
export const transactionService = {
@@ -39,12 +39,12 @@ export const transactionService = {
return response.data;
},
- bulkDeleteTransactions: async (ids: string[]): Promise => {
+ bulkDeleteTransactions: async (ids: string[]): Promise => {
const response = await api.post('/transactions/bulk-delete', ids);
return response.data;
},
- importTransactions: async (file: File, accountId?: string): Promise => {
+ importTransactions: async (file: File, accountId?: string): Promise<{ status: string; message: string; errors?: string[] }> => {
const formData = new FormData();
formData.append('file', file);
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index cf9c65d..4622417 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -18,8 +18,9 @@
"name": "next"
}
],
+ "baseUrl": ".",
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["src/*"]
}
},
"include": [
diff --git a/k8s/finance-manager/.helmignore b/k8s/finance-manager/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/k8s/finance-manager/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/k8s/finance-manager/Chart.yaml b/k8s/finance-manager/Chart.yaml
new file mode 100644
index 0000000..b00a6d9
--- /dev/null
+++ b/k8s/finance-manager/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+name: finance-manager
+description: A Helm chart for Kubernetes
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 0.2.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "1.16.0"
diff --git a/k8s/finance-manager/README.md b/k8s/finance-manager/README.md
new file mode 100644
index 0000000..a99fe3e
--- /dev/null
+++ b/k8s/finance-manager/README.md
@@ -0,0 +1,81 @@
+# Finance Manager Helm Chart
+
+A Helm chart for deploying the Finance Manager application (Frontend & Backend) on Kubernetes.
+
+## Prerequisites
+
+- Kubernetes 1.19+
+- Helm 3.2.0+
+- A running PostgreSQL database
+
+## Installation
+
+### 1. Database Prerequisite
+
+This chart expects a PostgreSQL database to be available. By default, it looks for a service named `postgres` in the same namespace.
+
+If you are using an external database or a cloud-managed PostgreSQL (like RDS or Cloud SQL), you need to update the `backend.env` values in `values.yaml`:
+
+- `POSTGRES_SERVER`: The hostname of your database
+- `POSTGRES_USER`: The database user
+- `POSTGRES_DB`: The database name
+
+### 2. Create the Database Secret
+
+Security best practices require sensitive information like passwords to be managed via Kubernetes Secrets. Before installing the chart, verify the namespace you are deploying to and create the secret:
+
+```bash
+# Replace 'your-password' with your actual database password
+kubectl create secret generic finance-db-secret \
+ --from-literal=postgres-password='your-password'
+```
+
+If you wish to use a different secret name or key, update the `backend.secret` section in `values.yaml`.
+
+### 3. Install the Chart
+
+Install the chart using Helm:
+
+```bash
+# Install from the local directory
+helm install finance-manager .
+```
+
+To install into a specific namespace:
+
+```bash
+helm install finance-manager . --namespace finance-ns --create-namespace
+```
+
+## Configuration
+
+The following table lists the configurable parameters and their default values.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `backend.replicaCount` | Number of backend replicas | `1` |
+| `backend.image.repository` | Backend image repository | `ghcr.io/teomarcdhio/finance-manager-backend` |
+| `backend.image.tag` | Backend image tag | `latest` |
+| `backend.service.port` | Backend service port | `8000` |
+| `backend.env.POSTGRES_SERVER` | DB Hostname | `postgres` |
+| `backend.env.POSTGRES_USER` | DB Username | `admin` |
+| `backend.env.POSTGRES_DB` | DB Name | `finance_manager` |
+| `frontend.replicaCount` | Number of frontend replicas | `1` |
+| `frontend.image.repository` | Frontend image repository | `ghcr.io/teomarcdhio/finance-manager-frontend` |
+| `frontend.image.tag` | Frontend image tag | `latest` |
+| `frontend.service.port` | Frontend service port | `3000` |
+| `frontend.ingress.enabled` | Enable Ingress for frontend | `true` |
+| `frontend.ingress.hosts` | Frontend Ingress hosts | `finance.local` |
+
+Refer to `values.yaml` for the complete list of variables.
+
+## Accessing the Application
+
+If Ingress is enabled and configured, you can access the application via the configured host (e.g., `http://finance.local`). Ensure your DNS or `/etc/hosts` file resolves to the Ingress Controller IP.
+
+If Ingress is disabled, you can port-forward the frontend service:
+
+```bash
+kubectl port-forward svc/finance-manager-frontend 3000:3000
+```
+Then visit `http://localhost:3000`.
diff --git a/k8s/finance-manager/templates/_helpers.tpl b/k8s/finance-manager/templates/_helpers.tpl
new file mode 100644
index 0000000..4ab6b6a
--- /dev/null
+++ b/k8s/finance-manager/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "finance-manager.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "finance-manager.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "finance-manager.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "finance-manager.labels" -}}
+helm.sh/chart: {{ include "finance-manager.chart" . }}
+{{ include "finance-manager.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "finance-manager.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "finance-manager.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "finance-manager.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "finance-manager.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/k8s/finance-manager/templates/backend-deployment.yaml b/k8s/finance-manager/templates/backend-deployment.yaml
new file mode 100644
index 0000000..4c8a6dc
--- /dev/null
+++ b/k8s/finance-manager/templates/backend-deployment.yaml
@@ -0,0 +1,83 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "finance-manager.fullname" . }}-backend
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ app.kubernetes.io/component: backend
+spec:
+ {{- if not .Values.autoscaling.enabled }}
+ replicas: {{ .Values.backend.replicaCount }}
+ {{- end }}
+ selector:
+ matchLabels:
+ {{- include "finance-manager.selectorLabels" . | nindent 6 }}
+ app.kubernetes.io/component: backend
+ template:
+ metadata:
+ {{- with .Values.podAnnotations }}
+ annotations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "finance-manager.labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ app.kubernetes.io/component: backend
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "finance-manager.serviceAccountName" . }}
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: {{ .Chart.Name }}-backend
+ {{- with .Values.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.backend.service.port }}
+ protocol: TCP
+ env:
+ {{- range $key, $val := .Values.backend.env }}
+ - name: {{ $key }}
+ value: {{ $val | quote }}
+ {{- end }}
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.backend.secret.name }}
+ key: {{ .Values.backend.secret.key }}
+ {{- with .Values.backend.livenessProbe }}
+ livenessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.backend.readinessProbe }}
+ readinessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.backend.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
diff --git a/k8s/finance-manager/templates/backend-service.yaml b/k8s/finance-manager/templates/backend-service.yaml
new file mode 100644
index 0000000..c0bdeba
--- /dev/null
+++ b/k8s/finance-manager/templates/backend-service.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "finance-manager.fullname" . }}-backend
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ app.kubernetes.io/component: backend
+spec:
+ type: {{ .Values.backend.service.type }}
+ ports:
+ - port: {{ .Values.backend.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ {{- include "finance-manager.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: backend
diff --git a/k8s/finance-manager/templates/frontend-deployment.yaml b/k8s/finance-manager/templates/frontend-deployment.yaml
new file mode 100644
index 0000000..a12ea40
--- /dev/null
+++ b/k8s/finance-manager/templates/frontend-deployment.yaml
@@ -0,0 +1,73 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "finance-manager.fullname" . }}-frontend
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ app.kubernetes.io/component: frontend
+spec:
+ {{- if not .Values.autoscaling.enabled }}
+ replicas: {{ .Values.frontend.replicaCount }}
+ {{- end }}
+ selector:
+ matchLabels:
+ {{- include "finance-manager.selectorLabels" . | nindent 6 }}
+ app.kubernetes.io/component: frontend
+ template:
+ metadata:
+ {{- with .Values.podAnnotations }}
+ annotations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ labels:
+ {{- include "finance-manager.labels" . | nindent 8 }}
+ {{- with .Values.podLabels }}
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ app.kubernetes.io/component: frontend
+ spec:
+ {{- with .Values.imagePullSecrets }}
+ imagePullSecrets:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ serviceAccountName: {{ include "finance-manager.serviceAccountName" . }}
+ {{- with .Values.podSecurityContext }}
+ securityContext:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ containers:
+ - name: {{ .Chart.Name }}-frontend
+ {{- with .Values.securityContext }}
+ securityContext:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.frontend.service.port }}
+ protocol: TCP
+ {{- with .Values.frontend.livenessProbe }}
+ livenessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.frontend.readinessProbe }}
+ readinessProbe:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.frontend.resources }}
+ resources:
+ {{- toYaml . | nindent 12 }}
+ {{- end }}
+ {{- with .Values.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
diff --git a/k8s/finance-manager/templates/frontend-ingress.yaml b/k8s/finance-manager/templates/frontend-ingress.yaml
new file mode 100644
index 0000000..931d1a5
--- /dev/null
+++ b/k8s/finance-manager/templates/frontend-ingress.yaml
@@ -0,0 +1,44 @@
+{{- if .Values.frontend.ingress.enabled -}}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ include "finance-manager.fullname" . }}-frontend
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ app.kubernetes.io/component: frontend
+ {{- with .Values.frontend.ingress.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- with .Values.frontend.ingress.className }}
+ ingressClassName: {{ . }}
+ {{- end }}
+ {{- if .Values.frontend.ingress.tls }}
+ tls:
+ {{- range .Values.frontend.ingress.tls }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- range .Values.frontend.ingress.hosts }}
+ - host: {{ .host | quote }}
+ http:
+ paths:
+ {{- range .paths }}
+ - path: {{ .path }}
+ {{- with .pathType }}
+ pathType: {{ . }}
+ {{- end }}
+ backend:
+ service:
+ name: {{ include "finance-manager.fullname" $ }}-frontend
+ port:
+ number: {{ $.Values.frontend.service.port }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/k8s/finance-manager/templates/frontend-service.yaml b/k8s/finance-manager/templates/frontend-service.yaml
new file mode 100644
index 0000000..03fa1d0
--- /dev/null
+++ b/k8s/finance-manager/templates/frontend-service.yaml
@@ -0,0 +1,17 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "finance-manager.fullname" . }}-frontend
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ app.kubernetes.io/component: frontend
+spec:
+ type: {{ .Values.frontend.service.type }}
+ ports:
+ - port: {{ .Values.frontend.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ {{- include "finance-manager.selectorLabels" . | nindent 4 }}
+ app.kubernetes.io/component: frontend
diff --git a/k8s/finance-manager/templates/gateways.yaml b/k8s/finance-manager/templates/gateways.yaml
new file mode 100644
index 0000000..976a6f8
--- /dev/null
+++ b/k8s/finance-manager/templates/gateways.yaml
@@ -0,0 +1,19 @@
+{{- if .Values.istio.enabled -}}
+apiVersion: networking.istio.io/v1
+kind: Gateway
+metadata:
+ name: {{ .Values.istio.gateway.name }}
+ namespace: {{ .Release.Namespace }}
+spec:
+ selector:
+ {{- toYaml .Values.istio.gateway.selector | nindent 4 }}
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ {{- range .Values.istio.gateway.hosts }}
+ - {{ . | quote }}
+ {{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/k8s/finance-manager/templates/namespace.yaml b/k8s/finance-manager/templates/namespace.yaml
new file mode 100644
index 0000000..1971910
--- /dev/null
+++ b/k8s/finance-manager/templates/namespace.yaml
@@ -0,0 +1,9 @@
+{{- if .Values.istio.enabled -}}
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: {{ .Release.Namespace }}
+ labels:
+ istio-injection: {{ .Values.istio.injection | default "enabled" }}
+ {{- include "finance-manager.labels" . | nindent 4 }}
+{{- end }}
diff --git a/k8s/finance-manager/templates/serviceaccount.yaml b/k8s/finance-manager/templates/serviceaccount.yaml
new file mode 100644
index 0000000..992eedd
--- /dev/null
+++ b/k8s/finance-manager/templates/serviceaccount.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "finance-manager.serviceAccountName" . }}
+ labels:
+ {{- include "finance-manager.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
+{{- end }}
diff --git a/k8s/finance-manager/templates/virtual-services.yaml b/k8s/finance-manager/templates/virtual-services.yaml
new file mode 100644
index 0000000..47bd3b2
--- /dev/null
+++ b/k8s/finance-manager/templates/virtual-services.yaml
@@ -0,0 +1,31 @@
+{{- if .Values.istio.enabled -}}
+apiVersion: networking.istio.io/v1
+kind: VirtualService
+metadata:
+ name: {{ .Values.istio.virtualService.name }}
+ namespace: {{ .Release.Namespace }}
+spec:
+ hosts:
+ {{- range .Values.istio.virtualService.hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ gateways:
+ - {{ .Values.istio.gateway.name }}
+ http:
+ - match:
+ - uri:
+ prefix: /api/v1
+ route:
+ - destination:
+ host: {{ include "finance-manager.fullname" . }}-backend
+ port:
+ number: {{ .Values.backend.service.port }}
+ - match:
+ - uri:
+ prefix: /
+ route:
+ - destination:
+ host: {{ include "finance-manager.fullname" . }}-frontend
+ port:
+ number: {{ .Values.frontend.service.port }}
+{{- end }}
\ No newline at end of file
diff --git a/k8s/finance-manager/values.yaml b/k8s/finance-manager/values.yaml
new file mode 100644
index 0000000..00d75f8
--- /dev/null
+++ b/k8s/finance-manager/values.yaml
@@ -0,0 +1,102 @@
+# Default values for finance-manager.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+nameOverride: ""
+fullnameOverride: ""
+
+serviceAccount:
+ create: true
+ automount: true
+ annotations: {}
+ name: ""
+
+podAnnotations: {}
+podLabels: {}
+podSecurityContext: {}
+securityContext: {}
+
+backend:
+ replicaCount: 1
+ image:
+ repository: ghcr.io/teomarcdhio/finance-manager-backend
+ pullPolicy: Always
+ tag: "latest"
+ service:
+ type: ClusterIP
+ port: 8000
+ targetPort: 8000
+ ingress:
+ enabled: false
+ resources: {}
+ env:
+ POSTGRES_SERVER: "postgres-rw.postgres.svc.cluster.local"
+ POSTGRES_USER: "finance-manager"
+ POSTGRES_DB: "finance-manager"
+ secret:
+ name: "finance-db-secret"
+ key: "postgres-password"
+ livenessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 60
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 60
+ periodSeconds: 10
+
+frontend:
+ replicaCount: 1
+ image:
+ repository: ghcr.io/teomarcdhio/finance-manager-frontend
+ pullPolicy: Always
+ tag: "latest"
+ service:
+ type: ClusterIP
+ port: 3000
+ targetPort: 3000
+ ingress:
+ enabled: true
+ className: ""
+ annotations: {}
+ hosts:
+ - host: finance.local
+ paths:
+ - path: /
+ pathType: ImplementationSpecific
+ tls: []
+ resources: {}
+ livenessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 30
+ readinessProbe:
+ httpGet:
+ path: /
+ port: http
+ initialDelaySeconds: 30
+
+autoscaling:
+ enabled: false
+ minReplicas: 1
+ maxReplicas: 100
+ targetCPUUtilizationPercentage: 80
+
+istio:
+ enabled: true
+ injection: enabled
+ gateway:
+ name: finance-manager-gateway
+ selector:
+ istio: ingress-gateway
+ hosts:
+ - "finance-manager.internal.nivetek.com"
+ virtualService:
+ name: finance-manager
+ hosts:
+ - "finance-manager.internal.nivetek.com"