This commit is contained in:
2026-08-18 09:37:59 +02:00
parent 8726ce1a45
commit ab5c611338
5 changed files with 112 additions and 74 deletions
+47 -23
View File
@@ -1,49 +1,73 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY="${GITEA_REGISTRY:-${1:-}}" REGISTRY="${GITEA_REGISTRY:-${1:-}}"
IMAGE_PATH="${GITEA_IMAGE_PATH:-${2:-}}" IMAGE_PATH="${GITEA_IMAGE_PATH:-${2:-}}"
TAG="${IMAGE_TAG:-${3:-latest}}" TAG="${IMAGE_TAG:-${3:-latest}}"
SECOND_TAG="${4:-}"
PLATFORM="${PLATFORM:-linux/amd64}" PLATFORM="${PLATFORM:-linux/amd64}"
if [[ -z "$REGISTRY" || -z "$IMAGE_PATH" ]]; then if [[ -z "${REGISTRY}" || -z "${IMAGE_PATH}" ]]; then
cat <<USAGE cat <<'EOF'
Verwendung: Verwendung:
$0 <registry> <owner/image> [tag] ./build-and-push-gitea.sh <registry> <owner/image> [tag] [zweiter-tag]
Beispiel: Beispiel:
$0 git.example.de meinuser/fixfin latest ./build-and-push-gitea.sh git.d-razz.de michael/fixfin V3.3 latest
Alternativ per Umgebungsvariablen: Einmalige Registry-Anmeldung:
GITEA_REGISTRY=git.example.de \\ docker login git.d-razz.de -u michael
GITEA_IMAGE_PATH=meinuser/fixfin \\ EOF
IMAGE_TAG=latest \\
$0
Vor dem ersten Push anmelden:
sudo docker login <registry>
USAGE
exit 1 exit 1
fi fi
command -v docker >/dev/null 2>&1 || { echo "Docker fehlt."; exit 1; } command -v docker >/dev/null 2>&1 || {
docker buildx version >/dev/null 2>&1 || { echo "Docker Buildx fehlt."; exit 1; } echo "FEHLER: Docker fehlt."
exit 1
}
docker info >/dev/null 2>&1 || {
echo "FEHLER: Docker ist für den aktuellen Benutzer nicht erreichbar."
echo "Teste: docker ps"
exit 1
}
docker buildx version >/dev/null 2>&1 || {
echo "FEHLER: Docker Buildx fehlt."
exit 1
}
FULL_IMAGE="${REGISTRY}/${IMAGE_PATH}:${TAG}" FULL_IMAGE="${REGISTRY}/${IMAGE_PATH}:${TAG}"
BUILD_ARGS=(
--platform "${PLATFORM}"
--tag "${FULL_IMAGE}"
)
if [[ -n "${SECOND_TAG}" ]]; then
BUILD_ARGS+=(--tag "${REGISTRY}/${IMAGE_PATH}:${SECOND_TAG}")
fi
BUILD_ARGS+=(--push .)
echo
echo ">>> Build + Push" echo ">>> Build + Push"
echo "Image: ${FULL_IMAGE}" echo "Image: ${FULL_IMAGE}"
if [[ -n "${SECOND_TAG}" ]]; then
echo "Zusatz: ${REGISTRY}/${IMAGE_PATH}:${SECOND_TAG}"
fi
echo "Platform: ${PLATFORM}" echo "Platform: ${PLATFORM}"
echo echo
cd "$ROOT" cd "${ROOT}"
docker buildx build \
--platform "$PLATFORM" \ docker buildx build "${BUILD_ARGS[@]}"
--tag "$FULL_IMAGE" \
--push \
.
echo echo
echo "Fertig: ${FULL_IMAGE}" echo "Fertig:"
echo " ${FULL_IMAGE}"
if [[ -n "${SECOND_TAG}" ]]; then
echo " ${REGISTRY}/${IMAGE_PATH}:${SECOND_TAG}"
fi
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "fixfin", "name": "fixfin",
"version": "1.3.0", "version": "1.3.1",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+44 -40
View File
@@ -11,22 +11,23 @@ Verwendung:
Beispiele: Beispiele:
./release.sh 3.3 ./release.sh 3.3
./release.sh v3.3
./release.sh V3.3 ./release.sh V3.3
./release.sh 3.3.1 ./release.sh 3.3.1
Ablauf: Ablauf:
1. git add -A 1. git add -A
2. git commit -m "V<version>" 2. git commit -m "V<version>"
3. git push über origin/SSH 3. git push origin <branch>
4. build-and-push-gitea.sh 4. Docker-Build + Push nach Gitea
5. Docker-Image mit Versionstag pushen 5. Versionstag + latest
6. denselben Stand zusätzlich als :latest markieren
Voraussetzungen: Voraussetzungen:
- origin zeigt per SSH auf Gitea, z.B. - Git-Push funktioniert über deinen konfigurierten origin-Remote.
git@git.d-razz.de:mboehmlaender/fixfin.git - Docker kann als aktueller Benutzer ausgeführt werden:
- Docker-Registry einmalig für root angemeldet: docker ps
sudo docker login git.d-razz.de - Einmalig bei der Gitea Registry anmelden:
docker login git.d-razz.de -u michael
EOF EOF
} }
@@ -37,13 +38,12 @@ if [[ -z "${VERSION_RAW}" ]]; then
exit 1 exit 1
fi fi
# V/v am Anfang entfernen und anschließend einheitlich als Vx.x verwenden.
VERSION="${VERSION_RAW#V}" VERSION="${VERSION_RAW#V}"
VERSION="${VERSION#v}" VERSION="${VERSION#v}"
if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
echo "FEHLER: Ungültige Version '${VERSION_RAW}'." echo "FEHLER: Ungültige Version '${VERSION_RAW}'."
echo "Erwartet z.B. 3.3, V3.3 oder 3.3.1" echo "Erwartet z.B. 3.3, v3.3, V3.3 oder 3.3.1"
exit 1 exit 1
fi fi
@@ -55,6 +55,23 @@ command -v git >/dev/null 2>&1 || {
exit 1 exit 1
} }
command -v docker >/dev/null 2>&1 || {
echo "FEHLER: docker wurde nicht gefunden."
exit 1
}
docker info >/dev/null 2>&1 || {
echo "FEHLER: Docker ist für den aktuellen Benutzer nicht erreichbar."
echo
echo "Teste:"
echo " docker ps"
echo
echo "Falls Permission denied:"
echo " sudo usermod -aG docker $USER"
echo " newgrp docker"
exit 1
}
[[ -x "${BUILD_SCRIPT}" ]] || { [[ -x "${BUILD_SCRIPT}" ]] || {
echo "FEHLER: ${BUILD_SCRIPT} fehlt oder ist nicht ausführbar." echo "FEHLER: ${BUILD_SCRIPT} fehlt oder ist nicht ausführbar."
echo "Fix:" echo "Fix:"
@@ -76,30 +93,30 @@ if [[ -z "${REMOTE}" ]]; then
exit 1 exit 1
fi fi
# Registry-Host und owner/repo automatisch aus einem SSH-Remote ableiten. # owner/repo + Registry aus SSH- oder HTTPS-Remote ableiten.
# Unterstützt: # Unterstützt z.B.:
# git@host:owner/repo.git # git@git.d-razz.de:michael/fixfin.git
# ssh://git@host/owner/repo.git # ssh://git@git.d-razz.de/michael/fixfin.git
# https://git.d-razz.de/michael/fixfin.git
if [[ "${REMOTE}" =~ ^git@([^:]+):(.+)$ ]]; then if [[ "${REMOTE}" =~ ^git@([^:]+):(.+)$ ]]; then
REGISTRY="${BASH_REMATCH[1]}" REGISTRY="${BASH_REMATCH[1]}"
IMAGE_PATH="${BASH_REMATCH[2]}" IMAGE_PATH="${BASH_REMATCH[2]}"
elif [[ "${REMOTE}" =~ ^ssh://git@([^/]+)/(.+)$ ]]; then elif [[ "${REMOTE}" =~ ^ssh://git@([^/]+)/(.+)$ ]]; then
REGISTRY="${BASH_REMATCH[1]}" REGISTRY="${BASH_REMATCH[1]}"
IMAGE_PATH="${BASH_REMATCH[2]}" IMAGE_PATH="${BASH_REMATCH[2]}"
elif [[ "${REMOTE}" =~ ^https?://([^/]+)/(.+)$ ]]; then
REGISTRY="${BASH_REMATCH[1]}"
IMAGE_PATH="${BASH_REMATCH[2]}"
else else
echo "FEHLER: origin ist kein unterstützter SSH-Remote:" echo "FEHLER: origin konnte nicht ausgewertet werden:"
echo " ${REMOTE}" echo " ${REMOTE}"
echo
echo "Erwartet z.B.:"
echo " git@git.d-razz.de:mboehmlaender/fixfin.git"
exit 1 exit 1
fi fi
IMAGE_PATH="${IMAGE_PATH%.git}" IMAGE_PATH="${IMAGE_PATH%.git}"
# Sicherheitsprüfung
if [[ ! "${IMAGE_PATH}" =~ ^[^/]+/[^/]+$ ]]; then if [[ ! "${IMAGE_PATH}" =~ ^[^/]+/[^/]+$ ]]; then
echo "FEHLER: owner/repo konnte aus dem Remote nicht sicher ermittelt werden:" echo "FEHLER: owner/repo konnte nicht sicher ermittelt werden:"
echo " ${IMAGE_PATH}" echo " ${IMAGE_PATH}"
exit 1 exit 1
fi fi
@@ -120,10 +137,10 @@ echo "Repository : ${REMOTE}"
echo "Branch : ${BRANCH}" echo "Branch : ${BRANCH}"
echo "Registry : ${REGISTRY}" echo "Registry : ${REGISTRY}"
echo "Image : ${REGISTRY}/${IMAGE_PATH}" echo "Image : ${REGISTRY}/${IMAGE_PATH}"
echo "Tag : ${IMAGE_TAG} + latest" echo "Tags : ${IMAGE_TAG}, latest"
echo echo
echo ">>> Prüfe SSH-Zugriff auf origin ..." echo ">>> Prüfe Git-Remote ..."
git ls-remote origin HEAD >/dev/null git ls-remote origin HEAD >/dev/null
echo ">>> Stage Änderungen ..." echo ">>> Stage Änderungen ..."
@@ -139,28 +156,16 @@ fi
echo ">>> Commit ${COMMIT_MESSAGE} ..." echo ">>> Commit ${COMMIT_MESSAGE} ..."
git commit -m "${COMMIT_MESSAGE}" git commit -m "${COMMIT_MESSAGE}"
echo ">>> Git Push über SSH ..." echo ">>> Git Push ..."
git push origin "${BRANCH}" git push origin "${BRANCH}"
echo echo
echo ">>> Docker Registry Authentifizierung prüfen ..." echo ">>> Docker Build + Push ..."
# sudo wird hier einmal initialisiert. Docker-Registry-Credentials liegen "${BUILD_SCRIPT}" \
# danach dauerhaft in der Docker-Konfiguration des root-Benutzers, sofern
# zuvor 'sudo docker login <registry>' ausgeführt wurde.
sudo -v
echo
echo ">>> Build + Push ${IMAGE_TAG} ..."
sudo "${BUILD_SCRIPT}" \
"${REGISTRY}" \ "${REGISTRY}" \
"${IMAGE_PATH}" \ "${IMAGE_PATH}" \
"${IMAGE_TAG}" "${IMAGE_TAG}" \
"latest"
echo
echo ">>> Markiere denselben Image-Stand zusätzlich als latest ..."
sudo docker buildx imagetools create \
--tag "${REGISTRY}/${IMAGE_PATH}:latest" \
"${REGISTRY}/${IMAGE_PATH}:${IMAGE_TAG}"
echo echo
echo "============================================================" echo "============================================================"
@@ -173,4 +178,3 @@ echo
echo "Docker:" echo "Docker:"
echo " ${REGISTRY}/${IMAGE_PATH}:${IMAGE_TAG}" echo " ${REGISTRY}/${IMAGE_PATH}:${IMAGE_TAG}"
echo " ${REGISTRY}/${IMAGE_PATH}:latest" echo " ${REGISTRY}/${IMAGE_PATH}:latest"
echo
+1 -1
View File
@@ -129,7 +129,7 @@ function Dashboard({totals,onToggle}){
<section className="toggle-panel"><div><strong>Periodische Kosten in Monats-Saldo einrechnen</strong><span>Jährliche, halbjährliche und vierteljährliche Beträge werden auf einen Monatswert heruntergerechnet.</span></div><button className={`switch ${totals.includePeriodic?'on':''}`} onClick={onToggle} role="switch" aria-checked={totals.includePeriodic}><span/></button></section> <section className="toggle-panel"><div><strong>Periodische Kosten in Monats-Saldo einrechnen</strong><span>Jährliche, halbjährliche und vierteljährliche Beträge werden auf einen Monatswert heruntergerechnet.</span></div><button className={`switch ${totals.includePeriodic?'on':''}`} onClick={onToggle} role="switch" aria-checked={totals.includePeriodic}><span/></button></section>
<section className="summary-grid"><Metric label="Eingänge / Monat" value={totals.income} positive/><Metric label="Ausgänge / Monat" value={totals.expense} negative/><Metric label="Periodische Rücklage / Monat" value={totals.periodicExpenseReserve}/><Metric label="Monatlicher Gesamtsaldo" value={totals.balance} positive={totals.balance>=0} negative={totals.balance<0} featured/></section> <section className="summary-grid"><Metric label="Eingänge / Monat" value={totals.income} positive/><Metric label="Ausgänge / Monat" value={totals.expense} negative/><Metric label="Periodische Rücklage / Monat" value={totals.periodicExpenseReserve}/><Metric label="Monatlicher Gesamtsaldo" value={totals.balance} positive={totals.balance>=0} negative={totals.balance<0} featured/></section>
{!totals.includePeriodic&&totals.periodicExpenseReserve>0&&<div className="info-note">Periodische Ausgaben von rechnerisch <strong>{formatMoney(totals.periodicExpenseReserve)} / Monat</strong> sind derzeit nicht im Saldo enthalten.</div>} {!totals.includePeriodic&&totals.periodicExpenseReserve>0&&<div className="info-note">Periodische Ausgaben von rechnerisch <strong>{formatMoney(totals.periodicExpenseReserve)} / Monat</strong> sind derzeit nicht im Saldo enthalten.</div>}
<section className="panel"><div className="section-head"><div><h2>Saldo je Konto</h2><p>Monatliche Fixwerte plus optional heruntergerechnete periodische Werte.</p></div></div>{totals.accounts.length===0?<Empty text="Lege zuerst ein Konto an."/>:<div className="account-balance-list">{totals.accounts.map(s=><details className="account-balance-card" key={s.account.id}><summary><div className="account-balance-title"><h3>{s.account.name}</h3><span>monatlicher Fix-Saldo</span></div><strong className={`account-balance-value ${s.balance>=0?'good-text':'bad-text'}`}>{s.balance>=0?'+':''}{formatMoney(s.balance)}</strong><span className="accordion-chevron" aria-hidden="true"></span></summary><div className="account-balance-details"><div className="mini-grid"><div><span>Eingänge</span><strong className="good-text">+{formatMoney(s.income)}</strong></div><div><span>Ausgänge</span><strong className="bad-text">{formatMoney(s.expense)}</strong></div><div><span>Transfers rein</span><strong className="good-text">+{formatMoney(s.transferIn)}</strong></div><div><span>Transfers raus</span><strong className="bad-text">{formatMoney(s.transferOut)}</strong></div></div></div></details>)}</div>}</section> <section className="panel"><div className="section-head"><div><h2>Saldo je Konto</h2><p>Monatliche Fixwerte plus optional heruntergerechnete periodische Werte.</p></div></div>{totals.accounts.length===0?<Empty text="Lege zuerst ein Konto an."/>:<div className="account-balance-list">{totals.accounts.map(s=><details className="account-balance-card" key={s.account.id}><summary><div className="account-balance-title"><h3>{s.account.name}</h3><span>monatlicher Fix-Saldo</span></div><div className="account-balance-right"><strong className={`account-balance-value ${s.balance>=0?'good-text':'bad-text'}`}>{s.balance>=0?'+':''}{formatMoney(s.balance)}</strong><span className="accordion-chevron" aria-hidden="true"></span></div></summary><div className="account-balance-details"><div className="mini-grid"><div><span>Eingänge</span><strong className="good-text">+{formatMoney(s.income)}</strong></div><div><span>Ausgänge</span><strong className="bad-text">{formatMoney(s.expense)}</strong></div><div><span>Transfers rein</span><strong className="good-text">+{formatMoney(s.transferIn)}</strong></div><div><span>Transfers raus</span><strong className="bad-text">{formatMoney(s.transferOut)}</strong></div></div></div></details>)}</div>}</section>
<section className="panel total-panel"><div><span className="eyebrow">GESAMT</span><h2>Monatlich nach Fixkosten übrig</h2><p>Interne Transfers verändern den Gesamtsaldo nicht.</p></div><div className={`grand-total ${totals.balance>=0?'good-text':'bad-text'}`}>{formatMoney(totals.balance)}</div></section> <section className="panel total-panel"><div><span className="eyebrow">GESAMT</span><h2>Monatlich nach Fixkosten übrig</h2><p>Interne Transfers verändern den Gesamtsaldo nicht.</p></div><div className={`grand-total ${totals.balance>=0?'good-text':'bad-text'}`}>{formatMoney(totals.balance)}</div></section>
</div>; </div>;
} }
+19 -9
View File
@@ -187,7 +187,7 @@ code { color:#b8c3ff; }
.year-month-list { gap:9px; } .year-month-list { gap:9px; }
.year-month-card summary { grid-template-columns:1fr 22px; gap:9px 10px; padding:13px 12px; } .year-month-card summary { grid-template-columns:1fr 22px; gap:9px 10px; padding:13px 12px; }
.year-month-heading { grid-column:1; } .year-month-heading { grid-column:1; }
.accordion-chevron { grid-column:2; grid-row:1; align-self:center; } .year-month-card .accordion-chevron { grid-column:2; grid-row:1; align-self:center; }
.year-month-basics { grid-column:1 / -1; grid-row:2; gap:6px; } .year-month-basics { grid-column:1 / -1; grid-row:2; gap:6px; }
.year-month-basics div { background:#101823; border:1px solid #202c3c; border-radius:9px; padding:8px; } .year-month-basics div { background:#101823; border:1px solid #202c3c; border-radius:9px; padding:8px; }
.year-month-basics strong { font-size:12px; } .year-month-basics strong { font-size:12px; }
@@ -213,7 +213,7 @@ code { color:#b8c3ff; }
.mobile-nav { grid-template-columns:repeat(7,minmax(50px,1fr)); } .mobile-nav { grid-template-columns:repeat(7,minmax(50px,1fr)); }
} }
/* v3.2: kompakte Kontosalden als Akkordeon */ /* v3.4: robuste Konto-Saldo-Anzeige auf der Übersicht */
.account-balance-list { .account-balance-list {
display:grid; display:grid;
gap:10px; gap:10px;
@@ -231,16 +231,16 @@ code { color:#b8c3ff; }
.account-balance-card summary { .account-balance-card summary {
list-style:none; list-style:none;
cursor:pointer; cursor:pointer;
display:grid; display:flex;
grid-template-columns:minmax(0,1fr) auto 22px; gap:12px;
gap:14px;
align-items:center; align-items:center;
justify-content:space-between;
min-height:68px; min-height:68px;
padding:12px 15px; padding:12px 15px;
user-select:none; user-select:none;
} }
.account-balance-card summary::-webkit-details-marker { display:none; } .account-balance-card summary::-webkit-details-marker { display:none; }
.account-balance-title { min-width:0; } .account-balance-title { min-width:0; overflow:hidden; flex:1 1 auto; }
.account-balance-title h3 { .account-balance-title h3 {
margin:0; margin:0;
overflow:hidden; overflow:hidden;
@@ -253,11 +253,21 @@ code { color:#b8c3ff; }
color:var(--muted); color:var(--muted);
font-size:11px; font-size:11px;
} }
.account-balance-right {
flex:0 0 auto;
display:flex;
align-items:center;
justify-content:flex-end;
gap:8px;
min-width:max-content;
}
.account-balance-value { .account-balance-value {
display:block;
font-size:19px; font-size:19px;
font-weight:850; font-weight:850;
letter-spacing:-.035em; letter-spacing:-.035em;
white-space:nowrap; white-space:nowrap;
flex:0 0 auto;
} }
.account-balance-card[open] .accordion-chevron { transform:rotate(180deg); } .account-balance-card[open] .accordion-chevron { transform:rotate(180deg); }
.account-balance-details { .account-balance-details {
@@ -272,15 +282,15 @@ code { color:#b8c3ff; }
@media (max-width:760px) { @media (max-width:760px) {
.account-balance-list { gap:8px; } .account-balance-list { gap:8px; }
.account-balance-card summary { .account-balance-card summary {
grid-template-columns:minmax(0,1fr) auto 18px;
gap:8px; gap:8px;
min-height:58px; min-height:58px;
padding:9px 10px; padding:9px 10px;
} }
.account-balance-title h3 { font-size:14px; } .account-balance-title h3 { font-size:14px; }
.account-balance-title span { font-size:9px; } .account-balance-title span { font-size:9px; }
.account-balance-value { font-size:16px; } .account-balance-value { font-size:15px; letter-spacing:-.025em; }
.account-balance-card .accordion-chevron { font-size:17px; } .account-balance-right { gap:6px; }
.account-balance-card .accordion-chevron { font-size:17px; flex:0 0 auto; }
.account-balance-details { padding:0 10px 10px; } .account-balance-details { padding:0 10px 10px; }
.account-balance-details .mini-grid { .account-balance-details .mini-grid {
gap:8px; gap:8px;