Skip to content

Logging system rebuild, profile management, and connection improvements - #2333

Merged
hiddify-com merged 90 commits into
hiddify:mainfrom
veto9292:refactor/logging-system
Oct 6, 2026
Merged

hiddify-com merged 90 commits into
hiddify:mainfrom
veto9292:refactor/logging-system

Conversation

@veto9292

@veto9292 veto9292 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #2325 — this branch contains all of its commits plus the logging
system rebuild, so the two are reviewed and merged together.

Logging system rebuild

Logging kept three separate records of what the app did: the engine held one in
memory, the logs page held another, and a file held a third, each with different
contents and different rules. None held the whole picture, and the page could
only ever show the engine's half.

It ran on loggy, which was suspected of freezing the app during heavy
logging. Measurement cleared loggy — the real cost was print() at 82-133ms per
2000 lines against 11-20ms for everything else — but it was replaced with
package:logging regardless: maintained by the Dart team, a Flutter Favorite,
and it separates producing a record from deciding where it goes.

Before After
Three separate records, none complete One stream, four independent listeners
Page showed engine logs only, starting empty each time Page shows every source, with history already there
No search or filter Filter by source, severity and text
Two share buttons, desktop only One sheet, every platform
Two logging controls in Settings that could contradict One control, on the logs page
Log file wiped at every launch Kept across runs, errors only
Two vocabularies for the same levels One: trace, debug, info, warn, error

Bugs fixed

Cost us crash evidence:

  • Crash reports arrived as one line with no stack — assigning FlutterError.onError
    replaced presentError, discarding Flutter's own report
  • The log file was erased at every launch, losing exactly what preceded a crash
  • Engine logs stopped permanently after any stream error: the re-arm sat behind
    if (!isSingleChannel()), which is false on desktop, and listenSingle drops a
    subscription for good when its stream errors
  • Trimming the log file wrote blocks of NUL bytes into it — IOSink.close() is
    asynchronous and the trim did not await it
  • Connection failures were logged at info, so they never reached the file

Wrong or misleading:

  • Saving a shared file reported success without writing anything: on desktop
    FilePicker.saveFile only returns a path, and its bytes argument throws on macOS
  • The file offered as a "hang report" was a startup snapshot; the real crash file
    is what the Go runtime writes via debug.SetCrashOutput
  • The engine log pointed at an always-empty path; the real file is data/box.log
  • Debug mode and log level could disagree — the core already derives one from the
    other in buildconfighelper.go
  • Phones could not share logs at all, and TRACE fell through to the INFO default
  • No text field in a debug build accepted keyboard input: enableFlutterDriverExtension
    installs a TestTextInput that takes over the platform text channel. It is now
    behind --dart-define=driver=true

Notes

  • Deleted: log_parser, log_entity, log_repository, log_failure,
    logger_controller, analytics_logger, the engine's private 300-message buffer,
    and the Debug mode switch
  • Nothing constructs a Logger directly; the mixins and factories in
    custom_loggers.dart are the only way to get one
  • All 11 locales updated
  • The Core crash entry stays hidden until hiddify-core is rebuilt: the
    crash-report mechanism is newer than the currently released binary

Also included, from #2325

Profile management, sorting and update-status improvements, localization,
connection haptics, native iOS swipe-back, and the hiddify-core bump to db74dfc.

CI

The three failing jobs fail identically on #2325 and are unrelated to this work:

  • Windows — MSIX packaging needs windows/sign.pfx, written from
    secrets.WINDOWS_SIGNING_KEY, which GitHub does not expose to fork pull
    requests. The .exe itself builds
  • Android (apk, aab) — Kotlin fails against the prebuilt core AAR that
    make android-libs downloads from the hiddify-core releases. The Kotlin glue
    expects androidPackageName, closeNeighborMonitor and dnsServerAddress.value,
    which that published build predates

Linux, macOS and the test job pass.

veto9292 and others added 30 commits July 6, 2026 12:30
Update the hiddify-core submodule to db74dfc (main HEAD), which moves
hiddify-sing-box onto the testing-merge-base line (AWG/amnezia option
API, public RedirectStderr, removed broken client submodules).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Return a CupertinoPage from customTransition() on iOS so pushed pages get
the native interactive edge-swipe (pop) gesture, which CustomTransitionPage
does not support. Other platforms keep the existing slide/fade transitions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the success heavy-impact from the Connected() transition to the
moment the tunnel has a valid url-test ping, so it fires when the
connection actually works. Add a medium then heavy buzz when a live
connection times out, and a heavy buzz when it recovers.

