feat: Add Makefile targets for git pre-commit hook installation and block --no-verify commits.
This commit is contained in:
25
.claude/hookify.block-no-verify.local.md
Normal file
25
.claude/hookify.block-no-verify.local.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: block-no-verify
|
||||
enabled: true
|
||||
event: bash
|
||||
pattern: git\s+commit\s+.*--no-verify|git\s+commit\s+--no-verify
|
||||
action: block
|
||||
---
|
||||
|
||||
**BLOCKED: git commit --no-verify**
|
||||
|
||||
You attempted to bypass pre-commit hooks with `--no-verify`.
|
||||
|
||||
**Why this is blocked:**
|
||||
- Pre-commit hooks run `make quality` to ensure code standards
|
||||
- Bypassing them allows broken code to be committed
|
||||
- The user explicitly requested this protection
|
||||
|
||||
**What to do instead:**
|
||||
1. Run `make quality` to see what's failing
|
||||
2. Fix the issues
|
||||
3. Commit normally: `git commit -m "your message"`
|
||||
|
||||
**If you truly need to bypass (rare cases):**
|
||||
- Ask the user for explicit permission first
|
||||
- They can temporarily disable this rule in `.claude/hookify.block-no-verify.local.md`
|
||||
23
Makefile
23
Makefile
@@ -3,7 +3,7 @@
|
||||
|
||||
.PHONY: all quality quality-ts quality-rs quality-py lint type-check test-quality \
|
||||
lint-rs clippy fmt fmt-rs fmt-check check help e2e e2e-ui e2e-grpc \
|
||||
ensure-py ensure-ts ensure-rs ensure-hygiene
|
||||
ensure-py ensure-ts ensure-rs ensure-hygiene install-hooks uninstall-hooks
|
||||
|
||||
# Default target
|
||||
all: quality
|
||||
@@ -215,6 +215,23 @@ e2e-grpc: ensure-rs
|
||||
@echo "Requires: gRPC server running on :50051"
|
||||
cd client/src-tauri && NOTEFLOW_INTEGRATION=1 cargo test --test grpc_integration -- --ignored --nocapture
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Git Hooks
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
## Install git hooks (pre-commit)
|
||||
install-hooks:
|
||||
@echo "=== Installing Git Hooks ==="
|
||||
@chmod +x scripts/hooks/pre-commit
|
||||
@ln -sf ../../scripts/hooks/pre-commit .git/hooks/pre-commit
|
||||
@echo "✓ Pre-commit hook installed"
|
||||
|
||||
## Uninstall git hooks
|
||||
uninstall-hooks:
|
||||
@echo "=== Uninstalling Git Hooks ==="
|
||||
@rm -f .git/hooks/pre-commit
|
||||
@echo "✓ Pre-commit hook removed"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Help
|
||||
#-------------------------------------------------------------------------------
|
||||
@@ -264,6 +281,10 @@ help:
|
||||
@echo " e2e-ui Run Playwright e2e tests with UI mode"
|
||||
@echo " e2e-grpc Run Rust gRPC integration tests (real backend wiring)"
|
||||
@echo ""
|
||||
@echo "Git Hooks:"
|
||||
@echo " install-hooks Install pre-commit hook"
|
||||
@echo " uninstall-hooks Remove pre-commit hook"
|
||||
@echo ""
|
||||
@echo "Dependencies:"
|
||||
@echo " ensure-py Ensure Python tooling is installed"
|
||||
@echo " ensure-ts Ensure client dependencies are installed"
|
||||
|
||||
393
docs/sprints/sprint-18.6.md
Normal file
393
docs/sprints/sprint-18.6.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# Sprint 18.6: Live Meeting UI Refinement
|
||||
|
||||
> **Size**: L | **Owner**: Frontend Team | **Prerequisites**: None
|
||||
> **Phase**: 1 - UI/UX Polish
|
||||
> **References**: [Meeting Explorer](https://github.com/aladagemre/meeting-explorer), [Transcript Seeker](https://github.com/Meeting-BaaS/transcript-seeker)
|
||||
|
||||
---
|
||||
|
||||
## Open Issues & Prerequisites
|
||||
|
||||
> **Review Date**: 2026-01-18 — Initial specification
|
||||
|
||||
### Blocking Issues
|
||||
|
||||
| ID | Issue | Status | Resolution |
|
||||
|----|-------|--------|------------|
|
||||
| **B1** | **No blocking issues identified** | N/A | N/A |
|
||||
|
||||
### Design Gaps to Address
|
||||
|
||||
| ID | Gap | Resolution |
|
||||
|----|-----|------------|
|
||||
| G1 | Sidebar recording state not synced with Recording page | Implement shared recording context via `Outlet` context |
|
||||
| G2 | In-transcript search UX not defined | Follow Slack-style "Jump to live" + filter pattern |
|
||||
| G3 | Notes quick-action buttons not specified | Add "Insert timestamp", "Add action item", "Add decision" |
|
||||
|
||||
### Prerequisite Verification
|
||||
|
||||
| Prerequisite | Status | Notes |
|
||||
|--------------|--------|-------|
|
||||
| Resizable panels working | **DONE** | `react-resizable-panels` in use |
|
||||
| Speaker preferences system | **DONE** | `preferences.getSpeakerName()` exists |
|
||||
| ApiModeIndicator component | **DONE** | Sprint GAP-007 completed |
|
||||
|
||||
---
|
||||
|
||||
## Validation Status (2026-01-18)
|
||||
|
||||
### PARTIALLY IMPLEMENTED
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| Status badge consolidation | Partial | `ApiModeIndicator` exists but Recording/Timer/Simulated badges are separate |
|
||||
| Notes panel resizing | Partial | Resizable but collapsed state is too narrow |
|
||||
| Speaker renaming | Partial | Backend exists, inline UI not implemented |
|
||||
| Transcript interactions | Partial | Timestamps shown, no click-to-seek or segment actions |
|
||||
|
||||
### NOT IMPLEMENTED
|
||||
|
||||
| Component | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| In-transcript search | Not implemented | Global search only in `top-bar.tsx` |
|
||||
| Notes quick actions | Not implemented | Only manual timestamp insertion exists |
|
||||
| Jump to live indicator | Not implemented | Auto-scroll exists but no manual trigger |
|
||||
| Stats panel warnings | Not implemented | Raw values only, no actionable alerts |
|
||||
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Refine the live meeting recording UI to reduce cognitive load, improve discoverability, and align with established "transcript viewer + notes" patterns from Meeting Explorer and Transcript Seeker. The goal is to create a calmer, more "enterprise-grade" interface where the recording controls, status indicators, transcript navigation, and notes capture work together cohesively.
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| **Primary recording control location** | Header only (right side) | Single canonical location reduces "which button?" confusion |
|
||||
| **Sidebar button behavior** | Navigate to recording page | Acts as shortcut, not duplicate control |
|
||||
| **Status badge strategy** | Unified status row in header | Consolidate Recording/Timer/Mode into one compact display |
|
||||
| **Notes panel default width** | 25% minimum (was allowing too narrow) | Ensure placeholder text doesn't wrap vertically |
|
||||
| **Transcript segment interaction** | Hover actions + click-to-pin | Builds toward transcript-as-navigation pattern |
|
||||
| **In-transcript search** | Command palette style (Cmd+F) | Separates from global search, familiar pattern |
|
||||
|
||||
---
|
||||
|
||||
## What Already Exists
|
||||
|
||||
| Asset | Location | Implication |
|
||||
|-------|----------|-------------|
|
||||
| `ApiModeIndicator` | `client/src/components/api-mode-indicator.tsx` | Reuse pattern for unified status display |
|
||||
| `SpeakerBadge` with preferences | `client/src/components/speaker-badge.tsx:26-28` | Extend for inline rename on click |
|
||||
| `TimestampedNotesEditor` | `client/src/components/timestamped-notes-editor.tsx` | Add quick-action buttons above editor |
|
||||
| `usePanelPreferences` hook | `client/src/hooks/use-panel-preferences.ts` | Already handles panel sizing persistence |
|
||||
| `TranscriptSegmentCard` with motion | `client/src/components/recording/transcript-segment-card.tsx` | Add hover actions overlay |
|
||||
| Outlet context pattern | `client/src/components/app-layout.tsx:30` | Use for shared recording state |
|
||||
| `formatElapsedTime` utility | `client/src/lib/format.ts` | Reuse for unified timer display |
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
| Task | Effort | Notes |
|
||||
|------|--------|-------|
|
||||
| **Phase 1: Control Consolidation** | | |
|
||||
| 1.1 Unify recording controls to header only | S | Remove duplicate from sidebar, keep navigation |
|
||||
| 1.2 Create `UnifiedStatusRow` component | M | Combine Recording badge + Timer + Mode indicator |
|
||||
| 1.3 Update `AppSidebar` to show recording state, not control | S | Button becomes "Go to Recording" when active |
|
||||
| **Phase 2: Panel Improvements** | | |
|
||||
| 2.1 Set minimum width constraint on Notes panel | S | Prevent text wrapping issue |
|
||||
| 2.2 Add tabbed drawer option for Notes/Stats | M | Optional: `Tabs` component in right rail |
|
||||
| 2.3 Make Stats panel collapsible by default | S | Lead with signal, hide raw telemetry |
|
||||
| **Phase 3: Notes Workflow Enhancement** | | |
|
||||
| 3.1 Add quick-action buttons to `NotesPanel` header | M | Insert timestamp, Add action item, Add decision |
|
||||
| 3.2 Add "Send to notes" hover action on transcript segments | M | Click segment → append to notes |
|
||||
| 3.3 Improve autosave indicator copy | S | "Auto-save: On" + "Saved 24s ago" format |
|
||||
| **Phase 4: Transcript Navigation** | | |
|
||||
| 4.1 Add hover actions to `TranscriptSegmentCard` | M | Copy, Pin, Add to Notes buttons |
|
||||
| 4.2 Implement "Jump to live" indicator | S | Appears when scrolled up, click returns to bottom |
|
||||
| 4.3 Add in-transcript search (Cmd+F) | L | Filter/highlight within current meeting |
|
||||
| **Phase 5: Speaker & Stats Polish** | | |
|
||||
| 5.1 Add inline speaker rename on badge click | M | Modal or inline input |
|
||||
| 5.2 Convert raw stats to actionable indicators | M | "Low input", "Clipping", "2 speakers detected" |
|
||||
| 5.3 Move confidence % to tooltip, show warning only | S | Reduce visual noise |
|
||||
|
||||
**Total Effort**: L (estimated 3-4 days)
|
||||
|
||||
---
|
||||
|
||||
## UI Components
|
||||
|
||||
### UnifiedStatusRow
|
||||
|
||||
```tsx
|
||||
// client/src/components/recording/unified-status-row.tsx
|
||||
|
||||
export interface UnifiedStatusRowProps {
|
||||
recordingState: RecordingState;
|
||||
elapsedTime: number;
|
||||
connectionMode: ConnectionMode;
|
||||
isSimulating: boolean;
|
||||
}
|
||||
|
||||
export function UnifiedStatusRow({
|
||||
recordingState,
|
||||
elapsedTime,
|
||||
connectionMode,
|
||||
isSimulating,
|
||||
}: UnifiedStatusRowProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{recordingState === 'recording' && (
|
||||
<>
|
||||
<Badge variant="recording" className="gap-1.5">
|
||||
<RecordingDot />
|
||||
Recording
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-mono">{formatElapsedTime(elapsedTime)}</span>
|
||||
</>
|
||||
)}
|
||||
<ApiModeIndicator mode={connectionMode} isSimulating={isSimulating} compact />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### TranscriptSegmentActions (Hover Overlay)
|
||||
|
||||
```tsx
|
||||
// client/src/components/recording/transcript-segment-actions.tsx
|
||||
|
||||
export interface TranscriptSegmentActionsProps {
|
||||
segment: FinalSegment;
|
||||
onCopy: () => void;
|
||||
onPin: () => void;
|
||||
onAddToNotes: () => void;
|
||||
}
|
||||
|
||||
export function TranscriptSegmentActions({
|
||||
segment,
|
||||
onCopy,
|
||||
onPin,
|
||||
onAddToNotes,
|
||||
}: TranscriptSegmentActionsProps) {
|
||||
return (
|
||||
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
|
||||
<Button variant="ghost" size="icon-sm" onClick={onCopy} title="Copy text">
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" onClick={onPin} title="Pin segment">
|
||||
<Pin className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" onClick={onAddToNotes} title="Add to notes">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### NotesQuickActions
|
||||
|
||||
```tsx
|
||||
// client/src/components/recording/notes-quick-actions.tsx
|
||||
|
||||
export interface NotesQuickActionsProps {
|
||||
onInsertTimestamp: () => void;
|
||||
onAddActionItem: () => void;
|
||||
onAddDecision: () => void;
|
||||
elapsedTime: number;
|
||||
}
|
||||
|
||||
export function NotesQuickActions({
|
||||
onInsertTimestamp,
|
||||
onAddActionItem,
|
||||
onAddDecision,
|
||||
elapsedTime,
|
||||
}: NotesQuickActionsProps) {
|
||||
return (
|
||||
<div className="flex gap-1 mb-2">
|
||||
<Button variant="outline" size="xs" onClick={onInsertTimestamp}>
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
{formatElapsedTime(elapsedTime)}
|
||||
</Button>
|
||||
<Button variant="outline" size="xs" onClick={onAddActionItem}>
|
||||
<CheckSquare className="h-3 w-3 mr-1" />
|
||||
Action
|
||||
</Button>
|
||||
<Button variant="outline" size="xs" onClick={onAddDecision}>
|
||||
<Gavel className="h-3 w-3 mr-1" />
|
||||
Decision
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### JumpToLiveIndicator
|
||||
|
||||
```tsx
|
||||
// client/src/components/recording/jump-to-live.tsx
|
||||
|
||||
export interface JumpToLiveIndicatorProps {
|
||||
isVisible: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function JumpToLiveIndicator({ isVisible, onClick }: JumpToLiveIndicatorProps) {
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onClick}
|
||||
className="fixed bottom-4 left-1/2 -translate-x-1/2 shadow-lg z-50"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4 mr-1" />
|
||||
Jump to live
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### InTranscriptSearch
|
||||
|
||||
```tsx
|
||||
// client/src/components/recording/in-transcript-search.tsx
|
||||
|
||||
export interface InTranscriptSearchProps {
|
||||
segments: FinalSegment[];
|
||||
onHighlight: (segmentIds: string[]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function InTranscriptSearch({
|
||||
segments,
|
||||
onHighlight,
|
||||
onClose,
|
||||
}: InTranscriptSearchProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
const [currentMatch, setCurrentMatch] = useState(0);
|
||||
|
||||
// Filter and highlight logic
|
||||
useEffect(() => {
|
||||
if (!query) {
|
||||
onHighlight([]);
|
||||
setMatchCount(0);
|
||||
return;
|
||||
}
|
||||
const matches = segments.filter((s) =>
|
||||
s.text.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
onHighlight(matches.map((m) => m.id));
|
||||
setMatchCount(matches.length);
|
||||
}, [query, segments, onHighlight]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-2 border-b bg-muted/50">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder="Find in transcript..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className="flex-1 h-8"
|
||||
/>
|
||||
{matchCount > 0 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{currentMatch + 1} of {matchCount}
|
||||
</span>
|
||||
)}
|
||||
<Button variant="ghost" size="icon-sm" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
### Client Components
|
||||
|
||||
- [ ] `client/src/components/recording/unified-status-row.tsx` — Consolidated recording status display
|
||||
- [ ] `client/src/components/recording/transcript-segment-actions.tsx` — Hover action overlay
|
||||
- [ ] `client/src/components/recording/notes-quick-actions.tsx` — Quick note capture buttons
|
||||
- [ ] `client/src/components/recording/jump-to-live.tsx` — Scroll-to-live indicator
|
||||
- [ ] `client/src/components/recording/in-transcript-search.tsx` — In-meeting search bar
|
||||
- [ ] `client/src/components/recording/speaker-rename-dialog.tsx` — Inline speaker rename modal
|
||||
|
||||
### Component Updates
|
||||
|
||||
- [ ] `client/src/components/recording/recording-header.tsx` — Use `UnifiedStatusRow`, remove duplicate badges
|
||||
- [ ] `client/src/components/recording/notes-panel.tsx` — Add `NotesQuickActions`, set min-width
|
||||
- [ ] `client/src/components/recording/stats-panel.tsx` — Make collapsible, add warning indicators
|
||||
- [ ] `client/src/components/recording/transcript-segment-card.tsx` — Add hover actions, group class
|
||||
- [ ] `client/src/components/speaker-badge.tsx` — Add onClick for rename
|
||||
- [ ] `client/src/components/app-sidebar.tsx` — Change recording button to navigation
|
||||
- [ ] `client/src/pages/Recording.tsx` — Integrate new components, add keyboard shortcuts
|
||||
|
||||
### Hooks
|
||||
|
||||
- [ ] `client/src/hooks/use-transcript-search.ts` — Search state and matching logic
|
||||
- [ ] `client/src/hooks/use-keyboard-shortcuts.ts` — Cmd+F binding for in-transcript search
|
||||
|
||||
---
|
||||
|
||||
## Test Strategy
|
||||
|
||||
### Fixtures to extend or create
|
||||
|
||||
- `client/src/test/mocks/segments.ts`: Add test segments with various speaker IDs
|
||||
- `client/src/test/mocks/recording-state.ts`: Add recording state mocks
|
||||
|
||||
### Core test cases
|
||||
|
||||
- **UnifiedStatusRow**: Renders all states (idle, recording, paused, stopping)
|
||||
- **TranscriptSegmentActions**: Hover reveals actions, click handlers fire
|
||||
- **NotesQuickActions**: Buttons insert correct content
|
||||
- **JumpToLiveIndicator**: Shows when scrolled up, hides at bottom
|
||||
- **InTranscriptSearch**: Filters segments, highlights matches, Escape closes
|
||||
- **SpeakerBadge**: Click opens rename, saves to preferences
|
||||
- **Integration**: Full recording flow with new components
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- [ ] `npm run test` passes for all new components
|
||||
- [ ] `npm run type-check` passes with no new errors
|
||||
- [ ] `npm run lint` passes with no new warnings
|
||||
- [ ] All new components have test coverage
|
||||
- [ ] No `any` types or `// @ts-ignore` comments
|
||||
- [ ] Accessibility: All buttons have aria-labels/titles
|
||||
- [ ] Responsive: Panels work at 1024px+ viewport widths
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
None — all changes are additive or modify internal component structure.
|
||||
|
||||
### Deprecations
|
||||
|
||||
- `ApiModeIndicator` in header alongside recording badge → replaced by `UnifiedStatusRow`
|
||||
- Sidebar "Start Recording" button → becomes "Go to Recording" during active session
|
||||
|
||||
---
|
||||
|
||||
## Post-Sprint
|
||||
|
||||
- [ ] Add transcript segment click-to-seek (requires audio playback integration)
|
||||
- [ ] Add speaker voice sample playback for identification
|
||||
- [ ] Implement multi-meeting transcript search
|
||||
- [ ] Add transcript export with timestamps
|
||||
- [ ] Consider tabbed notes/entities/stats drawer pattern
|
||||
30
scripts/hooks/pre-commit
Executable file
30
scripts/hooks/pre-commit
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pre-commit hook for NoteFlow
|
||||
# Runs quality checks before allowing commits
|
||||
#
|
||||
# Install: make install-hooks
|
||||
# Bypass: git commit --no-verify
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}Running pre-commit quality checks...${NC}"
|
||||
|
||||
# Get the repo root (in case hook is run from subdirectory)
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Run make quality
|
||||
if make quality; then
|
||||
echo -e "${GREEN}✓ All quality checks passed${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Quality checks failed${NC}"
|
||||
echo -e "${YELLOW}Fix the issues above or use 'git commit --no-verify' to bypass${NC}"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user