Skip to content

Commit f4a95fe

Browse files
authored
Merge pull request #5 from huntrw6/agent/release-v1.1.5
Release StagePilot 1.1.5
2 parents f83d195 + b187557 commit f4a95fe

42 files changed

Lines changed: 876 additions & 78 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
release_tag:
77
description: GitHub release tag that receives the disk images
88
required: true
9-
default: v1.1.0
9+
default: v1.1.5
1010
push:
1111
tags:
1212
- "v*"

.github/workflows/release-windows.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
release_tag:
77
description: GitHub release tag that receives the Windows installer
88
required: true
9-
default: v1.1.0
9+
default: v1.1.5
1010
push:
1111
tags:
1212
- "v*"

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
StagePilot brings the moving parts of a live production into one dependable dashboard. It loads service plans from Planning Center, listens for MIDI cues from MultiTracks Playback, keeps a ProPresenter countdown in sync, and sends scheduled MIDI cues to a lighting controller such as Lightkey.
44

5-
## Download StagePilot 1.1
5+
## Download StagePilot 1.1.5
66

7-
Download the installer for your computer from the [StagePilot 1.1.0 release](https://github.com/huntrw6/stagepilot/releases/tag/v1.1.0):
7+
Download the installer for your computer from the [StagePilot 1.1.5 release](https://github.com/huntrw6/stagepilot/releases/tag/v1.1.5):
88

9-
- [Windows x64 installer](https://github.com/huntrw6/stagepilot/releases/download/v1.1.0/StagePilot_1.1.0_x64-setup.exe)
10-
- [Intel Mac DMG](https://github.com/huntrw6/stagepilot/releases/download/v1.1.0/StagePilot_1.1.0_x64.dmg)
11-
- [Apple Silicon Mac DMG](https://github.com/huntrw6/stagepilot/releases/download/v1.1.0/StagePilot_1.1.0_aarch64.dmg)
9+
- [Windows x64 installer](https://github.com/huntrw6/stagepilot/releases/download/v1.1.5/StagePilot_1.1.5_x64-setup.exe)
10+
- [Intel Mac DMG](https://github.com/huntrw6/stagepilot/releases/download/v1.1.5/StagePilot_1.1.5_x64.dmg)
11+
- [Apple Silicon Mac DMG](https://github.com/huntrw6/stagepilot/releases/download/v1.1.5/StagePilot_1.1.5_aarch64.dmg)
1212

1313
On **Windows**, run the installer and open StagePilot from the Start menu.
1414

@@ -37,7 +37,9 @@ All normal setup happens inside StagePilot; command-line variables are not requi
3737
4. Click **ProPresenter**, enter its host and API port, select the song timer and optional Look, then test the connection.
3838
5. Click **Lights** to select an output and add elapsed-time lighting cues when lighting automation is needed.
3939

40-
Settings persist between launches. Planning Center credentials are stored separately in the operating system credential store and are never returned by the StagePilot API.
40+
Settings persist between launches. On future launches, StagePilot automatically refreshes the saved MIDI input and reconnects configured Planning Center, ProPresenter, and Lights integrations after startup finishes. Planning Center credentials are stored separately in the operating system credential store and are never returned by the StagePilot API.
41+
42+
The title-bar **X** hides the dashboard while StagePilot and its integrations continue running. Restore it from the StagePilot system-tray icon on Windows or the StagePilot Dock/menu-bar icon on macOS. Use **Quit StagePilot** from that menu when you want to stop the application and its managed backend completely.
4143

4244
## Browser dashboard
4345

backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "stagepilot"
7-
version = "1.1.0"
7+
version = "1.1.5"
88
description = "Event-driven production automation backend for StagePilot"
99
readme = "README.md"
1010
requires-python = ">=3.12"

backend/src/stagepilot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""StagePilot backend package."""
22

3-
__version__ = "1.1.0"
3+
__version__ = "1.1.5"

backend/src/stagepilot/core/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ class PlanningCenterSettings(BaseModel):
232232
upcoming_lookahead_days: int = Field(default=30, ge=0, le=365)
233233
request_timeout_seconds: float = Field(default=10.0, ge=1.0, le=60.0)
234234
user_agent: str = Field(
235-
default="StagePilot/1.1.0 (https://github.com/huntrw6/stagepilot)",
235+
default="StagePilot/1.1.5 (https://github.com/huntrw6/stagepilot)",
236236
min_length=1,
237237
max_length=256,
238238
)
@@ -279,7 +279,7 @@ class Settings(BaseModel):
279279
"""Validated runtime settings; integration secrets remain server-side only."""
280280

281281
app_name: str = "StagePilot"
282-
version: str = "1.1.0"
282+
version: str = "1.1.5"
283283
bind_host: str = "127.0.0.1"
284284
bind_port: int = Field(default=8765, ge=1, le=65535)
285285
log_level: str = "INFO"

backend/src/stagepilot/core/plugin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,19 @@ async def stop_all(self) -> None:
6464
*(self._stop_plugin(plugin) for plugin in reversed(self._plugins.values()))
6565
)
6666

67+
async def restart_failed(self) -> None:
68+
"""Retry plugins whose first startup attempt failed after dependencies settle."""
69+
70+
failed = [
71+
plugin
72+
for plugin in self._plugins.values()
73+
if self._health[plugin.name].status is PluginStatus.ERROR
74+
]
75+
for plugin in failed:
76+
await self._stop_plugin(plugin)
77+
if self._health[plugin.name].status is PluginStatus.STOPPED:
78+
await self._start_plugin(plugin)
79+
6780
async def health(self) -> list[PluginHealth]:
6881
return list(
6982
await asyncio.gather(

backend/src/stagepilot/main.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import os
67
import sys
78
from collections.abc import AsyncIterator
8-
from contextlib import asynccontextmanager
9+
from contextlib import asynccontextmanager, suppress
910
from pathlib import Path
1011

1112
import uvicorn
@@ -39,6 +40,7 @@
3940
)
4041
from stagepilot.plugins.propresenter import ProPresenterClientFactory, ProPresenterPlugin
4142
from stagepilot.services.planning_center_setup import PlanningCenterSetupService
43+
from stagepilot.services.startup_activation import StartupActivationService
4244
from stagepilot.services.state_service import StateService
4345

4446

@@ -93,6 +95,7 @@ def create_app(
9395
)
9496
plugin_manager = PluginManager(event_bus)
9597
midi_plugin: MidiPlaybackPlugin | None = None
98+
planning_center_plugin: PlanningCenterPlugin | None = None
9699
propresenter_plugin: ProPresenterPlugin | None = None
97100
lights_plugin = LightsPlugin(
98101
event_bus,
@@ -121,17 +124,16 @@ def create_app(
121124
)
122125
)
123126
else:
124-
plugin_manager.register(
125-
PlanningCenterPlugin(
126-
event_bus,
127-
state_store,
128-
resolved_settings.planning_center,
129-
timezone_name=resolved_settings.timezone,
130-
client_factory=planning_center_client_factory,
131-
today_provider=planning_center_today_provider,
132-
plan_cache_store=resolved_plan_cache_store,
133-
)
127+
planning_center_plugin = PlanningCenterPlugin(
128+
event_bus,
129+
state_store,
130+
resolved_settings.planning_center,
131+
timezone_name=resolved_settings.timezone,
132+
client_factory=planning_center_client_factory,
133+
today_provider=planning_center_today_provider,
134+
plan_cache_store=resolved_plan_cache_store,
134135
)
136+
plugin_manager.register(planning_center_plugin)
135137

136138
real_midi_enabled = (
137139
resolved_settings.midi.enabled
@@ -175,6 +177,16 @@ def create_app(
175177
propresenter_controller=propresenter_plugin,
176178
lights_controller=lights_plugin,
177179
)
180+
startup_activation = StartupActivationService(
181+
plugin_manager=plugin_manager,
182+
state_store=state_store,
183+
midi=midi_plugin,
184+
propresenter=propresenter_plugin,
185+
propresenter_settings=resolved_settings.propresenter,
186+
lights=lights_plugin,
187+
lights_settings=resolved_settings.lights,
188+
planning_center=planning_center_plugin,
189+
)
178190

179191
@asynccontextmanager
180192
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
@@ -183,10 +195,17 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
183195
await plugin_manager.start_all()
184196
await event_bus.publish(new_event(EventType.APPLICATION_STARTED, source="application"))
185197
logger.info("application_started", version=resolved_settings.version)
198+
activation_task = asyncio.create_task(
199+
_run_startup_activation(startup_activation),
200+
name="stagepilot-startup-activation",
201+
)
186202
try:
187203
yield
188204
finally:
189205
logger.info("application_stopping")
206+
activation_task.cancel()
207+
with suppress(asyncio.CancelledError):
208+
await activation_task
190209
await event_bus.publish(new_event(EventType.APPLICATION_STOPPING, source="application"))
191210
await plugin_manager.stop_all()
192211
await state_service.stop()
@@ -223,6 +242,13 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
223242
return application
224243

225244

245+
async def _run_startup_activation(service: StartupActivationService) -> None:
246+
"""Let macOS and network integrations settle, then reconcile saved settings."""
247+
248+
await asyncio.sleep(1)
249+
await service.run()
250+
251+
226252
app = create_app()
227253

228254

backend/src/stagepilot/plugins/midi_playback/plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ async def input_snapshot(self, *, refresh: bool = False) -> MidiInputSnapshot:
253253
if refresh and self._backend is not None and self._executor is not None:
254254
try:
255255
await self._refresh_input_names()
256+
self._wake.set()
256257
except Exception:
257258
await self._set_connection(
258259
ConnectionStatus.ERROR,

backend/src/stagepilot/plugins/planning_center/plugin.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ async def health(self) -> PluginHealth:
191191
last_activity_at=self._last_activity_at,
192192
)
193193

194+
async def refresh_now(self) -> None:
195+
"""Refresh the saved service after the desktop startup reconciliation pass."""
196+
197+
if self._status is not PluginStatus.RUNNING or self._client is None:
198+
return
199+
await self._request_regular_refresh(wait=True)
200+
194201
async def _on_reload_requested(self, _event: StagePilotEvent) -> None:
195202
if self._stopping:
196203
return

0 commit comments

Comments
 (0)