Compare commits
7 Commits
v0.1.2
..
6e5b565e5e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e5b565e5e | |||
| 52797cf52f | |||
| f4635967c7 | |||
| 41ab2f21a4 | |||
| 333dd291cb | |||
| bf15ead1ef | |||
| 6c460cbff7 |
@@ -1,7 +1,13 @@
|
|||||||
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="docs/logo.png" alt="VIGIL" width="96" height="96" />
|
||||||
|
|
||||||
# VIGIL
|
# VIGIL
|
||||||
|
|
||||||
**What's been following you?**
|
**What's been following you?**
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
A native Android (Kotlin) app that watches for personal item-trackers — Apple
|
A native Android (Kotlin) app that watches for personal item-trackers — Apple
|
||||||
AirTags, Tile, Samsung Galaxy SmartTags, and Google Find My Device / DULT tags —
|
AirTags, Tile, Samsung Galaxy SmartTags, and Google Find My Device / DULT tags —
|
||||||
that are **travelling with you over time**. It is the temporal counterpart to
|
that are **travelling with you over time**. It is the temporal counterpart to
|
||||||
@@ -22,7 +28,7 @@ that idea.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Privacy first — like OVERWATCH
|
## Privacy first — fully offline
|
||||||
|
|
||||||
VIGIL requests **no `INTERNET` permission at all.** There is no server, no account,
|
VIGIL requests **no `INTERNET` permission at all.** There is no server, no account,
|
||||||
no telemetry. Every tracker, every sighting, and the entire learned baseline live
|
no telemetry. Every tracker, every sighting, and the entire learned baseline live
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "org.soulstone.vigil"
|
applicationId = "org.soulstone.vigil"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 3
|
versionCode = 8
|
||||||
versionName = "0.1.2"
|
versionName = "0.1.7"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed debug keystore committed to the repo (a debug key is non-secret — its
|
// Fixed debug keystore committed to the repo (a debug key is non-secret — its
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
onDistrust = { id ->
|
onDistrust = { id ->
|
||||||
lifecycleScope.launch { repo.clearBaseline(id) }
|
lifecycleScope.launch { repo.clearBaseline(id) }
|
||||||
},
|
},
|
||||||
onRing = { tracker -> ringTracker(tracker) }
|
onRing = { tracker -> ringTracker(tracker) },
|
||||||
|
onClearAll = { lifecycleScope.launch { repo.clearAll() } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ class TrackerRepository(private val db: VigilDatabase) {
|
|||||||
db.trackerDao().pruneStale(cutoff)
|
db.trackerDao().pruneStale(cutoff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wipe the whole tracker list + sighting history (they re-populate as devices
|
||||||
|
* are re-detected). Keeps learned baseline places. */
|
||||||
|
suspend fun clearAll() = mutex.withLock {
|
||||||
|
db.trackerDao().clearAll()
|
||||||
|
db.sightingDao().clearAll()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ingest one sighting. Persists it, updates the baseline, and re-evaluates the
|
* Ingest one sighting. Persists it, updates the baseline, and re-evaluates the
|
||||||
* tracker's risk. Returns the updated row and whether this crossed into ALERTING.
|
* tracker's risk. Returns the updated row and whether this crossed into ALERTING.
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ interface TrackerDao {
|
|||||||
// stale, non-approved rows so the table and the UI list don't grow without bound.
|
// stale, non-approved rows so the table and the UI list don't grow without bound.
|
||||||
@Query("DELETE FROM trackers WHERE lastSeen < :cutoff AND approved = 0")
|
@Query("DELETE FROM trackers WHERE lastSeen < :cutoff AND approved = 0")
|
||||||
suspend fun pruneStale(cutoff: Long)
|
suspend fun pruneStale(cutoff: Long)
|
||||||
|
|
||||||
|
@Query("DELETE FROM trackers")
|
||||||
|
suspend fun clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -103,6 +106,9 @@ interface SightingDao {
|
|||||||
|
|
||||||
@Query("DELETE FROM sightings WHERE timestamp < :cutoff")
|
@Query("DELETE FROM sightings WHERE timestamp < :cutoff")
|
||||||
suspend fun prune(cutoff: Long)
|
suspend fun prune(cutoff: Long)
|
||||||
|
|
||||||
|
@Query("DELETE FROM sightings")
|
||||||
|
suspend fun clearAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ object TrackerRinger {
|
|||||||
done("Ringing… listen for the AirTag.", g)
|
done("Ringing… listen for the AirTag.", g)
|
||||||
} else {
|
} else {
|
||||||
done(
|
done(
|
||||||
if (status == BluetoothGatt.GATT_SUCCESS) "Ringing… listen for the tracker."
|
if (status == BluetoothGatt.GATT_SUCCESS)
|
||||||
|
"Ring command sent. A tag only chirps when it's separated from its owner — your own tag that's with you won't."
|
||||||
else "The tracker refused the ring command.", g
|
else "The tracker refused the ring command.", g
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.soulstone.vigil.ui
|
package org.soulstone.vigil.ui
|
||||||
|
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material.icons.filled.NotificationsActive
|
import androidx.compose.material.icons.filled.NotificationsActive
|
||||||
import androidx.compose.material.icons.filled.Sensors
|
import androidx.compose.material.icons.filled.Sensors
|
||||||
@@ -8,7 +9,9 @@ import androidx.compose.material3.FilledTonalButton
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.graphics.lerp
|
import androidx.compose.ui.graphics.lerp
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import org.soulstone.vigil.service.ScanService
|
import org.soulstone.vigil.service.ScanService
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -27,6 +30,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bluetooth
|
import androidx.compose.material.icons.filled.Bluetooth
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.DeleteSweep
|
||||||
import androidx.compose.material.icons.filled.GppMaybe
|
import androidx.compose.material.icons.filled.GppMaybe
|
||||||
import androidx.compose.material.icons.filled.Lock
|
import androidx.compose.material.icons.filled.Lock
|
||||||
import androidx.compose.material.icons.filled.Radar
|
import androidx.compose.material.icons.filled.Radar
|
||||||
@@ -41,6 +45,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.FilterChipDefaults
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -67,6 +72,7 @@ import org.soulstone.vigil.BuildConfig
|
|||||||
import org.soulstone.vigil.data.db.TrackerEntity
|
import org.soulstone.vigil.data.db.TrackerEntity
|
||||||
import org.soulstone.vigil.model.Sensitivity
|
import org.soulstone.vigil.model.Sensitivity
|
||||||
import org.soulstone.vigil.model.TrackerEcosystem
|
import org.soulstone.vigil.model.TrackerEcosystem
|
||||||
|
import org.soulstone.vigil.R
|
||||||
import org.soulstone.vigil.model.TrackerStatus
|
import org.soulstone.vigil.model.TrackerStatus
|
||||||
import org.soulstone.vigil.ui.theme.VigilGreen
|
import org.soulstone.vigil.ui.theme.VigilGreen
|
||||||
import org.soulstone.vigil.ui.theme.VigilPeach
|
import org.soulstone.vigil.ui.theme.VigilPeach
|
||||||
@@ -83,7 +89,8 @@ fun MainScreen(
|
|||||||
onSetSensitivity: (Sensitivity) -> Unit,
|
onSetSensitivity: (Sensitivity) -> Unit,
|
||||||
onApprove: (String, Boolean) -> Unit,
|
onApprove: (String, Boolean) -> Unit,
|
||||||
onDistrust: (String) -> Unit,
|
onDistrust: (String) -> Unit,
|
||||||
onRing: (TrackerEntity) -> Unit
|
onRing: (TrackerEntity) -> Unit,
|
||||||
|
onClearAll: () -> Unit
|
||||||
) {
|
) {
|
||||||
var detail by remember { mutableStateOf<TrackerEntity?>(null) }
|
var detail by remember { mutableStateOf<TrackerEntity?>(null) }
|
||||||
var finding by remember { mutableStateOf<TrackerEntity?>(null) }
|
var finding by remember { mutableStateOf<TrackerEntity?>(null) }
|
||||||
@@ -102,12 +109,25 @@ fun MainScreen(
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(Icons.Filled.Shield, null, tint = MaterialTheme.colorScheme.primary)
|
Image(
|
||||||
|
painterResource(R.drawable.vigil_logo),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(26.dp)
|
||||||
|
)
|
||||||
Spacer(Modifier.size(8.dp))
|
Spacer(Modifier.size(8.dp))
|
||||||
Text("VIGIL", fontWeight = FontWeight.Bold)
|
Text("VIGIL", fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
if (trackers.isNotEmpty()) {
|
||||||
|
IconButton(onClick = onClearAll) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.DeleteSweep,
|
||||||
|
contentDescription = "Clear all trackers",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp)) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.Lock, null,
|
Icons.Filled.Lock, null,
|
||||||
@@ -432,6 +452,8 @@ private fun DetailRow(label: String, value: String) {
|
|||||||
* proximity meter. Works even on silent/modified tags that refuse to ring. */
|
* proximity meter. Works even on silent/modified tags that refuse to ring. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun FinderScreen(t: TrackerEntity, onRing: (TrackerEntity) -> Unit, onClose: () -> Unit) {
|
private fun FinderScreen(t: TrackerEntity, onRing: (TrackerEntity) -> Unit, onClose: () -> Unit) {
|
||||||
|
// Intercept system back so it closes the finder instead of exiting the app.
|
||||||
|
BackHandler(onBack = onClose)
|
||||||
val rssi by ScanService.finderRssi.collectAsState()
|
val rssi by ScanService.finderRssi.collectAsState()
|
||||||
val smoothed = remember { mutableStateOf(-100f) }
|
val smoothed = remember { mutableStateOf(-100f) }
|
||||||
LaunchedEffect(rssi) { rssi?.let { smoothed.value = 0.35f * it + 0.65f * smoothed.value } }
|
LaunchedEffect(rssi) { rssi?.let { smoothed.value = 0.35f * it + 0.65f * smoothed.value } }
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Mocha radial ground: a subtle lift toward the top-centre, falling to crust. -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M0,0h108v108h-108z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:type="radial"
|
||||||
|
android:centerX="54"
|
||||||
|
android:centerY="42"
|
||||||
|
android:gradientRadius="82"
|
||||||
|
android:startColor="#2A2A40"
|
||||||
|
android:endColor="#11111B" />
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
</vector>
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- VIGIL "Radar Eye" — a watchful eye whose iris is a radar target. Content sits
|
||||||
|
within the adaptive-icon safe zone (centre ~66dp of the 108dp canvas). -->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
<!-- eye outline -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#1FAA59"
|
android:pathData="M26,54 Q54,30 82,54 Q54,78 26,54 Z"
|
||||||
android:pathData="M54,30 m-18,0 a18,18 0 1,0 36,0 a18,18 0 1,0 -36,0" />
|
android:strokeColor="#89B4FA"
|
||||||
|
android:strokeWidth="4.5"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<!-- radar iris — outer ring -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#F4F6FA"
|
android:pathData="M54,54 m-11,0 a11,11 0 1,0 22,0 a11,11 0 1,0 -22,0"
|
||||||
android:pathData="M54,30 m-6,0 a6,6 0 1,0 12,0 a6,6 0 1,0 -12,0" />
|
android:strokeColor="#89B4FA"
|
||||||
|
android:strokeWidth="3.2"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<!-- radar iris — inner ring -->
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54 m-6.5,0 a6.5,6.5 0 1,0 13,0 a6.5,6.5 0 1,0 -13,0"
|
||||||
|
android:strokeColor="#B4BEFE"
|
||||||
|
android:strokeWidth="2.8"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<!-- centre contact (the detected device) -->
|
||||||
|
<path
|
||||||
|
android:pathData="M54,54 m-3.2,0 a3.2,3.2 0 1,0 6.4,0 a3.2,3.2 0 1,0 -6.4,0"
|
||||||
|
android:fillColor="#A6E3A1" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- VIGIL wordmark glyph (Radar Eye), tight-cropped for in-app use (top bar). -->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:pathData="M4,24 Q24,8 44,24 Q24,40 4,24 Z"
|
||||||
|
android:strokeColor="#89B4FA"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M24,24 m-7.5,0 a7.5,7.5 0 1,0 15,0 a7.5,7.5 0 1,0 -15,0"
|
||||||
|
android:strokeColor="#89B4FA"
|
||||||
|
android:strokeWidth="2.4"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M24,24 m-4.4,0 a4.4,4.4 0 1,0 8.8,0 a4.4,4.4 0 1,0 -8.8,0"
|
||||||
|
android:strokeColor="#B4BEFE"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#00000000" />
|
||||||
|
<path
|
||||||
|
android:pathData="M24,24 m-2.2,0 a2.2,2.2 0 1,0 4.4,0 a2.2,2.2 0 1,0 -4.4,0"
|
||||||
|
android:fillColor="#A6E3A1" />
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/bg_dark" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/bg_dark" />
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/* OVERWATCH landing — count-up stats + scroll reveal. No network, no deps. */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
/* count-up numbers when the stats row scrolls into view */
|
||||||
|
function countUp(el) {
|
||||||
|
const target = parseInt(el.dataset.target, 10);
|
||||||
|
if (isNaN(target)) return;
|
||||||
|
if (!target || reduceMotion) { el.textContent = String(target); return; }
|
||||||
|
const dur = 1100, start = performance.now();
|
||||||
|
(function tick(now) {
|
||||||
|
const p = Math.min(1, (now - start) / dur);
|
||||||
|
el.textContent = String(Math.round(target * (1 - Math.pow(1 - p, 3))));
|
||||||
|
if (p < 1) requestAnimationFrame(tick);
|
||||||
|
})(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
const statsRow = document.getElementById('stats-row');
|
||||||
|
if (statsRow) {
|
||||||
|
const nums = statsRow.querySelectorAll('.num[data-target]');
|
||||||
|
if ('IntersectionObserver' in window && !reduceMotion) {
|
||||||
|
const io = new IntersectionObserver((entries, obs) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) { nums.forEach(countUp); obs.disconnect(); }
|
||||||
|
});
|
||||||
|
}, { threshold: 0.4 });
|
||||||
|
io.observe(statsRow);
|
||||||
|
} else {
|
||||||
|
nums.forEach((el) => { el.textContent = el.dataset.target; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* scroll-triggered reveal */
|
||||||
|
const reveals = document.querySelectorAll('.reveal');
|
||||||
|
if (!('IntersectionObserver' in window) || reduceMotion) {
|
||||||
|
reveals.forEach((el) => el.classList.add('in'));
|
||||||
|
} else {
|
||||||
|
const ro = new IntersectionObserver((entries, obs) => {
|
||||||
|
entries.forEach((e) => {
|
||||||
|
if (e.isIntersecting) { e.target.classList.add('in'); obs.unobserve(e.target); }
|
||||||
|
});
|
||||||
|
}, { threshold: 0.12, rootMargin: '0px 0px -40px 0px' });
|
||||||
|
reveals.forEach((el) => ro.observe(el));
|
||||||
|
}
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
+375
@@ -0,0 +1,375 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VIGIL — what's been following you?</title>
|
||||||
|
<meta name="description" content="A native Android app that watches for personal item-trackers — AirTags, Tile, Samsung SmartTags, Google Find My — travelling with you over time. Fully offline, no INTERNET permission. The temporal counterpart to OVERWATCH.">
|
||||||
|
<meta property="og:title" content="VIGIL — what's been following you?">
|
||||||
|
<meta property="og:description" content="Android app that flags AirTags, Tile, SmartTags & Find My trackers travelling WITH you over time — even rotating-key clones. Fully offline, listens only.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://vigil.netslum.io/">
|
||||||
|
<meta property="og:image" content="https://vigil.netslum.io/og.png">
|
||||||
|
<meta property="og:image:width" content="1200">
|
||||||
|
<meta property="og:image:height" content="630">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:image" content="https://vigil.netslum.io/og.png">
|
||||||
|
<meta name="theme-color" content="#07070d">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="bg-mesh" aria-hidden="true">
|
||||||
|
<div class="mesh-blob mesh-blob-1"></div>
|
||||||
|
<div class="mesh-blob mesh-blob-2"></div>
|
||||||
|
<div class="mesh-blob mesh-blob-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<div class="container nav-inner">
|
||||||
|
<a class="nav-brand" href="#">
|
||||||
|
<img class="nav-logo" src="logo.svg" width="30" height="30" alt="" aria-hidden="true">
|
||||||
|
VIGIL
|
||||||
|
</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="#detects">What it detects</a>
|
||||||
|
<a href="#decides">How it decides</a>
|
||||||
|
<a href="#clones">Clones</a>
|
||||||
|
<a href="#download">Download</a>
|
||||||
|
<a class="nav-github" href="https://github.com/KaraZajac/VIGIL" aria-label="GitHub">
|
||||||
|
<svg height="18" viewBox="0 0 16 16" width="18" fill="currentColor" aria-hidden="true">
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ──────────────── HERO ──────────────── -->
|
||||||
|
<header class="hero">
|
||||||
|
<div class="container hero-inner">
|
||||||
|
<div class="hero-eyebrow">
|
||||||
|
<span class="dot dot-pulse"></span>
|
||||||
|
v0.1.7 · Android · prototype
|
||||||
|
</div>
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="display-wordmark">VIGIL</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-tag">
|
||||||
|
<strong>What's been following you?</strong> A native Android app that
|
||||||
|
watches for personal item-trackers — AirTags, Tile, Samsung SmartTags,
|
||||||
|
Google Find My — that are <strong>travelling with you over time</strong>.
|
||||||
|
<span class="hero-tag-pop">A tracker being near you means nothing. The signal is persistence.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="phone-hero">
|
||||||
|
<img class="phone" src="img/vigil-main.png" width="1096" height="2560"
|
||||||
|
alt="VIGIL main screen: a green "You're clear — nothing has been following you" card, a Stop Watching button, and a High/Medium/Low sensitivity selector." loading="eager">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-row" id="stats-row">
|
||||||
|
<div class="stat-chip"><span class="num" data-target="5">0</span><span>tracker ecosystems</span></div>
|
||||||
|
<div class="stat-chip stat-vfy"><span class="num" data-target="3">0</span><span>escalation tiers</span></div>
|
||||||
|
<div class="stat-chip"><span class="num" data-target="0">0</span><span>internet permission</span></div>
|
||||||
|
<div class="stat-chip"><span class="num">v0.1.7</span><span>latest APK</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="btn btn-primary" href="https://github.com/KaraZajac/VIGIL/releases/latest">
|
||||||
|
↓ Download APK
|
||||||
|
</a>
|
||||||
|
<a class="btn" href="#clones">The clone problem</a>
|
||||||
|
<a class="btn btn-ghost" href="https://github.com/KaraZajac/VIGIL">
|
||||||
|
<svg height="16" viewBox="0 0 16 16" width="16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
||||||
|
Source on GitHub
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hero-warn">Prototype / work in progress. Debug-signed APK — sideload; no Play Store. <a href="https://github.com/KaraZajac/VIGIL">Read the README.</a></p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ──────────────── OVERWATCH SIBLING ──────────────── -->
|
||||||
|
<section class="section section-callout reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="callout">
|
||||||
|
<div class="callout-mark">🛰</div>
|
||||||
|
<div>
|
||||||
|
<h3>The temporal counterpart to <a href="https://overwatch.netslum.io">OVERWATCH</a></h3>
|
||||||
|
<p>
|
||||||
|
<strong>OVERWATCH is spatial</strong> — what surveillance is watching
|
||||||
|
<em>this place</em>, right now. <strong>VIGIL is temporal</strong> —
|
||||||
|
what has been with <em>you</em>, across time and places. VIGIL reuses
|
||||||
|
OVERWATCH's proven passive-scanning stack and adds a persistent
|
||||||
|
on-device store to reason about a tracker's history.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── WHAT IT DETECTS ──────────────── -->
|
||||||
|
<section id="detects" class="section section-bento reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-head">
|
||||||
|
<span class="section-tag">what it detects</span>
|
||||||
|
<h2>Five ecosystems. One tell: separated from its owner.</h2>
|
||||||
|
<p class="lead">VIGIL recognises each BLE wire format and, where the
|
||||||
|
ecosystem signals it, filters to the <strong>separated-from-owner</strong>
|
||||||
|
state — the only state in which a <em>following</em> tracker is even
|
||||||
|
detectable. Chipolo, Pebblebee, eufy and the rest inherit whichever
|
||||||
|
network their SKU joined, so VIGIL detects the <em>network</em>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bento">
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🍎</div>
|
||||||
|
<h3>Apple Find My / AirTag</h3>
|
||||||
|
<p>Mfg data, company <code>0x004C</code>, type <code>0x12</code>. Separated when the "maintained" status bit is cleared. ~24 h re-link window.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🟢</div>
|
||||||
|
<h3>Google Find My Device</h3>
|
||||||
|
<p>Service data <code>0xFEAA</code>, frame <code>0x40/0x41</code>. Frame <code>0x41</code> is the cleartext separated state.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🔵</div>
|
||||||
|
<h3>Samsung Galaxy SmartTag</h3>
|
||||||
|
<p>Service data <code>0xFD5A</code>; the state byte flags lost / overmature-lost — the following state.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🟩</div>
|
||||||
|
<h3>Tile</h3>
|
||||||
|
<p>Service data <code>0xFEED / 0xFEEC</code>. No separated signal and a static MAC — always findable, indefinitely.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card bento-lg">
|
||||||
|
<div class="bento-icon">🔗</div>
|
||||||
|
<h3>DULT (unified, emerging)</h3>
|
||||||
|
<p>Service data <code>0xFCB2</code>; the near-owner bit (byte 14 LSB) marks separation. The cross-industry standard the whole ecosystem is converging on — VIGIL parses it today.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── HOW IT DECIDES ──────────────── -->
|
||||||
|
<section id="decides" class="section reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-head">
|
||||||
|
<span class="section-tag">how it decides</span>
|
||||||
|
<h2>Persistence, not proximity.</h2>
|
||||||
|
<p class="lead">A tracker is escalated only when it clears the
|
||||||
|
<strong>co-movement test</strong> — the same device, seen at many of
|
||||||
|
<em>your</em> distinct places, over a sustained window, while close enough
|
||||||
|
to actually be on you.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bento">
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">📍</div>
|
||||||
|
<h3>≥ N distinct places</h3>
|
||||||
|
<p>Geohash-7 cells; N = 2 / 3 / 4 by sensitivity. A tracker seen only where you dwell isn't following — it lives there.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">⏱</div>
|
||||||
|
<h3>≥ 3 sightings over ≥ T minutes</h3>
|
||||||
|
<p>Debounced to one per 15 min; T = 30 / 45 / 90 by sensitivity. Persistence across time, not a single blip.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">📶</div>
|
||||||
|
<h3>RSSI proximity gate</h3>
|
||||||
|
<p>It must have been genuinely close — on-body / in-bag — at least once. This is the piece AirGuard omits; it rejects "a Tile in a passing car."</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tiers" style="margin-top:2.4rem">
|
||||||
|
<div class="tier tier-green">
|
||||||
|
<div class="tier-name" style="font-size:1.15rem">OBSERVED</div>
|
||||||
|
<p>Seen, logged, geotagged — but hasn't cleared the co-movement test. No alarm.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tier tier-yellow">
|
||||||
|
<div class="tier-name" style="font-size:1.15rem">SUSPICIOUS</div>
|
||||||
|
<p>Co-moving across your places. VIGIL is watching it closely.</p>
|
||||||
|
</div>
|
||||||
|
<div class="tier tier-red">
|
||||||
|
<div class="tier-name" style="font-size:1.15rem">ALERTING</div>
|
||||||
|
<p>Confirmed following you. Surfaced with the "Make it ring" and hot/cold finder tools.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feature-split" style="margin-top:2.8rem">
|
||||||
|
<div class="explain-annotations" style="grid-column:1 / -1">
|
||||||
|
<div class="annotation">
|
||||||
|
<span class="anno-num">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong>Allowlist — "This is mine"</strong>
|
||||||
|
<p>Tap a tracker to approve it — your own AirTag, your partner's Tile — and it never alerts again.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="annotation">
|
||||||
|
<span class="anno-num">✓</span>
|
||||||
|
<div>
|
||||||
|
<strong>Learned offline baseline</strong>
|
||||||
|
<p>VIGIL learns the places you dwell (home, work) as anchors; a tracker seen at an anchor across several days is auto-marked <strong>Known (home)</strong> — so household tags fall silent on their own, entirely on-device.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── CLONES ──────────────── -->
|
||||||
|
<section id="clones" class="section section-feature reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-head">
|
||||||
|
<span class="section-tag">the hard part</span>
|
||||||
|
<h2>Catching the clone.</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Every shipping detector — AirGuard, iOS, Android's built-in — keys on
|
||||||
|
<strong>device identity</strong>. A key-rotating clone (Positive
|
||||||
|
Security's <em>Find You</em>: ~2,000 Find My keys, a new one every 30 s)
|
||||||
|
looks like 2,000 one-off devices and evades them all — it tracked a
|
||||||
|
phone for five days with zero alerts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout" style="border-color:rgba(239,68,68,.32)">
|
||||||
|
<div class="callout-mark">🎯</div>
|
||||||
|
<div>
|
||||||
|
<h3>Detect the attack, not the device</h3>
|
||||||
|
<p>
|
||||||
|
A rotating clone is <em>one</em> physical radio holding an unbroken,
|
||||||
|
close-range, co-moving RF presence — even as its identity churns
|
||||||
|
thousands of times faster than any standards-compliant tracker is
|
||||||
|
allowed to. VIGIL's <code>PresenceEngine</code> pairs a CUSUM churn
|
||||||
|
trigger with an identity-agnostic presence-track confirmer and the
|
||||||
|
co-movement gate — an "identity-path × churn-path squeeze" that leaves
|
||||||
|
no safe rotation rate. First version implemented and unit-tested
|
||||||
|
against synthetic clone/ambient traces; field-tuning is what remains.
|
||||||
|
</p>
|
||||||
|
<a href="https://github.com/KaraZajac/VIGIL/blob/main/docs/detection-rotation-clone.md" class="audience-link" style="margin-top:.8rem;display:inline-block">Read the algorithm →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── FINDING + PRIVACY ──────────────── -->
|
||||||
|
<section class="section section-bento reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-head">
|
||||||
|
<span class="section-tag">on-device, private</span>
|
||||||
|
<h2>Fully offline. It only listens.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bento">
|
||||||
|
<article class="bento-card bento-lg">
|
||||||
|
<div class="bento-icon">🔒</div>
|
||||||
|
<h3>No <code>INTERNET</code> permission at all</h3>
|
||||||
|
<p>
|
||||||
|
There is no server, no account, no telemetry. Every tracker, every
|
||||||
|
sighting, and the entire learned baseline live in an on-device SQLite
|
||||||
|
database and <strong>never leave the phone</strong>. Detection is
|
||||||
|
entirely passive — it listens only.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🔔</div>
|
||||||
|
<h3>Make it ring</h3>
|
||||||
|
<p>Tap a suspected tracker to connect over GATT and play its own sound — the DULT-standard way for a victim to locate a hidden AirTag / Find My / Chipolo tag.</p>
|
||||||
|
</article>
|
||||||
|
<article class="bento-card">
|
||||||
|
<div class="bento-icon">🌡</div>
|
||||||
|
<h3>Hot / cold finder</h3>
|
||||||
|
<p>A passive proximity meter — warmer/colder from live signal. Works even on <strong>silent or modified tags that refuse to ring</strong>, which is exactly when you need it.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── DOWNLOAD ──────────────── -->
|
||||||
|
<section id="download" class="section section-timeline reveal">
|
||||||
|
<div class="container">
|
||||||
|
<div class="section-head">
|
||||||
|
<span class="section-tag">get it</span>
|
||||||
|
<h2>Sideload in three steps.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="timeline dl-steps" style="grid-template-columns:repeat(3,1fr) !important">
|
||||||
|
<div class="tl-col tl-shipped">
|
||||||
|
<div class="tl-tag">1 · download</div>
|
||||||
|
<ul><li>Grab the latest debug-signed APK from <a href="https://github.com/KaraZajac/VIGIL/releases/latest">Releases</a> (currently <strong>v0.1.7</strong>).</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="tl-col tl-active">
|
||||||
|
<div class="tl-tag">2 · install</div>
|
||||||
|
<ul><li>Sideload — allow "install unknown apps" for your browser or files app, then open the APK.</li></ul>
|
||||||
|
</div>
|
||||||
|
<div class="tl-col tl-next">
|
||||||
|
<div class="tl-tag">3 · grant + scan</div>
|
||||||
|
<ul><li>Grant nearby-devices + location + notifications, pick a sensitivity, and let it run in the background.</li></ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="tl-foot">
|
||||||
|
Build from source, read the paper, or file issues:
|
||||||
|
<a href="https://github.com/KaraZajac/VIGIL">github.com/KaraZajac/VIGIL</a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/KaraZajac/VIGIL/blob/main/paper/vigil.md">the paper</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ──────────────── FOOTER ──────────────── -->
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<div class="footer-col">
|
||||||
|
<div class="footer-brand">
|
||||||
|
<img class="nav-logo" src="logo.svg" width="26" height="26" alt="" aria-hidden="true">
|
||||||
|
VIGIL
|
||||||
|
</div>
|
||||||
|
<p class="footer-tag">
|
||||||
|
Temporal counter-tracking for Android. Fully offline, listens only. A
|
||||||
|
DREAMMAKER project · sibling to OVERWATCH.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>Project</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/KaraZajac/VIGIL">Source</a></li>
|
||||||
|
<li><a href="https://github.com/KaraZajac/VIGIL/releases">Releases</a></li>
|
||||||
|
<li><a href="https://github.com/KaraZajac/VIGIL/blob/main/paper/vigil.md">The paper</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>Detects</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Apple Find My / AirTag</li>
|
||||||
|
<li>Google Find My · Tile</li>
|
||||||
|
<li>Samsung SmartTag · DULT</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-col">
|
||||||
|
<h4>More</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://overwatch.netslum.io">OVERWATCH</a></li>
|
||||||
|
<li><a href="https://netslum.io">netslum.io</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container footer-bottom">
|
||||||
|
<p>
|
||||||
|
VIGIL is a passive, on-device situational-awareness tool. It does not
|
||||||
|
transmit, probe, or interfere — except the user-initiated "Make it ring".
|
||||||
|
</p>
|
||||||
|
<p class="footer-meta">
|
||||||
|
v0.1.7 · <a href="https://github.com/KaraZajac/VIGIL">github.com/KaraZajac/VIGIL</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="app.js" defer></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108 108" width="108" height="108" role="img" aria-label="VIGIL">
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bg" cx="50%" cy="36%" r="78%">
|
||||||
|
<stop offset="0" stop-color="#2a2a40"/>
|
||||||
|
<stop offset="1" stop-color="#11111b"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="4" y="4" width="100" height="100" rx="26" fill="url(#bg)" stroke="#313244"/>
|
||||||
|
<path d="M26 54 Q54 30 82 54 Q54 78 26 54 Z" fill="none" stroke="#89b4fa" stroke-width="4.5" stroke-linejoin="round"/>
|
||||||
|
<circle cx="54" cy="54" r="11" fill="none" stroke="#89b4fa" stroke-width="3.2"/>
|
||||||
|
<circle cx="54" cy="54" r="6.5" fill="none" stroke="#b4befe" stroke-width="2.8"/>
|
||||||
|
<circle cx="54" cy="54" r="3.2" fill="#a6e3a1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 740 B |
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&family=Space+Grotesk:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{width:1200px;height:630px;color:#cdd6f4;font-family:Inter,sans-serif;position:relative;overflow:hidden;
|
||||||
|
background:radial-gradient(130% 130% at 12% 6%,#26263c 0%,#1e1e2e 52%,#181825 100%)}
|
||||||
|
.wrap{padding:76px 82px;height:100%;display:flex;flex-direction:column;justify-content:center}
|
||||||
|
.brand{display:flex;align-items:center;gap:24px;margin-bottom:26px}
|
||||||
|
.brand img{width:92px;height:92px}
|
||||||
|
.name{font-family:"Space Grotesk",sans-serif;font-weight:700;font-size:94px;letter-spacing:.04em;color:#fff;line-height:1}
|
||||||
|
.tag{font-size:33px;line-height:1.36;color:#bac2de;font-weight:500;max-width:980px}
|
||||||
|
.tag b{color:#fff;font-weight:700}
|
||||||
|
.meta{position:absolute;bottom:82px;left:82px;font-family:"JetBrains Mono",monospace;font-size:19px;color:#9399b2}
|
||||||
|
.meta b{color:#89b4fa}
|
||||||
|
.url{position:absolute;bottom:50px;right:82px;font-family:"JetBrains Mono",monospace;font-size:20px;color:#6c7086}
|
||||||
|
</style></head><body>
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="brand"><img src="logo.svg" alt=""><span class="name">VIGIL</span></div>
|
||||||
|
<div class="tag"><b>What's been following you?</b> A passive Android app that flags AirTags, Tile, SmartTags & Find My trackers travelling <b>with you over time</b> — even rotating-key clones.</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">Apple · Google · Samsung · Tile · <b>DULT</b> — fully offline, listens only</div>
|
||||||
|
<div class="url">vigil.netslum.io</div>
|
||||||
|
</body></html>
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
+1091
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user