Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 12 additions & 15 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from visualex_api.tools.nl_parser import parse_nl_query
from visualex_api.tools.alias_resolver import resolve_alias
from visualex_api.tools.citation_linker import extract_citations as extract_citations_from_text
from visualex_api.tools.changelog import build_changelog, changelog_range, SCAN_LIMIT
from visualex_api.tools.exceptions import (
ValidationError,
ResourceNotFoundError,
Expand Down Expand Up @@ -1319,23 +1320,19 @@ def run_git_command(args: list[str]) -> str:
commit_author = await asyncio.to_thread(run_git_command, ['log', '-1', '--format=%an'])
branch = await asyncio.to_thread(run_git_command, ['rev-parse', '--abbrev-ref', 'HEAD'])

# Get changelog (last 10 commits)
changelog_raw = await asyncio.to_thread(
# Changelog: what landed in this version, one merge at a time. The
# first-parent log gives a line per branch that landed rather than per
# development step; visualex_api/tools/changelog.py drops the rest.
version_file_log = await asyncio.to_thread(
run_git_command,
['log', '-10', '--format=%h|%s|%ci|%an']
['log', '-n', '2', '--format=%H', '--', 'version.txt']
)
changelog = []
if changelog_raw:
for line in changelog_raw.split('\n'):
if line and '|' in line:
parts = line.split('|', 3)
if len(parts) >= 4:
changelog.append({
'hash': parts[0],
'message': parts[1],
'date': parts[2],
'author': parts[3]
})
log_args = ['log', '--first-parent', '--format=%h|%s|%ci|%an', '-n', str(SCAN_LIMIT)]
commit_range = changelog_range(version_file_log)
if commit_range:
log_args.append(commit_range)
changelog_raw = await asyncio.to_thread(run_git_command, log_args)
changelog = build_changelog(changelog_raw)

return jsonify({
'version': version,
Expand Down
14 changes: 12 additions & 2 deletions docs/backend/python_api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,15 +549,25 @@ Get application version and git information.
"changelog": [
{
"hash": "abc1234",
"message": "feat: Add new feature",
"message": "feat(reading): Add new feature",
"summary": "Add new feature",
"type": "feat",
"scope": "reading",
"date": "2024-01-15T10:30:45Z",
"author": "Developer Name"
}
]
}
```

**Notes:** Changelog contains last 10 commits.
**Notes:** The changelog is what landed in the current version, read from the
first-parent log so each entry is a branch that landed rather than a development
step. `visualex_api/tools/changelog.py` drops reverted work (a revert cancels
the merge it undid, and with it everything that merge brought in), housekeeping
types (`build/chore/ci/docs/refactor/style/test`) and toolchain scopes, then
caps the list at 20 entries. `message` is the raw commit subject; `summary`,
`type` and `scope` are its parsed parts, `type` and `scope` null when the
subject carries no conventional prefix.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ cd backend && npm test
cd frontend && npm run build && npx vitest run
```

Expected today: 355 Python (1 deselected — the `live` marker), 28 backend, 84 frontend.
Expected today: 400 Python (1 deselected — the `live` marker), 35 backend, 192 frontend.

The backend suite needs `backend/.env.test` pointing at a **separate** database
(`visualex_test`, not `visualex_platform`) — it runs `prisma migrate reset` on
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/components/ui/ChangelogNotification.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ChangelogContent } from './ChangelogNotification';
import type { ChangelogEntry } from '../../config/versionConfig';

const entry = (over: Partial<ChangelogEntry> = {}): ChangelogEntry => ({
hash: 'd3bb80b1',
message: 'fix(sidebar): lift hover tooltips above page content',
summary: 'lift hover tooltips above page content',
type: 'fix',
scope: 'sidebar',
date: '2026-08-20 10:00:00 +0200',
author: 'capazme',
...over,
});

describe('ChangelogContent', () => {
it('shows a change without its conventional-commit prefix', () => {
render(<ChangelogContent changelog={[entry()]} />);

expect(screen.getByText('lift hover tooltips above page content')).toBeInTheDocument();
expect(screen.queryByText(/fix\(sidebar\):/)).not.toBeInTheDocument();
});

it('labels a change with the area it touched', () => {
render(<ChangelogContent changelog={[entry()]} />);

expect(screen.getByText('sidebar')).toBeInTheDocument();
});

it('leaves out the commit hash and the author', () => {
render(<ChangelogContent changelog={[entry()]} />);

expect(screen.queryByText(/d3bb80b/)).not.toBeInTheDocument();
expect(screen.queryByText(/capazme/)).not.toBeInTheDocument();
});

it('falls back to the raw message when an older server sends no summary', () => {
render(
<ChangelogContent
changelog={[entry({ summary: undefined, type: undefined, scope: undefined })]}
/>
);

expect(
screen.getByText('fix(sidebar): lift hover tooltips above page content')
).toBeInTheDocument();
});

it('reports an empty changelog instead of rendering nothing', () => {
render(<ChangelogContent changelog={[]} />);

expect(screen.getByText('Nessun changelog disponibile')).toBeInTheDocument();
});
});
137 changes: 75 additions & 62 deletions frontend/src/components/ui/ChangelogNotification.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { motion, AnimatePresence } from 'framer-motion';
import { Sparkles, X, GitCommit, Calendar, User } from 'lucide-react';
import { Sparkles, X, Calendar } from 'lucide-react';
import { useVersionCheck } from '../../hooks/useVersionCheck';
import { Modal } from './Modal';
import { cn } from '../../lib/utils';
Expand Down Expand Up @@ -52,14 +52,14 @@ export function ChangelogNotification() {
className={cn(
'fixed z-[60] flex items-center gap-3 px-4 py-3 rounded-xl border max-w-[90vw] md:max-w-md',
'bottom-4 left-1/2 -translate-x-1/2 md:left-auto md:translate-x-0 md:bottom-8 md:right-8',
'bg-white dark:bg-slate-900 border-primary-200 dark:border-primary-900/30',
'text-slate-800 dark:text-slate-100 shadow-lg shadow-primary-500/10'
'bg-white dark:bg-slate-900 border-blue-200 dark:border-blue-900/30',
'text-slate-800 dark:text-slate-100 shadow-lg shadow-blue-500/10'
)}
role="alert"
>
{/* Icon */}
<div className="shrink-0 w-9 h-9 rounded-lg bg-primary-50 dark:bg-primary-900/20 flex items-center justify-center">
<Sparkles size={18} className="text-primary-500" />
<div className="shrink-0 w-9 h-9 rounded-lg bg-blue-50 dark:bg-blue-900/20 flex items-center justify-center">
<Sparkles size={18} className="text-blue-500" />
</div>

{/* Content */}
Expand All @@ -75,7 +75,7 @@ export function ChangelogNotification() {
onClick={handleViewChangelog}
className={cn(
'shrink-0 px-3 py-1.5 rounded-lg text-xs font-medium',
'bg-primary-500 hover:bg-primary-600 text-white',
'bg-blue-500 hover:bg-blue-600 text-white',
'transition-colors'
)}
>
Expand Down Expand Up @@ -111,9 +111,36 @@ export function ChangelogNotification() {
}

/**
* Changelog content for the modal
* Italian label for the kinds of change worth naming. Anything else - a merge
* that landed a branch, a subject with no prefix - is shown without a label:
* the summary already says what happened.
*/
function ChangelogContent({ changelog }: { changelog: ChangelogEntry[] }) {
const TYPE_BADGES: Record<string, { label: string; className: string }> = {
feat: {
label: 'Novità',
className: 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300',
},
fix: {
label: 'Correzione',
className: 'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300',
},
perf: {
label: 'Prestazioni',
className: 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300',
},
};

/**
* Changelog content for the modal.
*
* The server hands over what landed in this version, already stripped of
* housekeeping and reverted work (visualex_api/tools/changelog.py). Here we
* only render it: no hashes, no author - this is the reader's changelog, not
* the technical panel in Impostazioni.
*
* Exported for its own test.
*/
export function ChangelogContent({ changelog }: { changelog: ChangelogEntry[] }) {
if (!changelog || changelog.length === 0) {
return (
<p className="text-slate-500 dark:text-slate-400 text-center py-4">
Expand All @@ -136,68 +163,54 @@ function ChangelogContent({ changelog }: { changelog: ChangelogEntry[] }) {
}
};

const formatMessage = (message: string) => {
// Highlight conventional commit prefixes
const prefixMatch = message.match(/^(feat|fix|docs|style|refactor|perf|test|chore|build|ci)(\(.+?\))?:/i);
if (prefixMatch) {
const prefix = prefixMatch[0];
const rest = message.slice(prefix.length).trim();
return (
<>
<span className="font-semibold text-primary-600 dark:text-primary-400">
{prefix}
</span>{' '}
{rest}
</>
);
}
return message;
};

return (
<div className="space-y-3">
<p className="text-sm text-slate-600 dark:text-slate-400 mb-4">
Ultimi {changelog.length} aggiornamenti:
Le modifiche entrate in questa versione:
</p>

<div className="space-y-2 max-h-[50vh] overflow-y-auto pr-2">
{changelog.map((entry, index) => (
<div
key={entry.hash || index}
className={cn(
'p-3 rounded-lg border',
'bg-slate-50 dark:bg-slate-800/50',
'border-slate-200 dark:border-slate-700'
)}
>
{/* Message */}
<p className="text-sm text-slate-800 dark:text-slate-200 mb-2 leading-relaxed">
{formatMessage(entry.message)}
</p>

{/* Meta */}
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
{entry.hash && (
<span className="flex items-center gap-1">
<GitCommit size={12} />
<code className="font-mono">{entry.hash.slice(0, 7)}</code>
</span>
)}
{entry.date && (
<span className="flex items-center gap-1">
<Calendar size={12} />
{formatDate(entry.date)}
</span>
)}
{entry.author && entry.author !== 'unknown' && (
<span className="flex items-center gap-1">
<User size={12} />
{entry.author}
</span>
{changelog.map((entry, index) => {
const badge = entry.type ? TYPE_BADGES[entry.type] : undefined;

return (
<div
key={entry.hash || index}
className={cn(
'p-3 rounded-lg border',
'bg-slate-50 dark:bg-slate-800/50',
'border-slate-200 dark:border-slate-700'
)}
>
{/* What changed */}
<p className="text-sm text-slate-800 dark:text-slate-200 mb-2 leading-relaxed">
{badge && (
<span
className={cn(
'inline-block mr-2 px-1.5 py-0.5 rounded align-middle',
'text-[10px] font-semibold uppercase tracking-wide',
badge.className
)}
>
{badge.label}
</span>
)}
{entry.summary || entry.message}
</p>

{/* Where, and when */}
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-500 dark:text-slate-400">
{entry.scope && <span className="font-medium">{entry.scope}</span>}
{entry.date && (
<span className="flex items-center gap-1">
<Calendar size={12} />
{formatDate(entry.date)}
</span>
)}
</div>
</div>
</div>
))}
);
})}
</div>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/config/versionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ export const VERSION_FETCH_TIMEOUT = 5000;
*/
export interface ChangelogEntry {
hash: string;
/** Raw commit subject, prefix included. Shown only in the technical panel. */
message: string;
date: string;
author: string;
/** Conventional-commit type, when the subject carries one. */
type?: string | null;
/** Conventional-commit scope: the area of the app the change touched. */
scope?: string | null;
/** The subject without its prefix. Absent from servers older than 1.4. */
summary?: string;
}

/**
Expand Down
Loading
Loading