Show the licence, attribution and source offer in the interface

Two obligations the licence creates had nowhere to land. The attribution
term requires the notice to stay visible in the Appropriate Legal Notices
a work displays, and section 13 owes the Corresponding Source to anyone
interacting with the program over a network - neither is discharged by a
file sitting in a repository the user of a deployment never sees.

So the About dialog is not decoration: it is the only place either
obligation is met, which is why the component says so in a comment. A
deployment that drops it stops complying with the licence it ships under,
and the attribution term would be asking of others something this
interface does not do itself.

The login page sits outside this layout, so an unauthenticated visitor
still sees none of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucio Lelii 2026-09-16 12:09:58 +02:00
parent 33150978aa
commit 4e1926588b
5 changed files with 275 additions and 1 deletions

View File

@ -1,3 +1,9 @@
<!--
SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
SPDX-License-Identifier: AGPL-3.0-or-later
Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
-->
<div class="main-layout flex h-screen flex-col">
<mat-toolbar class="app-topbar !fixed !left-0 !right-0 !top-0 !z-50 !bg-[#d8f4e7] !text-slate-800">
<div class="flex items-center gap-4">
@ -31,6 +37,10 @@
<span>Admin users</span>
</button>
}
<button mat-menu-item type="button" (click)="openAboutDialog()">
<mat-icon fontIcon="info"></mat-icon>
<span>About</span>
</button>
</mat-menu>
@if (changePasswordSuccess()) {
@ -40,6 +50,10 @@
</div>
}
@if (aboutOpen()) {
<app-about-dialog (closed)="closeAboutDialog()"></app-about-dialog>
}
@if (changePasswordOpen() && loggedUser()?.username; as username) {
<app-change-password-dialog
[username]="username"

View File

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import { afterNextRender, ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@ -9,12 +13,13 @@ import { ChangePasswordRequest } from '@models/user';
import { Authorization } from '@services/authorization/authorization';
import { BlocksService } from '@services/blocks/blocks';
import { ContainersService } from '@services/containers/containers';
import { AboutDialogComponent } from '@shared/about-dialog/about-dialog';
import { ChangePasswordDialogComponent } from '@shared/change-password-dialog/change-password-dialog';
import { scheduleSignalClear } from '@utilities/temporary-signal';
@Component({
selector: 'app-app-layout',
imports: [RouterOutlet, RouterLink, RouterLinkActive, ChangePasswordDialogComponent, MatButtonModule, MatIconModule, MatMenuModule, MatTabsModule, MatToolbarModule],
imports: [RouterOutlet, RouterLink, RouterLinkActive, AboutDialogComponent, ChangePasswordDialogComponent, MatButtonModule, MatIconModule, MatMenuModule, MatTabsModule, MatToolbarModule],
templateUrl: './app-layout.html',
styleUrl: './app-layout.css',
changeDetection: ChangeDetectionStrategy.OnPush
@ -28,6 +33,7 @@ export class AppLayout {
private containersService = inject(ContainersService);
loggedUser = this.authService.loggedInUser;
aboutOpen = signal(false);
changePasswordOpen = signal(false);
changePasswordSaving = signal(false);
changePasswordError = signal<string | null>(null);
@ -58,6 +64,14 @@ logout() {
});
}
openAboutDialog() {
this.aboutOpen.set(true);
}
closeAboutDialog() {
this.aboutOpen.set(false);
}
openChangePasswordDialog() {
if (!this.loggedUser()?.username?.trim()) return;
this.changePasswordError.set(null);

View File

@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
* SPDX-License-Identifier: AGPL-3.0-or-later
* Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
*/
/* Matches the chrome of the password dialogs; kept separate because this one is not a form. */
.about-dialog-backdrop {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(15, 23, 42, 0.5);
}
.about-dialog-modal {
width: min(560px, calc(100vw - 32px));
max-height: min(80vh, 720px);
overflow: auto;
border: 1px solid #e2e8f0;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
padding: 24px;
}
.about-dialog-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.about-dialog-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #0f172a;
}
.about-dialog-subtitle {
margin: 4px 0 0;
font-size: 13px;
color: #64748b;
}
.about-dialog-body {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 18px;
font-size: 13.5px;
line-height: 1.6;
color: #334155;
}
.about-dialog-body p {
margin: 0;
}
.about-dialog-section-title {
margin: 0 0 6px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #1d4ed8;
}
.about-dialog-copyright {
font-weight: 600;
color: #0f172a;
}
.about-dialog-affiliation {
font-size: 12.5px;
color: #64748b;
}
.about-dialog-note {
font-size: 12.5px;
color: #64748b;
margin-bottom: 6px;
}
.about-dialog-attribution {
margin: 0;
padding: 10px 14px;
border-left: 3px solid #2563eb;
border-radius: 0 8px 8px 0;
background: #f1f5f9;
font-style: italic;
color: #0f172a;
}
.about-dialog-links {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.about-dialog-links li {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.about-dialog-link-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: #64748b;
}
.about-dialog-body a {
color: #1d4ed8;
word-break: break-all;
}
.about-dialog-body code {
padding: 1px 5px;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #f1f5f9;
font-size: 12px;
}

View File

@ -0,0 +1,66 @@
<!--
SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
SPDX-License-Identifier: AGPL-3.0-or-later
Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
-->
<div class="about-dialog-backdrop" (click)="close($event)">
<div class="about-dialog-modal" (click)="$event.stopPropagation()">
<div class="about-dialog-header">
<div>
<h3 class="about-dialog-title">About HumAIn Flow</h3>
<p class="about-dialog-subtitle">Licence, attribution and source code</p>
</div>
<button type="button" mat-stroked-button (click)="close($event)">Close</button>
</div>
<div class="about-dialog-body">
<section class="about-dialog-section">
<p class="about-dialog-copyright">{{ copyright }}</p>
<p class="about-dialog-affiliation">{{ affiliation }}</p>
</section>
<section class="about-dialog-section">
<h4 class="about-dialog-section-title">Licence</h4>
<p>
HumAIn Flow is free software, released under the
<a [href]="licenseUrl" target="_blank" rel="noopener noreferrer">{{ licenseName }}</a>,
with an additional attribution term under section 7(b) of that licence.
</p>
</section>
<section class="about-dialog-section">
<h4 class="about-dialog-section-title">Attribution</h4>
<p class="about-dialog-note">
This notice must be preserved in any work based on HumAIn Flow.
</p>
<blockquote class="about-dialog-attribution">{{ attribution }}</blockquote>
</section>
<section class="about-dialog-section">
<h4 class="about-dialog-section-title">Source code</h4>
<p class="about-dialog-note">
You are entitled to the complete source of the version you are using, including any
modification made to it.
</p>
<ul class="about-dialog-links">
@for (source of sources; track source.url) {
<li>
<mat-icon class="about-dialog-link-icon" fontIcon="code"></mat-icon>
<span>{{ source.label }}:</span>
<a [href]="source.url" target="_blank" rel="noopener noreferrer">{{ source.url }}</a>
</li>
}
</ul>
</section>
<section class="about-dialog-section">
<h4 class="about-dialog-section-title">Citation</h4>
<p>
If you use HumAIn Flow in academic work, please cite it. The citation metadata is in the
<code>CITATION.cff</code> file of each repository.
</p>
</section>
</div>
</div>
</div>

View File

@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
// SPDX-License-Identifier: AGPL-3.0-or-later
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
import { ChangeDetectionStrategy, Component, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
/**
* The application's Appropriate Legal Notices, in the sense section 0 of the GNU AGPL gives that
* term.
*
* This is not decoration. Two obligations are discharged here and nowhere else in the interface:
* the attribution that LICENSE-ADDENDUM requires every derivative to keep visible to its users,
* and the offer of the Corresponding Source that section 13 owes to anyone interacting with the
* program over a network. Changing the wording is fine; removing either one puts the deployment
* out of compliance with the licence it ships under.
*/
@Component({
selector: 'app-about-dialog',
imports: [MatButtonModule, MatIconModule],
templateUrl: './about-dialog.html',
styleUrl: './about-dialog.css',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AboutDialogComponent {
readonly closed = output<void>();
readonly copyright = 'Copyright © 2025-2026 Lucio Lelii';
readonly affiliation =
'Institute of Information Science and Technologies "A. Faedo" (ISTI-CNR), Pisa, Italy';
readonly attribution = 'Based on HumAIn Flow, originally developed by Lucio Lelii (ISTI-CNR)';
readonly licenseName = 'GNU Affero General Public License v3.0 or later';
readonly licenseUrl = 'https://www.gnu.org/licenses/agpl-3.0.html';
readonly sources = [
{ label: 'Web interface', url: 'https://github.com/luciolelii/humanaiFlow-angular' },
{ label: 'Backend service', url: 'https://github.com/luciolelii/humainflow-service' }
];
close(event?: Event) {
event?.stopPropagation();
this.closed.emit();
}
}