mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-13 13:21:28 +02:00
Add room hash history
This commit is contained in:
@@ -40,10 +40,15 @@ uv run uvicorn app.main:app --reload
|
|||||||
|
|
||||||
# Or specify port explicitly
|
# Or specify port explicitly
|
||||||
MESHCORE_SERIAL_PORT=/dev/cu.usbserial-0001 uv run uvicorn app.main:app --reload
|
MESHCORE_SERIAL_PORT=/dev/cu.usbserial-0001 uv run uvicorn app.main:app --reload
|
||||||
|
|
||||||
|
# or disable hot reload for more permanent deployments
|
||||||
|
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
Backend runs at http://localhost:8000, and will preferentially serve from `./frontend/dist` for the GUI. If you want to do GUI development, see below and use http://localhost:5173 for the GUI.
|
Backend runs at http://localhost:8000, and will preferentially serve from `./frontend/dist` for the GUI. If you want to do GUI development, see below and use http://localhost:5173 for the GUI.
|
||||||
|
|
||||||
|
See the `HTTPS` section below if you're serving this anywhere but localhost and need the GPU cracker to function.
|
||||||
|
|
||||||
**If you just want to run this as-is (all commits push a distribution-ready frontend build), you can just run the backend and access the GUI from there; no need to boot the frontend**
|
**If you just want to run this as-is (all commits push a distribution-ready frontend build), you can just run the backend and access the GUI from there; no need to boot the frontend**
|
||||||
|
|
||||||
### Frontend Dev
|
### Frontend Dev
|
||||||
|
|||||||
+542
File diff suppressed because one or more lines are too long
-542
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>RemoteTerm for MeshCore</title>
|
<title>RemoteTerm for MeshCore</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D9RuHvp1.js"></script>
|
<script type="module" crossorigin src="/assets/index-BDVBgoJZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-RWG6EjFU.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-RWG6EjFU.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
+81
-3
@@ -41,6 +41,38 @@ function getMessageContentKey(msg: Message): string {
|
|||||||
return `${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`;
|
return `${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse URL hash to get conversation (e.g., #channel/Public or #contact/JohnDoe or #raw)
|
||||||
|
function parseHashConversation(): { type: 'channel' | 'contact' | 'raw'; name: string } | null {
|
||||||
|
const hash = window.location.hash.slice(1); // Remove leading #
|
||||||
|
if (!hash) return null;
|
||||||
|
|
||||||
|
if (hash === 'raw') {
|
||||||
|
return { type: 'raw', name: 'raw' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const slashIndex = hash.indexOf('/');
|
||||||
|
if (slashIndex === -1) return null;
|
||||||
|
|
||||||
|
const type = hash.slice(0, slashIndex);
|
||||||
|
const name = decodeURIComponent(hash.slice(slashIndex + 1));
|
||||||
|
|
||||||
|
if ((type === 'channel' || type === 'contact') && name) {
|
||||||
|
return { type, name };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate URL hash from conversation
|
||||||
|
function getConversationHash(conv: Conversation | null): string {
|
||||||
|
if (!conv) return '';
|
||||||
|
if (conv.type === 'raw') return '#raw';
|
||||||
|
// Strip leading # from channel names for cleaner URLs
|
||||||
|
const name = conv.type === 'channel' && conv.name.startsWith('#')
|
||||||
|
? conv.name.slice(1)
|
||||||
|
: conv.name;
|
||||||
|
return `#${conv.type}/${encodeURIComponent(name)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const messageInputRef = useRef<MessageInputHandle>(null);
|
const messageInputRef = useRef<MessageInputHandle>(null);
|
||||||
const activeConversationRef = useRef<Conversation | null>(null);
|
const activeConversationRef = useRef<Conversation | null>(null);
|
||||||
@@ -305,11 +337,49 @@ export function App() {
|
|||||||
fetchUndecryptedCount();
|
fetchUndecryptedCount();
|
||||||
}, [fetchConfig, fetchAppSettings, fetchUndecryptedCount]);
|
}, [fetchConfig, fetchAppSettings, fetchUndecryptedCount]);
|
||||||
|
|
||||||
// Select Public channel by default when channels first load
|
// Resolve URL hash to a conversation
|
||||||
|
const resolveHashToConversation = useCallback((): Conversation | null => {
|
||||||
|
const hashConv = parseHashConversation();
|
||||||
|
if (!hashConv) return null;
|
||||||
|
|
||||||
|
if (hashConv.type === 'raw') {
|
||||||
|
return { type: 'raw', id: 'raw', name: 'Raw Packet Feed' };
|
||||||
|
}
|
||||||
|
if (hashConv.type === 'channel') {
|
||||||
|
// Match with or without leading # (URL strips it for cleaner URLs)
|
||||||
|
const channel = channels.find(c => c.name === hashConv.name || c.name === `#${hashConv.name}`);
|
||||||
|
if (channel) {
|
||||||
|
return { type: 'channel', id: channel.key, name: channel.name };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hashConv.type === 'contact') {
|
||||||
|
const contact = contacts.find(c => getContactDisplayName(c.name, c.public_key) === hashConv.name);
|
||||||
|
if (contact) {
|
||||||
|
return {
|
||||||
|
type: 'contact',
|
||||||
|
id: contact.public_key,
|
||||||
|
name: getContactDisplayName(contact.name, contact.public_key),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [channels, contacts]);
|
||||||
|
|
||||||
|
// Set initial conversation from URL hash or default to Public channel
|
||||||
const hasSetDefaultConversation = useRef(false);
|
const hasSetDefaultConversation = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasSetDefaultConversation.current || channels.length === 0 || activeConversation) return;
|
if (hasSetDefaultConversation.current || activeConversation) return;
|
||||||
|
if (channels.length === 0 && contacts.length === 0) return;
|
||||||
|
|
||||||
|
// Try to restore from URL hash first
|
||||||
|
const conv = resolveHashToConversation();
|
||||||
|
if (conv) {
|
||||||
|
setActiveConversation(conv);
|
||||||
|
hasSetDefaultConversation.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to Public channel
|
||||||
const publicChannel = channels.find(c => c.name === 'Public');
|
const publicChannel = channels.find(c => c.name === 'Public');
|
||||||
if (publicChannel) {
|
if (publicChannel) {
|
||||||
setActiveConversation({
|
setActiveConversation({
|
||||||
@@ -319,7 +389,7 @@ export function App() {
|
|||||||
});
|
});
|
||||||
hasSetDefaultConversation.current = true;
|
hasSetDefaultConversation.current = true;
|
||||||
}
|
}
|
||||||
}, [channels, activeConversation]);
|
}, [channels, contacts, activeConversation, resolveHashToConversation]);
|
||||||
|
|
||||||
// Fetch messages and count unreads for all conversations on load (single bulk request)
|
// Fetch messages and count unreads for all conversations on load (single bulk request)
|
||||||
const fetchedChannels = useRef<Set<string>>(new Set());
|
const fetchedChannels = useRef<Set<string>>(new Set());
|
||||||
@@ -427,6 +497,14 @@ export function App() {
|
|||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update URL hash (replaceState doesn't add to history)
|
||||||
|
if (activeConversation) {
|
||||||
|
const newHash = getConversationHash(activeConversation);
|
||||||
|
if (newHash !== window.location.hash) {
|
||||||
|
window.history.replaceState(null, '', newHash);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, [activeConversation]);
|
}, [activeConversation]);
|
||||||
|
|
||||||
// Fetch messages when conversation changes
|
// Fetch messages when conversation changes
|
||||||
|
|||||||
+6
-6
@@ -22,7 +22,7 @@ class TestHealthEndpoint:
|
|||||||
from app.main import app
|
from app.main import app
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.get("/health")
|
response = client.get("/api/health")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -40,7 +40,7 @@ class TestHealthEndpoint:
|
|||||||
from app.main import app
|
from app.main import app
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.get("/health")
|
response = client.get("/api/health")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -63,7 +63,7 @@ class TestMessagesEndpoint:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/messages/direct",
|
"/api/messages/direct",
|
||||||
json={"destination": "abc123", "text": "Hello"}
|
json={"destination": "abc123", "text": "Hello"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class TestMessagesEndpoint:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/messages/channel",
|
"/api/messages/channel",
|
||||||
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"}
|
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class TestMessagesEndpoint:
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/messages/direct",
|
"/api/messages/direct",
|
||||||
json={"destination": "nonexistent", "text": "Hello"}
|
json={"destination": "nonexistent", "text": "Hello"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ class TestPacketsEndpoint:
|
|||||||
from app.main import app
|
from app.main import app
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
response = client.get("/packets/undecrypted/count")
|
response = client.get("/api/packets/undecrypted/count")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json()["count"] == 42
|
assert response.json()["count"] == 42
|
||||||
|
|||||||
Reference in New Issue
Block a user