Extract the delay-validity threshold into ConnectionConst.maxDelay /
isValidDelay as the single source of truth, and route the connection
button, system tray, proxy tile, and delay indicator through it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the leading action icon with a live status: a spinning indicator while updating, then a transient success/failure icon (cross-faded). The result is held for 2s inside UpdateProfileNotifier so it survives list rebuilds/reordering. Also tidy ProfileTile (extract body/title/tap helpers, drop commented-out code and the unused color param) and remove the never-used ProfileTileMain widget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route bulk updates through UpdateProfileNotifier.updateProfile so each tile shows its status, and report one aggregate toast (N success, M failed with names) instead of one per profile. Single user-initiated updates no longer toast on success. Toast titles now wrap to multiple lines and error toasts stay longer to fit the failed names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Key profile tiles by id so their update indicator and animation aren't lost when the list reorders (it sorts by last-update, so an updated profile jumps position).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix swapped sort labels, replace the ambiguous up/down arrow with explicit direction labels (A -> Z / Z -> A, Newest/Oldest first) shown as a subtitle, and default to the natural direction per sort type. Persist the sort choice via a new general preference so it survives app restarts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add translations for bulkSuccess, bulkPartial and the sort direction labels (newestFirst/oldestFirst) across all locales, matching the keys added to en.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove pages.profiles.msg.update.successNamed (single updates no longer toast on success) and components.subscriptionInfo.{remainingTime, remainingDurationNew, remainingUsage, profileSite, profileSupport} (only used by the deleted New*SubscriptionInfo widgets) from en and all locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
English base-locale strings for the profile pin/unpin action. Kept as its own commit (source locale); other languages follow separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a pinned column (DB schema v7 + migration; drift steps/schema/test helpers regenerated) plus a README documenting the migration workflow. The profiles list now orders active -> pinned -> sorted; Pin/Unpin is the first profile-menu item and pinned list tiles show a pin badge. Pin state persists and a subscription refresh no longer clobbers it (toUpdateEntry omits pinned).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Translate common.pin / common.unpin into all non-English locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the aider-based translation helper and migrate its workflow into
Claude Code project instructions.

- delete .aider.conf.yml and SLANG.md
- remove aider history/cache entries from .gitignore
- add CLAUDE.md documenting the slang translation workflow
- git-ignore slang analyze reports (_missing/_unused_translations*.json)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `?url=` deep-link form documented as
`hiddify://install-config?url=<uri>#<name>` never picked up the name:
the parser only read a `name=` query parameter and ignored the fragment.
Prefer an explicit `name=` and fall back to the fragment.

Adds tests covering both deep-link forms and the other panel schemes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a `showLeading` flag to LanSharingPreferenceWidget and turns it off
in the quick settings modal, where the leading icon is redundant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Switches the profiles modal from a Column to a Stack so the buttons float
above the list instead of taking their own row. Bottom list padding is
increased to keep the last profile reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Strings for the profile web page / support chips and for the warning shown
when a subscription-provided link is not in the trusted list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`profile-web-page-url` and `support-url` were stored on SubscriptionInfo,
so they were only kept when the `subscription-userinfo` header was also
present. They are independent headers, so they move onto RemoteProfileEntity
and are now parsed unconditionally.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scammers publish look-alike channels and sites (e.g. t.me/Hiddiify vs the
real t.me/hiddify) inside fake subscriptions to collect donations meant for
real providers. This adds the list used to tell a verified provider link from
an unverified one.

Matching is exact on the whole url, after normalising the scheme, a leading
`www.`, host case, a trailing slash and any fragment — so a look-alike handle
never matches. The list is managed by the project manager and holds Hiddify
only for now; there is no public request flow to be added to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reworks the unverified-link dialog so it informs rather than alarms: an
unverified link is not necessarily malicious, so it drops the icon and the
error styling and states plainly that the link cannot be verified.

The Open button stays disabled for 10 seconds with a visible countdown, the
same friction used for sensitive permissions, so the note is actually read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renders the provider's web page and support links as a single chip below the
main profile card. With both present the chip cycles every 10s: it slides up
behind the card, then the next one slides down into place. The card's bottom
corners square off so the chip reads as attached to it.

