- si_send_data_batch: неправильный SQL SELECT с несуществующей колонкой author
вместо node_id → ломал всю передачу данных в REFINE/SEND_DATA
- Добавлена проверка si_prep и возврат int с кодом ошибки
- Все вызовы si_send_data_batch проверяют возврат, при ошибке сброс sync_state
- db_handle_push: добавлен db_cascade_from после вставки (раньше chain_hash
не пересчитывался после PUSH)
- db_handle_refine: DEBUG_WARN при scnt==0 (fm за пределами диапазона)
- Добавлена публичная db_sync_chain_verify() для верификации цепочек
- test_db_sync: фаза 4 (divergence через разныe hashes + REFINE/SEND_DATA),
фаза 5 (PUSH без хвоста + cascade verification)
- Убран мёртвый код db_sync_on_new_conn
- chatgui: адаптивная раскраска пузырей исходящих сообщений
- AnimTimer::shutdown(): stop QTimer, clear m_icons before icon destruction
- ~MainWindow: AnimTimer::shutdown() -> cleanupAnimations() (correct order)
- main: replace ExitProcess with TerminateProcess(GetCurrentProcess,0)
TerminateProcess skips DLL_PROCESS_DETACH, avoiding libwinpthread
pthread_join hang on rlottie/miniaudio threads.
rlottie C API has no global shutdown - internal RleTaskScheduler threads
cannot be stopped through public API. Same for miniaudio WASAPI threads.
All owned resources already freed before TerminateProcess.
Root cause: librlottie RleTaskScheduler creates 32 worker threads on first
lottie_animation_from_data call. These pthreads wait in WaitForMultipleObjects(INFINITE)
and never exit. On MinGW, exit()/pthread_join blocks forever.
Changes:
- messagelist: s_icons moved to file-static; cleanupAnimations() clears them,
calling lottie_animation_destroy BEFORE static destruction
- mainwindow: ~MainWindow calls MessageList::cleanupAnimations() after node stop
- main: ShutdownSentinel counts threads at exit; if >4 remaining, calls
ExitProcess(0) to bypass CRT pthread_join hang
- main: if rlottie properly shuts down (<=4 threads), normal return (no ExitProcess)
Three diagnostic points covering the gap between ~MainWindow done and CRT exit:
- DbManager: destroyed — confirms local dtors started after ~MainWindow
- SoundManager: static destructor entered — confirms static destruction phase
- main: static destructors done (g_shutdown_sentinel dtor) — confirms ALL statics done
If process hangs, we know exactly which phase failed.
Signal handler via g_shutdown + uasync_wakeup ensures clean uasync poll exit.
updateReadPosition() now saves both lastReadMsgId and lastPosMsgId on every
scroll stop (300ms debounce), not just when read position changes.
connect(stopped, quit) triggers graceful Qt shutdown after uasync cleanup.
QString::fromUtf8(ch_id) read past length-prefixed string, capturing garbage.
Use fromUtf8(ch_id, ch_id_len) — length already available from bridge but was unused.
Messages now appear on refresh and unread badge correctly matches channel.
Replace gui_bridge_post_uasync_fn (async queue lost on shutdown) with direct
chat_core_save_ui_state (sync SQLite write). SQLite WAL handles concurrent access.
Add DEBUG_DEBUG traces in closeEvent, ~MainWindow, aboutToQuit.
Add author_node_id:8 to GUI_EVT_MSG_RECEIVED data format.
Callback now includes uint64_t author parameter.
SoundManager::play('msg_incoming') only when author != myNodeId.
- Same-channel refresh: save scroll pos in memory, restore from memory (no DB race)
- Force m_readDebounce->start() after loadChannel (scrollToBottom may not trigger valueChanged)
- DEBUG_DEBUG logs in updateReadPosition, saveChannelState, onMessageReceived, setChannelUnreadCount
- Debounce timer (300ms) advances lastReadMsgId only when message is fully in viewport
- Unread badge no longer resets on channel select — only when actual scroll reads msgs
- saveChannelState() uses tracked m_lastReadMsgId instead of viewport bottom
- getChannels() reads lastReadMsgId from ui_state (was reading unused channels column)
Store read/scroll positions per channel via ui_state table (key-value).
Draw red +N badge on channel list items for unread messages.
Fix getMessages() to fetch last N messages (was fetching first N).
chat_channel.c: new chat_core_create_channel_auto(name) — generates
X25519/Ed25519 keys, random channel_id, Ed25519 signature, creates channel.
chat_core_create_channel_auto_trampoline for gui_bridge_post_uasync_fn.
mainwindow.cpp: onCreateGroupRequested() reduced from 100 to 15 lines.
Removed openssl/evp.h and openssl/rand.h includes from UI code.
- selectionCommand: remove Current flag to avoid redundant
currentChanged emission during mouse press
- loadChannels: wrap clear+repopulate in blockSignals(true/false)
to prevent selectionChanged and currentChanged from firing
on intermediate model states
- selectChannel: use explicit selectionModel()->select() with
ClearAndSelect + setCurrentIndex with NoUpdate
- simplified selection restore: only track selectedCid by ID,
removed fallback-to-row logic
- channellist.cpp: selectionChanged guard now checks currentIndex().row()
is within model->rowCount() to avoid re-selecting stale index after
model clear, which caused both channels to stay blue when switching.
- chat_sync.c: cs_refresh_channels now posts CHANNEL_PEERS_ONLINE
for all channels after loading, fixing online count showing only
on the last connected group.
- chat_sync.c: after invite trampoline, check if connection to invite
node is already UP and trigger cs_on_conn_up to send CHANNEL_INFO_REQ.
Fixes invite not completing when connection is reused (Fix 1 side effect).
- mainwindow.cpp: use name.toUtf8().constData() instead of qPrintable()
to preserve UTF-8 encoding on Windows (Russian channel names).
- Remove is_dm column from channels table (dm detection by member count)
- Rename member name column to userinfo — extensible json_flat format
- Add json_flat.c/h library for compact JSON serialization
- Fix join_sig message format: use ch_x25519+ch_ed25519 instead of channel_id string
- Remove name from join_msg body (now in userinfo via json_flat)
- Update chat_core, chat_sync, member_sync, db_manager to new format
- Add topo_strategy.txt — архитектурные заметки по топологии
- Sound files loaded from filesystem (sound_dir in INI config, absolute or relative)
- Per-event enable/disable + volume stored in DB (ui_state table)
- SoundManager: setEvent(), effective volume = master * per-event
- SoundSettingsPage UI: 4 event rows (checkbox + slider)
- chat_core_save_ui_state() for async DB writes via uasync thread
- DbManager::getUiStateInt() for reading settings from GUI thread
- Default sounds copied to sounds/ dir next to DB, config entries written
- showJoinDialog: don't call loadChannels after dialog close — channel already in model via CHANNEL_UPDATED event, online badge via CHANNEL_PEERS_ONLINE. Duplicate loadChannels was resetting the peer online count.
- onConnectResult: remove 800ms QTimer — dialog closes immediately. Channel data arrives independently through the event pipeline.
- Замена контекстного меню 'Пригласить' на кнопку + (18x18) справа снизу каждого канала с hover-эффектом
- InviteDialog: авто-копирование ссылки в буфер (Clipboard + Selection) при открытии
- JoinDialog: параметр autoJoinLink — если в буфере валидная utun:// ссылка, диалог сразу стартует подключение
- mainwindow.cpp: swap setupMessaging() before setupNode() so g_receiver is created first
- gui_bridge_impl.cpp: add DEBUG_WARN when gui_bridge_post drops event due to NULL g_receiver
- NodesPage: show full 64-bit Node ID (hex16) instead of truncated 4 hex chars
- NodesPage: show diagnostic when DB handle is NULL (was silent return)
- DatabaseSettingsPage: show 'Database not connected' when DB handle is NULL
- DatabaseSettingsPage: add refreshStatus() called on tab select
- SettingsDialog: refresh Database tab on category change