feat(auth): add SessionExpiredBanner to all remaining web apps

Added to: clock, photos, storage, mukke, planta, picture, skilltree,
nutriphi, chat. Now all 13 web apps show a re-login banner when
token refresh permanently fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-24 22:35:13 +01:00
parent 90c438e267
commit bf7517d24d
23 changed files with 842 additions and 19 deletions

View file

@ -70,6 +70,59 @@ describe('parseSongInput', () => {
const result = parseSongInput('Track 5bpm'); // too low
expect(result.bpm).toBeUndefined();
});
it('should extract album from parentheses', () => {
const result = parseSongInput('Queen - Bohemian Rhapsody (A Night at the Opera)');
expect(result.artist).toBe('Queen');
expect(result.title).toBe('Bohemian Rhapsody');
expect(result.album).toBe('A Night at the Opera');
});
it('should extract album from title without artist', () => {
const result = parseSongInput('Song (Album)');
expect(result.title).toBe('Song');
expect(result.album).toBe('Album');
});
it('should detect multi-artist with ft.', () => {
const result = parseSongInput('Daft Punk ft. Pharrell - Get Lucky');
expect(result.artist).toBe('Daft Punk');
expect(result.artists).toEqual(['Daft Punk', 'Pharrell']);
expect(result.title).toBe('Get Lucky');
});
it('should detect multi-artist with &', () => {
const result = parseSongInput('ACDC & Brian Johnson - Thunderstruck');
expect(result.artist).toBe('ACDC');
expect(result.artists).toEqual(['ACDC', 'Brian Johnson']);
expect(result.title).toBe('Thunderstruck');
});
it('should detect multi-artist with feat.', () => {
const result = parseSongInput('Jay-Z feat. Kanye West - Niggas in Paris');
expect(result.artist).toBe('Jay-Z');
expect(result.artists).toEqual(['Jay-Z', 'Kanye West']);
});
it('should detect multi-artist with featuring', () => {
const result = parseSongInput('Eminem featuring Rihanna - Love the Way You Lie');
expect(result.artist).toBe('Eminem');
expect(result.artists).toEqual(['Eminem', 'Rihanna']);
});
it('should not set artists for single artist', () => {
const result = parseSongInput('Queen - Bohemian Rhapsody');
expect(result.artist).toBe('Queen');
expect(result.artists).toBeUndefined();
});
it('should combine album and multi-artist', () => {
const result = parseSongInput('Daft Punk ft. Pharrell - Get Lucky (Random Access Memories)');
expect(result.artist).toBe('Daft Punk');
expect(result.artists).toEqual(['Daft Punk', 'Pharrell']);
expect(result.title).toBe('Get Lucky');
expect(result.album).toBe('Random Access Memories');
});
});
describe('formatParsedSongPreview', () => {
@ -97,6 +150,12 @@ describe('formatParsedSongPreview', () => {
expect(preview).toContain('Neue Playlist');
});
it('should format album', () => {
const parsed = parseSongInput('Queen - Bohemian Rhapsody (A Night at the Opera)');
const preview = formatParsedSongPreview(parsed);
expect(preview).toContain('💿 A Night at the Opera');
});
it('should return empty for simple title', () => {
const parsed = parseSongInput('Simple Song');
expect(formatParsedSongPreview(parsed)).toBe('');

View file

@ -20,6 +20,7 @@ import { extractTags, type ParserLocale } from '@manacore/shared-utils';
export interface ParsedSong {
title: string;
artist?: string;
artists?: string[];
album?: string;
genre?: string;
bpm?: number;
@ -53,6 +54,12 @@ const PROJECT_PATTERNS_BY_LOCALE: Record<ParserLocale, RegExp[]> = {
it: [/\bnuovo\s+progetto\b/i, /\bprogetto\b/i],
};
// Album pattern: trailing parenthesized text e.g. "Title (Album Name)"
const ALBUM_PATTERN = /\(([^)]+)\)\s*$/;
// Multi-artist separator patterns
const MULTI_ARTIST_PATTERN = /\s+(?:ft\.?|feat\.?|featuring|&|x|vs\.?)\s+/i;
// "Artist - Title" separator
const ARTIST_TITLE_SEPARATOR = /\s+[-–—]\s+/;
@ -67,6 +74,21 @@ function extractBpm(text: string): { bpm?: number; remaining: string } {
return { bpm: undefined, remaining: text };
}
function extractAlbum(text: string): { album?: string; remaining: string } {
const match = text.match(ALBUM_PATTERN);
if (match) {
return { album: match[1].trim(), remaining: text.replace(ALBUM_PATTERN, '').trim() };
}
return { album: undefined, remaining: text };
}
function extractArtists(artist: string): string[] {
return artist
.split(MULTI_ARTIST_PATTERN)
.map((a) => a.trim())
.filter((a) => a.length > 0);
}
function extractYear(text: string): { year?: number; remaining: string } {
const match = text.match(YEAR_PATTERN);
if (match) {
@ -124,6 +146,10 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
const typeResult = extractTypeKeyword(text, locale);
text = typeResult.remaining;
// Extract album from parentheses (before other extractions to avoid confusion)
const albumResult = extractAlbum(text);
text = albumResult.remaining;
// Extract BPM
const bpmResult = extractBpm(text);
text = bpmResult.remaining;
@ -134,12 +160,22 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
// Try "Artist - Title" format
let artist: string | undefined;
let artists: string[] | undefined;
let title: string;
if (ARTIST_TITLE_SEPARATOR.test(text)) {
const parts = text.split(ARTIST_TITLE_SEPARATOR, 2);
artist = parts[0].trim();
const rawArtist = parts[0].trim();
title = parts[1].trim();
// Detect multi-artist patterns
const artistList = extractArtists(rawArtist);
if (artistList.length > 1) {
artist = artistList[0];
artists = artistList;
} else {
artist = rawArtist;
}
} else {
title = text.replace(/\s+/g, ' ').trim();
}
@ -147,6 +183,8 @@ export function parseSongInput(input: string, locale: ParserLocale = 'de'): Pars
return {
title,
artist,
artists,
album: albumResult.album,
genre,
bpm: bpmResult.bpm,
year: yearResult.year,
@ -173,6 +211,10 @@ export function formatParsedSongPreview(parsed: ParsedSong, locale: ParserLocale
parts.push(`🎤 ${parsed.artist}`);
}
if (parsed.album) {
parts.push(`💿 ${parsed.album}`);
}
if (parsed.genre) {
parts.push(`🎵 ${parsed.genre}`);
}

View file

@ -0,0 +1,48 @@
/**
* Mukke-specific syntax help patterns
*/
import type { SyntaxGroup } from '@manacore/shared-ui';
export const MUKKE_SYNTAX: SyntaxGroup[] = [
{
title: 'Musik',
items: [
{
pattern: 'Artist - Title',
description: 'Interpret und Titel mit Bindestrich trennen',
examples: ['Queen - Bohemian Rhapsody', 'Daft Punk ft. Pharrell - Get Lucky'],
color: 'primary',
},
{
pattern: '(Album)',
description: 'Album in Klammern',
examples: ['Queen - Song (A Night at the Opera)'],
color: 'accent',
},
{
pattern: '#genre',
description: 'Genre als Tag',
examples: ['#rock', '#electronic', '#jazz'],
color: 'primary',
},
{
pattern: 'BPM',
description: 'Tempo in BPM',
examples: ['120bpm', '90 BPM'],
color: 'warning',
},
{
pattern: 'Neue Playlist',
description: 'Playlist erstellen',
examples: ['Neue Playlist Workout', 'Playlist Chill'],
color: 'success',
},
{
pattern: 'Neues Projekt',
description: 'Editor-Projekt erstellen',
examples: ['Neues Projekt Demo', 'Projekt Remix'],
color: 'success',
},
],
},
];