Chronos is a standalone desktop application for FRC teams on macOS and Windows. Built with Electron and React, it connects directly to your robot over NetworkTables 4 (NT4) and replaces the default Shuffleboard/SmartDashboard workflow with a structured, stage-driven match flow, real-time telemetry panels, keybind-based robot control, match recording, and offline log replay. Check out the vendordep backend here to start developing today: https://github.com/Daboss-1/ChronosBackend/
Chronos works with any FRC team's robot. Configure your robot's address in the settings panel and start driving.
- Requirements
- Installation
- Running the App
- Development
- Robot Address Configuration
- Match Flow
- Dashboard Features
- Backend Integration (Java)
- NT4 Topic Reference
- Project Structure
- Build and Packaging
Dashboard (Desktop)
- macOS 11 (Big Sur) or later, arm64 or x86-64
- Windows 10 or later, x64
- Node.js 20 or later (for building from source)
- npm 10 or later
Robot (Java)
- WPILib 2025 or later
- PathPlanner (for autonomous path preview and sync)
- The
Dashboard.javasubsystem class from the Chronos backend library
- Open
Chronos-1.0.0-universal.dmgfrom therelease/directory. - Drag
Chronos.appto/Applications. - On first launch, macOS will block the app because it is not notarized. Clear this with:
xattr -cr /Applications/Chronos.app- Open Chronos from Finder or Spotlight.
- Open
Chronos Setup 1.0.0.exefrom therelease/directory and complete the installer. - Optional: use the portable build (
Chronos 1.0.0.exe) if you prefer no installation. - Launch Chronos from the Start Menu (installed) or by double-clicking the portable executable.
git clone <repo-url>
cd Chronos
npm install --legacy-peer-deps
npm run dist:macUse npm run dist:win instead to produce Windows .exe artifacts.
Build artifacts are written to release/.
Launch Chronos from your packaged target:
- macOS: double-click
Chronos.appin/Applicationsorrelease/mac-universal/ - Windows: run the installed Start Menu app, or open the portable
.exefromrelease/
On launch the app will:
- Probe TCP port 5810 on
localhostand127.0.0.1for a simulation. Enter your robot's address in the settings panel to connect to a real robot. - Establish an NT4 WebSocket connection directly to the resolved address.
- Start the
sync-pathsbackground process, which watches NT for PathPlanner auto files and syncs them locally. - Open the Checklist stage, which waits for the robot to publish its pre-match status.
No Terminal window is required. Everything runs inside the desktop app.
To run the app with Vite hot-module reloading and Electron side-by-side:
cd Chronos
npm run devThis starts the Vite dev server on port 5173 and launches Electron pointed at it. DevTools open automatically in a detached window.
Available scripts
| Command | Description |
|---|---|
npm run dev |
Vite dev server + Electron with HMR and DevTools |
npm run build |
Production Vite build to dist/ |
npm run build:electron |
Alias for production Vite build |
npm run dist:mac |
Vite build then electron-builder universal macOS package |
npm run dist:win |
Vite build then electron-builder Windows package (.exe) |
npm run preview |
Preview built Vite output |
The resolved robot address is shown in the header as a small button. Click it to open the address panel.
- Connect — enter your robot's hostname or IP address and press Connect. Common formats:
10.TE.AM.2(e.g.10.1.72.2for Team 172)roboRIO-TEAM-FRC.local(e.g.roboRIO-172-FRC.local)- Any static IP or hostname on your network The setting is saved and persists across restarts.
- Auto-Discover — clears the override and re-runs the local probe sequence.
The setting is also accessible from the Robot application menu.
Simulation (WPILib Simulation mode) is automatically reached at localhost when no field robot is present.
Chronos enforces a linear pre-match workflow that mirrors the steps a drive team follows at a real event.
Checklist -> Auto Selection -> Confirmation -> Autonomous -> Teleop -> Post-Game
| Stage | Purpose |
|---|---|
| Checklist | Displays health status items published by the robot. All items must be ok before proceeding, or the driver can override. |
| Auto Selection | Lists PathPlanner routines discovered from NT. Shows a 2D field preview with animated path. Sends the selection back to the robot. |
| Confirmation | Reviews match info from FMS (event, match type, alliance, station, game message). Advances automatically when FMS signals match start. |
| Autonomous | 20-second autonomous period. Shows field map with robot pose, path overlay, auto routine name, subsystem status, and NT controls panel. Transitions to Teleop when FMS leaves autonomous. |
| Teleop | 140-second teleop period. Shows field map, match phase (Shift 1-4, Endgame), hub active/inactive state, scoring counter, and NT controls. Plays audio cues at 30 s, 10 s, and 0 s. Transitions to Post-Game when FMS disables the robot. |
| Post-Game | Displays match score summary. Download or replay the match recording. Upload external .wpilog or .json files for replay. |
The dashboard tabs in the header (Driver, Developer, SysId, etc.) are discovered dynamically from NT and can be accessed at any point during the match.
Live ring-buffer rewind The RewindBar at the bottom of the screen maintains a 3-minute rolling buffer of all NT values sampled at 20 Hz. Drag the scrubber left to go back in time. All panels reflect the historical values at the selected timestamp. Click "Live" to return to real-time.
Match recording
When Autonomous begins, Chronos starts recording all /ChronosDashboard/ and /Robot/ NT topics at 20 Hz into a WPILog v1 binary file. Up to 10 recordings are kept in app storage.
Log replay
In Post-Game, or from the header upload button, upload a .wpilog (robot recording) or .json (dashboard export) file. The log viewer provides a full scrubber, playback at 0.25x–4x speed, and a FieldMap-based pose replay. Log replay is also accessible from the Log View button in the header at any time.
Drag-and-drop widget layout The Autonomous and Teleop stages, and all discovered NT tabs, use a drag-and-resize grid layout. Click "Edit Layout" in the header to enter edit mode. Layouts are persisted per stage in app storage.
Graph panels Any NT number value can be dropped onto a Graph panel to plot it over time. Window sizes of 10 s, 30 s, 60 s, and 120 s are available. Graph configurations are saved per panel.
Keybinds panel
The Keybinds tab shows all keys the robot has registered via putKeybind. Each key displays its description and running state. Holding a key in Chronos publishes pressed = true to NT; releasing publishes pressed = false. Multiple keys can be held simultaneously.
Camera streams
Camera streams registered via putCameraStream or putLimelightStream appear in the Camera Switcher panel. Click a thumbnail to open a fullscreen overlay.
Alerts overlay
Toast notifications appear on-screen when the robot publishes an alert via putAlert. Alerts auto-dismiss after 6 seconds.
Theme and language Dark, light, and high-contrast themes are available from the theme button in the header. English, Spanish, and Portuguese translations are available from the language button.
Dashboard is a WPILib SubsystemBase singleton. Add it to your robot container — it registers itself with the CommandScheduler and its periodic() method handles all NT writes and reads each robot loop iteration.
// In your RobotContainer constructor:
private final Dashboard dashboard = Dashboard.INSTANCE;
// Dashboard extends SubsystemBase, so the CommandScheduler picks it up automatically.
// No explicit register() call is needed.Because Dashboard.INSTANCE is static final, it is safe to access from any class without passing a reference:
Dashboard.INSTANCE.putNumber("Driver", "Flywheel RPM", () -> shooter.getRPM());All registrations (putCommand, putNumber, etc.) must be called during robot initialization, not from periodic(). Each method is idempotent — calling it a second time with the same key is a no-op.
Register PathPlanner autos to make them appear in the Auto Selection stage. The dashboard detects PathPlannerAuto instances and reads the .auto file to generate a path preview.
// Register the default routine (pre-selected on dashboard open)
dashboard.putDefaultAutonomousCommand(
"S1-DEPOT",
"Starts under left trench and empties depot.",
new PathPlannerAuto("S1-DEPOT")
);
// Register additional routines
dashboard.putAutonomousCommand(
"S2-CLIMB",
"Starts center, shoots loaded balls, climbs left post.",
new PathPlannerAuto("S2-CLIMB")
);Retrieve the driver's selection at the start of autonomous:
@Override
public Command getAutonomousCommand() {
return dashboard.getSelectedAutonomousCommand();
}The sync-paths process running inside Chronos watches NT for PathPlanner path references and copies .auto and .path files from the robot deploy directory into the dashboard's local public/ folder so the field preview renders correctly without a live robot connection.
Commands are displayed as buttons in any named tab. Clicking a button toggles the command (schedules it if idle, cancels it if running).
// Simple command button
dashboard.putCommand("Driver", "Reset Turret",
Commands.runOnce(() -> turret.resetAngle()).ignoringDisable(true)
);
// Specify a custom table path (advanced — use the two-arg form for a different NT root)
dashboard.putCommand("Driver", "/MyRobot", "Custom Command", myCommand);The first argument is the tab name — this controls which tab the button appears in on the dashboard. Use any string; if the tab does not already exist it will be created automatically.
Keybinds link a keyboard key (held down in Chronos) to a robot command. When the key is pressed, onTrue is scheduled. When released, onFalse is scheduled (if provided).
// Key with only an onTrue handler
dashboard.putKeybind("space", "Shoot", shootCommand);
// Key with both press and release handlers
dashboard.putKeybind("w", "Drive forward",
Commands.runOnce(() -> driveSpeed = 1.0),
Commands.runOnce(() -> driveSpeed = 0.0)
);The key string must match the browser KeyboardEvent.key value, lowercased. Common values:
| Key | String |
|---|---|
| Letter keys | "a" through "z" |
| Spacebar | "space" |
| Arrow keys | "arrowup", "arrowdown", "arrowleft", "arrowright" |
| F-keys | "f1" through "f12" |
| Escape | "escape" |
| Enter | "enter" |
| Backspace | "backspace" |
Multiple keybinds can be active simultaneously. The robot reads each key's NT topic (/ChronosDashboard/commands/Keybinds/<key>/pressed) independently every loop iteration, so holding w, a, and arrowleft at the same time works correctly.
Reading key state in periodic code:
The pressed boolean is available directly over NT if you need to read it outside of a Command:
boolean wHeld = NetworkTableInstance.getDefault()
.getTable("/ChronosDashboard/commands/Keybinds/w")
.getEntry("pressed")
.getBoolean(false);A common integration pattern uses putKeybind with Commands.runOnce to flip a double field that is then consumed by the drive default command, enabling full WASD keyboard control of the drivetrain:
// In your container constructor:
dashboard.putKeybind("w", "Drive forward", Commands.runOnce(() -> wKey = 1), Commands.runOnce(() -> wKey = 0));
dashboard.putKeybind("s", "Drive backward", Commands.runOnce(() -> sKey = 1), Commands.runOnce(() -> sKey = 0));
dashboard.putKeybind("a", "Strafe left", Commands.runOnce(() -> aKey = 1), Commands.runOnce(() -> aKey = 0));
dashboard.putKeybind("d", "Strafe right", Commands.runOnce(() -> dKey = 1), Commands.runOnce(() -> dKey = 0));
dashboard.putKeybind("j", "Rotate left", Commands.runOnce(() -> arrowLeft = 1), Commands.runOnce(() -> arrowLeft = 0));
dashboard.putKeybind("l", "Rotate right", Commands.runOnce(() -> arrowRight = 1), Commands.runOnce(() -> arrowRight = 0));
// In your OI bindings:
drive.setDefaultCommand(drive.driveByJoystick(
() -> dKey - aKey,
() -> wKey - sKey,
() -> arrowLeft - arrowRight
));Read-only values displayed in the Values panel of any tab.
// Publish a number updated every loop
dashboard.putNumber("Driver", "Shooter RPM", () -> shooter.getRPM());
// Publish a boolean
dashboard.putBoolean("Developer", "Intake Deployed", () -> intake.isDeployed());
// Publish a string
dashboard.putString("Driver", "Robot State", () -> stateMachine.getCurrentState().name());
// With a custom NT table root (advanced overload)
dashboard.putNumber("Developer", "/MyCustomTable", "Voltage", () -> pdh.getVoltage());Tunables appear as editable inputs in the Tunables panel. When the driver changes a value and submits it, runOnChange is called on the robot with the new value.
// Number tunable
dashboard.putNumberTunable("Developer", "Shooter P Gain", 0.5, kP -> {
shooter.setPGain(kP);
});
// Boolean toggle
dashboard.putBooleanTunable("Developer", "Brake Mode", false, brake -> {
drivetrain.setBrakeMode(brake);
});
// String tunable
dashboard.putStringTunable("Developer", "Auto Strategy", "default", strategy -> {
autoSelector.setStrategy(strategy);
});Tunables use a changed flag handshake. The robot sets changed = false after consuming the new value. The dashboard will not allow re-submission until the robot acknowledges the previous change. This prevents dropped updates over a lossy connection.
Camera streams appear in the Camera Switcher panel of the specified tab. Click a stream to open it fullscreen.
// Register a Limelight (automatically constructs the URL from mDNS name)
dashboard.putLimelightStream("Developer", "limelight-front");
// Published URL: http://limelight-front.local:5800
// Register any MJPEG stream
dashboard.putCameraStream("Driver", "Intake Cam", "http://10.1.72.11:1181/stream.mjpg");Checklist items appear on the Checklist stage before the match. Each item reports a status of ok, warn, error, or unknown. All items must be ok before the "All Clear" button is enabled (the driver can override).
// Simple healthy/unhealthy check
dashboard.putChecklistItem("Drivetrain", () -> drivetrain.isCalibrated());
// With a dynamic message
dashboard.putChecklistItem("Vision",
() -> vision.isConnected(),
() -> vision.isConnected() ? "Tracking " + vision.getTagCount() + " tags" : "No targets detected"
);
dashboard.putChecklistItem("Battery",
() -> pdh.getVoltage() >= 12.0,
() -> String.format("%.1f V", pdh.getVoltage())
);The status shown is:
ok— supplier returnstrueerror— supplier returnsfalse
warn and unknown states are available for items that have not yet been evaluated (the topic is absent from NT).
Registers a voltage supplier. The header battery indicator updates every loop.
dashboard.putBatteryVoltage(() -> Volts.of(pdh.getVoltage()));Sets the background color of the entire dashboard. Useful for alliance color indication or match state signaling.
// Set a static color
dashboard.setDashboardLight(Color.kBlue);
// Use as a command (e.g., in an auto routine or state machine)
Commands.sequence(
dashboard.setDashboardLightCommand(Color.kGreen),
shootCommand,
dashboard.setDashboardLightCommand(Color.kBlue)
);Passing a color with value kBlack or any color matching the background restores the default appearance. The transition animates over 120 ms.
The Systems API groups related commands, tunables, and values under a named subsystem in the Systems panel. This is the cleanest way to expose an entire subsystem's interface without placing everything in a flat list.
dashboard.putSystem("Developer", "Drivetrain")
.withCommand("SysId Quasistatic Fwd", drive.sysIdQuasistaticTranslation(Direction.kForward))
.withCommand("SysId Quasistatic Rev", drive.sysIdQuasistaticTranslation(Direction.kReverse))
.withCommand("SysId Dynamic Fwd", drive.sysIdDynamicTranslation(Direction.kForward))
.withCommand("SysId Dynamic Rev", drive.sysIdDynamicTranslation(Direction.kReverse))
.withNumber("Left Velocity", () -> drive.getLeftVelocity())
.withNumber("Right Velocity", () -> drive.getRightVelocity())
.withBoolean("At Pose", () -> drive.isAtTargetPose())
.withNumberTunable("Max Speed", 3.5, v -> drive.setMaxSpeed(v))
.withBooleanTunable("Brake Mode", true, b -> drive.setBrakeMode(b));
dashboard.putSystem("Developer", "Turret")
.withCommand("Reset Angle", Commands.runOnce(() -> turret.resetAngle()))
.withNumber("Angle Deg", () -> turret.getAngle().in(Degrees))
.withBoolean("At Target", () -> turret.isAtTarget());Each system appears as a collapsible card in the Systems panel. Its commands, tunables, and read values are grouped inside it.
@DashboardTunable and @DashboardTunableConstants are field-level and class-level annotations that automatically register matching fields as tunables when dashboard.register(object) is called.
// Register all @DashboardTunable fields on a subsystem instance
dashboard.register(turret.getSuzie());
// Register all static fields in a constants class (pass the class itself)
dashboard.register(LobbyConstants.class);Annotate individual fields:
public class ShooterConstants {
@DashboardTunable(name = "P Gain", tab = "Developer")
public static double kP = 0.5;
@DashboardTunable(name = "Target RPM", tab = "Driver")
public static double kTargetRPM = 3500.0;
}Annotate an entire class to register all its fields without annotating each one individually:
@DashboardTunableConstants(name = "CarouselConstants", tab = "Developer")
public class CarouselConstants {
public static double kSpeed = 0.7;
public static double kGearRatio = 10.0;
public static boolean kInverted = false;
}Supported field types: double, int, boolean, String, char, Angle (WPILib), Distance (WPILib).
Register a named field and one or more robot overlays for display on the dashboard's FieldMap widget.
// Create a field and attach a robot
dashboard.putField("Driver", "Main Field")
.withRobot("Lobby", () -> drive.getPose());
// Multiple robots on the same field (e.g., vision ghost robot)
dashboard.putField("Developer", "Vision Debug")
.withRobot("Estimated", () -> drive.getPose())
.withRobot("Vision", () -> vision.getVisionPose());You can also call putRobot directly:
dashboard.putRobot("Driver", "Main Field", "Opponent", () -> opponentPose);The FieldMap widget redraws every loop at 20 Hz. Pose coordinates use the WPILib standard (meters, origin at the blue alliance wall corner).
The dashboard writes the driver's autonomous selection back to NT. Read it from the robot code:
// In getAutonomousCommand():
return dashboard.getSelectedAutonomousCommand();This looks up the command registered under the name stored at /ChronosDashboard/selectedAutonomous/Match. If the name does not match any registered command, Commands.none() is returned.
All topics are under the /ChronosDashboard root unless noted.
| Topic | Type | Direction | Description |
|---|---|---|---|
/ChronosDashboard/commands/<tab>/<name>/running |
boolean | Robot → Dashboard | Whether the command is currently scheduled |
/ChronosDashboard/commands/<tab>/<name>/requestId |
int | Dashboard → Robot | Incremented to toggle command |
/ChronosDashboard/commands/<tab>/<name>/lastHandledRequestId |
int | Robot → Dashboard | Last requestId the robot acted on |
/ChronosDashboard/commands/Keybinds/<key>/pressed |
boolean | Dashboard → Robot | True while the key is held |
/ChronosDashboard/commands/Keybinds/<key>/running |
boolean | Robot → Dashboard | True while the bound command is running |
/ChronosDashboard/numbers/<tab>/<name>/value |
double | Robot → Dashboard | Read-only number |
/ChronosDashboard/strings/<tab>/<name>/value |
string | Robot → Dashboard | Read-only string |
/ChronosDashboard/booleans/<tab>/<name>/value |
boolean | Robot → Dashboard | Read-only boolean |
/ChronosDashboard/tunableNumbers/<tab>/<name>/value |
double | Bidirectional | Current tunable value |
/ChronosDashboard/tunableNumbers/<tab>/<name>/changed |
boolean | Dashboard → Robot | Set true when driver submits a new value |
/ChronosDashboard/tunableBooleans/<tab>/<name>/value |
boolean | Bidirectional | Current tunable value |
/ChronosDashboard/tunableStrings/<tab>/<name>/value |
string | Bidirectional | Current tunable value |
/ChronosDashboard/autonomousCommands/Match/<name>/PathPlannerPath |
string | Robot → Dashboard | Absolute path to .auto file |
/ChronosDashboard/autonomousCommands/Match/<name>/ClassName |
string | Robot → Dashboard | Java class name of the command |
/ChronosDashboard/autonomousCommands/Match/<name>/Description |
string | Robot → Dashboard | Human-readable description |
/ChronosDashboard/selectedAutonomous/Match |
string | Bidirectional | Name of the selected auto routine |
/ChronosDashboard/cameraStreams/<tab>/<name>/url |
string | Robot → Dashboard | MJPEG stream URL |
/ChronosDashboard/checklist/<name>/status |
string | Robot → Dashboard | ok, warn, error, or unknown |
/ChronosDashboard/checklist/<name>/message |
string | Robot → Dashboard | Optional status message |
/ChronosDashboard/alerts/<name>/active |
boolean | Robot → Dashboard | Whether the alert is currently firing |
/ChronosDashboard/battery/voltage |
double | Robot → Dashboard | Battery voltage in volts |
/ChronosDashboard/dashboardLight/color |
string | Robot → Dashboard | Hex color string (e.g. #0000ff) |
/ChronosDashboard/systems/<system>/commands/<tab>/<name>/... |
— | Bidirectional | Same structure as commands, scoped to system |
/ChronosDashboard/robots/<tab>/<field>/<name>/x |
double | Robot → Dashboard | Robot pose X in meters |
/ChronosDashboard/robots/<tab>/<field>/<name>/y |
double | Robot → Dashboard | Robot pose Y in meters |
/ChronosDashboard/robots/<tab>/<field>/<name>/rotation |
double | Robot → Dashboard | Robot heading in degrees |
Chronos/
├── electron/
│ ├── main.js # Electron main process: BrowserWindow, IPC, robot auto-discovery, sync-paths
│ └── preload.js # contextBridge: exposes window.electronAPI to renderer
├── src/
│ ├── App.jsx # Root component: stage routing, keybind capture, dashboard light
│ ├── main.jsx # React entry point: NT4Provider with dynamic robot address
│ ├── stages/ # One component per match stage
│ │ ├── Checklist.jsx
│ │ ├── AutoSelection.jsx
│ │ ├── Confirmation.jsx
│ │ ├── Autonomous.jsx
│ │ ├── Teleop.jsx
│ │ ├── PostGame.jsx
│ │ ├── NTTabView.jsx # NT panel renderer (commands, tunables, values, cameras)
│ │ └── DeveloperDashboard.jsx
│ ├── components/ # Reusable UI components
│ │ ├── Header.jsx # Top bar: tabs, alliance, battery, settings
│ │ ├── FieldMap.jsx # Canvas-based 2D field with robot pose and paths
│ │ ├── WidgetGrid.jsx # Drag-and-resize grid for Autonomous/Teleop stages
│ │ ├── NTTabWidgetGrid.jsx # Drag-and-resize grid for discovered NT tabs
│ │ ├── GraphPanel.jsx # Time-series graph with drag-and-drop NT values
│ │ ├── CameraSwitcher.jsx # MJPEG camera thumbnails and fullscreen overlay
│ │ ├── AlertsOverlay.jsx # Toast notification stack
│ │ ├── RewindBar.jsx # Live ring-buffer timeline scrubber
│ │ ├── LogReplayBar.jsx # Log file playback transport
│ │ ├── LogReplayDashboard.jsx # Full-screen log replay shell
│ │ ├── MatchReplayViewer.jsx # Post-game pose replay with FieldMap
│ │ └── DownloadMenu.jsx # WPILog/JSON/PDF download FAB
│ ├── contexts/ # React context providers
│ │ ├── ThemeContext.jsx
│ │ ├── I18nContext.jsx
│ │ ├── LayoutContext.jsx
│ │ ├── RewindContext.jsx
│ │ └── LogReplayContext.jsx
│ ├── hooks/
│ │ ├── useMatchRecorder.js # NT recording to WPILog v1
│ │ ├── useAdvantageScope.js # AdvantageScope NT bridge
│ │ ├── useNTRingBuffer.js # 3-minute rolling sample buffer
│ │ ├── useSoundCues.js # Web Audio match milestone tones
│ │ ├── useDiscoveredTabs.js # Scans NT for dashboard tab names
│ │ └── useEntryOrHistorical.js # Live/rewind value selector
│ ├── utils/
│ │ ├── pathLoader.js # PathPlanner .auto/.path file parser and Bezier interpolation
│ │ ├── wpilog.js # WPILog v1 encoder/decoder
│ │ ├── matchPhase.js # Match shift/endgame phase calculator
│ │ ├── ntTabData.js # NT topic tree parser for tab data
│ │ ├── pdfGenerator.jsx # Auto-routine PDF export
│ │ └── icons.jsx # Inline SVG icon components
│ ├── styles/
│ │ ├── main.css
│ │ ├── stages.css
│ │ └── features.css
│ └── i18n/
│ ├── en.json
│ ├── es.json
│ └── pt.json
├── scripts/
│ └── sync-paths.js # NT4 watcher that copies PathPlanner files from robot to public/
├── public/
│ ├── field.png # 2025 Reefscape field image
│ ├── autos/ # PathPlanner .auto files (synced from robot)
│ └── paths/ # PathPlanner .path files + automap.json (synced from robot)
├── build/ # electron-builder resources (optional app icons)
├── release/ # Packaged output (.app/.dmg on macOS, .exe on Windows)
├── index.html
├── vite.config.js
└── package.json
Chronos can be packaged for both macOS and Windows using electron-builder.
| Command | Description |
|---|---|
npm run dist:mac |
Build a macOS .app and .dmg into release/ |
npm run dist:win |
Build Windows .exe artifacts (NSIS installer and portable EXE) into release/ |
# Development (HMR + Electron)
npm run dev
# Production Vite build only
npm run build
# Full macOS package (builds Vite then runs electron-builder)
npm run dist:mac
# Full Windows package (builds Vite then runs electron-builder)
npm run dist:winOutput at release/ includes:
Chronos-1.0.0-universal.dmgandmac-universal/Chronos.appon macOS buildsChronos Setup 1.0.0.exe(NSIS installer) andChronos 1.0.0.exe(portable) on Windows builds
The macOS package is built as a universal binary targeting both Apple Silicon (arm64) and Intel (x86-64) Macs.
Code signing is not configured. To distribute signed builds, configure platform signing in electron-builder:
- macOS: Apple Developer ID certificate + notarization (
afterSign) - Windows: Authenticode certificate