chore: remove CLAUDE.md and stale config files
This commit is contained in:
127
BUILD-GUIDE.md
127
BUILD-GUIDE.md
@@ -1,127 +0,0 @@
|
||||
# Quick Build Guide - Archipelago Beta Release
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Make sure you have:
|
||||
- Docker or Podman installed
|
||||
- `xorriso` installed (for ISO creation)
|
||||
- Access to dev server: archipelago@192.168.1.228
|
||||
|
||||
**Note**: When building on the target server with `sudo`, the script will automatically install missing dependencies (`xorriso`, `podman`).
|
||||
|
||||
## Build Auto-Installer ISO
|
||||
|
||||
### Option 1: Build on Target Server (Recommended)
|
||||
|
||||
```bash
|
||||
# SSH to target server
|
||||
ssh archipelago@192.168.1.228
|
||||
|
||||
# Navigate to project
|
||||
cd ~/archy/image-recipe
|
||||
|
||||
# Run build (auto-installs missing deps)
|
||||
sudo ./build-auto-installer-iso.sh
|
||||
|
||||
# Copy ISO back to your Mac
|
||||
# On your Mac:
|
||||
scp archipelago@192.168.1.228:~/archy/image-recipe/results/archipelago-auto-installer-*.iso .
|
||||
```
|
||||
|
||||
### Option 2: Build from Mac (requires Docker)
|
||||
|
||||
**Important**: This requires Docker Desktop installed on macOS.
|
||||
|
||||
```bash
|
||||
cd /Users/dorian/Projects/archy/image-recipe
|
||||
|
||||
# Capture current live server state
|
||||
DEV_SERVER=archipelago@192.168.1.228 ./build-auto-installer-iso.sh
|
||||
|
||||
# ISO will be created in: results/archipelago-auto-installer-*.iso
|
||||
```
|
||||
|
||||
## What the ISO Includes
|
||||
|
||||
✅ Complete Debian 13 root filesystem
|
||||
✅ Pre-built Archipelago backend
|
||||
✅ Pre-built frontend (web UI)
|
||||
✅ **Prepackaged container images** (Bitcoin Knots, LND, UIs, and other bundled apps), loaded on first boot
|
||||
✅ Nginx configuration (HTTPS ready)
|
||||
✅ Auto-installer that:
|
||||
- Detects internal disk
|
||||
- Creates partitions (EFI + root)
|
||||
- Extracts pre-built system
|
||||
- Installs bootloader
|
||||
- Reboots to working system
|
||||
|
||||
## What Users Need to Do Post-Install
|
||||
|
||||
1. **Start apps from the Web UI** – Container images are prepackaged and loaded on first boot. Bitcoin Knots + UI, LND + UI, and other bundled apps are ready to start from the Web UI without manual `podman run`. No need to pull or deploy core containers.
|
||||
|
||||
2. **Access Web UI** – Navigate to `http://[server-ip]`
|
||||
|
||||
## Testing the ISO
|
||||
|
||||
```bash
|
||||
# Use VirtualBox, QEMU, or real hardware
|
||||
qemu-system-x86_64 \
|
||||
-m 4G \
|
||||
-cdrom results/archipelago-auto-installer-*.iso \
|
||||
-hda archipelago-test.qcow2 \
|
||||
-boot d
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
⚠️ **The auto-installer will ERASE the target disk!**
|
||||
⚠️ Make sure to test on a non-production machine first
|
||||
⚠️ Minimum 20GB disk space required (500GB+ recommended for Bitcoin)
|
||||
|
||||
## Build from Source (Alternative)
|
||||
|
||||
If you want to build everything from scratch instead of capturing the live server:
|
||||
|
||||
```bash
|
||||
BUILD_FROM_SOURCE=1 ./build-auto-installer-iso.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Build backend from Rust source
|
||||
- Build frontend with `npm run build`
|
||||
- Create fresh SSL certificates
|
||||
- Generate default configs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**ISO won't boot:**
|
||||
- Ensure UEFI mode is enabled
|
||||
- Try disabling Secure Boot
|
||||
|
||||
**Installer hangs:**
|
||||
- Check the auto-start script fix is applied (see DEPLOYMENT.md)
|
||||
|
||||
**Backend doesn't detect containers:**
|
||||
- Verify `/etc/sudoers.d/archipelago-podman` exists
|
||||
- Check backend can run `sudo podman ps`
|
||||
|
||||
## Version Naming
|
||||
|
||||
ISOs are automatically named with timestamp:
|
||||
```
|
||||
archipelago-auto-installer-YYYYMMDD-HHMMSS.iso
|
||||
```
|
||||
|
||||
For releases, rename to:
|
||||
```
|
||||
archipelago-v0.1.0-beta.1.iso
|
||||
```
|
||||
|
||||
## Next Steps After Building
|
||||
|
||||
1. Test the ISO on VM
|
||||
2. Verify web UI loads
|
||||
3. Test container deployment
|
||||
4. Document any issues
|
||||
5. Tag the release in git
|
||||
6. Upload ISO to distribution point
|
||||
59
CLAUDE.md
59
CLAUDE.md
@@ -1,59 +0,0 @@
|
||||
# CLAUDE.md — Archipelago (Archy)
|
||||
|
||||
Archipelago is a **Bitcoin Node OS** — bootable, self-sovereign personal server. Flash to USB, install on hardware, manage via web UI.
|
||||
|
||||
**Stack**: Rust backend + Vue 3 + TypeScript (strict) + Vite 7 + Tailwind + Pinia + Podman on Debian 13
|
||||
**Version**: 1.3.5 | **Target**: x86_64 and ARM64
|
||||
|
||||
## Beta Freeze (2026-03-18)
|
||||
|
||||
Phase 1: Feature Testing (internal). Feature set is locked.
|
||||
Only: bug fixes, security hardening, ISO build fixes, UI polish, testing.
|
||||
Track: `docs/BETA-PROGRESS.md` | Checklist: `docs/BETA-RELEASE-CHECKLIST.md`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
cd neode-ui && npm start # Local dev (mock backend :5959, Vite :8100)
|
||||
cd neode-ui && npm run build # Build (outputs to web/dist/neode-ui/)
|
||||
./scripts/deploy-to-target.sh --live # Deploy to live server (.228)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Debian 13
|
||||
├── Podman (rootless, user archipelago)
|
||||
├── Nginx (80/443 → backend, app proxies)
|
||||
├── Rust Backend (core/) on 127.0.0.1:5678
|
||||
└── Vue.js UI (neode-ui/)
|
||||
```
|
||||
|
||||
**Data paths**: `/var/lib/archipelago/{app-id}/` (data), `/opt/archipelago/web-ui/` (frontend), `/usr/local/bin/archipelago` (binary)
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. Do not build Rust on macOS — deploy script handles cross-compilation via rsync + remote build.
|
||||
2. Always deploy after changes — `./scripts/deploy-to-target.sh --live`
|
||||
3. Frontend builds to `web/dist/neode-ui/` — not `neode-ui/dist/`
|
||||
4. Container images: `scripts/image-versions.sh` is the single source of truth. All scripts use `$*_IMAGE` variables, not hardcoded registry paths.
|
||||
5. Type-check before committing — `cd neode-ui && npx vue-tsc -b --noEmit`
|
||||
|
||||
## App Integration Checklist
|
||||
|
||||
When adding/fixing apps, check all of these:
|
||||
- `core/archipelago/src/api/rpc/package/` — config, capabilities, deps
|
||||
- `neode-ui/src/views/marketplace/marketplaceData.ts` — marketplace entry
|
||||
- `image-recipe/configs/nginx-archipelago.conf` — proxy rules (HTTP + HTTPS)
|
||||
- `scripts/image-versions.sh` — pinned image version
|
||||
- `scripts/first-boot-containers.sh` — first boot creation
|
||||
- `scripts/deploy-to-target.sh` — deploy logic
|
||||
|
||||
## Git
|
||||
|
||||
Commits: `type: description` (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`, `perf:`)
|
||||
Push to: `git push tx1138 main`
|
||||
|
||||
## Compact Instructions
|
||||
|
||||
When compacting, preserve: list of modified files, test results, deploy target state, current branch, infrastructure IPs.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Demo Deployment via Portainer
|
||||
|
||||
Deploy Archipelago with the **mock backend** for demos. No real node required.
|
||||
|
||||
## Quick Deploy (Portainer)
|
||||
|
||||
1. In Portainer: **Stacks** → **Add stack**
|
||||
2. Name: `archy-demo`
|
||||
3. **Web editor** → paste contents of `docker-compose.demo.yml`
|
||||
4. Or **Build from repository**: use this repo URL and set Compose path to `docker-compose.demo.yml`
|
||||
5. Deploy
|
||||
|
||||
**Access:** http://your-host:4848
|
||||
|
||||
## Mock Backend
|
||||
|
||||
- Uses the Node.js mock backend (not the Rust backend)
|
||||
- Pre-loaded apps, fake data, simulated install/start/stop
|
||||
- **Login password:** `password123`
|
||||
|
||||
## Port
|
||||
|
||||
Default: **4848**. To change, edit the ports mapping in `docker-compose.demo.yml`:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "YOUR_PORT:80"
|
||||
```
|
||||
|
||||
## Chat (Claude AI)
|
||||
|
||||
Set `ANTHROPIC_API_KEY` in the Portainer stack environment to enable real AI chat:
|
||||
|
||||
1. In the stack editor, add under **Environment variables**:
|
||||
- `ANTHROPIC_API_KEY` = your Anthropic API key (starts with `sk-ant-api...`)
|
||||
2. Redeploy the stack
|
||||
|
||||
Without this key, chat shows a "not configured" error. The key is passed to the `neode-backend` container which proxies requests to `api.anthropic.com`.
|
||||
|
||||
## Dev Mode
|
||||
|
||||
`VITE_DEV_MODE=existing` skips setup/onboarding and goes straight to login. For other flows:
|
||||
|
||||
- `setup` – Password setup screen first
|
||||
- `onboarding` – Experimental onboarding flow
|
||||
- `existing` – Login only (default for demo)
|
||||
@@ -9,6 +9,7 @@ impl ApiHandler {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Incoming {
|
||||
from_pubkey: Option<String>,
|
||||
from_name: Option<String>,
|
||||
message: Option<String>,
|
||||
signature: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -67,7 +68,8 @@ impl ApiHandler {
|
||||
tracing::info!("Received message from {}: {}", safe_from, safe_msg);
|
||||
let clean_from = sanitize_html(from);
|
||||
let clean_msg = sanitize_html(&plaintext);
|
||||
node_msg::store_received(&clean_from, &clean_msg).await;
|
||||
let clean_name = incoming.from_name.as_deref().map(sanitize_html);
|
||||
node_msg::store_received(&clean_from, &clean_msg, clean_name.as_deref()).await;
|
||||
}
|
||||
Ok(build_response(StatusCode::OK, "application/json", hyper::Body::from(r#"{"ok":true}"#)))
|
||||
}
|
||||
|
||||
@@ -288,6 +288,7 @@ impl RpcHandler {
|
||||
"mesh.peers" => self.handle_mesh_peers().await,
|
||||
"mesh.messages" => self.handle_mesh_messages(params).await,
|
||||
"mesh.send" => self.handle_mesh_send(params).await,
|
||||
"mesh.send-channel" => self.handle_mesh_send_channel(params).await,
|
||||
"mesh.broadcast" => self.handle_mesh_broadcast().await,
|
||||
"mesh.configure" => self.handle_mesh_configure(params).await,
|
||||
"mesh.send-invoice" => self.handle_mesh_send_invoice(params).await,
|
||||
|
||||
@@ -40,6 +40,42 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// mesh.send-channel — Send a text message to a mesh channel (broadcast).
|
||||
pub(in crate::api::rpc) async fn handle_mesh_send_channel(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
|
||||
let channel = params
|
||||
.get("channel")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u8;
|
||||
|
||||
let message = params
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing message"))?;
|
||||
|
||||
if message.is_empty() {
|
||||
anyhow::bail!("Message cannot be empty");
|
||||
}
|
||||
|
||||
let service = self.mesh_service.read().await;
|
||||
let svc = service
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?;
|
||||
|
||||
let msg = svc.send_channel_message(channel, message).await?;
|
||||
info!(channel, "Sent mesh channel message");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"sent": true,
|
||||
"message_id": msg.id,
|
||||
"channel": channel,
|
||||
}))
|
||||
}
|
||||
|
||||
/// mesh.broadcast — Broadcast our node identity over mesh.
|
||||
pub(in crate::api::rpc) async fn handle_mesh_broadcast(&self) -> Result<serde_json::Value> {
|
||||
let service = self.mesh_service.read().await;
|
||||
|
||||
@@ -101,12 +101,16 @@ impl RpcHandler {
|
||||
|| format!("{}.onion", n.onion) == onion)
|
||||
.map(|n| n.pubkey.clone());
|
||||
|
||||
// Include our node name so the recipient can display it
|
||||
let node_name = data.server_info.name.clone();
|
||||
|
||||
node_message::send_to_peer(
|
||||
onion,
|
||||
&pubkey,
|
||||
message,
|
||||
Some(node_id.signing_key()),
|
||||
recipient_pubkey.as_deref(),
|
||||
node_name.as_deref(),
|
||||
).await?;
|
||||
Ok(serde_json::json!({ "ok": true, "sent_to": onion }))
|
||||
}
|
||||
|
||||
@@ -509,6 +509,59 @@ impl MeshService {
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Send a message to a mesh channel (broadcast).
|
||||
/// Routes through the background listener which owns the serial port.
|
||||
pub async fn send_channel_message(&self, channel: u8, text: &str) -> Result<MeshMessage> {
|
||||
let status = self.state.status.read().await;
|
||||
if !status.device_connected {
|
||||
anyhow::bail!("No mesh device connected");
|
||||
}
|
||||
drop(status);
|
||||
|
||||
let payload = text.as_bytes().to_vec();
|
||||
|
||||
if payload.len() > protocol::MAX_MESSAGE_LEN {
|
||||
anyhow::bail!(
|
||||
"Message too large for LoRa: {} bytes (max {})",
|
||||
payload.len(),
|
||||
protocol::MAX_MESSAGE_LEN
|
||||
);
|
||||
}
|
||||
|
||||
// Send through the listener's command channel
|
||||
self.state
|
||||
.cmd_tx
|
||||
.send(listener::MeshCommand::BroadcastChannel {
|
||||
channel,
|
||||
payload,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
|
||||
|
||||
let chan_contact_id = u32::MAX - (channel as u32);
|
||||
let chan_name = format!("Channel {}", channel);
|
||||
let msg_id = self.state.next_id().await;
|
||||
|
||||
let msg = MeshMessage {
|
||||
id: msg_id,
|
||||
direction: MessageDirection::Sent,
|
||||
peer_contact_id: chan_contact_id,
|
||||
peer_name: Some(chan_name),
|
||||
plaintext: text.to_string(),
|
||||
timestamp: chrono::Utc::now().to_rfc3339(),
|
||||
delivered: false,
|
||||
encrypted: false,
|
||||
};
|
||||
|
||||
self.state.store_message(msg.clone()).await;
|
||||
{
|
||||
let mut status = self.state.status.write().await;
|
||||
status.messages_sent += 1;
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Broadcast our advertisement over mesh so other nodes can discover us.
|
||||
/// Sends an immediate advert via the listener's command channel.
|
||||
pub async fn broadcast_identity(&self) -> Result<()> {
|
||||
|
||||
@@ -14,6 +14,9 @@ pub struct IncomingMessage {
|
||||
pub from_pubkey: String,
|
||||
#[serde(default)]
|
||||
pub from_onion: Option<String>,
|
||||
/// Sender's node name (for display in group chat).
|
||||
#[serde(default)]
|
||||
pub from_name: Option<String>,
|
||||
pub message: String,
|
||||
pub timestamp: String,
|
||||
/// "sent" or "received"
|
||||
@@ -73,7 +76,7 @@ fn persist() {
|
||||
}
|
||||
|
||||
/// Store a received message (called from HTTP handler).
|
||||
pub fn store_received_sync(from_pubkey: &str, message: &str) {
|
||||
pub fn store_received_sync(from_pubkey: &str, message: &str, from_name: Option<&str>) {
|
||||
let ts = chrono::Utc::now().to_rfc3339();
|
||||
let mut guard = store().lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
@@ -89,6 +92,7 @@ pub fn store_received_sync(from_pubkey: &str, message: &str) {
|
||||
guard.messages.push(IncomingMessage {
|
||||
from_pubkey: from_pubkey.to_string(),
|
||||
from_onion: None,
|
||||
from_name: from_name.map(|s| s.to_string()),
|
||||
message: message.to_string(),
|
||||
timestamp: ts,
|
||||
direction: "received".to_string(),
|
||||
@@ -98,8 +102,8 @@ pub fn store_received_sync(from_pubkey: &str, message: &str) {
|
||||
persist();
|
||||
}
|
||||
|
||||
pub async fn store_received(from_pubkey: &str, message: &str) {
|
||||
store_received_sync(from_pubkey, message);
|
||||
pub async fn store_received(from_pubkey: &str, message: &str, from_name: Option<&str>) {
|
||||
store_received_sync(from_pubkey, message, from_name);
|
||||
}
|
||||
|
||||
/// Store a sent message (for display in Archipelago channel).
|
||||
@@ -231,6 +235,7 @@ pub async fn send_to_peer(
|
||||
message: &str,
|
||||
signing_key: Option<&ed25519_dalek::SigningKey>,
|
||||
recipient_pubkey: Option<&str>,
|
||||
from_name: Option<&str>,
|
||||
) -> Result<()> {
|
||||
validate_onion(onion)?;
|
||||
|
||||
@@ -255,12 +260,15 @@ pub async fn send_to_peer(
|
||||
_ => (message.to_string(), false),
|
||||
};
|
||||
|
||||
let body = serde_json::json!({
|
||||
let mut body = serde_json::json!({
|
||||
"from_pubkey": from_pubkey,
|
||||
"message": payload_message,
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
"encrypted": encrypted,
|
||||
});
|
||||
if let Some(name) = from_name {
|
||||
body["from_name"] = serde_json::Value::String(name.to_string());
|
||||
}
|
||||
|
||||
let proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY).context("Invalid Tor proxy")?;
|
||||
let client = reqwest::Client::builder()
|
||||
|
||||
@@ -398,7 +398,7 @@ class RPCClient {
|
||||
})
|
||||
}
|
||||
|
||||
async getReceivedMessages(): Promise<{ messages: Array<{ from_pubkey: string; message: string; timestamp: string }> }> {
|
||||
async getReceivedMessages(): Promise<{ messages: Array<{ from_pubkey: string; from_name?: string; message: string; timestamp: string; direction?: string }> }> {
|
||||
return this.call({
|
||||
method: 'node-messages-received',
|
||||
params: {},
|
||||
|
||||
@@ -276,6 +276,31 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
return result
|
||||
}
|
||||
|
||||
async function sendChannelMessage(channel: number, message: string) {
|
||||
const doSend = async () => {
|
||||
try {
|
||||
sending.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<{ sent: boolean; message_id: number; channel: number }>({
|
||||
method: 'mesh.send-channel',
|
||||
params: { channel, message: message.trim() },
|
||||
})
|
||||
if (res.sent) {
|
||||
await fetchMessages()
|
||||
}
|
||||
return res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to send channel message'
|
||||
throw err
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
const result = sendQueue.then(doSend, doSend)
|
||||
sendQueue = result.then(() => {}, () => {})
|
||||
return result
|
||||
}
|
||||
|
||||
async function broadcastIdentity() {
|
||||
try {
|
||||
error.value = null
|
||||
@@ -456,6 +481,7 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
fetchPeers,
|
||||
fetchMessages,
|
||||
sendMessage,
|
||||
sendChannelMessage,
|
||||
broadcastIdentity,
|
||||
configure,
|
||||
refreshAll,
|
||||
|
||||
@@ -37,18 +37,34 @@ let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
// The Public channel (always available on Meshcore)
|
||||
const publicChannel = { index: 0, name: 'Public' }
|
||||
|
||||
// Channel contact_id convention: matches backend u32::MAX - channel_index
|
||||
function channelContactId(channelIndex: number): number {
|
||||
return 4294967295 - channelIndex // u32::MAX - index
|
||||
}
|
||||
|
||||
// Archipelago Channel — Tor-based messaging to all federated/peered nodes
|
||||
const archChannelActive = ref(false)
|
||||
const archMessages = ref<Array<{ from_pubkey: string; message: string; timestamp: string }>>([])
|
||||
const archMessages = ref<Array<{ from_pubkey: string; from_name?: string; message: string; timestamp: string; direction?: string }>>([])
|
||||
const archUnread = ref(0)
|
||||
let archPollInterval: ReturnType<typeof setInterval> | null = null
|
||||
// Federation node name cache: pubkey -> node name
|
||||
const fedNodeNames = ref<Record<string, string>>({})
|
||||
|
||||
function openArchChannel() {
|
||||
async function openArchChannel() {
|
||||
activeChatPeer.value = null
|
||||
activeChatChannel.value = null
|
||||
archChannelActive.value = true
|
||||
archUnread.value = 0
|
||||
mobileShowChat.value = true
|
||||
// Load federation node names for resolving pubkeys to names
|
||||
try {
|
||||
const res = await rpcClient.federationListNodes()
|
||||
const names: Record<string, string> = {}
|
||||
for (const node of res.nodes) {
|
||||
if (node.pubkey) names[node.pubkey] = node.name || node.did.slice(0, 12) + '...'
|
||||
}
|
||||
fedNodeNames.value = names
|
||||
} catch { /* non-fatal */ }
|
||||
loadArchMessages()
|
||||
if (!archPollInterval) {
|
||||
archPollInterval = setInterval(loadArchMessages, 15000)
|
||||
@@ -58,7 +74,18 @@ function openArchChannel() {
|
||||
async function loadArchMessages() {
|
||||
try {
|
||||
const res = await rpcClient.getReceivedMessages()
|
||||
archMessages.value = res.messages || []
|
||||
const newMessages = res.messages || []
|
||||
// Track unread: count new received messages since last load
|
||||
if (archMessages.value.length > 0 && !archChannelActive.value) {
|
||||
const newReceived = newMessages.filter(
|
||||
m => m.direction !== 'sent' && m.from_pubkey !== 'me'
|
||||
&& !archMessages.value.some(existing =>
|
||||
existing.from_pubkey === m.from_pubkey && existing.timestamp === m.timestamp
|
||||
)
|
||||
)
|
||||
archUnread.value += newReceived.length
|
||||
}
|
||||
archMessages.value = newMessages
|
||||
} catch { /* silent */ }
|
||||
}
|
||||
|
||||
@@ -142,6 +169,11 @@ async function handleToggleOffGrid() {
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
await Promise.all([mesh.refreshAll(), transport.fetchStatus()])
|
||||
// Start background polling for Archipelago (Tor) messages so unread count works
|
||||
loadArchMessages()
|
||||
if (!archPollInterval) {
|
||||
archPollInterval = setInterval(loadArchMessages, 15000)
|
||||
}
|
||||
pollInterval = setInterval(() => {
|
||||
mesh.fetchStatus()
|
||||
mesh.fetchPeers()
|
||||
@@ -154,6 +186,7 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (pollInterval) clearInterval(pollInterval)
|
||||
if (archPollInterval) { clearInterval(archPollInterval); archPollInterval = null }
|
||||
})
|
||||
|
||||
// Active chat name for the header
|
||||
@@ -177,11 +210,21 @@ const hasActiveChat = computed(() => !!activeChatPeer.value || !!activeChatChann
|
||||
const chatMessages = computed(() => {
|
||||
if (archChannelActive.value) {
|
||||
return archMessages.value.map((m, i) => {
|
||||
const isSent = (m as Record<string, unknown>).direction === 'sent' || m.from_pubkey === 'me'
|
||||
const isSent = m.direction === 'sent' || m.from_pubkey === 'me'
|
||||
let peerName = 'Unknown'
|
||||
if (isSent) {
|
||||
peerName = 'You'
|
||||
} else if (m.from_name) {
|
||||
peerName = m.from_name
|
||||
} else if (fedNodeNames.value[m.from_pubkey]) {
|
||||
peerName = fedNodeNames.value[m.from_pubkey]
|
||||
} else {
|
||||
peerName = m.from_pubkey.slice(0, 12) + '...'
|
||||
}
|
||||
return {
|
||||
id: i,
|
||||
peer_contact_id: -99,
|
||||
peer_name: isSent ? 'You' : (m.from_pubkey.slice(0, 12) + '...'),
|
||||
peer_name: peerName,
|
||||
direction: (isSent ? 'sent' : 'received') as 'sent' | 'received',
|
||||
plaintext: m.message,
|
||||
timestamp: m.timestamp,
|
||||
@@ -193,7 +236,7 @@ const chatMessages = computed(() => {
|
||||
})
|
||||
}
|
||||
if (activeChatChannel.value) {
|
||||
const chanId = -(activeChatChannel.value.index + 1)
|
||||
const chanId = channelContactId(activeChatChannel.value.index)
|
||||
return mesh.messages.filter(m => m.peer_contact_id === chanId)
|
||||
}
|
||||
if (activeChatPeer.value) {
|
||||
@@ -236,6 +279,7 @@ function openChannelChat(channel: { index: number; name: string }) {
|
||||
messageText.value = ''
|
||||
activeTab.value = 'chat'
|
||||
mobileShowChat.value = true
|
||||
mesh.markChatRead(channelContactId(channel.index))
|
||||
nextTick(() => scrollChatToBottom())
|
||||
}
|
||||
|
||||
@@ -254,12 +298,18 @@ async function handleSendMessage() {
|
||||
nextTick(() => scrollChatToBottom())
|
||||
return
|
||||
}
|
||||
if (!activeChatPeer.value || !messageText.value.trim()) return
|
||||
if (!messageText.value.trim()) return
|
||||
sendError.value = ''
|
||||
try {
|
||||
if (activeChatChannel.value) {
|
||||
await mesh.sendChannelMessage(activeChatChannel.value.index, messageText.value)
|
||||
messageText.value = ''
|
||||
nextTick(() => scrollChatToBottom())
|
||||
} else if (activeChatPeer.value) {
|
||||
await mesh.sendMessage(activeChatPeer.value.contact_id, messageText.value)
|
||||
messageText.value = ''
|
||||
nextTick(() => scrollChatToBottom())
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
sendError.value = err instanceof Error ? err.message : 'Send failed'
|
||||
}
|
||||
@@ -467,6 +517,7 @@ function truncatePubkey(hex: string | null): string {
|
||||
<div class="mesh-peer-name">Public</div>
|
||||
<div class="mesh-peer-sub">Mesh radio</div>
|
||||
</div>
|
||||
<span v-if="mesh.unreadCounts[channelContactId(0)]" class="ml-auto text-[10px] px-1.5 py-0.5 rounded-full bg-orange-500/30 text-orange-300">{{ mesh.unreadCounts[channelContactId(0)] }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="peer in sortedPeers" :key="peer.contact_id"
|
||||
|
||||
@@ -162,7 +162,7 @@ export function resolveAppUrl(id: string, routeQueryPath?: string): string {
|
||||
if (httpsProxy) return `${window.location.origin}${httpsProxy}`
|
||||
}
|
||||
|
||||
// HTTP: direct port access (iframes break with proxy paths due to root-relative assets)
|
||||
// HTTP: direct port access
|
||||
const port = APP_PORTS[id]
|
||||
if (!port) return ''
|
||||
let base = `http://${window.location.hostname}:${port}`
|
||||
|
||||
Reference in New Issue
Block a user