Skip to content

Commit 4a66b7d

Browse files
authored
Merge pull request #10 from scalytics/mobile
Mobile
2 parents 100c7b1 + 7d9b0b1 commit 4a66b7d

24 files changed

Lines changed: 1860 additions & 69 deletions

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ FROM caddy:2.10-alpine
1616

1717
COPY docker/Caddyfile /etc/caddy/Caddyfile
1818
COPY --from=build /app/dist /srv
19+
COPY --from=build /app/mobile/manifest.json /srv/mobile/manifest.json
1920

2021
EXPOSE 80 443
2122

docker/Caddyfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@
143143
}
144144
}
145145

146+
@mobile_bare path /m
147+
redir @mobile_bare /m/ permanent
148+
149+
handle /m/* {
150+
uri strip_prefix /m
151+
root * /srv
152+
try_files {path} /mobile/index.html
153+
file_server
154+
}
155+
146156
handle {
147157
try_files {path} /index.html
148158
file_server

mobile/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon-shield.svg" />
6+
<meta
7+
name="viewport"
8+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
9+
/>
10+
<meta name="apple-mobile-web-app-capable" content="yes" />
11+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
12+
<meta name="theme-color" content="#030610" />
13+
<link rel="manifest" href="/mobile/manifest.json" />
14+
<title>EUOSINT</title>
15+
</head>
16+
<body>
17+
<div id="root"></div>
18+
<script type="module" src="/src/mobile/main.tsx"></script>
19+
</body>
20+
</html>

mobile/manifest.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "EUOSINT",
3+
"short_name": "EUOSINT",
4+
"description": "Open-Source Intelligence Monitor",
5+
"start_url": "/m/",
6+
"display": "standalone",
7+
"background_color": "#030610",
8+
"theme_color": "#030610",
9+
"icons": [
10+
{
11+
"src": "/favicon-shield.svg",
12+
"sizes": "any",
13+
"type": "image/svg+xml"
14+
}
15+
]
16+
}

src/components/AlertDetail.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,10 @@ export function AlertDetail({ alert, onClose }: Props) {
7979
</h2>
8080
<button
8181
onClick={onClose}
82-
className="p-1 rounded hover:bg-siem-accent/12 hover:text-siem-accent text-siem-muted transition-colors"
82+
className="p-2 -mr-1 rounded-lg active:bg-siem-accent/12 active:text-siem-accent text-siem-muted transition-colors"
83+
style={{ WebkitTapHighlightColor: "transparent", minWidth: 44, minHeight: 44, display: "flex", alignItems: "center", justifyContent: "center" }}
8384
>
84-
<X size={14} />
85+
<X size={18} />
8586
</button>
8687
</div>
8788
<div className="flex-1 overflow-y-auto p-4 space-y-4">
@@ -239,7 +240,8 @@ export function AlertDetail({ alert, onClose }: Props) {
239240
href={alert.canonical_url}
240241
target="_blank"
241242
rel="noopener noreferrer"
242-
className="flex items-center justify-center gap-2 w-full py-3 px-4 bg-siem-accent hover:bg-siem-accent/80 text-white font-bold text-sm rounded-lg transition-colors"
243+
className="flex items-center justify-center gap-2 w-full py-3.5 px-4 bg-siem-accent active:bg-siem-accent/80 text-white font-bold text-sm rounded-lg transition-colors"
244+
style={{ WebkitTapHighlightColor: "transparent", minHeight: 48, touchAction: "manipulation" }}
243245
>
244246
<ExternalLink size={16} />
245247
GO TO OFFICIAL ALERT

src/components/AlertFeed.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -829,16 +829,18 @@ export function AlertFeed({
829829
</div>
830830
)}
831831

832-
<div>
833-
<div className="text-xs uppercase tracking-[0.14em] text-siem-muted">Violence / focus</div>
834-
<div className="mt-1 flex flex-wrap gap-1.5">
835-
{activeConflictBrief.violenceTypes.map((item) => (
836-
<span key={item} className="inline-flex items-center gap-1 rounded-full border border-siem-border bg-white/5 px-2 py-1 text-xs text-siem-text">
837-
{item}
838-
</span>
839-
))}
832+
{activeConflictBrief.violenceTypes.length > 0 && (
833+
<div>
834+
<div className="text-xs uppercase tracking-[0.14em] text-siem-muted">Violence / focus</div>
835+
<div className="mt-1 flex flex-wrap gap-1.5">
836+
{activeConflictBrief.violenceTypes.map((item) => (
837+
<span key={item} className="inline-flex items-center gap-1 rounded-full border border-siem-border bg-white/5 px-2 py-1 text-xs text-siem-text">
838+
{item}
839+
</span>
840+
))}
841+
</div>
840842
</div>
841-
</div>
843+
)}
842844

843845
{activeConflictBrief.latestAlert && (
844846
<button

src/hooks/useAlerts.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See NOTICE for provenance and LICENSE for repository-local terms.
55
*/
66

7-
import { useEffect, useMemo, useState } from "react";
7+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
88
import type { Alert } from "@/types/alert";
99
import { appURL } from "@/lib/app-url";
1010