A tapped link is checked against the trusted list first: a trusted link opens
after a plain confirmation, anything else goes through the countdown warning.

Hidden behind ProfileTileConst.showProviderLinks while the design is decided.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header was split on `;` and every segment was assumed to be a
`key=value` pair, reading index 1 unconditionally. A trailing `;`, a stray
`;;` or any malformed token therefore threw — and because parse() is wrapped
in a single try/catch, the whole profile failed to load: no name, no traffic,
nothing. A trailing semicolon is common in the wild.

Segments without a `key=value` pair are now skipped instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The interval was read with int.parse, so a decimal or any non-numeric value
threw and — like the subscription-userinfo case — failed the entire profile
parse rather than just the auto-update setting.

The convention is a positive integer number of hours, so anything else
(garbage, a decimal, zero or negative) is now ignored and the profile keeps
its default update behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps in the name hierarchy:

content-disposition was matched with `filename="..."` only, so the RFC 5987
extended form `filename*=UTF-8''<percent-encoded>` — how servers send a
non-ASCII name — was skipped and the provider's real name was lost. The
extended form is now preferred and decoded, falling back to the quoted form.

The steps also mixed `isEmpty` with `isBlank`: a whitespace-only result was
treated as a name by the later steps but as blank by the final fallback, so a
real url fragment or filename could be dropped in favour of "Remote Profile".
Every step now treats whitespace as blank.

Also documents that the url-based steps are remote-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eader

Replaces the `enable-fragment` boolean with `fragment`, which carries the same
value as a proxy link's own `fragment` parameter, so a provider can set the
size and sleep instead of only switching fragmentation on.

The value is read exactly like a proxy link: a full `size,sleep,method`
triplet, or `tlshello,size,sleep`. Anything shorter, or a size or sleep that
isn't a valid range, is incomplete and leaves fragmentation off rather than
enabling it with half the values.

The user's own setting still wins, and enables fragmentation with the size and
sleep from their settings — it carries no values of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warp and psiphon are mutually exclusive — the core runs one chain mode at a
time — so the two boolean headers become a single `extra-security` header
holding the mode. UserOverride follows, replacing `enableWarp`/`enablePsiphon`
with `extraSecurity`, bumped to v2 with a read-time migration; psiphon wins
when both were set, matching v1 where it overrode warp.

The value is matched strictly against ChainMode: it must equal a documented
mode key exactly or it is ignored, so no case folding and no lists. `profile`
is never accepted from a subscription, since chaining to another profile is the
user's choice. ChainMode and ChainStatus also replace the hardcoded strings.

The raw header is consumed rather than passed through — `extra-security` is on
the output allow-list, so an unrecognised value would otherwise reach the built
config as a string where the core expects an object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tile no longer shows or edits a password, so the title itself has to
tell the user the shared link is protected.

Drop the two keys the old password UI needed:
- lanSharingPassword (input dialog title)
- lanSharingPasswordNotSet (subtitle placeholder)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirror the English change in the other ten locales: lanSharingPassword
and lanSharingPasswordNotSet no longer exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The user had to invent and maintain the LAN sharing password, and an
empty one silently shared the VPN with no authentication at all.

Generate a 16-character password with Random.secure the first time
sharing is turned on, store it, and reuse it from then on. Sharing that
was already enabled without a password gets one as soon as the tile is
shown. Visually ambiguous characters are left out of the alphabet so the
password survives being typed by hand from the QR code.

With the password out of the user's hands, the edit dialog, the subtitle
and the no-password link branch are gone, and the tile is a plain
SwitchListTile again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The protobuf/gRPC output under lib/hiddifycore/generated/ is produced by
protoc and json_editor.dart is a vendored copy of json_editor_flutter.
Neither is ever hand-edited, yet together they accounted for 148 of the
243 analyzer diagnostics, burying the findings in our own code.

Compilation still covers both; only the analyzer stops reporting them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
veto9292 and others added 22 commits August 24, 2026 11:32
FlutterError.onError defaults to presentError, which prints the full formatted
report. Assigning a handler replaced it, so an error that used to arrive as a
complete report with the widget context became a single terse line — which is
why crashes have been hard to read.

  - presentError is called again in debug builds
  - details.library and details.context are now logged; exceptionAsString()
    leaves both out, and they are the parts that say which subsystem failed and
    what it was doing

