Thingsboard Integration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Integrate Thingsboard API into the frontend with backend TB credentials endpoint, enabling device CRUD, RPC commands, attribute read/write, and telemetry reading.
Architecture: Backend exposes a NestJS module that returns per-user Thingsboard credentials. Frontend uses a Zustand store to persist TB auth state, a singleton ThingsboardService for TB API calls, and TBDevice class instances for device-specific operations. useThingsboardAuth hook auto-connects on backend login.
Tech Stack: NestJS, Zustand, Axios, React hooks
File Structure
backend/src/
├── thingsboard/
│ ├── thingsboard.controller.ts # GET /thingsboard/credentials
│ ├── thingsboard.service.ts # Fetch TB credentials per user
│ ├── thingsboard.module.ts # Module + route registration
│ └── dto/
│ └── tb-credentials.dto.ts # Response DTO
frontend/src/
├── stores/
│ └── thingsboard.store.ts # Zustand store (persist)
├── services/
│ └── thingsboard/
│ ├── index.ts # Barrel export
│ ├── thingsboard.service.ts # Singleton (init, login, logout, getClient)
│ └── tb-device.class.ts # TBDevice class
└── hooks/
└── useThingsboardAuth.ts # Auto-login hook
Task 1: Backend — Thingsboard Module
Files:
- Create:
backend/src/thingsboard/thingsboard.module.ts - Create:
backend/src/thingsboard/thingsboard.controller.ts - Create:
backend/src/thingsboard/thingsboard.service.ts - Create:
backend/src/thingsboard/dto/tb-credentials.dto.ts - Modify:
backend/src/app.module.ts— add ThingsboardModule import
Steps
- Step 1: Create DTO
// backend/src/thingsboard/dto/tb-credentials.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
export class TbCredentialsDto {
@IsString()
@IsNotEmpty()
thingsboardUrl: string;
@IsString()
@IsNotEmpty()
username: string;
@IsString()
@IsNotEmpty()
password: string;
}
- Step 2: Create Service
// backend/src/thingsboard/thingsboard.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UsersService } from '@/users/users.service';
import { TbCredentialsDto } from './dto/tb-credentials.dto';
@Injectable()
export class ThingsboardService {
constructor(
private readonly configService: ConfigService,
private readonly usersService: UsersService,
) {}
async getCredentials(userId: string): Promise<TbCredentialsDto> {
const user = await this.usersService.findById(userId);
const thingsboardUrl =
this.configService.get<string>('TB_API_URL') ||
'http://localhost:9090/api/v1';
return {
thingsboardUrl,
username: user.thingsboardUsername,
password: user.thingsboardPassword,
};
}
}
- Step 3: Create Controller
// backend/src/thingsboard/thingsboard.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
import { CurrentUser } from '@/auth/decorators/current-user.decorator';
import { ThingsboardService } from './thingsboard.service';
import { TbCredentialsDto } from './dto/tb-credentials.dto';
@Controller('thingsboard')
@UseGuards(JwtAuthGuard)
export class ThingsboardController {
constructor(private readonly thingsboardService: ThingsboardService) {}
@Get('credentials')
async getCredentials(
@CurrentUser('id') userId: string,
): Promise<TbCredentialsDto> {
return this.thingsboardService.getCredentials(userId);
}
}
- Step 4: Create Module
// backend/src/thingsboard/thingsboard.module.ts
import { Module } from '@nestjs/common';
import { ThingsboardController } from './thingsboard.controller';
import { ThingsboardService } from './thingsboard.service';
import { UsersModule } from '@/users/users.module';
@Module({
imports: [UsersModule],
controllers: [ThingsboardController],
providers: [ThingsboardService],
exports: [ThingsboardService],
})
export class ThingsboardModule {}
- Step 5: Add ThingsboardModule to AppModule
In backend/src/app.module.ts, add import:
import { ThingsboardModule } from './thingsboard/thingsboard.module';
// In @Module({ imports: [...] }):
ThingsboardModule,
- Step 6: Add TB fields to User entity
In backend/src/auth/entities/user.entity.ts, add columns:
@Column({ name: 'thingsboard_username', nullable: true })
thingsboardUsername: string;
@Column({ name: 'thingsboard_password', nullable: true })
thingsboardPassword: string;
- Step 7: Run database migration (generate + run)
cd backend && bun run migration:generate -- thingsboard-tb-credentials && bun run migration:run
- Step 8: Test the endpoint
curl -H "Authorization: Bearer <token>" http://localhost:3000/api/v1/thingsboard/credentials
Expected: {"thingsboardUrl":"...","username":"...","password":"..."}
- Step 9: Commit
git add backend/src/thingsboard/ backend/src/auth/entities/user.entity.ts
git commit -m "feat(thingsboard): add backend module for TB credentials endpoint"
Task 2: Frontend — Thingsboard Store
Files:
- Create:
frontend/src/stores/thingsboard.store.ts - Modify:
frontend/.env.example— addVITE_TB_URL
Steps
- Step 1: Write the failing test
Create frontend/src/stores/thingsboard.store.spec.ts:
import { useThingsboardStore } from './thingsboard.store';
describe('useThingsboardStore', () => {
beforeEach(() => {
useThingsboardStore.setState({
url: null,
token: null,
isConnected: false,
});
});
it('should store url and token on init', () => {
const { init } = useThingsboardStore.getState();
init('http://localhost:9090/api/v1', 'tb-token-123');
const state = useThingsboardStore.getState();
expect(state.url).toBe('http://localhost:9090/api/v1');
expect(state.token).toBe('tb-token-123');
expect(state.isConnected).toBe(false);
});
it('should set isConnected to true', () => {
const { setConnected } = useThingsboardStore.getState();
setConnected(true);
expect(useThingsboardStore.getState().isConnected).toBe(true);
});
it('should clear all state', () => {
const store = useThingsboardStore.getState();
store.init('http://localhost:9090/api/v1', 'tb-token-123');
store.setConnected(true);
store.clear();
const state = useThingsboardStore.getState();
expect(state.url).toBeNull();
expect(state.token).toBeNull();
expect(state.isConnected).toBe(false);
});
});
- Step 2: Run test to verify it fails
bun test -- frontend/src/stores/thingsboard.store.spec.ts
Expected: FAIL — file does not exist
- Step 3: Write store implementation
// frontend/src/stores/thingsboard.store.ts
import { create } from 'zustand';
import { devtools, persist } from 'zustand/middleware';
interface ThingsboardState {
url: string | null;
token: string | null;
isConnected: boolean;
init(url: string, token: string): void;
setConnected(connected: boolean): void;
clear(): void;
}
export const useThingsboardStore = create<ThingsboardState>()(
devtools(
persist(
(set) => ({
url: null,
token: null,
isConnected: false,
init: (url, token) => set({ url, token }),
setConnected: (connected) => set({ isConnected: connected }),
clear: () => set({ url: null, token: null, isConnected: false }),
}),
{
name: 'thingsboard-storage',
},
),
),
);
- Step 4: Run test to verify it passes
bun test -- frontend/src/stores/thingsboard.store.spec.ts
Expected: PASS
- Step 5: Add VITE_TB_URL to .env.example
Append to frontend/.env.example:
VITE_TB_URL=http://localhost:9090/api/v1
- Step 6: Commit
git add frontend/src/stores/thingsboard.store.ts frontend/src/stores/thingsboard.store.spec.ts frontend/.env.example
git commit -m "feat(thingsboard): add thingsboard Zustand store"
Task 3: Frontend — ThingsboardService Module
Files:
- Create:
frontend/src/services/thingsboard/thingsboard.service.ts - Create:
frontend/src/services/thingsboard/index.ts
Steps
- Step 1: Write the failing test
Create frontend/src/services/thingsboard/thingsboard.service.spec.ts:
import { ThingsboardService } from './thingsboard.service';
import { useThingsboardStore } from '@/stores/thingsboard.store';
// Mock store
vi.mock('@/stores/thingsboard.store', () => ({
useThingsboardStore: vi.fn(() => ({
url: null,
token: null,
isConnected: false,
init: vi.fn(),
setConnected: vi.fn(),
clear: vi.fn(),
})),
}));
// Mock axios
vi.mock('axios', () => {
const mockAxios = vi.fn(() => ({
interceptors: {
request: { use: vi.fn().mockReturnThis() },
response: { use: vi.fn().mockReturnThis() },
},
defaults: { headers: { common: {} } },
}));
mockAxios.create = vi.fn(() => mockAxios);
return { default: mockAxios };
});
describe('ThingsboardService', () => {
beforeEach(() => {
ThingsboardService.reset();
});
describe('init', () => {
it('should store baseUrl', () => {
ThingsboardService.init('http://localhost:9090/api/v1');
expect(ThingsboardService.getClient()).toBeDefined();
});
});
describe('reset', () => {
it('should clear all state', () => {
ThingsboardService.init('http://localhost:9090/api/v1');
ThingsboardService.reset();
expect(ThingsboardService.isConnected()).toBe(false);
});
});
});
- Step 2: Run test to verify it fails
bun test -- frontend/src/services/thingsboard/thingsboard.service.spec.ts
Expected: FAIL — file does not exist
- Step 3: Write ThingsboardService implementation
// frontend/src/services/thingsboard/thingsboard.service.ts
import axios, { type AxiosInstance } from 'axios';
import { useThingsboardStore } from '@/stores/thingsboard.store';
import { TBDevice } from './tb-device.class';
let _client: AxiosInstance | null = null;
let _baseUrl: string | null = null;
class ThingsboardService {
static init(baseUrl: string): void {
_baseUrl = baseUrl;
_client = axios.create({
baseURL: baseUrl,
headers: {
'Content-Type': 'application/json',
},
});
_client.interceptors.request.use((config) => {
const token = useThingsboardStore.getState().token;
if (token) {
config.headers['X-Authorization'] = `Bearer ${token}`;
}
return config;
});
_client.interceptors.response.use(
(response) => response,
(error) => {
const normalized = {
message:
error.response?.data?.message || error.message || 'Thingsboard request failed',
statusCode: error.response?.status || 500,
cause: error,
};
return Promise.reject(normalized);
},
);
}
static async login(username: string, password: string): Promise<void> {
if (!_client || !_baseUrl) {
throw new Error('ThingsboardService not initialized');
}
const response = await _client.post('/api/auth/login', {
username,
password,
});
const token = response.data.token;
useThingsboardStore.getState().init(_baseUrl, token);
}
static async logout(): Promise<void> {
useThingsboardStore.getState().clear();
}
static getToken(): string | null {
return useThingsboardStore.getState().token;
}
static isConnected(): boolean {
return useThingsboardStore.getState().isConnected;
}
static getClient(): AxiosInstance {
if (!_client) {
throw new Error('ThingsboardService not initialized. Call init() first.');
}
return _client;
}
static device(tbDeviceId: string): TBDevice {
if (!_client) {
throw new Error('ThingsboardService not initialized. Call init() first.');
}
return new TBDevice(tbDeviceId, _client);
}
static reset(): void {
_client = null;
_baseUrl = null;
useThingsboardStore.getState().clear();
}
}
export { ThingsboardService };
- Step 4: Run test to verify it passes
bun test -- frontend/src/services/thingsboard/thingsboard.service.spec.ts
Expected: PASS
- Step 5: Create barrel export
// frontend/src/services/thingsboard/index.ts
export { ThingsboardService } from './thingsboard.service';
export { TBDevice } from './tb-device.class';
- Step 6: Commit
git add frontend/src/services/thingsboard/
git commit -m "feat(thingsboard): add ThingsboardService singleton module"
Task 4: Frontend — TBDevice Class
Files:
- Create:
frontend/src/services/thingsboard/tb-device.class.ts - Test:
frontend/src/services/thingsboard/tb-device.class.spec.ts
Steps
- Step 1: Write the failing test
Create frontend/src/services/thingsboard/tb-device.class.spec.ts:
import { TBDevice } from './tb-device.class';
import type { AxiosInstance } from 'axios';
const createMockClient = () =>
({
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
}) as unknown as AxiosInstance;
describe('TBDevice', () => {
let mockClient: ReturnType<typeof createMockClient>;
beforeEach(() => {
mockClient = createMockClient();
});
describe('get', () => {
it('should return device info', async () => {
const deviceData = {
id: { id: 'tb-device-123' },
name: 'Test Device',
type: 'default',
};
mockClient.get = vi.fn().mockResolvedValue({ data: deviceData });
const device = new TBDevice('tb-device-123', mockClient);
const result = await device.get();
expect(result).toEqual(deviceData);
expect(mockClient.get).toHaveBeenCalledWith('/api/device/tb-device-123');
});
});
describe('delete', () => {
it('should call delete endpoint', async () => {
mockClient.delete = vi.fn().mockResolvedValue({ status: 200 });
const device = new TBDevice('tb-device-123', mockClient);
await device.delete();
expect(mockClient.delete).toHaveBeenCalledWith('/api/device/tb-device-123');
});
});
describe('getAttributes', () => {
it('should call attributes endpoint with keys', async () => {
const attrData = { key1: 'value1' };
mockClient.get = vi.fn().mockResolvedValue({ data: attrData });
const device = new TBDevice('tb-device-123', mockClient);
const result = await device.getAttributes(['key1']);
expect(result).toEqual(attrData);
expect(mockClient.get).toHaveBeenCalledWith(
'/api/plugins/telemetry/tb-device-123/values/attributes',
{ params: { keys: 'key1' } },
);
});
});
describe('saveAttributes', () => {
it('should post attributes to scope endpoint', async () => {
mockClient.post = vi.fn().mockResolvedValue({ status: 200 });
const device = new TBDevice('tb-device-123', mockClient);
await device.saveAttributes({ key1: 'value1' });
expect(mockClient.post).toHaveBeenCalledWith(
'/api/plugins/telemetry/tb-device-123/CLIENT_SCOPE',
{ key1: 'value1' },
);
});
});
describe('getLatestTelemetry', () => {
it('should call timeseries endpoint without range params', async () => {
const telemetryData = [{ key: 'temp', value: 25 }];
mockClient.get = vi.fn().mockResolvedValue({ data: telemetryData });
const device = new TBDevice('tb-device-123', mockClient);
const result = await device.getLatestTelemetry(['temp']);
expect(result).toEqual(telemetryData);
expect(mockClient.get).toHaveBeenCalledWith(
'/api/plugins/telemetry/tb-device-123/values/timeseries',
{ params: { keys: 'temp' } },
);
});
});
describe('getTelemetry', () => {
it('should include fromTs and toTs params', async () => {
const telemetryData = [{ key: 'temp', value: 25 }];
mockClient.get = vi.fn().mockResolvedValue({ data: telemetryData });
const device = new TBDevice('tb-device-123', mockClient);
await device.getTelemetry(['temp'], 1000, 2000);
expect(mockClient.get).toHaveBeenCalledWith(
'/api/plugins/telemetry/tb-device-123/values/timeseries',
{ params: { keys: 'temp', startTs: 1000, endTs: 2000 } },
);
});
});
describe('rpc', () => {
it('should send oneway RPC when waitForResponse is false', async () => {
mockClient.post = vi.fn().mockResolvedValue({ status: 200 });
const device = new TBDevice('tb-device-123', mockClient);
await device.rpc('turnOn', { relay: 1 }, { waitForResponse: false });
expect(mockClient.post).toHaveBeenCalledWith(
'/api/plugins/rpc/oneway/tb-device-123',
JSON.stringify({ method: 'turnOn', params: { relay: 1 } }),
);
});
it('should send twoway RPC when waitForResponse is true', async () => {
const rpcResponse = { result: 'ok' };
mockClient.post = vi.fn().mockResolvedValue({ data: rpcResponse });
const device = new TBDevice('tb-device-123', mockClient);
const result = await device.rpc(
'turnOn',
{ relay: 1 },
{ waitForResponse: true, timeout: 5000 },
);
expect(mockClient.post).toHaveBeenCalledWith(
'/api/plugins/rpc/twoway/tb-device-123',
JSON.stringify({ method: 'turnOn', params: { relay: 1 } }),
);
expect(result).toEqual(rpcResponse);
});
});
});
- Step 2: Run test to verify it fails
bun test -- frontend/src/services/thingsboard/tb-device.class.spec.ts
Expected: FAIL — file does not exist
- Step 3: Write TBDevice implementation
// frontend/src/services/thingsboard/tb-device.class.ts
import type { AxiosInstance } from 'axios';
export interface TBDeviceInfo {
id: { id: string };
name: string;
type: string;
label?: string;
[key: string]: unknown;
}
export interface TelemetryDataPoint {
ts: number;
value: unknown;
}
export class TBDevice {
private readonly tbDeviceId: string;
private readonly client: AxiosInstance;
constructor(tbDeviceId: string, client: AxiosInstance) {
this.tbDeviceId = tbDeviceId;
this.client = client;
}
async get(): Promise<TBDeviceInfo> {
const response = await this.client.get<TBDeviceInfo>(
`/api/device/${this.tbDeviceId}`,
);
return response.data;
}
async update(payload: Partial<TBDeviceInfo>): Promise<TBDeviceInfo> {
const response = await this.client.post<TBDeviceInfo>('/api/device', {
id: { id: this.tbDeviceId },
...payload,
});
return response.data;
}
async delete(): Promise<void> {
await this.client.delete(`/api/device/${this.tbDeviceId}`);
}
async getAttributes(keys?: string[]): Promise<Record<string, unknown>> {
const params = keys ? { keys: keys.join(',') } : undefined;
const response = await this.client.get<Record<string, unknown>>(
`/api/plugins/telemetry/${this.tbDeviceId}/values/attributes`,
{ params },
);
return response.data;
}
async saveAttributes(attributes: Record<string, unknown>): Promise<void> {
await this.client.post(
`/api/plugins/telemetry/${this.tbDeviceId}/CLIENT_SCOPE`,
attributes,
);
}
async getLatestTelemetry(keys?: string[]): Promise<Record<string, unknown>> {
const params = keys ? { keys: keys.join(',') } : undefined;
const response = await this.client.get<Record<string, unknown>>(
`/api/plugins/telemetry/${this.tbDeviceId}/values/timeseries`,
{ params },
);
return response.data;
}
async getTelemetry(
keys?: string[],
fromTs?: number,
toTs?: number,
): Promise<Record<string, unknown>> {
const params: Record<string, unknown> = {};
if (keys) params['keys'] = keys.join(',');
if (fromTs) params['startTs'] = fromTs;
if (toTs) params['endTs'] = toTs;
const response = await this.client.get<Record<string, unknown>>(
`/api/plugins/telemetry/${this.tbDeviceId}/values/timeseries`,
{ params },
);
return response.data;
}
async rpc(
method: string,
params?: Record<string, unknown>,
options?: { waitForResponse?: boolean; timeout?: number },
): Promise<unknown> {
const { waitForResponse = false, timeout = 5000 } = options ?? {};
const endpoint = waitForResponse
? `/api/plugins/rpc/twoway/${this.tbDeviceId}`
: `/api/plugins/rpc/oneway/${this.tbDeviceId}`;
const body = JSON.stringify({ method, params });
if (waitForResponse) {
const response = await this.client.post(endpoint, body, {
timeout,
headers: { 'Content-Type': 'application/json' },
});
return response.data;
}
await this.client.post(endpoint, body, {
timeout,
headers: { 'Content-Type': 'application/json' },
});
return undefined;
}
}
- Step 4: Run test to verify it passes
bun test -- frontend/src/services/thingsboard/tb-device.class.spec.ts
Expected: PASS
- Step 5: Commit
git add frontend/src/services/thingsboard/tb-device.class.ts frontend/src/services/thingsboard/tb-device.class.spec.ts
git commit -m "feat(thingsboard): add TBDevice class with CRUD, attributes, telemetry, and RPC"
Task 5: Frontend — useThingsboardAuth Hook + fetchTBcredentials
Files:
- Create:
frontend/src/hooks/useThingsboardAuth.ts - Create:
frontend/src/services/thingsboard/fetch-tb-credentials.ts(helper)
Steps
- Step 1: Write the failing test
Create frontend/src/hooks/useThingsboardAuth.spec.tsx:
import { renderHook, act, waitFor } from '@testing-library/react';
import { useThingsboardAuth } from './useThingsboardAuth';
import { useAuthStore } from '@/stores/auth.store';
import { useThingsboardStore } from '@/stores/thingsboard.store';
import { ThingsboardService } from '@/services/thingsboard';
import type { ReactNode } from 'react';
import { vi, describe, it, expect, beforeEach } from 'vitest';
vi.mock('@/services/thingsboard', () => ({
ThingsboardService: {
init: vi.fn(),
login: vi.fn().mockResolvedValue(undefined),
logout: vi.fn().mockResolvedValue(undefined),
isConnected: () => false,
},
}));
const wrapper = ({ children }: { children: ReactNode }) => (
<>{children}</>
);
describe('useThingsboardAuth', () => {
beforeEach(() => {
useAuthStore.setState({ isAuthenticated: false });
useThingsboardStore.setState({ isConnected: false, url: null, token: null });
ThingsboardService.init.mockClear();
ThingsboardService.login.mockClear();
});
it('should attempt TB login when backend auth succeeds', async () => {
useAuthStore.setState({ isAuthenticated: true });
// Mock fetch to return TB credentials
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
thingsboardUrl: 'http://localhost:9090/api/v1',
username: 'tbuser',
password: 'tbpass',
}),
});
const { result } = renderHook(() => useThingsboardAuth(), { wrapper });
await waitFor(() => {
expect(ThingsboardService.init).toHaveBeenCalledWith(
'http://localhost:9090/api/v1',
);
});
expect(ThingsboardService.login).toHaveBeenCalledWith('tbuser', 'tbpass');
});
it('should not trigger TB login when not authenticated', async () => {
const { result } = renderHook(() => useThingsboardAuth(), { wrapper });
expect(ThingsboardService.init).not.toHaveBeenCalled();
expect(ThingsboardService.login).not.toHaveBeenCalled();
});
});
- Step 2: Run test to verify it fails
bun test -- frontend/src/hooks/useThingsboardAuth.spec.tsx
Expected: FAIL — file does not exist
- Step 3: Write fetchTBcredentials helper
// frontend/src/services/thingsboard/fetch-tb-credentials.ts
interface TbCredentials {
thingsboardUrl: string;
username: string;
password: string;
}
export async function fetchTBcredentials(): Promise<TbCredentials> {
const response = await fetch(
`${import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1'}/thingsboard/credentials`,
{
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
},
);
if (!response.ok) {
throw {
message: 'Failed to fetch Thingsboard credentials',
statusCode: response.status,
};
}
return response.json();
}
- Step 4: Write useThingsboardAuth hook
// frontend/src/hooks/useThingsboardAuth.ts
import { useEffect } from 'react';
import { useAuthStore } from '@/stores/auth.store';
import { useThingsboardStore } from '@/stores/thingsboard.store';
import { ThingsboardService } from '@/services/thingsboard';
import { fetchTBcredentials } from '@/services/thingsboard/fetch-tb-credentials';
export const useThingsboardAuth = () => {
const { isAuthenticated } = useAuthStore();
const thingsboardStore = useThingsboardStore();
useEffect(() => {
if (isAuthenticated && !thingsboardStore.isConnected) {
fetchTBcredentials()
.then(({ thingsboardUrl, username, password }) => {
ThingsboardService.init(thingsboardUrl);
return ThingsboardService.login(username, password);
})
.then(() => thingsboardStore.setConnected(true))
.catch((error) => {
console.error('Failed to connect to Thingsboard:', error);
});
}
}, [isAuthenticated]);
useEffect(() => {
if (!isAuthenticated) {
ThingsboardService.logout();
thingsboardStore.clear();
}
}, [isAuthenticated]);
};
- Step 5: Update ThingsboardService barrel export
Update frontend/src/services/thingsboard/index.ts:
export { ThingsboardService } from './thingsboard.service';
export { TBDevice } from './tb-device.class';
export { fetchTBcredentials } from './fetch-tb-credentials';
- Step 6: Run test to verify it passes
bun test -- frontend/src/hooks/useThingsboardAuth.spec.tsx
Expected: PASS (may need minor test adjustment)
- Step 7: Commit
git add frontend/src/hooks/useThingsboardAuth.ts frontend/src/services/thingsboard/fetch-tb-credentials.ts frontend/src/services/thingsboard/index.ts
git commit -m "feat(thingsboard): add useThingsboardAuth hook for auto-login"
Task 6: Integration — Wire up useThingsboardAuth in App
Files:
- Modify:
frontend/src/App.tsxor main layout — adduseThingsboardAuthhook call
Steps
- Step 1: Find and read App.tsx
cat frontend/src/App.tsx
- Step 2: Add useThingsboardAuth call
In the root component (or AuthLayout), add:
// In App.tsx or root layout component
import { useThingsboardAuth } from '@/hooks/useThingsboardAuth';
// Inside the component:
useThingsboardAuth();
- Step 3: Run check
bun run check
Expected: No errors
- Step 4: Commit
git add frontend/src/App.tsx
git commit -m "feat(thingsboard): wire up useThingsboardAuth in App"
Spec Coverage Check
| Spec Section | Task |
|---|---|
| Backend TB credentials endpoint | Task 1 |
| Thingsboard Store (Zustand) | Task 2 |
| ThingsboardService singleton | Task 3 |
| TBDevice class | Task 4 |
| useThingsboardAuth hook | Task 5 |
| Wire up in App | Task 6 |
VITE_TB_URL env var |
Task 2 |
TB auth header X-Authorization |
Task 3 |
| RPC one-way / two-way | Task 4 |
| Telemetry range + latest | Task 4 |
| Attributes get/save | Task 4 |
All spec items covered. No placeholders found.
Next Step
Execution via subagent-driven-development or inline execution.