Skip to content

Commit 376954e

Browse files
Merge pull request #175 from OpenHD/codex/add-footer-with-last-update-info
Add build metadata footer to frontend
2 parents 0365095 + 9aa5c1d commit 376954e

11 files changed

Lines changed: 431 additions & 20 deletions

Frontend/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ WORKDIR /app
55
# Installiere pnpm global
66
RUN npm install -g pnpm
77

8+
# Build-Metadaten (können als Build-Args gesetzt werden)
9+
ARG FRONTEND_BUILD_TIME
10+
ARG FRONTEND_COMMIT
11+
ARG BACKEND_BUILD_TIME
12+
ARG BACKEND_COMMIT
13+
14+
ENV FRONTEND_BUILD_TIME=${FRONTEND_BUILD_TIME}
15+
ENV FRONTEND_COMMIT=${FRONTEND_COMMIT}
16+
ENV BACKEND_BUILD_TIME=${BACKEND_BUILD_TIME}
17+
ENV BACKEND_COMMIT=${BACKEND_COMMIT}
18+
819
# Kopiere package-Dateien (pnpm verwendet pnpm-lock.yaml)
920
COPY package.json pnpm-lock.yaml ./
1021
RUN pnpm install --frozen-lockfile

Frontend/README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,35 @@ For a complete list of available schematics (such as `components`, `directives`,
2626
ng generate --help
2727
```
2828

29-
## Building
30-
31-
To build the project run:
32-
33-
```bash
34-
ng build
35-
```
36-
37-
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
29+
## Building
30+
31+
To build the project run:
32+
33+
```bash
34+
ng build
35+
```
36+
37+
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
38+
39+
### Build-Metadaten
40+
41+
Vor jedem lokalen Build wird automatisch `scripts/generate-build-info.js` ausgeführt. Das Skript erzeugt die Datei `src/app/build-info.generated.ts`, die die Informationen für die Fußzeile (Build-Zeitpunkte und Commits für Frontend und Backend) enthält. Standardmäßig werden die Daten aus dem Git-Verlauf ermittelt; falls keine Git-Historie vorhanden ist (z. B. beim Docker-Build), können die Werte über Umgebungsvariablen gesetzt werden:
42+
43+
* `FRONTEND_BUILD_TIME` – ISO-Zeitstempel des Frontend-Builds
44+
* `FRONTEND_COMMIT` – Commit-SHA des Frontends
45+
* `BACKEND_BUILD_TIME` – ISO-Zeitstempel des Backend-Builds
46+
* `BACKEND_COMMIT` – Commit-SHA des Backends
47+
48+
Im Docker-Build können diese Werte als Build-Argumente gesetzt werden, z. B.:
49+
50+
```bash
51+
docker build \
52+
--build-arg FRONTEND_BUILD_TIME="$(date -Iseconds)" \
53+
--build-arg FRONTEND_COMMIT="$(git rev-parse HEAD)" \
54+
--build-arg BACKEND_BUILD_TIME="${BACKEND_BUILD_TIME}" \
55+
--build-arg BACKEND_COMMIT="${BACKEND_COMMIT}" \
56+
-t openhd-frontend .
57+
```
3858

3959
## Running unit tests
4060

Frontend/package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
"scripts": {
55
"ng": "ng",
66
"start": "ng serve",
7-
"start:port": "node -e 'const fs=require(\"fs\"); const port=process.argv[2]||\"8080\"; fs.writeFileSync(\"src/assets/env.js\", `(function(w){w.env=w.env||{};w.env.BACKEND_PORT=\"${port}\"})(this);`); console.log(\"Backend port set to:\", port);' && ng serve",
7+
"start:port": "node -e 'const fs=require(\"fs\"); const port=process.argv[2]||\"8080\"; fs.writeFileSync(\"src/assets/env.js\", `(function(w){w.env=w.env||{};w.env.BACKEND_PORT=\"${port}\"})(this);`); console.log(\"Backend port set to:\", port);' && ng serve",
88
"build": "ng build",
99
"watch": "ng build --watch --configuration development",
1010
"test": "ng test",
1111
"serve:ssr:OpenHD-Management-Frontend": "node dist/open-hd-management-frontend/server/server.mjs",
12-
"sbom": "npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --omit dev --spec-version 1.5 --output-format json --output-file sbom-frontend.json || true",
13-
"sbom:xml": "npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --omit dev --spec-version 1.5 --output-format xml --output-file sbom-frontend.xml || true"
12+
"sbom": "npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --omit dev --spec-version 1.5 --output-format json --output-file sbom-frontend.json || true",
13+
"sbom:xml": "npx @cyclonedx/cyclonedx-npm --ignore-npm-errors --omit dev --spec-version 1.5 --output-format xml --output-file sbom-frontend.xml || true",
14+
"prestart": "node scripts/generate-build-info.js",
15+
"prestart:port": "node scripts/generate-build-info.js",
16+
"prebuild": "node scripts/generate-build-info.js",
17+
"prewatch": "node scripts/generate-build-info.js"
1418
},
1519
"private": true,
1620
"dependencies": {
@@ -58,6 +62,6 @@
5862
"karma-jasmine": "~5.1.0",
5963
"karma-jasmine-html-reporter": "~2.1.0",
6064
"typescript": "~5.8.3",
61-
"@cyclonedx/cyclonedx-npm": "^1.18.0"
65+
"@cyclonedx/cyclonedx-npm": "^1.18.0"
6266
}
63-
}
67+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { execSync } = require('child_process');
6+
7+
function readEnv(name) {
8+
const value = process.env[name];
9+
return value && value.trim() ? value.trim() : undefined;
10+
}
11+
12+
function tryGitCommand(args, options) {
13+
try {
14+
return execSync(`git ${args}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...options }).trim();
15+
} catch (error) {
16+
return undefined;
17+
}
18+
}
19+
20+
function getGitInfoForPath(targetPath, repoRoot) {
21+
const options = repoRoot ? { cwd: repoRoot } : undefined;
22+
const commit = tryGitCommand(`log -1 --format=%H -- ${targetPath}`, options);
23+
const timestamp = tryGitCommand(`log -1 --format=%cI -- ${targetPath}`, options);
24+
return { commit, timestamp };
25+
}
26+
27+
function selectValue(primary, fallback, defaultValue) {
28+
return primary ?? fallback ?? defaultValue;
29+
}
30+
31+
function buildInfo() {
32+
const scriptDir = __dirname;
33+
const frontendRoot = path.resolve(scriptDir, '..');
34+
const repoRoot = tryDetermineRepoRoot(frontendRoot);
35+
36+
const frontendGit = repoRoot ? getGitInfoForPath('Frontend', repoRoot) : { commit: undefined, timestamp: undefined };
37+
const backendGit = repoRoot ? getGitInfoForPath('Backend', repoRoot) : { commit: undefined, timestamp: undefined };
38+
39+
const frontendTimestamp = selectValue(
40+
readEnv('FRONTEND_BUILD_TIME'),
41+
frontendGit.timestamp,
42+
new Date().toISOString()
43+
);
44+
45+
const backendTimestamp = selectValue(
46+
readEnv('BACKEND_BUILD_TIME'),
47+
backendGit.timestamp,
48+
undefined
49+
);
50+
51+
const info = {
52+
generatedAt: new Date().toISOString(),
53+
frontend: {
54+
buildTimestamp: frontendTimestamp,
55+
commit: selectValue(readEnv('FRONTEND_COMMIT'), frontendGit.commit, undefined)
56+
},
57+
backend: {
58+
buildTimestamp: backendTimestamp,
59+
commit: selectValue(readEnv('BACKEND_COMMIT'), backendGit.commit, undefined)
60+
}
61+
};
62+
63+
const outputPath = path.resolve(frontendRoot, 'src', 'app', 'build-info.generated.ts');
64+
const fileContent = `// This file is auto-generated by scripts/generate-build-info.js\n` +
65+
`// Do not edit manually.\n` +
66+
`export interface BuildInfoMeta {\n` +
67+
` generatedAt: string;\n` +
68+
` frontend: BuildInfoEntry;\n` +
69+
` backend: BuildInfoEntry;\n` +
70+
`}\n` +
71+
`export interface BuildInfoEntry {\n` +
72+
` buildTimestamp?: string;\n` +
73+
` commit?: string;\n` +
74+
`}\n` +
75+
`export const BUILD_INFO: BuildInfoMeta = ${JSON.stringify(info, null, 2)};\n`;
76+
77+
fs.writeFileSync(outputPath, fileContent);
78+
}
79+
80+
function tryDetermineRepoRoot(frontendRoot) {
81+
let current = frontendRoot;
82+
for (let i = 0; i < 5; i++) {
83+
const potentialGit = path.join(current, '.git');
84+
if (fs.existsSync(potentialGit)) {
85+
return current;
86+
}
87+
const parent = path.dirname(current);
88+
if (parent === current) {
89+
break;
90+
}
91+
current = parent;
92+
}
93+
return undefined;
94+
}
95+
96+
buildInfo();
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<!-- app.component.html -->
22

3-
<app-error-banner></app-error-banner>
4-
<app-navbar></app-navbar>
5-
<router-outlet></router-outlet>
3+
<app-error-banner></app-error-banner>
4+
<app-navbar></app-navbar>
5+
<main class="app-content">
6+
<router-outlet></router-outlet>
7+
</main>
8+
<app-footer></app-footer>

Frontend/src/app/app.component.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
// app.component.scss
22

3+
:host {
4+
display: flex;
5+
flex-direction: column;
6+
min-height: 100vh;
7+
}
8+
9+
.app-content {
10+
flex: 1 0 auto;
11+
padding: 1.5rem 2rem;
12+
}
13+
314
.nav-link.active {
415
color: white !important;
516
font-weight: bold;

Frontend/src/app/app.component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Component } from '@angular/core';
22
import { RouterOutlet } from '@angular/router';
33

4-
import { NavbarComponent } from './navbar/navbar.component';
5-
import { ErrorBannerComponent } from './components/error-banner/error-banner.component';
4+
import { NavbarComponent } from './navbar/navbar.component';
5+
import { ErrorBannerComponent } from './components/error-banner/error-banner.component';
6+
import { FooterComponent } from './components/footer/footer.component';
67

78
@Component({
89
selector: 'app-root',
910
standalone: true,
10-
imports: [RouterOutlet, NavbarComponent, ErrorBannerComponent],
11+
imports: [RouterOutlet, NavbarComponent, ErrorBannerComponent, FooterComponent],
1112
templateUrl: './app.component.html',
1213
styleUrls: ['./app.component.scss']
1314
})
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This file is auto-generated by scripts/generate-build-info.js
2+
// Do not edit manually.
3+
export interface BuildInfoMeta {
4+
generatedAt: string;
5+
frontend: BuildInfoEntry;
6+
backend: BuildInfoEntry;
7+
}
8+
export interface BuildInfoEntry {
9+
buildTimestamp?: string;
10+
commit?: string;
11+
}
12+
export const BUILD_INFO: BuildInfoMeta = {
13+
"generatedAt": "2025-10-06T09:12:37.214Z",
14+
"frontend": {
15+
"buildTimestamp": "2025-10-02T17:13:25+02:00",
16+
"commit": "e61c58696e1db67b189977f6f5276f7d412809a0"
17+
},
18+
"backend": {
19+
"buildTimestamp": "2025-10-02T16:48:39+02:00",
20+
"commit": "106aaa33e889535584ae7d0b14a47052d5fb72f3"
21+
}
22+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<footer class="app-footer">
2+
<div class="footer-content">
3+
<div class="footer-meta" *ngIf="frontendEntry">
4+
<span class="footer-label">{{ frontendEntry.label }}:</span>
5+
<span class="footer-value">{{ frontendEntry.timestampText }}</span>
6+
<span class="footer-relative" *ngIf="frontendEntry.relativeText">({{ frontendEntry.relativeText }})</span>
7+
<span class="footer-commit" *ngIf="frontendEntry.commitText">{{ frontendEntry.commitText }}</span>
8+
</div>
9+
10+
<div class="footer-meta" *ngIf="backendEntry">
11+
<span class="footer-label">{{ backendEntry.label }}:</span>
12+
<span class="footer-value">{{ backendEntry.timestampText }}</span>
13+
<span class="footer-relative" *ngIf="backendEntry.relativeText">({{ backendEntry.relativeText }})</span>
14+
<span class="footer-commit" *ngIf="backendEntry.commitText">{{ backendEntry.commitText }}</span>
15+
</div>
16+
17+
<div class="footer-meta" *ngIf="latestEntry">
18+
<span class="footer-label">{{ latestEntry.label }}:</span>
19+
<span class="footer-value">{{ latestEntry.timestampText }}</span>
20+
<span class="footer-relative" *ngIf="latestEntry.relativeText">({{ latestEntry.relativeText }})</span>
21+
</div>
22+
23+
<div class="footer-meta" *ngIf="!frontendEntry && !backendEntry">
24+
<span class="footer-label">Build-Informationen:</span>
25+
<span class="footer-value">Keine Angaben verfügbar</span>
26+
</div>
27+
</div>
28+
</footer>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
.app-footer {
2+
background: #101820;
3+
color: #e1e5ee;
4+
padding: 1.2rem 2rem;
5+
font-size: 0.9rem;
6+
border-top: 1px solid rgba(255, 255, 255, 0.1);
7+
}
8+
9+
.footer-content {
10+
display: flex;
11+
flex-wrap: wrap;
12+
gap: 1.5rem;
13+
align-items: center;
14+
justify-content: center;
15+
text-align: center;
16+
}
17+
18+
.footer-meta {
19+
display: flex;
20+
flex-wrap: wrap;
21+
gap: 0.4rem;
22+
align-items: baseline;
23+
justify-content: center;
24+
}
25+
26+
.footer-label {
27+
font-weight: 600;
28+
color: #8fbff2;
29+
}
30+
31+
.footer-value {
32+
font-variant-numeric: tabular-nums;
33+
}
34+
35+
.footer-relative {
36+
color: #b0beca;
37+
}
38+
39+
.footer-commit {
40+
color: #d4e3f3;
41+
}
42+
43+
@media (max-width: 768px) {
44+
.app-footer {
45+
padding: 1rem;
46+
}
47+
48+
.footer-content {
49+
flex-direction: column;
50+
gap: 1rem;
51+
}
52+
53+
.footer-meta {
54+
justify-content: center;
55+
}
56+
}

0 commit comments

Comments
 (0)