Also completes log_level_compat with trace() and fatal(), so all six levels the
engine can produce have a short name, not just the four loggy happened to have.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every log line that carries a stack now ends with the method, file and line it
came from, taken from the first frame belonging to this package:

  could not save the profile  <-  ProfileRepository.save (profile_repository.dart:88)

It is appended to the message rather than kept in a separate field, because the
message is the one thing DevTools, the logs page and app.log all display.

error() and fatal() capture StackTrace.current when the caller does not pass
one, so a bare loggy.error('...') still knows its origin. Errors are rare
enough to afford the stack walk.

The stack handed to dev.log is trimmed to this package's frames as well. A raw
trace is about twenty frames, nearly all of them inside Flutter, and the
console printed every one — three lines instead of twenty-two, and the ones
that survive are still clickable in the terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The step that moves every app log at once. PresLogger, AppLogger and
InfraLogger now hand out a package:logging Logger.

The getter keeps the name `loggy`, and log_level_compat is re-exported from
this file, so the ~200 classes that write logs are untouched. Only five call
sites needed editing — the ones that passed loggy's own LogLevel as an
argument, which is a type the extension cannot alias away.

Logger names gain a layer prefix: ui., app. and infra. That is what will let
the logs page filter a whole layer, and taking the rest from $runtimeType means
a renamed class cannot leave a stale name behind.

Note app.log receives nothing until the next step. loggy's file printer still
owns the file but no longer has any writers, and the replacement file sink is
not attached yet — two IOSinks on one path would truncate each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing outside custom_loggers.dart builds a Logger any more. A name without a
category would quietly break the logs page filter, so the only way to get one
is through a mixin or a factory, and both stamp the prefix.

Classes use a mixin. Static and top level code cannot — a mixin needs an
instance — so it uses the matching factory. UriUtils, the FFI interface and the
preferences provider were all constructing loggers by hand; they now go through
infraLogger() and appLogger().

Adds CoreLogger, so the engine bridge is a category like the rest rather than a
bare name, and boot., which separates startup from the app layer. Startup logs
are what you read first when the app fails to start, and they were mixed in
with everything else.

Drops LoggerMixin. It declared "this class has a loggy" for ExceptionHandler,
which never used it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every LogMessage from the engine already carries a type — CORE, SERVICE or
CONFIG — and hiddify dropped it, so a failed profile import and a thousand
connection lines landed in the same pile.

  core.core     the proxy engine: connections, routing, dns
  core.service  the background tunnel: start, stop, permissions
  core.config   reading and validating a profile

Nothing new is transmitted; the field was already on the wire. When a profile
fails to import you can now ask for core.config alone.

Also renames the boot logger to boot.bootstrap, after the file it reports on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last step of the switch. Sentry becomes a listener like every other sink,
the file sink is attached, and loggy's plumbing is deleted.

  sinks/sentry_sink.dart  replaces analytics_logger.dart. Same rule as before —
                          info and up becomes a breadcrumb, severe and up
                          becomes a reported event — but as a subscription the
                          analytics toggle attaches and cancels, rather than an
                          entry in a printer map. Levels map onto Sentry's five
                          properly now: SHOUT is fatal instead of collapsing
                          into error.

  bootstrap.dart          the three LoggerController calls become logStart,
                          logAddFile and logFinish. This reattaches app.log,
                          which had no writer since the mixins switched.

Deleted: logger_controller.dart, custom_logger.dart, analytics_logger.dart, and
the dependency itself. Nothing imports loggy any more.

Whether to keep the log file is unchanged in effect but clearer to read: worth
its cost on desktop or while debugging, pure waste on a phone in release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A phone in release keeps no app.log — writing one continuously costs storage
nobody reads — so the share menu was hidden there entirely and the user had no
way to send app logs at all.

The history is in memory anyway. Sharing now writes the ring out to
app-logs.txt at the moment the user asks, so the file exists exactly when it is
needed and never otherwise. Continuous cost stays zero, and the menu is useful
on every platform except web.

The export uses its own file name rather than app.log, which the file sink is
still writing to on desktop.

Engine logs were never affected: box.log is written by the engine itself on
every platform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects the first real run exposed, neither visible to the analyzer.

