Skip to content

Commit eaa4a2f

Browse files
committed
Enable Android shrink; make health API robust
Enable release minification and resource shrinking in Android build (and apply signingConfig when RELEASE_STORE_FILE is provided). Remove the manual WebView mixed-content override and switch Capacitor to use the http androidScheme with hostname 'localhost'. Normalize health API responses by defaulting missing arrays (containers, bouncers, decisions) and computing allRunning when absent. Update DashboardPage to use those safe defaults and adjust diagnostics UI accordingly. Add a unit test to cover diagnostics responses that omit bouncers.
1 parent 824a274 commit eaa4a2f

6 files changed

Lines changed: 67 additions & 19 deletions

File tree

mobile/android/app/build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ android {
3939
}
4040
buildTypes {
4141
release {
42-
minifyEnabled false
42+
minifyEnabled true
43+
shrinkResources true
4344
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
45+
if (project.hasProperty('RELEASE_STORE_FILE'))
46+
signingConfig signingConfigs.release
4447
}
4548
}
4649
}
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
package com.crowdsec.manager.mobile;
22

33
import android.os.Bundle;
4-
import android.webkit.WebSettings;
54
import com.getcapacitor.BridgeActivity;
65

76
public class MainActivity extends BridgeActivity {
87
@Override
98
public void onCreate(Bundle savedInstanceState) {
109
super.onCreate(savedInstanceState);
11-
// Allow ws:// WebSocket connections from the https://localhost WebView context.
12-
// Required because androidScheme:'https' causes the WebView to run at
13-
// https://localhost, which would otherwise block insecure ws:// connections.
14-
getBridge().getWebView().getSettings()
15-
.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
1610
}
1711
}

