Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Lint

on:
pull_request:
branches: [main, dev]
workflow_dispatch:

jobs:
lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: corepack enable
- run: yarn install --immutable
- run: yarn lint
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ To contribute to Beacon, you should run it locally in Node.js. To start off foll
There are multiple commands usable for development:
- `yarn dev`: Runs in development mode. This will hot reload the ui, and doesn't compile the Typescript.
- `yarn typecheck`: Runs the typecheck, first for the backend, then the UI.
- `yarn lint`: Runs ESLint.
- `yarn build`: Builds vite and then the backend.
- `yarn serve`: Starts the project in production mode. `yarn build` must have been run before this.
- `yarn start`: Runs `build`, then `serve`.
33 changes: 33 additions & 0 deletions app/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @ts-check
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";

export default tseslint.config(
{ ignores: ["dist/**", "node_modules/**"] },
js.configs.recommended,
tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
},
},
{
files: ["ui/src/**/*.tsx", "ui/src/**/*.ts"],
plugins: { "react-hooks": reactHooks },
rules: {
...reactHooks.configs.recommended.rules,
// These v7 rules are too strict for patterns used throughout the codebase
"react-hooks/set-state-in-effect": "off",
"react-hooks/immutability": "off",
},
},
);
5 changes: 5 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
"license": "AGPL-3.0-only",
"scripts": {
"dev": "tsx supervisor.ts",
"lint": "eslint .",
"typecheck": "tsc --noEmit && tsc --noEmit -p ui/tsconfig.json",
"build": "vite build && tsup",
"serve": "NODE_ENV=production node dist/supervisor.js",
"start": "yarn build && NODE_ENV=production node dist/supervisor.js"
},
"packageManager": "yarn@4.13.0",
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^5.0.6",
"@types/node": "^25.5.0",
Expand All @@ -26,10 +28,13 @@
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.5.14",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.4.0",
"eslint-plugin-react-hooks": "^7.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.60.0",
"vite": "^8.0.7"
},
"dependencies": {
Expand Down
10 changes: 5 additions & 5 deletions app/src/database/CoreDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { ProducerStatus } from '../tally/producer/AbstractTallyProducer';
import { GlobalSourceTools, type ProducerBundle, type SourceInfo } from '../tally/types/ProducerStates';
import { DeviceTallyState, GlobalDeviceTools, type DeviceAddress, type TallyDevice } from '../tally/types/ConsumerStates';
import { Logger } from '../logging/Logger';
import type { LifecycleConfig, LifeCycleConsumerConfig } from '../tally/TallyLifecycle';
import { TallyOrchestrator, type OrchestratorConfig } from '../tally/TallyOrchestrator';
import type { LifeCycleConsumerConfig } from '../tally/TallyLifecycle';
import type { OrchestratorConfig } from '../tally/TallyOrchestrator';
export const SettingKey = {
consumers: {
aedes: "consumers.aedes",
Expand Down Expand Up @@ -41,7 +41,7 @@ type SettingType<K extends string, T = SettingMap> =

// TODO add more try catch.
export class CoreDatabase {
private static instance: CoreDatabase;
private static instance: CoreDatabase | undefined;
private db: Database.Database;

private logger: Logger;
Expand Down Expand Up @@ -236,9 +236,9 @@ export class CoreDatabase {
}
CoreDatabase.instance.logger.info(`Database closed successfully.`);
} catch (err) {
this.instance.logger.error(`Error closing database:`, err);
CoreDatabase.instance?.logger.error(`Error closing database:`, err);
} finally {
CoreDatabase.instance = undefined as any;
CoreDatabase.instance = undefined;
}

}
Expand Down
16 changes: 8 additions & 8 deletions app/src/logging/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class Logger {
}
}

private print(level: LogLevel, ...data: any[]) {
private print(level: LogLevel, ...data: unknown[]) {

if (level < Logger.GlobalFileLevel && level < Logger.GlobalConsoleLevel) return;

Expand All @@ -72,8 +72,8 @@ export class Logger {
}
}

private parseData(data: any): string {
return data.map((item: any) => {
private parseData(data: unknown[]): string {
return data.map((item: unknown) => {
if (item instanceof Error) return item.stack || item.message;

if (typeof item === 'object' && item !== null) {
Expand Down Expand Up @@ -112,12 +112,12 @@ export class Logger {
}
}

public debug(...data: any[]) { this.print(LogLevel.DEBUG, ...data); }
public info(...data: any[]) { this.print(LogLevel.INFO, ...data); }
public warn(...data: any[]) { this.print(LogLevel.WARN, ...data); }
public error(...data: any[]) { this.print(LogLevel.ERROR, ...data); }
public debug(...data: unknown[]) { this.print(LogLevel.DEBUG, ...data); }
public info(...data: unknown[]) { this.print(LogLevel.INFO, ...data); }
public warn(...data: unknown[]) { this.print(LogLevel.WARN, ...data); }
public error(...data: unknown[]) { this.print(LogLevel.ERROR, ...data); }

public fatal(message: string, ...extraData: any[]): never {
public fatal(message: string, ...extraData: unknown[]): never {
this.print(LogLevel.FATAL, message, ...extraData);

const errorString = extraData.length > 0
Expand Down
2 changes: 1 addition & 1 deletion app/src/system/SystemInfoUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default class SystemInfoUtil {
// Check GPIO pin config for newer versions.

return HardwareVersion.V2;
} catch (e) {
} catch {
return HardwareVersion.UNKNOWN;
}
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/tally/TallyLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ type ConsumerConfigMap = { // TODO Add consumer info

type RegisteredConsumerId = keyof ConsumerConfigMap;

type ConsumerMap<Extra extends object = {}> = {
type ConsumerMap<Extra extends object = object> = {
[K in keyof ConsumerConfigMap]: LifeCycleConsumerConfig<ConsumerConfigMap[K]> & Extra;
};

type ConsumerRuntime = {
factory: (config: any) => AbstractConsumer;
factory: (config: ConsumerConfig) => AbstractConsumer;
isAvailable: () => boolean;
isDisableable: () => boolean;
};
Expand Down Expand Up @@ -383,7 +383,7 @@ export class TallyLifecycle {

// Dry-construct: runs checkConfig() in ctor, no port binding until init().
// Throws here if config is invalid, before the old consumer is touched.
const consumer = entry.factory(entry.config);
const consumer = entry.factory(entry.config ?? {});

if (this.orchestrator.hasConsumer(id)) {
this.logger.info(`Stopping consumer:`, id);
Expand Down
2 changes: 1 addition & 1 deletion app/src/tally/TallyOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class TallyOrchestrator extends EventEmitter<OrchestratorEvents> {
}
}

protected checkConfig(config: OrchestratorConfig){
protected checkConfig(_config: OrchestratorConfig){

}

Expand Down
4 changes: 2 additions & 2 deletions app/src/tally/consumer/AbstractConsumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type ConsumerEvents = {
}

// TODO: Maybe IConnection to force getId and get and setName and other shared ops like db?
export abstract class AbstractConsumer<T extends ConsumerEvents & Record<string, any[]> = ConsumerEvents> extends EventEmitter<T> {
export abstract class AbstractConsumer<T extends ConsumerEvents & Record<string, unknown[]> = ConsumerEvents> extends EventEmitter<T> {

protected readonly conType: string = "CONS";

Expand Down Expand Up @@ -235,7 +235,7 @@ export abstract class AbstractConsumer<T extends ConsumerEvents & Record<string,
}

protected abstract sendDeviceTally(device: TallyDevice): void;
protected sendDeviceConfig(device: TallyDevice): void {}; // TODO: Right place?
protected sendDeviceConfig(_device: TallyDevice): void {}; // TODO: Right place?

consumeTally(state: TallyState): void {
this.tallyState = state;
Expand Down
2 changes: 1 addition & 1 deletion app/src/tally/consumer/IGlobalBroadcastConsumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export interface IGlobalBroadcastConsumer {
}

export function isGlobalBroadcastConsumer(consumer: AbstractConsumer): consumer is AbstractConsumer & IGlobalBroadcastConsumer {
return 'publishDeviceTally' in consumer && typeof (consumer as any).publishDeviceTally === 'function';
return 'publishDeviceTally' in consumer && typeof (consumer as AbstractConsumer & IGlobalBroadcastConsumer).publishDeviceTally === 'function';
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { ConnectionType, DeviceAlertState, DeviceTallyState, GlobalDeviceTools,
import { HARDWARE_VERSION_STRING, HardwareVersion } from "../../../types/SystemInfo";
import type { Gpio } from 'pigpio';

// TODO: check if this is the right GPIO library. Was rpi-gpio before, but it's not updated.
// TODO: Populate this. Pinouts?

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface GpioConsumerConfig extends ConsumerConfig {
// Pin mappings TBD
// TODO move hw discovery to another class.
Expand Down Expand Up @@ -167,7 +168,7 @@ export class RpiGpioHardwareConsumer extends AbstractConsumer {
long: `Local ${i+1}`
},
connection: ConnectionType.HARDWARE,
patch: new Array(),
patch: [],
state: DeviceTallyState.NONE,
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { AbstractConsumer, ConsumerStatus, type ConsumerConfig, type ConsumerEvents, type ConsumerInfo } from "../AbstractConsumer";
import type { TallyState } from "../../types/ProducerStates";
import type { TallyDevice } from "../../types/DeviceTypes";


export interface NetworkConsumerInfo extends ConsumerInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ export class AedesNetworkConsumer extends AbstractNetworkConsumer implements IGl
}

await new Promise<void>((resolve) => {
const stream = this.aedes.persistence.createRetainedStream('#');
const persistence = (this.aedes as unknown as { persistence: { createRetainedStream(topic: string): NodeJS.ReadableStream; cleanRetained(topic: string, cb: () => void): void } }).persistence;
const stream = persistence.createRetainedStream('#');
const clears: Promise<void>[] = [];
stream.on('data', (packet: { topic: string }) => {
clears.push(new Promise<void>((res) =>
this.aedes.persistence.cleanRetained(packet.topic, () => res())
persistence.cleanRetained(packet.topic, () => res())
Comment on lines +83 to +88
));
});
stream.on('end', async () => { await Promise.all(clears); resolve(); });
Expand Down Expand Up @@ -144,7 +145,7 @@ export class AedesNetworkConsumer extends AbstractNetworkConsumer implements IGl
}
}

this.aedes.on('subscribe', (subscriptions: Subscription[], client: Client) => {
this.aedes.on('subscribe', (subscriptions: Subscription[], _client: Client) => {
this.logger.debug('Subscription:', subscriptions);

// if (subscriptions.some(sub => sub.topic == 'tally' || sub.topic.startsWith('tally/') ))
Expand Down Expand Up @@ -406,7 +407,7 @@ export class AedesNetworkConsumer extends AbstractNetworkConsumer implements IGl

// TODO: Make flips sides boolean?
const payload = JSON.stringify({
brightness: device.brightness ? (device.brightness * 255 / 100) : 255,
brightness: device.brightness !== undefined ? (device.brightness * 255 / 100) : 255,
// brightness: 10,
name: device.name,
state_on_disconnect: this.disconnectState,
Expand Down
2 changes: 1 addition & 1 deletion app/src/tally/producer/AbstractTallyProducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export type TallyProducerEvents = {
}

// TODO: Add an AbstractProducer that can be extended by an AbstractAlertProducer
export abstract class AbstractTallyProducer<T extends TallyProducerEvents & Record<string, any[]> =TallyProducerEvents> extends EventEmitter<T> {
export abstract class AbstractTallyProducer<T extends TallyProducerEvents & Record<string, unknown[]> =TallyProducerEvents> extends EventEmitter<T> {

public readonly conType: string = "PROD";
public readonly prodType: string = "SWTCHR";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { Enums as AtemEnums } from "atem-connection";
import { GlobalSourceTools, type ProducerModel, type SourceInfo, type SourceMap, type TallyState } from "../../types/ProducerStates";


// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface AtemNetClientProducerConfig extends NetClientProducerConfig {
// me: number[] | null; //TODO
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface AtemNetClientProducerInfo extends NetClientProducerInfo {
}
export interface AtemNetClientProducerInfoInternal extends NetClientProducerInfo {
Expand Down Expand Up @@ -188,7 +190,7 @@ export class AtemNetClientTallyProducer extends AbstractNetClientTallyProducer {
}

protected _parseSources(): SourceMap {
let sources: SourceMap = new Map();
const sources: SourceMap = new Map();

if (!this.info.state || !this.info.connected){
return sources;
Expand Down
4 changes: 2 additions & 2 deletions app/src/tally/types/DeviceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,11 @@ export interface DeviceInfo {
output_count?: number;
}

interface DeviceDiscoveryPacket extends DeviceInfo {
export interface DeviceDiscoveryPacket extends DeviceInfo {
id: DeviceId;
}

interface NewTallyDevice { // TODO
export interface NewTallyDevice { // TODO
id: DeviceAddress;
state: { // Tally logic, on base device topic.
patch: Array<GlobalSource>;
Expand Down
3 changes: 2 additions & 1 deletion app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"noUncheckedSideEffectImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
}
},
"exclude": ["ui", "node_modules"]
}
2 changes: 1 addition & 1 deletion app/ui/src/api/BeaconApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function request<T = void>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, init)
if (!res.ok) {
let message = `${init?.method ?? 'GET'} ${path} failed: ${res.status}`
try { const body = await res.json(); if (body?.error) message = body.error } catch {}
try { const body = await res.json(); if (body?.error) message = body.error } catch { /* empty */ }
throw new Error(message)
}
if (res.status === 204) return undefined as T
Expand Down
2 changes: 1 addition & 1 deletion app/ui/src/components/PatchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function PatchModal({
const toggle = (key: string) => {
setSelected(prev => {
const next = new Set(prev)
next.has(key) ? next.delete(key) : next.add(key)
if (next.has(key)) next.delete(key); else next.add(key)
return next
})
}
Expand Down
14 changes: 0 additions & 14 deletions app/ui/src/components/devices/DeviceEditPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useState } from 'react'
import { UITallyDevice } from '../../types/DeviceStates'
import type { GlobalTallySource, SourceInfo } from '../../../../src/tally/types/ProducerStates'
import { useBeacon } from '../../context/BeaconContext'
import { Toggle } from '../Toggle'

// ? Consumer-specific config sections
Expand Down Expand Up @@ -71,21 +69,9 @@ export function DeviceEditModal({ device, open, onSave, onRemove, onClose }: Dev
const [long, setLong] = useState(device.name.long)
const [brightness, setBrightness] = useState<number>(device.brightness ?? 100)
const [flip, setFlip] = useState<boolean>(device.flip ?? false)
const { producers } = useBeacon()

if (!open) return null

const patch = device.patch

function shortName(producer: string, source: string): string {
const key = `${producer}:${source}`
for (const p of producers) {
const sources = p.info?.sources as unknown as Record<string, SourceInfo>
if (sources?.[key]) return sources[key].short ?? source
}
return source
}

const handleSave = () => {
onSave({ name: { long, short: short || undefined }, brightness, flip })
}
Expand Down
Loading
Loading