The origin pointed at the logger itself. error() captures StackTrace.current
inside the extension, so frame zero was log_level_compat.dart rather than
whoever called it. Frames from core/logger are skipped when picking the origin
now, and the '.<anonymous closure>' suffix is dropped — it said nothing the
file and line did not already say, and splitting it on whitespace produced
'closure>'.

  before  status  <-  LogLevelCompat.error (log_level_compat.dart:29)
  after   status  <-  HiddifyCoreService.startListeningStatus (hiddify_core_service.dart:442)

A terminal showed no logs at all. dev.log writes to the VM service, never to
stdout — verified with a probe: print and debugPrint appear there, dev.log does
not. Printing every record as well would fix the terminal but show everything
twice in an IDE console, so stdout gets one notice at startup naming the three
places records do appear.

CI is unaffected: build failures come from the compiler and the flutter tool,
never through the app's logger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…'s real one

Three changes to what lands on disk.

The file is no longer emptied at launch. A crash leaves its evidence in the
previous run's lines, and wiping on startup threw exactly that away. Growth is
bounded instead: past 5 MB the older half is dropped and the newer half kept,
checked every 500 lines rather than on every write. Each run writes a marker
line, since one file now spans several.

Engine lines are filtered to warnings and above. The engine keeps its own
complete log, so repeating its routine chatter here only flooded the file and
ate the cap — while an engine failure still sits next to the app line that led
to it.

coreFile() pointed at <workingDir>/box.log, which nothing writes. The engine
logs into <workingDir>/data/box.log — so "share core logs" has been handing
over an empty file. Adds coreHangFile() for the goroutine dump the engine
writes beside it, which is what a hang needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
enableFlutterDriverExtension registers a TestTextInput, which takes over
the text input channel. Every debug build called it, so no text field in
the app could accept a real keyboard — the search box on the logs page
would focus and show a caret, and then swallow every keystroke.

It now runs only when asked for, so a normal debug build types again and
the driver is still one flag away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the strings the redesigned page needs: the three filter chips, the
share sheet's sources, the export action and its match count, and the
buffer chip in the app bar.

Drops shareCoreLogs and shareAppLogs, since sharing is no longer two
buttons, and the Settings strings for debug mode and log level, since
neither setting lives there any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same 17 strings in the other ten locales, written from the English
rather than from each other, and anchored on the words each language
already uses for core, app and log so the page reads like the rest of
the app.

Drops the two old share buttons and the Settings strings for debug mode
and log level, which no longer exist in the base locale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
package:logging spells its levels FINEST, FINE, INFO, WARNING, SEVERE.
The engine says trace, debug, info, warn, error. Both names appeared on
the same page, next to each other, for the same idea.

The engine's win, since those are the ones the user picks from, and the
log file now writes them too. uiLevels is the subset a filter can offer:
FINER and CONFIG are never produced, and SHOUT never needs its own entry
because the filter is a floor and error already covers it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two controls produced one outcome. The app had a debug mode switch it
passed to the core at startup, and the core turns the same flag on by
itself whenever the level is trace or debug:

    static.debug = static.debug || static.logLevel <= LogLevel_DEBUG

So the two could disagree — switch off, level debug, core in debug, app
believing otherwise. The switch is gone and the app reads the level with
the same rule the core uses, which is what isVerbose says.

The buffer size preference lands here too. It answers the same question
as the level does: how much gets recorded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page filtered by five prefixes: core, app, ui, infra, boot. Two of
them told the reader nothing — a logger named HomePage already says it
is a widget — so ui is gone and PresLogger writes app like AppLogger.

The remaining three become a type rather than a prefix string, and app
is defined as whatever boot and core do not claim, so loggers from
packages that never heard of our naming still land somewhere.

The ring also returns oldest first now, which is how a log reads on the
page, in the clipboard and in a file, and drops the per-category counts
nothing was reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file exists so a crash leaves something behind. Everything quieter
than an error is already somewhere better: the page reads the in-memory
ring, and the core keeps its own complete log in data/box.log.

Narrowing it to errors removed three mechanisms that only existed to
make a busy file affordable — the write batching, its timer, and the
size cap with the trim that enforced it. Errors are rare, so each one is
written straight through and is on disk before the crash that follows
it. The file grows by the handful, so it no longer needs bounding.

