feat: steadier live updates and notices for public proposals

- The web pings the live connection, reconnects when it goes quiet and
  reloads what each screen shows after reconnecting.
- Dropping a card on a column it cannot go to puts it back.
- Owners and admins get a notice when a proposal arrives.
- The proposal limit lives in Redis, so it holds across instances.
- REST tells whether I voted; pasted images leave the attachment list.
- Deleting a project removes its Git integration; no lint warnings left.
- Reactions and the blocked badge are components of their own.
- Docs: what is done and what is left (chapter 10), Word and PDF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 19:57:21 +02:00
parent 74903f7842
commit c91df110f8
50 changed files with 615 additions and 321 deletions

View File

@@ -0,0 +1,54 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PiIconComponent } from '@pi/ui';
import { IssueParent } from '@task-stream/web/tracker/domain';
/**
* 🔒 «Bloqueado»: the ticket waits for others still open. `compact` for
* cards and rows (the keys go in the tooltip); in full, they link.
*/
@Component({
selector: 'ts-blocked',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [PiIconComponent, RouterLink],
host: { role: 'status', '[title]': 'tooltip()' },
template: `
<pi-icon icon="lock" size="x-small" aria-hidden="true" />
@if (compact()) {
Bloqueado
} @else {
Bloqueado por
@for (blocker of blockers(); track blocker.id) {
<a class="ts-key" [routerLink]="['..', blocker.key]">{{
blocker.key
}}</a>
}
}
`,
styles: `
:host {
display: inline-flex;
flex-wrap: wrap;
gap: 0.25rem 0.375rem;
align-items: center;
padding: 0.0625rem 0.5rem;
border-radius: 999px;
background: color-mix(in srgb, var(--ts-c-red, #d33) 12%, transparent);
color: var(--ts-c-red, var(--ts-ink));
font-size: 0.75rem;
font-weight: 600;
}
.ts-key {
color: inherit;
}
`,
})
export class BlockedComponent {
readonly blockers = input.required<IssueParent[]>();
readonly compact = input(false);
protected tooltip(): string {
return this.blockers()
.map((b) => `${b.key}: ${b.title}`)
.join('\n');
}
}