4 Commits

Author SHA1 Message Date
Mikhail Kiselev
41bc35fd43 feat: add Jira URL integration and relay functionality
- Implemented OpenBrowserURL function to open Jira links
- Updated components to utilize the new Jira URL feature
- Added relay server to manage real-time URL updates
- Set default Jira URL in settings if not specified
2026-04-03 03:31:00 +03:00
Mikhail Kiselev
79214910f1 fix: update readyToStart label RU/EN 2026-03-13 04:20:41 +03:00
Mikhail Kiselev
9b65c95000 feat: open Jira in named window on start and speaker change 2026-03-13 04:19:05 +03:00
Mikhail Kiselev
577b1abe9b fix: use default Jira URL when meeting URL is not set 2026-03-13 04:10:53 +03:00
10 changed files with 105 additions and 43 deletions

View File

@@ -7,7 +7,7 @@
import History from './components/History.svelte'
import Setup from './components/Setup.svelte'
import { EventsOn, EventsOff, WindowSetSize, ScreenGetAll } from '../wailsjs/runtime/runtime'
import { GetParticipants, StartMeeting, GetSettings, SkipSpeaker, RemoveFromQueue, SwitchToSpeaker } from '../wailsjs/go/app/App'
import { GetParticipants, StartMeeting, GetSettings, SkipSpeaker, RemoveFromQueue, SwitchToSpeaker, OpenBrowserURL } from '../wailsjs/go/app/App'
import { t, initLocale } from './lib/i18n'
import { attendance } from './lib/stores'
@@ -36,6 +36,7 @@
EventsOn('timer:meeting_warning', handleMeetingWarning)
EventsOn('timer:meeting_ended', handleMeetingEnded)
EventsOn('timer:speaker_changed', handleSpeakerChanged)
EventsOn('jira:open', (url) => { if (url) OpenBrowserURL(url) })
// Warm up AudioContext on first user interaction
const warmUpAudio = async () => {
@@ -96,6 +97,7 @@
EventsOff('timer:meeting_warning')
EventsOff('timer:meeting_ended')
EventsOff('timer:speaker_changed')
EventsOff('jira:open')
})
function handleTimerEvent(state) {
@@ -290,7 +292,7 @@
{#if currentView === 'main'}
{#if meetingActive && timerState}
<div class="timer-view">
<Timer {timerState} />
<Timer {timerState} jiraUrl={settings?.defaultJiraUrl || ''} />
<ParticipantList {timerState} on:skip={handleSkipFromList} on:switch={handleSwitchSpeaker} />
<Controls {timerState} on:stop={() => meetingActive = false} />
</div>
@@ -305,6 +307,9 @@
<button class="secondary-btn" on:click={() => currentView = 'setup'}>
{$t('timer.editParticipants')}
</button>
{#if settings?.defaultJiraUrl}
<button class="secondary-btn" on:click={() => OpenBrowserURL(settings.defaultJiraUrl)}>🔗 Jira</button>
{/if}
</div>
{:else}
<div class="no-meeting">

View File

@@ -6,8 +6,9 @@
let sessions = []
let stats = null
let loading = true
let dateFrom = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]
let dateTo = new Date().toISOString().split('T')[0]
const _now = new Date()
let dateFrom = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, '0')}-01`
let dateTo = `${_now.getFullYear()}-${String(_now.getMonth() + 1).padStart(2, '0')}-${String(_now.getDate()).padStart(2, '0')}`
let exporting = false
let showDeleteAllConfirm = false
let deletingSessionId = null

View File

@@ -1,6 +1,6 @@
<script>
import { onMount, createEventDispatcher } from 'svelte'
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting, GetSettings } from '../../wailsjs/go/app/App'
import { GetParticipants, GetMeeting, StartMeeting, AddParticipant, DeleteParticipant, ReorderParticipants, UpdateParticipant, UpdateMeeting, GetSettings, OpenBrowserURL } from '../../wailsjs/go/app/App'
import { t } from '../lib/i18n'
import { attendance } from '../lib/stores'

View File

@@ -1,8 +1,10 @@
<script>
import { onMount, onDestroy } from 'svelte'
import { t } from '../lib/i18n'
import { OpenBrowserURL } from '../../wailsjs/go/app/App'
export let timerState
export let jiraUrl = ''
let currentTime = ''
let clockInterval
@@ -63,6 +65,9 @@
<div class="timer-container" class:warning={timerState?.warning} class:overtime={timerState?.speakerOvertime}>
<div class="header-row">
<div class="current-clock">{currentTime}</div>
{#if jiraUrl}
<button class="jira-btn" on:click={() => OpenBrowserURL(jiraUrl)} title="Открыть Jira">🔗</button>
{/if}
<div class="help-icon">
?
<div class="help-tooltip">
@@ -204,6 +209,17 @@
margin-bottom: 8px;
}
.jira-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 2px 4px;
opacity: 0.6;
transition: opacity 0.2s;
}
.jira-btn:hover { opacity: 1; }
.current-clock {
font-size: 14px;
color: #8899a6;

View File

@@ -44,7 +44,7 @@ export const translations = {
noSpeaker: 'Нет спикера',
noActiveMeeting: 'Собрание не запущено',
goToParticipants: 'Перейдите в раздел Участники, чтобы добавить участников',
readyToStart: 'Всё готово к началу',
readyToStart: 'Всё готово к началу собрания',
editParticipants: 'Редактировать участников',
noParticipants: 'Участники не добавлены',
registeredParticipants: 'Зарегистрированные участники',
@@ -223,7 +223,7 @@ export const translations = {
noSpeaker: 'No speaker',
noActiveMeeting: 'No active meeting',
goToParticipants: 'Go to Participants to add participants',
readyToStart: 'Ready to start',
readyToStart: 'Ready to start the meeting',
editParticipants: 'Edit participants',
noParticipants: 'No participants added',
registeredParticipants: 'Registered participants',

View File

@@ -43,6 +43,8 @@ export function GetVersion():Promise<string>;
export function NextSpeaker():Promise<void>;
export function OpenBrowserURL(arg1:string):Promise<void>;
export function PauseMeeting():Promise<void>;
export function RemoveFromQueue(arg1:number):Promise<void>;

View File

@@ -82,6 +82,10 @@ export function NextSpeaker() {
return window['go']['app']['App']['NextSpeaker']();
}
export function OpenBrowserURL(arg1) {
return window['go']['app']['App']['OpenBrowserURL'](arg1);
}
export function PauseMeeting() {
return window['go']['app']['App']['PauseMeeting']();
}

View File

@@ -9,6 +9,7 @@ import (
"time"
"daily-timer/internal/models"
"daily-timer/internal/relay"
"daily-timer/internal/services/updater"
"daily-timer/internal/storage"
"daily-timer/internal/timer"
@@ -24,6 +25,8 @@ type App struct {
session *models.MeetingSession
currentLogs map[uint]*models.ParticipantLog
participantURLs map[uint]string
jiraBaseURL string
relay *relay.Server
updater *updater.Updater
}
@@ -31,12 +34,20 @@ func New(store *storage.Storage) *App {
return &App{
store: store,
currentLogs: make(map[uint]*models.ParticipantLog),
relay: relay.New(),
updater: updater.New(),
}
}
func (a *App) Startup(ctx context.Context) {
a.ctx = ctx
a.relay.Start()
}
func (a *App) OpenBrowserURL(url string) {
if url != "" {
runtime.BrowserOpenURL(a.ctx, url)
}
}
func (a *App) OnDomReady(ctx context.Context) {
@@ -185,23 +196,39 @@ func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) er
}
}
// Use meeting-specific URL, fall back to default from settings
jiraURL := meeting.JiraURL
if jiraURL == "" && settings != nil {
jiraURL = settings.DefaultJiraUrl
}
a.jiraBaseURL = jiraURL
a.participantURLs = make(map[uint]string)
if meeting.JiraURL != "" {
if jiraURL != "" {
for _, p := range participants {
url := meeting.JiraURL
url := jiraURL
if p.JiraFilter != "" {
url = meeting.JiraURL + "&quickFilter=" + p.JiraFilter
url = jiraURL + "&quickFilter=" + p.JiraFilter
}
a.participantURLs[p.ID] = url
}
}
if jiraURL != "" {
a.relay.Broadcast(jiraURL)
go runtime.BrowserOpenURL(a.ctx, fmt.Sprintf("http://127.0.0.1:%d/", relay.Port))
}
a.timer = timer.New(meeting.TimeLimit, warningThreshold)
a.timer.SetQueue(queue)
a.currentLogs = make(map[uint]*models.ParticipantLog)
go a.handleTimerEvents()
a.timer.Start()
if jiraURL != "" {
go runtime.BrowserOpenURL(a.ctx, jiraURL)
}
return nil
}
@@ -221,7 +248,11 @@ func (a *App) handleTimerEvents() {
case timer.EventSpeakerChanged:
a.saveSpeakerLog(event.State)
if url, ok := a.participantURLs[event.State.CurrentSpeakerID]; ok && url != "" {
runtime.BrowserOpenURL(a.ctx, url)
a.relay.Broadcast(url)
runtime.EventsEmit(a.ctx, "jira:open", url)
} else if a.jiraBaseURL != "" {
a.relay.Broadcast(a.jiraBaseURL)
runtime.EventsEmit(a.ctx, "jira:open", a.jiraBaseURL)
}
runtime.EventsEmit(a.ctx, "timer:speaker_changed", event.State)
case timer.EventMeetingEnded:

View File

@@ -79,6 +79,9 @@ func (s *Storage) ensureDefaults() error {
// Migrate old default value to new default (60 min instead of 15)
s.db.Model(&settings).Update("default_meeting_time", 3600)
}
if settings.DefaultJiraUrl == "" {
s.db.Model(&settings).Update("default_jira_url", "https://jira.ncloudtech.ru/secure/RapidBoard.jspa?rapidView=1431&projectKey=RNDIN")
}
var meeting models.Meeting
result = s.db.First(&meeting)

View File

@@ -105,16 +105,8 @@ func (t *Timer) Start() {
t.speakerWarned = false
t.meetingWarned = false
hasSpeakers := len(t.queue) > 0
if hasSpeakers {
t.startNextSpeaker(now, 0)
}
t.mu.Unlock()
if hasSpeakers {
t.emit(EventSpeakerChanged)
}
// Не активируем участника автоматически!
go t.tick()
}
@@ -461,7 +453,9 @@ func (t *Timer) buildState() models.TimerState {
if t.running && !t.paused {
now := time.Now()
if t.currentSpeakerID != 0 {
speakerElapsed = now.Sub(t.speakerStartTime)
}
meetingElapsed = now.Sub(t.meetingStartTime)
}
@@ -519,9 +513,15 @@ func (t *Timer) tick() {
}
now := time.Now()
speakerElapsed := now.Sub(t.speakerStartTime)
meetingElapsed := now.Sub(t.meetingStartTime)
if t.currentSpeakerID == 0 {
t.mu.Unlock()
t.emit(EventTick)
continue
}
speakerElapsed := now.Sub(t.speakerStartTime)
remaining := t.speakerLimit - speakerElapsed
if !t.speakerWarned && remaining <= t.warningThreshold && remaining > 0 {
t.speakerWarned = true