Four tests cover what the sink now promises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The file used to hold everything the app recorded. It now holds errors
from the app and the core alike, so "App log file" named the wrong two
things: the wrong source, and the wrong amount.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four pieces did one job. The engine kept its own buffer of the last 300
messages and re-emitted the whole list on every line, a parser turned
those messages into an entity, a repository wrapped the parser, and the
page watched the repository.

The engine already writes each message straight into the shared ring:

    coreLogFor(event.type).log(getLogLevel(event.level), event.message)

so the buffer, the parser, the entity and its failure type are gone. The
repository's last job — making sure the file exists — moved into the
sink that opens it, which leaves nothing for the repository to hold.

The path resolver now points at the engine's real crash file rather than
the goroutine dump it writes at every start. That dump is not a crash
report: it exists on a healthy machine and says nothing about a failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page used to show only what the engine said, from a buffer that
started empty every time it opened. It now reads the shared ring, so it
holds every category and already has history the moment it opens.

The list reads like a terminal: oldest at the top, newest at the bottom,
and the view follows the newest line until you scroll away from it. A
long press or a right click copies one line.

Three chips and a search field sit under the list, next to the newest
line rather than the oldest. Two of them hide rows; the third is the
core's log level, which changes what gets recorded, so a toast says it
lands on the next connection. Below 700px the search takes its own row.

Sharing became a choice rather than two buttons: the visible history,
which can be filtered again before it leaves, or one of the files, each
listed only when it exists and has something in it. A history too long
for the clipboard falls back to a file.

Both sheets go through BottomSheetsNotifier, so they get the app's own
width, corners and route rather than a bare showModalBottomSheet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every status went through one info call, success and failure alike. A
failure carries the core's own panic text, which is the last thing that
should need hunting for among the ordinary comings and goings — and the
app log file now keeps errors only, so at info it was not kept at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The confirmation parsed the deep link itself, whose host is always the
literal "import", so every link claimed to come from the same place. It
now unwraps the link to the subscription URL first and reports that host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@veto9292
veto9292 force-pushed the refactor/logging-system branch from ebfe779 to 038bde8 Compare September 19, 2026 16:26
veto9292 and others added 4 commits September 20, 2026 22:10
The picker allowed .txt and .json only, so a Clash .yaml, a WireGuard
.conf or an .ovpn had to be opened and pasted by hand. It now takes
.yaml, .yml, .conf, .ovpn and .mobileconfig as well.

Widening it exposed two ways the read could fail. utf8.decode is strict,
so one byte that is not valid UTF-8 threw and took the whole import with
it; it now runs with allowMalformed, and a UTF-8 byte order mark is
skipped rather than left in front of the first character, where it
breaks JSON parsing. The path was also dereferenced with a bang, which
the web build never has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PopScope that sent back to home lived inside the settings branch. Branches
stay mounted after the user leaves them, so it kept answering presses meant for
other branches and the app could no longer be closed with back at all. Reported
upstream as flutter/flutter#193098.

Move the handling to the shell, where exactly one instance exists and it always
belongs to the branch on screen. The NotificationListener beside it keeps the
platform told that Flutter handles back; without it Android stops believing that
once a branch pops a page, and closes the app without calling Flutter.

Verified on an Android 16 emulator with both the back button and the edge swipe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A .mobileconfig holds an Apple IKEv2 profile. Hiddify is a sing-box
client and cannot read one, so offering it in the picker only ever led
to a failed import, with a message that reads like a broken file rather
than an unsupported format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The portable package is repacked with tar -a, which picks the archive
format from the file extension. The recipe runs in Git's sh, where tar
is GNU tar. GNU tar does not know .zip, so it quietly wrote an
uncompressed tar under a .zip name; v4.0.4 and v4.1.1 both shipped that
way. Windows' own zip support shows the file as empty and Expand-Archive
rejects it, so only tools that read the content, such as 7-Zip, could
open it.

Call Windows' own tar.exe, which is bsdtar and writes a real, compressed
zip for -a. Checked by running the recipe with a stand-in fastforge: the
result opens in Explorer and in Expand-Archive, with the Hiddify folder
intact.

Reported in hiddify#2348.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@veto9292
veto9292 force-pushed the refactor/logging-system branch from 6515524 to 8aaeb36 Compare October 1, 2026 09:12
@hiddify-com
hiddify-com merged commit 8aaeb36 into hiddify:main Oct 6, 2026
12 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants