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
This commit is contained in:
Mikhail Kiselev
2026-04-03 03:31:00 +03:00
parent 79214910f1
commit 41bc35fd43
9 changed files with 85 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,7 +36,7 @@
EventsOn('timer:meeting_warning', handleMeetingWarning)
EventsOn('timer:meeting_ended', handleMeetingEnded)
EventsOn('timer:speaker_changed', handleSpeakerChanged)
EventsOn('jira:open', handleJiraOpen)
EventsOn('jira:open', (url) => { if (url) OpenBrowserURL(url) })
// Warm up AudioContext on first user interaction
const warmUpAudio = async () => {
@@ -134,10 +134,6 @@
timerState = state
}
function handleJiraOpen(url) {
window.open(url, 'jira-kanban')
}
let audioContext = null
function getAudioContext() {
@@ -296,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>
@@ -311,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

@@ -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"
@@ -25,6 +26,7 @@ type App struct {
currentLogs map[uint]*models.ParticipantLog
participantURLs map[uint]string
jiraBaseURL string
relay *relay.Server
updater *updater.Updater
}
@@ -32,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) {
@@ -205,7 +215,8 @@ func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) er
}
if jiraURL != "" {
runtime.EventsEmit(a.ctx, "jira:open", 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)
@@ -214,6 +225,10 @@ func (a *App) StartMeeting(participantOrder []uint, attendance map[uint]bool) er
go a.handleTimerEvents()
a.timer.Start()
if jiraURL != "" {
go runtime.BrowserOpenURL(a.ctx, jiraURL)
}
return nil
}
@@ -233,8 +248,10 @@ func (a *App) handleTimerEvents() {
case timer.EventSpeakerChanged:
a.saveSpeakerLog(event.State)
if url, ok := a.participantURLs[event.State.CurrentSpeakerID]; ok && 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)

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