mobile/capacitor.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ const config: CapacitorConfig = {
66
webDir: 'dist',
77
server: {
88
cleartext: true,
9-
androidScheme: 'https',
9+
androidScheme: 'http',
1010
iosScheme: 'http',
11+
hostname: 'localhost',
1112
},
1213
plugins: {
1314
CapacitorHttp: {

mobile/src/lib/api/health.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@ import type { CrowdsecHealth, DiagnosticResult, StackHealth } from './types';
44
export function createHealthApi(client: ApiClient) {
55
return {
66
async getStack() {
7-
return (await client.get<StackHealth>('/api/health/stack')).data;
7+
const data = (await client.get<StackHealth>('/api/health/stack')).data;
8+
const containers = data?.containers ?? [];
9+
10+
return {
11+
...data,
12+
containers,
13+
allRunning: data?.allRunning ?? containers.every((container) => container.running),
14+
};
815
},
916
async getCrowdsec() {
1017
return (await client.get<CrowdsecHealth>('/api/health/crowdsec')).data;
1118
},
1219
async getComplete() {
13-
return (await client.get<DiagnosticResult>('/api/health/complete')).data;
20+
const data = (await client.get<DiagnosticResult>('/api/health/complete')).data;
21+
22+
return {
23+
...data,
24+
bouncers: data?.bouncers ?? [],
25+
decisions: data?.decisions ?? [],
26+
};
1427
},
1528
};
1629
}

mobile/src/pages/DashboardPage.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export default function DashboardPage() {
105105
fetchData();
106106
});
107107

108+
const completeBouncers = complete?.bouncers ?? [];
109+
108110
return (
109111
<PullToRefresh onRefresh={fetchData}>
110112
<div className="pb-nav">
@@ -146,8 +148,8 @@ export default function DashboardPage() {
146148
<StatCard
147149
icon={Server}
148150
title="Bouncers"
149-
value={complete ? String(complete.bouncers.length) : '—'}
150-
tone={complete && complete.bouncers.length > 0 ? 'ok' : 'warn'}
151+
value={complete ? String(completeBouncers.length) : '—'}
152+
tone={complete && completeBouncers.length > 0 ? 'ok' : 'warn'}
151153
/>
152154
</div>
153155

@@ -260,28 +262,31 @@ function HealthChecksPanel({ crowdsec }: { crowdsec: CrowdsecHealth }) {
260262
/* ────────────────────────── Diagnostics Summary Panel ────────────────────────── */
261263

262264
function DiagnosticsSummaryPanel({ diagnostics }: { diagnostics: DiagnosticResult }) {
265+
const bouncers = diagnostics.bouncers ?? [];
266+
const decisions = diagnostics.decisions ?? [];
267+
263268
return (
264269
<div className="rounded-xl border border-border bg-card p-4 space-y-3">
265270
<h3 className="text-sm font-semibold">Diagnostics Summary</h3>
266271

267272
<div className="grid grid-cols-2 gap-2">
268273
<MetricCard
269274
label="Bouncers"
270-
value={diagnostics.bouncers.length}
271-
variant={diagnostics.bouncers.length > 0 ? 'success' : 'warning'}
275+
value={bouncers.length}
276+
variant={bouncers.length > 0 ? 'success' : 'warning'}
272277
/>
273278
<MetricCard
274279
label="Decisions"
275-
value={diagnostics.decisions.length}
280+
value={decisions.length}
276281
variant="default"
277282
/>
278283
</div>
279284

280285
{/* Bouncers mini-list */}
281-
{diagnostics.bouncers.length > 0 && (
286+
{bouncers.length > 0 && (
282287
<div className="space-y-1">
283288
<h4 className="text-xs font-medium text-muted-foreground">Active Bouncers</h4>
284-
{diagnostics.bouncers.slice(0, 5).map((bouncer: Bouncer) => (
289+
{bouncers.slice(0, 5).map((bouncer: Bouncer) => (
285290
<div key={bouncer.name} className="flex items-center justify-between py-1">
286291
<div className="flex items-center gap-2 min-w-0">
287292
<StatusDot color={bouncer.valid ? 'success' : 'error'} />
@@ -290,8 +295,8 @@ function DiagnosticsSummaryPanel({ diagnostics }: { diagnostics: DiagnosticResul
290295
<span className="text-[10px] text-muted-foreground font-mono shrink-0">{bouncer.ip_address}</span>
291296
</div>
292297
))}
293-
{diagnostics.bouncers.length > 5 && (
294-
<p className="text-[10px] text-muted-foreground">+{diagnostics.bouncers.length - 5} more</p>
298+
{bouncers.length > 5 && (
299+
<p className="text-[10px] text-muted-foreground">+{bouncers.length - 5} more</p>
295300
)}
296301
</div>
297302
)}

mobile/src/test/dashboard-page.test.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { render, screen, waitFor } from '@testing-library/react';
22
import { describe, expect, it, vi } from 'vitest';
33
import DashboardPage from '@/pages/DashboardPage';
4+
import type { DiagnosticResult } from '@/lib/api';
45

56
const mockUseApi = vi.fn();
67

@@ -73,6 +74,37 @@ describe('DashboardPage', () => {
7374
expect(screen.queryByText('API unreachable')).not.toBeInTheDocument();
7475
});
7576

77+
it('renders dashboard content when diagnostics omit bouncers', async () => {
78+
mockUseApi.mockReset();
79+
const api = createBaseApi();
80+
api.health.getStack.mockResolvedValue({
81+
containers: [{ id: 'abc', name: 'crowdsec', running: true, status: 'running' }],
82+
allRunning: true,
83+
timestamp: '2026-03-20T12:00:00Z',
84+
});
85+
api.health.getCrowdsec.mockResolvedValue({
86+
status: 'healthy',
87+
checks: {},
88+
timestamp: '2026-03-20T12:00:00Z',
89+
});
90+
api.health.getComplete.mockResolvedValue({
91+
decisions: [{ id: '1' }],
92+
timestamp: '2026-03-20T12:00:00Z',
93+
} as unknown as DiagnosticResult);
94+
api.ip.getPublicIP.mockResolvedValue({ ip: '1.2.3.4' });
95+
api.crowdsec.alertsAnalysis.mockResolvedValue(null);
96+
mockUseApi.mockReturnValue({ api });
97+
98+
render(<DashboardPage />);
99+
100+
await waitFor(() => {
101+
expect(screen.getByText('Diagnostics Summary')).toBeInTheDocument();
102+
});
103+
104+
expect(screen.getByText('Diagnostics Summary')).toBeInTheDocument();
105+
expect(screen.queryByText('API unreachable')).not.toBeInTheDocument();
106+
});
107+
76108
it('shows retryable inline error when critical requests fail', async () => {
77109
mockUseApi.mockReset();
78110
const api = createBaseApi();

0 commit comments

Comments
 (0)