Files
daily-timer/.github/copilot-instructions.md
2026-02-10 02:39:38 +03:00

7.6 KiB

Daily Timer - AI Assistant Instructions

Project Overview

  • Purpose: Desktop meeting timer application with speaker management
  • Framework: Wails v2.11.0 (Go backend + Svelte frontend)
  • Status: Active development

Technology Stack

Component Technology
Backend Go 1.21+
Framework Wails v2.11.0
Frontend Svelte 4 + Vite
Database SQLite via GORM
Audio Web Audio API (oscillators)
Linter golangci-lint

Project Structure

daily-timer/
├── main.go                    # App entry point
├── wails.json                 # Wails configuration
├── internal/
│   ├── app/
│   │   └── app.go             # Main App struct, Wails bindings
│   ├── models/
│   │   └── models.go          # Data models (Participant, Meeting, etc.)
│   ├── storage/
│   │   └── storage.go         # GORM database operations
│   └── timer/
│       └── timer.go           # Timer logic, event emission
├── frontend/
│   ├── src/
│   │   ├── App.svelte         # Main component, event handlers
│   │   ├── components/
│   │   │   ├── Timer.svelte
│   │   │   ├── ParticipantList.svelte
│   │   │   ├── Controls.svelte
│   │   │   ├── Settings.svelte
│   │   │   ├── History.svelte
│   │   │   └── Setup.svelte
│   │   └── lib/
│   │       └── i18n.js        # Translations (RU/EN)
│   └── wailsjs/               # Auto-generated Go bindings
└── build/                     # Build output

Critical Rules - MUST FOLLOW

1. Wails Terminal Directory

ALWAYS run wails dev from project root, not from subdirectories:

# ✅ CORRECT
cd /Users/admin-msk/git/daily-timer && wails dev

# ❌ WRONG - causes "wails.json not found" error
cd /Users/admin-msk/git/daily-timer/internal/timer && wails dev

2. TypeScript Model Casing

Go structs use PascalCase, but Wails generates TypeScript with camelCase:

// Go model
type Session struct {
    ID        uint
    StartTime time.Time
}
// Generated TypeScript - use lowercase!
session.id; // ✅ CORRECT
session.ID; // ❌ WRONG - undefined
session.startTime; // ✅ CORRECT
session.StartTime; // ❌ WRONG - undefined

3. GORM Partial Updates

NEVER use db.Save() for partial updates - it overwrites ALL fields with zero values:

// ❌ WRONG - overwrites Active, Order, etc. with zero values
func (s *Storage) UpdateParticipant(p *models.Participant) error {
    return s.db.Save(p).Error
}

// ✅ CORRECT - updates only specified fields
func (s *Storage) UpdateParticipant(p *models.Participant) error {
    return s.db.Model(p).Updates(map[string]interface{}{
        "name":       p.Name,
        "email":      p.Email,
        "time_limit": p.TimeLimit,
    }).Error
}

4. Timer Event Flags

Use separate flags for different event types to prevent blocking:

// ❌ WRONG - one flag blocks both events
speakerWarned bool  // After warning, timeup never fires

// ✅ CORRECT - separate flags
speakerWarned        bool  // For warning event
speakerTimeUpEmitted bool  // For timeup event

5. Sound in Wails (macOS)

  • Use Web Audio API oscillators, not audio files
  • macOS "Do Not Disturb" mode blocks sounds
  • Always add test buttons in Settings for verification
function playBeep(frequency, duration) {
  const ctx = new AudioContext();
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.connect(gain);
  gain.connect(ctx.destination);
  osc.frequency.value = frequency;
  gain.gain.setValueAtTime(0.3, ctx.currentTime);
  gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
  osc.start();
  osc.stop(ctx.currentTime + duration);
}

6. Svelte Event Dispatching

When adding interactivity to list items:

<!-- Parent component -->
<ChildList on:customEvent={handleEvent} />

<!-- Child component -->
<script>
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()

  function handleClick(id) {
    dispatch('customEvent', { id })
  }
</script>

7. i18n Translations

Always update BOTH languages (RU and EN) in frontend/src/lib/i18n.js:

translations: {
  ru: {
    history: {
      deleteAll: 'Удалить историю',
    }
  },
  en: {
    history: {
      deleteAll: 'Delete History',
    }
  }
}

8. Go Linter (golangci-lint)

Run before committing:

/opt/homebrew/bin/golangci-lint run ./...

Common fixes:

  • Remove explicit types when inferring: idx := -1 not var idx int = -1
  • Remove unused functions
  • Handle all error returns

Common Bugs & Solutions

Bug Cause Solution
wails.json not found Wrong directory Run from project root
Data disappears on edit db.Save() overwrites Use Updates(map[string]interface{})
Event never fires Shared flag blocks Use separate flags per event
Sounds don't play DND mode or no AudioContext Add test buttons, check DND
undefined in template Wrong case (ID vs id) Check generated TS types
Drag-drop doesn't save Missing backend call Call ReorderParticipants()
"App is damaged" error macOS Gatekeeper Run xattr -cr "Daily Timer.app"

Development Commands

# Run in development mode (ALWAYS run manually from clean shell!)
cd /Users/admin-msk/git/daily-timer && wails dev

# Build for production
wails build

# Run linter
/opt/homebrew/bin/golangci-lint run ./...

# Generate bindings (automatic in wails dev)
wails generate module

Release Workflow

Building Release

# Build and package for current architecture
make release

# Build for all macOS architectures (arm64 + amd64)
make release-all

Output: dist/Daily-Timer-vX.X.X-macos-arm64.zip

Publishing Release to Gitea

# Create and push tag
git tag -a v0.2.0 -m "Release description"
git push origin v0.2.0

# Build and upload to Gitea (requires GITEA_TOKEN)
GITEA_TOKEN=your_token make release-publish

macOS Gatekeeper Fix

IMPORTANT: Built app is not signed. macOS shows "app is damaged" error.

Fix for users after downloading:

xattr -cr "Daily Timer.app"

Or: Right-click → Open → Open (first launch only)

For apps in /Applications:

xattr -cr "/Applications/Daily Timer.app"

Event System

Timer emits events via Wails runtime:

  • timer:tick - Every 100ms during active timer
  • timer:speaker_warning - When speaker time is running low
  • timer:speaker_timeup - When speaker time expired
  • timer:meeting_warning - When meeting time running low
  • timer:meeting_ended - When meeting ends
  • timer:speaker_changed - When switching speakers

Frontend listens in App.svelte:

EventsOn('timer:speaker_warning', handleWarning);

Debugging

Open DevTools in Wails app: Cmd+Option+I

Add console.log for event debugging:

function handleWarning(state) {
  console.log('=== handleWarning EVENT RECEIVED ===');
  console.log('settings:', settings);
  // ...
}

Last Updated: February 2026