north-star / commit
removed gps positions, added lots of comments
.vscode/settings.json | 3 +++
readme.md | 38 +++++++++++++++++++++++++++++----
src/app.ts | 2 +-
src/core/room.ts | 2 +-
src/core/utils.ts | 6 ------
src/features/compass/compass.ts | 2 +-
src/features/friends/panel.ts | 31 +++++++++------------------
src/features/participants/panel.ts | 20 +++--------------
src/features/session/session.ts | 2 +-
src/features/ui/wake_lock_controller.ts | 5 ++++-
src/index.html | 5 ++---
src/platform/darkmode.ts | 8 ++++++-
src/{core => }/public_settings.ts | 18 ++++++++++++++++
src/style.css | 6 ------
14 files changed, 85 insertions(+), 63 deletions(-)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b9c2b23..3dea3a3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -23,9 +23,12 @@
"darkmode",
"Flaticon",
"Freepik",
+ "geofencing",
+ "geolocation",
"Icongeek26",
"noopener",
"noreferrer",
+ "subfolders",
"wakelock"
]
}
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 1423991..66ca7e5 100644
--- a/readme.md
+++ b/readme.md
@@ -71,17 +71,39 @@ Firebase console: [https://console.firebase.google.com/project/north-star-e5dee/
npm run preview
```
-## New Features
+### Features Implemented
-Ideas worth tracking (not implemented yet):
+#### User-Facing Features
+
+- **Wake-Lock (Stay-Awake Mode)** — Emoji toggle button (💤/🔆) keeps device screen active during tracking
+- **Toast Notifications** — Contextual feedback for compass events, calibration status, and wake-lock actions
+- **Compass Calibration Feedback** — Toast messages for permission denied, enabled, unavailable, and calibrated states
+- **User Profile Persistence** — Custom name and color saved locally and restored on app reload
+- **Dark Mode Support** — Toggle dark mode with persistent preference storage
+- **Accessibility Improvements** — ARIA labels, semantic HTML, and keyboard navigation support
+
+#### Technical Improvements
+
+- **Strict TypeScript Mode** — All ~40 files migrated; zero type errors
+- **Domain-Driven Folder Structure** — Organized into `src/core`, `src/features`, `src/platform` with domain subfolders
+- **Barrel Index Files** — Simplified imports across feature modules
+- **Naming Convention Standardization** — All filenames use underscores, not dashes
+
+### Future Work
+
+#### Planned Features
- Optional user-facing low-power tracking mode toggle (battery saver style control).
- Friend stale-state UX polish (for example: clearer badges, subtle icons, better wording).
-- Optional wake-lock mode for situations where people want to keep the app active longer.
-- Optional native-wrapper path (Capacitor/PWA strategy) for more reliable background location updates.
- Export/share diagnostics mode for easier field testing (sensor availability, last sync times, etc.).
- Continue migrating remaining behavior-tuning constants into public settings (for example initial seed position and other non-sensitive runtime defaults).
+#### Mobile Platform Support
+
+- **React Native + Expo Migration** — Reuse ~80% of TypeScript logic (crypto, utils, Firebase sync) while rebuilding UI layer for iOS/Android native support with background geolocation
+- **Background Geolocation** — Continuous location tracking even when app is in the background (requires native implementation)
+- **Battery Optimization** — Configurable tracking intervals and geofencing, so users can define regions where they care about updates (or where they do not)
+
### Firebase database rules
This repo includes Realtime Database security rules in [config/firebase/database.rules.json](config/firebase/database.rules.json).
@@ -95,6 +117,14 @@ Before sharing the app publicly, add these rules in Firebase Console:
Without these rules, your database may allow unsafe writes.
+### Absolute location
+
+I explicitly chose not to expose direct map deep links in the UI, and this feature will **not** be added.
+
+The app keeps precise coordinates internally for relative-arrow calculations, but the app intentionally shows only relative directions.
+
+This is a current design choice. Feel free to bring this up in an issue if you have strong feelings about it.
+
### Is it open source?
You're looking at it.
diff --git a/src/app.ts b/src/app.ts
index a13ac49..f81e40f 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -8,7 +8,7 @@ import { initSession, initSelfProfile, bumpQrAutoCollapseCountdown } from './fea
import { parseRoomFromHash } from './core/room';
import { subscribeToFriends, createFriendsPanelController, renderArrows } from './features/friends';
import { initCompassController } from './features/compass';
-import { PUBLIC_SETTINGS } from './core/public_settings';
+import { PUBLIC_SETTINGS } from './public_settings';
const debugCommit = document.getElementById('debug-commit');
const nameForm = document.getElementById('name-form');
diff --git a/src/core/room.ts b/src/core/room.ts
index cd6fe0c..832df2f 100644
--- a/src/core/room.ts
+++ b/src/core/room.ts
@@ -1,4 +1,4 @@
-import { PUBLIC_SETTINGS } from './public_settings';
+import { PUBLIC_SETTINGS } from '../public_settings';
const ROOM_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
diff --git a/src/core/utils.ts b/src/core/utils.ts
index a9cf0d8..a1fb6d9 100644
--- a/src/core/utils.ts
+++ b/src/core/utils.ts
@@ -61,12 +61,6 @@ export const getFriendPresenceState = (friend: FriendLike, now: number, threshol
return 'stale-offline';
};
-export const PRESENCE_EMOJI: Record<string, string> = {
- online: '🟢',
- 'recent-offline': '🟡',
- 'stale-offline': '⚪'
-};
-
export const getDeviceHint = async () => {
const userAgentData = (navigator as Navigator & { userAgentData?: UserAgentDataWithModel }).userAgentData;
if (userAgentData) {
diff --git a/src/features/compass/compass.ts b/src/features/compass/compass.ts
index 3c9cb0b..5840677 100644
--- a/src/features/compass/compass.ts
+++ b/src/features/compass/compass.ts
@@ -1,5 +1,5 @@
import { normalizeAngle, normalizeSignedAngle } from '../../core/utils';
-import { PUBLIC_SETTINGS } from '../../core/public_settings';
+import { PUBLIC_SETTINGS } from '../../public_settings';
import { createToast } from '../../core/toast';
type CompassControllerOptions = {
diff --git a/src/features/friends/panel.ts b/src/features/friends/panel.ts
index 3343990..59b054f 100644
--- a/src/features/friends/panel.ts
+++ b/src/features/friends/panel.ts
@@ -1,6 +1,6 @@
-import { getFriendPresenceState, PRESENCE_EMOJI } from '../../core/utils';
+import { getFriendPresenceState } from '../../core/utils';
import type { PresenceThresholds } from '../../core/utils';
-import { PUBLIC_SETTINGS } from '../../core/public_settings';
+import { PUBLIC_SETTINGS } from '../../public_settings';
type FriendModel = {
id: string;
@@ -29,6 +29,11 @@ export const createFriendsPanelController = ({
presenceThresholds: PresenceThresholds;
}) => {
const guardianAngelEnabled = PUBLIC_SETTINGS.features.guardianAngelEnabled;
+ const presenceEmoji = {
+ online: PUBLIC_SETTINGS.ui.emojis.presence.online,
+ 'recent-offline': PUBLIC_SETTINGS.ui.emojis.presence.recentlyOffline,
+ 'stale-offline': PUBLIC_SETTINGS.ui.emojis.presence.staleOffline
+ } as const;
const northMarkerId = 'north-marker';
const guardianAngelId = 'guardian-angel';
const GUARDIAN_ORBIT_RADIUS_METERS = 75;
@@ -67,10 +72,6 @@ export const createFriendsPanelController = ({
return 2;
};
- const shouldShowMapLink = (friendId: string) => {
- return friendId !== northMarkerId && (!guardianAngelEnabled || friendId !== guardianAngelId);
- };
-
const getGuardianAngelPosition = (nowMs: number) => {
const latRad = selfPosition.lat * (Math.PI / 180);
const metersPerDegLat = 111320;
@@ -353,7 +354,7 @@ export const createFriendsPanelController = ({
presenceStatus.className = 'friend-presence-status';
presenceStatus.dataset.friendId = friend.id;
const initialPresence = getFriendPresenceState(friend, Date.now(), presenceThresholds);
- presenceStatus.textContent = PRESENCE_EMOJI[initialPresence] ?? '🟡';
+ presenceStatus.textContent = presenceEmoji[initialPresence] ?? PUBLIC_SETTINGS.ui.emojis.presence.recentlyOffline;
presenceStatus.title = initialPresence === 'online' ? 'Online'
: initialPresence === 'recent-offline' ? 'Recently seen'
: 'Offline';
@@ -365,24 +366,12 @@ export const createFriendsPanelController = ({
nameText.textContent = friend.name;
nameWrapper.appendChild(nameText);
- const mapLink = document.createElement('a');
- mapLink.className = 'friend-map-link';
- mapLink.href = `https://www.google.com/maps?q=${friend.position.lat},${friend.position.lon}`;
- mapLink.target = '_blank';
- mapLink.rel = 'noopener noreferrer';
- mapLink.textContent = '📍';
- mapLink.title = `Open ${friend.name} in your maps app`;
- mapLink.setAttribute('aria-label', `Open ${friend.name} in your maps app`);
-
row.appendChild(checkbox);
- if (shouldShowMapLink(friend.id)) {
- nameWrapper.appendChild(mapLink);
- }
const dragHandle = document.createElement('button');
dragHandle.type = 'button';
dragHandle.className = 'friend-drag-handle';
- dragHandle.textContent = '☰';
+ dragHandle.textContent = PUBLIC_SETTINGS.ui.emojis.dragHandle;
dragHandle.title = `Drag to reorder ${friend.name}`;
dragHandle.setAttribute('aria-label', `Drag to reorder ${friend.name}`);
dragHandle.addEventListener('pointerdown', (event: PointerEvent) => {
@@ -415,7 +404,7 @@ export const createFriendsPanelController = ({
const friend = lastFriendsToRender.find((f: FriendModel) => f.id === friendId);
if (!friend) return;
const state = getFriendPresenceState(friend, now, presenceThresholds);
- const emoji = PRESENCE_EMOJI[state] ?? '🟡';
+ const emoji = presenceEmoji[state] ?? PUBLIC_SETTINGS.ui.emojis.presence.recentlyOffline;
if (el.textContent !== emoji) el.textContent = emoji;
const title = state === 'online' ? 'Online'
: state === 'recent-offline' ? 'Recently seen'
diff --git a/src/features/participants/panel.ts b/src/features/participants/panel.ts
index 34da9cf..2904bee 100644
--- a/src/features/participants/panel.ts
+++ b/src/features/participants/panel.ts
@@ -1,3 +1,5 @@
+import { PUBLIC_SETTINGS } from '../../public_settings';
+
type ParticipantModel = {
id: string;
name: string;
@@ -56,10 +58,6 @@ export const createParticipantsPanelController = ({
return 1;
};
- const shouldShowMapLink = (participantId: string) => {
- return participantId !== northMarkerId;
- };
-
const ensureParticipantOrder = (participantsToRender: ParticipantModel[]) => {
const ids = participantsToRender.map((participant: ParticipantModel) => participant.id);
const idSet = new Set(ids);
@@ -307,24 +305,12 @@ export const createParticipantsPanelController = ({
nameText.textContent = participant.name;
nameWrapper.appendChild(nameText);
- const mapLink = document.createElement('a');
- mapLink.className = 'participant-map-link';
- mapLink.href = `https://www.google.com/maps?q=${participant.position.lat},${participant.position.lon}`;
- mapLink.target = '_blank';
- mapLink.rel = 'noopener noreferrer';
- mapLink.textContent = '📍';
- mapLink.title = `Open ${participant.name} in your maps app`;
- mapLink.setAttribute('aria-label', `Open ${participant.name} in your maps app`);
-
row.appendChild(checkbox);
- if (shouldShowMapLink(participant.id)) {
- nameWrapper.appendChild(mapLink);
- }
const dragHandle = document.createElement('button');
dragHandle.type = 'button';
dragHandle.className = 'participant-drag-handle';
- dragHandle.textContent = '☰';
+ dragHandle.textContent = PUBLIC_SETTINGS.ui.emojis.dragHandle;
dragHandle.title = `Drag to reorder ${participant.name}`;
dragHandle.setAttribute('aria-label', `Drag to reorder ${participant.name}`);
dragHandle.addEventListener('pointerdown', (event) => {
diff --git a/src/features/session/session.ts b/src/features/session/session.ts
index 7bebafa..0aeb03e 100644
--- a/src/features/session/session.ts
+++ b/src/features/session/session.ts
@@ -1,7 +1,7 @@
import { generateRoomKey, exportRoomKey, importRoomKey } from '../../core/crypto';
import { buildRoomHash, parseRoomFromHash } from '../../core/room';
import { renderInviteQrCard } from './qr';
-import { PUBLIC_SETTINGS } from '../../core/public_settings';
+import { PUBLIC_SETTINGS } from '../../public_settings';
/**
* Parses an existing room from the URL hash, or generates a new one.
diff --git a/src/features/ui/wake_lock_controller.ts b/src/features/ui/wake_lock_controller.ts
index 4348fb5..ba2574d 100644
--- a/src/features/ui/wake_lock_controller.ts
+++ b/src/features/ui/wake_lock_controller.ts
@@ -1,5 +1,6 @@
import { createWakeLockManager } from '../../platform/wakelock';
import { createToast } from '../../core/toast';
+import { PUBLIC_SETTINGS } from '../../public_settings';
type WakeLockControllerOptions = {
wakeLockToggle: HTMLElement | null;
@@ -33,7 +34,9 @@ export const initWakeLockController = ({
const enabled = forcedByFullscreen || wakeLockEnabledByToggle;
wakeLockToggle.disabled = !supported;
- wakeLockToggle.textContent = enabled ? '🔆' : '💤';
+ wakeLockToggle.textContent = enabled
+ ? PUBLIC_SETTINGS.ui.emojis.wakeLock.on
+ : PUBLIC_SETTINGS.ui.emojis.wakeLock.off;
wakeLockToggle.classList.toggle('is-on', enabled);
wakeLockToggle.setAttribute('aria-pressed', String(enabled));
diff --git a/src/index.html b/src/index.html
index b646024..49185ee 100644
--- a/src/index.html
+++ b/src/index.html
@@ -40,10 +40,9 @@
<div class="controls-row controls-row-main">
<input id="self-name-input" name="selfName" type="text" maxlength="32" enterkeyhint="done" />
<input id="self-color-input" name="selfColor" type="color" />
- <button id="dark-mode-toggle" class="dark-mode-toggle" type="button"
- aria-label="Toggle dark mode">🌙</button>
+ <button id="dark-mode-toggle" class="dark-mode-toggle" type="button" aria-label="Toggle dark mode"></button>
<button id="wake-lock-toggle" class="wake-lock-toggle" type="button" aria-pressed="false"
- aria-label="Keep screen awake">💤</button>
+ aria-label="Keep screen awake"></button>
<button id="clean-mode-toggle" class="clean-mode-toggle" type="button"
aria-pressed="false">Fullscreen</button>
</div>
diff --git a/src/platform/darkmode.ts b/src/platform/darkmode.ts
index 4956c8c..9982c6f 100644
--- a/src/platform/darkmode.ts
+++ b/src/platform/darkmode.ts
@@ -1,9 +1,15 @@
+import { PUBLIC_SETTINGS } from '../public_settings';
+
export const DARK_MODE_STORAGE_KEY = 'friend-compass-dark-mode';
export const initDarkMode = (toggleButton: HTMLElement | null) => {
const applyDarkMode = (dark: boolean) => {
document.body.classList.toggle('dark', dark);
- if (toggleButton) toggleButton.textContent = dark ? '☀️' : '🌙';
+ if (toggleButton) {
+ toggleButton.textContent = dark
+ ? PUBLIC_SETTINGS.ui.emojis.darkMode.on
+ : PUBLIC_SETTINGS.ui.emojis.darkMode.off;
+ }
localStorage.setItem(DARK_MODE_STORAGE_KEY, dark ? '1' : '0');
};
diff --git a/src/core/public_settings.ts b/src/public_settings.ts
similarity index 79%
rename from src/core/public_settings.ts
rename to src/public_settings.ts
index 296f561..c09324a 100644
--- a/src/core/public_settings.ts
+++ b/src/public_settings.ts
@@ -42,6 +42,24 @@ export const PUBLIC_SETTINGS = {
links: {
/** Hosted app base URL used in invite QR cards for local runs. */
gitlabPagesBaseUrl: 'https://pavelsafronov.gitlab.io/north-star/'
+ },
+ ui: {
+ emojis: {
+ darkMode: {
+ off: '🌙',
+ on: '☀️'
+ },
+ wakeLock: {
+ off: '💤',
+ on: '🔆'
+ },
+ presence: {
+ online: '🟢',
+ recentlyOffline: '🟡',
+ staleOffline: '⚪'
+ },
+ dragHandle: '☰'
+ }
}
} as const;
diff --git a/src/style.css b/src/style.css
index f006db6..aad1e27 100644
--- a/src/style.css
+++ b/src/style.css
@@ -567,12 +567,6 @@ body.clean-mode .arrow {
line-height: 1;
}
-.friend-map-link {
- font-size: 17px;
- line-height: 1;
- text-decoration: none;
-}
-
.friend-drag-handle {
width: 24px;
height: 24px;