Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2506c8c
feat: add ParkTrack logo
nawinds Jun 2, 2026
e16fc11
feat: align analytics API contracts
666mxvbee Jun 3, 2026
cefe242
feat: load admin analytics dashboard
666mxvbee Jun 3, 2026
e8532bf
feat: enable analytics detail pages
666mxvbee Jun 3, 2026
b351dcb
feat: clarify analytics scope selection
666mxvbee Jun 3, 2026
51ec14a
feat: add forecast quality analytics
666mxvbee Jun 3, 2026
01dc63b
feat: enrich analytics chart tooltips
666mxvbee Jun 3, 2026
3865d31
feat: add analytics camera actions
666mxvbee Jun 3, 2026
a7e3480
chore: remove analytics placeholder stubs
666mxvbee Jun 3, 2026
1741d0a
Merge pull request #50 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 3, 2026
12a2b49
style: improve analytics map layout
666mxvbee Jun 3, 2026
51f8aff
chore: hide internal dashboard metadata
666mxvbee Jun 3, 2026
86cfbe8
Merge pull request #51 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 4, 2026
f436518
style: prioritize dashboard attention zones
666mxvbee Jun 4, 2026
2e07552
chore: polish user-facing admin labels
666mxvbee Jun 4, 2026
ba8ccf3
Improves the presentation and readability of the ParkTrack admin inte…
666mxvbee Jun 4, 2026
6d33525
fix: keep camera map centered on resize
666mxvbee Jun 4, 2026
090c9c6
style: fit admin workspaces to viewport
666mxvbee Jun 4, 2026
5b5340d
fix: contain dashboard camera cards
666mxvbee Jun 4, 2026
c3f3346
chore: remove redundant session status
666mxvbee Jun 4, 2026
66d4635
feat: add analytics time controls
666mxvbee Jun 4, 2026
343b67b
feat: show relative analytics freshness
666mxvbee Jun 4, 2026
bc7ad5e
feat: improve detector health table
666mxvbee Jun 4, 2026
e7a05b8
fix: wrap analytics chart tooltips
666mxvbee Jun 4, 2026
48515ff
style: expand analytics charts
666mxvbee Jun 4, 2026
786c53e
fix: scope forecast snapshot query
666mxvbee Jun 4, 2026
c2ffa43
Merge pull request #53 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 4, 2026
71f5d94
refactor: combine occupancy forecast chart
666mxvbee Jun 4, 2026
d52f1c4
fix: contain analytics axis labels
666mxvbee Jun 4, 2026
9c8aa21
feat: simplify zone occupancy chart
666mxvbee Jun 4, 2026
7f34d78
fix: enable detector health scrolling
666mxvbee Jun 4, 2026
05a85e8
feat: pin analytics controls
666mxvbee Jun 4, 2026
3e18d45
feat: cache camera analytics snapshots
666mxvbee Jun 4, 2026
8cfecf9
Merge pull request #54 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 4, 2026
09b20ba
style: refine analytics refresh action
666mxvbee Jun 4, 2026
1a7d0a6
fix: fill fullscreen camera previews
666mxvbee Jun 4, 2026
b2f2f7a
Merge pull request #55 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 4, 2026
ac1fd89
fix: center fullscreen labeler image
666mxvbee Jun 4, 2026
f107948
Merge pull request #56 from ParkTrack-Project/AnalyticsPanel
666mxvbee Jun 4, 2026
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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ParkTrack Labeler</title>
<title>ParkTrack Admin</title>
<link rel="stylesheet" href="/src/styles.css"/>
<link rel="icon" type="image/svg+xml" href="/labeler-icon.jpg" />
</head>
Expand Down
Binary file added public/parktrack.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function renderRoute(route: AppRoute, viewMode: ViewMode) {
if (route === 'sources') return <SourcesPage />;
if (route === 'cameras') {
return (
<div className="legacy-map-grid">
<div className="legacy-map-grid camera-map-grid">
<CamerasPage />
</div>
);
Expand Down
119 changes: 103 additions & 16 deletions src/api/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,32 @@ export type AnalyticsRange = {

export type AnalyticsQuery = AnalyticsRange & {
partner_id?: number;
zone_ids?: Array<number | string>;
camera_ids?: Array<number | string>;
zone_id?: number | string;
camera_id?: number | string;
granularity?: AnalyticsGranularity;
forecast_created_at?: string;
status?: string;
limit?: number;
top?: number;
offset?: number;
};

export type AnalyticsSummary = {
active_zones_count?: number | null;
active_zones?: number | null;
current_occupied_count?: number | null;
total_capacity?: number | null;
current_free_count?: number | null;
occupied_now?: number | null;
free_now?: number | null;
avg_occupancy_percent?: number | null;
average_occupancy_percent?: number | null;
freshest_update_at?: string | null;
newest_update_at?: string | null;
oldest_update_at?: string | null;
avg_update_interval_sec?: number | null;
max_update_interval_sec?: number | null;
avg_confidence?: number | null;
average_confidence?: number | null;
zones?: AnalyticsZoneSummary[];
cameras?: AnalyticsCameraSummary[];
Expand All @@ -34,9 +44,12 @@ export type AnalyticsZoneSummary = {
zone_id: number | string;
camera_id?: number | null;
capacity?: number | null;
occupied_count?: number | null;
occupied?: number | null;
free_count?: number | null;
free?: number | null;
occupancy_percent?: number | null;
confidence_avg?: number | null;
confidence?: number | null;
last_update_at?: string | null;
status?: AnalyticsDetectorStatus | string | null;
Expand All @@ -51,23 +64,32 @@ export type AnalyticsCameraSummary = {
};

export type AnalyticsUpdateFrequency = {
avg_update_interval_sec?: number | null;
average_interval_seconds?: number | null;
max_update_interval_sec?: number | null;
max_interval_seconds?: number | null;
freshest_update_at?: string | null;
newest_update_at?: string | null;
oldest_update_at?: string | null;
by_zone?: AnalyticsUpdateFrequencyItem[];
items?: AnalyticsUpdateFrequencyItem[];
};

export type AnalyticsUpdateFrequencyItem = {
zone_id?: number | string | null;
camera_id?: number | null;
avg_update_interval_sec?: number | null;
average_interval_seconds?: number | null;
max_update_interval_sec?: number | null;
max_interval_seconds?: number | null;
last_update_at?: string | null;
newest_update_at?: string | null;
oldest_update_at?: string | null;
};

export type AnalyticsConfidence = {
granularity?: string;
avg_confidence?: number | null;
average_confidence?: number | null;
points?: AnalyticsConfidencePoint[];
items?: AnalyticsConfidencePoint[];
Expand All @@ -79,11 +101,16 @@ export type AnalyticsConfidencePoint = {
zone_id?: number | string | null;
camera_id?: number | null;
confidence?: number | null;
confidence_avg?: number | null;
confidence_min?: number | null;
confidence_max?: number | null;
average_confidence?: number | null;
observations_count?: number | null;
observations?: number | null;
};

export type AnalyticsHistory = {
granularity?: string;
series?: AnalyticsSeries[];
points?: AnalyticsHistoryPoint[];
items?: AnalyticsHistoryPoint[];
Expand All @@ -102,28 +129,61 @@ export type AnalyticsHistoryPoint = {
timestamp?: string;
zone_id?: number | string | null;
camera_id?: number | null;
occupied_count?: number | null;
occupied?: number | null;
free_count?: number | null;
free?: number | null;
total?: number | null;
capacity?: number | null;
occupancy_percent?: number | null;
confidence_avg?: number | null;
confidence?: number | null;
observations_count?: number | null;
observations?: number | null;
};

export type AnalyticsForecast = {
available?: boolean;
reason?: string | null;
series?: AnalyticsSeries[];
points?: AnalyticsForecastPoint[];
items?: AnalyticsForecastPoint[];
};

export type AnalyticsForecastPoint = AnalyticsHistoryPoint & {
predicted_for?: string | null;
forecast_created_at?: string | null;
model_version?: string | null;
predicted_occupied_count?: number | null;
predicted_occupied?: number | null;
predicted_free_count?: number | null;
predicted_free?: number | null;
predicted_occupancy_percent?: number | null;
};

export type ForecastQualityMetrics = {
mae_occupied_count?: number | null;
mae_occupancy_percent?: number | null;
bias_occupancy_percent?: number | null;
points_count?: number | null;
};

export type ForecastQualityPoint = {
timestamp?: string;
zone_id?: number | string | null;
actual_occupied_count?: number | null;
actual_occupancy_percent?: number | null;
predicted_occupied_count?: number | null;
predicted_occupancy_percent?: number | null;
absolute_error_occupancy_percent?: number | null;
};

export type ForecastQualityResponse = {
granularity?: string;
metrics?: ForecastQualityMetrics;
points?: ForecastQualityPoint[];
};

export type AnalyticsObservationsRate = {
points?: AnalyticsObservationPoint[];
items?: AnalyticsObservationPoint[];
Expand All @@ -134,6 +194,7 @@ export type AnalyticsObservationPoint = {
timestamp?: string;
zone_id?: number | string | null;
camera_id?: number | null;
observations_count?: number | null;
observations?: number | null;
count?: number | null;
};
Expand All @@ -143,7 +204,8 @@ export type AnalyticsDetectorStatus =
| 'stale'
| 'offline'
| 'no_data'
| 'low_confidence';
| 'low_confidence'
| 'error';

export type AnalyticsDetectorHealth = {
items: AnalyticsDetectorHealthItem[];
Expand All @@ -153,14 +215,21 @@ export type AnalyticsDetectorHealth = {
export type AnalyticsDetectorHealthItem = {
zone_id: number | string;
camera_id?: number | null;
camera_title?: string | null;
capacity?: number | null;
occupied_count?: number | null;
occupied?: number | null;
free_count?: number | null;
free?: number | null;
occupancy_percent?: number | null;
confidence_avg?: number | null;
confidence?: number | null;
last_update_at?: string | null;
sec_ago?: number | null;
stale_seconds?: number | null;
avg_update_interval_sec?: number | null;
average_interval_seconds?: number | null;
max_update_interval_sec?: number | null;
max_interval_seconds?: number | null;
status?: AnalyticsDetectorStatus | string | null;
};
Expand All @@ -178,17 +247,26 @@ export type DetectionRunListItem = {
finished_at?: string | null;
status?: string | null;
processing_time_ms?: number | null;
detected_cars_count?: number | null;
cars_detected?: number | null;
occupied_count?: number | null;
occupied?: number | null;
free_count?: number | null;
free?: number | null;
capacity?: number | null;
confidence_avg?: number | null;
confidence?: number | null;
has_feedback?: boolean | null;
};

export type DetectionRunDetail = DetectionRunListItem & {
model_version?: string | null;
total?: number | null;
error_code?: string | null;
error_message?: string | null;
error?: string | null;
raw_snapshot_url?: string | null;
annotated_snapshot_url?: string | null;
raw_image_url?: string | null;
annotated_image_url?: string | null;
feedback?: DetectionFeedback | null;
Expand All @@ -197,21 +275,25 @@ export type DetectionRunDetail = DetectionRunListItem & {
export type DetectionFeedbackRating = 'correct' | 'partially_correct' | 'incorrect';

export type DetectionFeedbackErrorType =
| 'extra_car'
| 'missing_car'
| 'wrong_zone'
| 'false_positive_car'
| 'false_negative_car'
| 'wrong_zone_assignment'
| 'bad_lighting'
| 'bad_angle'
| 'calibration_issue'
| 'bad_camera_angle'
| 'calibration_problem'
| 'other';

export type DetectionFeedback = {
feedback_id?: number | string;
created_at?: string | null;
updated_at?: string | null;
created_by_user_id?: number | null;
created_by_email?: string | null;
user_id?: number | null;
user_email?: string | null;
rating?: DetectionFeedbackRating | string | null;
expected_occupied_count?: number | null;
expected_free_count?: number | null;
correct_occupied?: number | null;
correct_free?: number | null;
error_type?: DetectionFeedbackErrorType | string | null;
Expand All @@ -221,8 +303,8 @@ export type DetectionFeedback = {

export type DetectionFeedbackRequest = {
rating: DetectionFeedbackRating;
correct_occupied?: number | null;
correct_free?: number | null;
expected_occupied_count?: number | null;
expected_free_count?: number | null;
error_type?: DetectionFeedbackErrorType | null;
comment?: string | null;
};
Expand Down Expand Up @@ -261,14 +343,18 @@ export type LegacySeriesQuery = AnalyticsRange & {
granularity?: AnalyticsGranularity;
};

function analyticsQuery(query: AnalyticsQuery = {}) {
function analyticsQuery(query: AnalyticsQuery = {}, includeForecastCreatedAt = false) {
const search = new URLSearchParams();
const scalarQuery = buildQuery({
partner_id: query.partner_id,
zone_id: query.zone_id,
camera_id: query.camera_id,
from: query.from,
to: query.to,
granularity: query.granularity,
forecast_created_at: query.forecast_created_at,
forecast_created_at: includeForecastCreatedAt ? query.forecast_created_at : undefined,
status: query.status,
limit: query.limit,
top: query.top,
offset: query.offset
});
Expand All @@ -278,9 +364,6 @@ function analyticsQuery(query: AnalyticsQuery = {}) {
scalarParams.forEach((value, key) => search.set(key, value));
}

query.zone_ids?.forEach(zoneId => search.append('zone_id', String(zoneId)));
query.camera_ids?.forEach(cameraId => search.append('camera_id', String(cameraId)));

const result = search.toString();
return result ? `?${result}` : '';
}
Expand Down Expand Up @@ -323,7 +406,7 @@ export const analyticsApi = {
},

async occupancyForecast(query?: AnalyticsQuery) {
return request<AnalyticsForecast>('GET', `/admin/analytics/occupancy-forecast${analyticsQuery(query)}`);
return request<AnalyticsForecast>('GET', `/admin/analytics/occupancy-forecast${analyticsQuery(query, true)}`);
},

async occupancyHeatmap(query?: AnalyticsQuery) {
Expand All @@ -338,6 +421,10 @@ export const analyticsApi = {
return request<AnalyticsDetectorHealth>('GET', `/admin/analytics/detector-health${analyticsQuery(query)}`);
},

async forecastQuality(query?: AnalyticsQuery) {
return request<ForecastQualityResponse>('GET', `/admin/analytics/forecast-quality${analyticsQuery(query)}`);
},

async cameraDetections(cameraId: number, query?: AnalyticsQuery) {
return request<DetectionRunList>('GET', `/admin/analytics/cameras/${encodeURIComponent(cameraId)}/detections${analyticsQuery(query)}`);
},
Expand Down
1 change: 1 addition & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export type {
DetectionRunDetail,
DetectionRunList,
DetectionRunListItem,
ForecastQualityResponse,
LegacyForecastSeriesPoint,
LegacyOccupancySeriesPoint,
LegacySeriesQuery
Expand Down
Loading
Loading