@@ -42,6 +42,8 @@ export function useAlerts() {
4242
const [alerts, setAlerts] = useState<Alert[]>([]);
4343
const [isLive, setIsLive] = useState(false);
4444
const [isLoading, setIsLoading] = useState(true);
45+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
46+
const loadRef = useRef<() => Promise<void>>(undefined);
4547

4648
useEffect(() => {
4749
let cancelled = false;
@@ -74,8 +76,9 @@ export function useAlerts() {
7476
}
7577
}
7678

79+
loadRef.current = load;
7780
load();
78-
const interval = setInterval(load, POLL_MS);
81+
intervalRef.current = setInterval(load, POLL_MS);
7982
const onFocus = () => load();
8083
const onVisible = () => {
8184
if (document.visibilityState === "visible") load();
@@ -85,13 +88,20 @@ export function useAlerts() {
8588

8689
return () => {
8790
cancelled = true;
88-
clearInterval(interval);
91+
if (intervalRef.current) clearInterval(intervalRef.current);
8992
window.removeEventListener("focus", onFocus);
9093
document.removeEventListener("visibilitychange", onVisible);
9194
};
9295
}, []);
9396

97+
const refetch = useCallback(() => {
98+
// Trigger immediate fetch and reset the poll timer
99+
if (intervalRef.current) clearInterval(intervalRef.current);
100+
loadRef.current?.();
101+
intervalRef.current = setInterval(() => loadRef.current?.(), POLL_MS);
102+
}, []);
103+
94104
const sourceCount = useMemo(() => new Set(alerts.map((a) => a.source_id)).size, [alerts]);
95105

96-
return { alerts, isLive, isLoading, sourceCount };
106+
return { alerts, isLive, isLoading, sourceCount, refetch };
97107
}

src/index.css

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,7 @@
66

77
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&family=Roboto+Mono:wght@400;500;600&display=swap");
88
@import "tailwindcss";
9-
10-
@theme {
11-
/* ── Relative type scale (scales with html font-size) ──────────── */
12-
--font-size-4xs: 0.615rem; /* ~8px at 13px root */
13-
--font-size-3xs: 0.692rem; /* ~9px at 13px root */
14-
--font-size-2xs: 0.77rem; /* ~10px at 13px root */
15-
--font-size-xxs: 0.846rem; /* ~11px at 13px root */
16-
17-
/* ── Brand ──────────────────────────────────────────────────────── */
18-
--color-siem-bg: #070E1A;
19-
--color-siem-panel: #0b1120;
20-
--color-siem-panel-strong: #131d2e;
21-
--color-siem-border: #1e293b;
22-
--color-siem-text: #e6edf5;
23-
--color-siem-muted: #5A7B95;
24-
--color-siem-accent: #E8630A;
25-
--color-siem-accent-strong: #FF8533;
26-
--color-siem-neutral: #9ca3af;
27-
28-
/* ── Severity ───────────────────────────────────────────────────── */
29-
--color-siem-critical: #ff5d5d;
30-
--color-siem-high: #f29d4b;
31-
--color-siem-medium: #e3c867;
32-
--color-siem-low: #4ccb8d;
33-
--color-siem-info: #60a5fa;
34-
35-
/* ── Category ───────────────────────────────────────────────────── */
36-
--color-cat-informational: #60a5fa;
37-
--color-cat-cyber: #3b82f6;
38-
--color-cat-education: #4f95a4;
39-
--color-cat-humanitarian: #2f8c8c;
40-
--color-cat-conflict: #725f95;
41-
--color-cat-humsec: #3a7395;
42-
--color-cat-wanted: #a14a5b;
43-
--color-cat-missing: #aa8b43;
44-
--color-cat-appeal: #5577a4;
45-
--color-cat-fraud: #338c66;
46-
--color-cat-safety: #5b6887;
47-
--color-cat-terrorism: #a34c4c;
48-
--color-cat-private: #8f6a46;
49-
--color-cat-travel: #c27a3a;
50-
--color-cat-health: #4ca38c;
51-
--color-cat-intel: #7a6eab;
52-
--color-cat-emergency: #b85c4a;
53-
--color-cat-environment: #4a8b6e;
54-
--color-cat-disease: #c45e8a;
55-
--color-cat-maritime: #2a7a9b;
56-
--color-cat-logistics: #8b6e4a;
57-
--color-cat-legislative: #6b7f45;
58-
}
9+
@import "./theme.css";
5910

6011
* {
6112
box-sizing: border-box;

src/lib/conflict-briefs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export function buildConflictBrief(alerts: Alert[], lens: ConflictLens | null):
204204
count: entry.count,
205205
})),
206206
actors: [],
207-
violenceTypes: buildRankedList(categoryCounts, 3).map((entry) => entry.label),
207+
violenceTypes: [],
208208
hotspots: deriveHotspots(lensAlerts, lens),
209209
latestAlert,
210210
recent7d,

src/main.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ import "./index.css";
1010
import App from "./App.tsx";
1111
import { ErrorBoundary } from "@/components/ErrorBoundary";
1212

13+
// Redirect mobile devices to the dedicated mobile app
14+
if (
15+
/Android|iPhone|iPod/.test(navigator.userAgent) &&
16+
window.innerWidth < 768 &&
17+
!new URLSearchParams(location.search).has("desktop") &&
18+
!document.cookie.includes("euosint_prefer_desktop")
19+
) {
20+
location.replace("/m/");
21+
}
22+
1323
createRoot(document.getElementById("root")!).render(
1424
<StrictMode>
1525
<ErrorBoundary>

0 commit comments

Comments
 (0)