Compare commits

...

112 Commits

Author SHA1 Message Date
lollipopkit🏳️‍⚧️
2d1e0a7edd bump: v1313 2026-02-04 14:54:34 +08:00
GT610
64dc00475a chore: Update l10n (#1039) 2026-01-30 15:24:48 +08:00
lollipopkit🏳️‍⚧️
6338c6ce6b opt.: docs l10n (#1036)
* opt.: docs l10n
Fixes #1035

* opt.

* rm: redundant docs

* rm: features chapter

* opt.: docs l10n
Fixes #1035

* fix
2026-01-29 20:27:21 +08:00
GT610
9281a578e7 fix(container): Parsing results in sudo mode (#1031)
* docs(l10n): fix un-updated English translation

* feat(container): Add support for requiring a sudo password

Add support for sudo password verification for Docker container operations, including:
1. Added ContainerErrType.sudoPasswordRequired error type
2. Add password prompt text in multi-language files
3. Modify the SSH execution logic to correctly handle the input of sudo password
4. Implement password caching and verification mechanism

* feat(container): Add sudo password error handling logic

Add a new error type `sudoPasswordIncorrect` to handle situations where the sudo password is incorrect

Modify the password verification logic in the SSH client, and return a specific error code when a password error is detected

Update multilingual files to support password error prompt information

* fix(ssh): Remove unnecessary stderr parameter and improve sudo command handling

Clean up the no longer needed stderr parameter in the SSH client, which was originally used to handle sudo password prompts

Unify the sudo command construction logic, always use the _buildSudoCmd method, and add stderr redirection
Clear cached passwords when passwords are incorrect

* fix(container): Improved sudo command handling and Podman simulation detection

Fix the sudo command processing logic, remove the masking of stderr to capture password errors

Override the detection logic simulated by Podman

Refactor the command building logic to support sh wrapping of multi-line commands

* fix(container): Improve the prompt message for sudo password errors

Update the sudo password error prompt messages for all languages to more accurately reflect situations of incorrect password or lack of permission

Fix the password error detection logic for both the SSH client and container providers simultaneously

* refactor(container): Remove unused sudo and password parameters in exec method

Simplify the exec method signature by removing the sudo and password parameters that are no longer needed, as these functions are no longer in use

* feat: Add new contributors and optimize container command handling

Add two new contributors to the GithubIds list and refactor the container command processing logic:
1. Simplify the command wrapping logic and uniformly use `sh -c` for processing
2. Specific error handling when adding a sudo password incorrectly
3. Remove redundant conditional checks and temporary variables
2026-01-29 18:07:20 +08:00
lollipopkit🏳️‍⚧️
d5e1d89394 chore: rm actions 2026-01-29 14:32:19 +08:00
lollipopkit🏳️‍⚧️
71e757fe13 new: docs website (#1033)
* new: docs website
Fixes #1032

* opt.

* opt.

* opt.

* fix
2026-01-29 14:24:08 +08:00
GT610
a0a62acdbc feat(database): Add database compression function (#1027)
* feat(database): Add database compression function

Add database compression functionality to the connection statistics page to reduce the size of the database file

Add multi-language support and related UI interactions

* fix(database): Update the description of database compression operations and fix the statistics cleanup logic

Update the description of database compression operations in the multilingual files to explicitly state that data will not be lost

Fix the connection statistics cleanup logic to ensure correct matching of server IDs

Add error handling for the compression operation to prevent the UI from freezing

* Update lib/l10n/app_en.arb

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-26 13:40:42 +08:00
GT610
9ac866644c ref:Refactor Settings UI and Fix Performance Issues (#1026)
* refactor(Settings page): Simplify the click handling logic of the cancel button

* fix(backup_service): Add a cancel button in the restore backup dialog

* refactor(Settings Page): Refactor the ordered list component and optimize state management

- Extract the logic for building list items into a separate method to improve maintainability
- Add animation effects to enhance the dragging experience
- Use PageStorageKey to maintain the scroll position
- Optimize the state management logic of the checkbox
- Add new contributors in github_id.dart

* fix: Add SafeArea to the settings page to prevent content from being obscured

Add SafeArea wrapping content in multiple settings pages to prevent content from being obscured by the navigation bar on certain devices, thereby enhancing user experience

* refactor: Extract file list retrieval method and optimize asynchronous loading of iOS settings page

Extract the `_getEntities` method from an inline function to a class member method to enhance code readability

Preload watch context and push token in the iOS settings page to avoid repeatedly creating Futures

* fix: Add a `key` attribute to the ChoiceChipX component to avoid rendering issues

* refactor(Settings page): Refactor the platform-related settings logic and merge the Android settings into the main page

Migrate the Android platform settings from a standalone page to the main settings page, and remove redundant Android settings page files

Adjust the platform setting logic, retaining only the special setting entry for the iOS platform

* build: Update fl_lib dependency to v1.0.363

* feat(Settings): Add persistent disable state for cards and virtual keys

Add persistent storage functionality for server detail cards and SSH virtual key disable status

Modify the logic of relevant pages to support the saving and restoration of disabled states

* refactor(setting): Simplify save logic and optimize file sorting performance

In the settings page, remove the unnecessary `enabledList` filtering and directly save the `_order` list

Optimize the sorting logic on the local file page by first retrieving the file status before proceeding with sorting

* fix: Optimize data filtering and backup service error handling on the settings page

Fix the data filtering logic in the settings page to only process key-value pairs with specific prefixes
Add error handling to the backup service, capture and display merge failure exceptions

* fix(Settings page): Fixed the issue where disabled items were not included in the order settings and asynchronously saved preference settings

Fix the issue where disabled items in the virtual keyboard and service details order settings are not included in the order list

Change the preference setting saving method to an asynchronous operation, and add a mounted check to prevent updating the state after the component is unmounted

* refactor: Optimize the reordering logic and remove redundant sorting methods

Narrow the scope of state updates in the reordering logic to only encompass the parts where data is actually modified

Remove the unused sorting methods in `_local.dart` to simplify the code

* refactor(view): Optimize the refresh logic of the local file page

Refactor the refresh method that directly calls setState into a unified _refresh method

Use the `_entitiesFuture` to cache the list of files to obtain results and avoid redundant calculations

* Update lib/view/page/storage/local.dart

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-01-25 18:43:57 +08:00
GT610
e226fec03d fix: Update dependencies and add a cancel button to the color selection dialog (#1025)
Update the fl_lib dependency to version v1.0.362

Add a cancel button in the color selection dialog to enhance user experience
2026-01-25 00:12:22 +08:00
GT610
7d47c9d673 bump: Replace discontinued deps and update fl_lib dep (#1024)
* build: Replace flutter_markdown with flutter_markdown_plus

* build: Update fl_lib dependency to v1.0.361 and synchronize related dependencies

Update the fl_lib dependency version in the pubspec.yaml and pubspec.lock files to v1.0.361, and simultaneously update the dependency versions of camera_avfoundation and objective_c

* build: Add flutter_markdown_plus dependency

* build: update the git reference of fl_lib
2026-01-24 15:04:05 +08:00
GT610
e49b31ed25 fix: Duplicate title bars when showing system title bar (#1021)
* fix: Fix the issue with the display of the window title bar

When initializing the window, add the configuration to set the title bar display, ensuring consistency with user settings

* build: Update fl_lib to  main for changes

* build: Update fl_lib dependency to version v1.0.359
2026-01-22 21:04:46 +08:00
GT610
87d7feb823 ref(systemd): Fix safety bugs and improve performance (#1020)
* fix(systemd): Fix the issue of special characters in unit names

In systemd unit processing, filtering of special characters in unit names has been added to prevent command injection and security issues. Additionally, the rendering performance of the unit list has been optimized by merging unnecessary watch calls and removing confirmation dialogs to simplify the operation process.

* feat(Systemd): Add a confirmation dialog for systemd unit operations

Display a confirmation dialog when stopping or restarting systemd units to prevent accidental operations. For other operations, directly navigate to the SSH page to execute commands.

* fix(systemd): Fix the range of characters allowed in unit names

Extend the regular expression to allow more valid characters, including dots, @, and colons, in system unit names, to support a broader range of unit naming conventions

* fix(systemd): Fix the issue of parsing service names with dots

When dealing with service IDs containing multiple dots (such as org.cups.cupsd.service), correctly extract the service name and type. When there are no dots in the service ID, set the type to an empty string.
2026-01-22 17:47:06 +08:00
GT610
84a1bd5519 fix(sftp): FIx Permission Denied While Accessing SFTP (#1019)
* fix(storage): Set the initial SFTP path based on the user's role

When the user is not root, set the initial path to `/home/$user`. For root users, use `/root` as the path

* chore: Add issue participants into github_id

* fix(sftp): Improved the logic for obtaining the initial path and asynchronously processed directory listings

Attempt to obtain the user's home directory path by executing a command, and fall back to the default path if it fails. At the same time, change the _listDir call to asynchronous to avoid potential issues.

* fix(storage): Fixed the logic for determining the initial path of SFTP

Change the condition from checking if the path is not equal to '~' to checking if the path starts with '/', to ensure the correct user home directory path is obtained
2026-01-22 13:49:48 +08:00
GT610
f47d1e7141 fix(server): Add boundary check for editing host name (#1018)
* feat(localization): Add validation prompt for invalid host formats

Add validation for host formats, allowing only IPv4, IPv6, and domain name formats

Add regular expression validation for host format on the server editing page

Update multilingual files to add the invalidHostFormat field

* chore: Update dependent package versions to the latest

* fix(server edit): Update the hostname regular expression to support IPv6 zone identifiers

Modify the regular expression for hostname validation to add support for IPv6 zone identifiers (such as %en0)
2026-01-22 12:03:08 +08:00
GT610
d14e97485f refactor: Optimize process page (#1017)
* refactor(process): Optimize the sorting logic of the process list and add a data integrity check flag

Add the `_checkedIncompleteData` flag to avoid repeatedly checking data integrity

Change `_sortModes` to a final variable and initialize it using `List.from`

Remove unnecessary calls to `_timer.cancel`

* refactor(process): Remove unused focus states and refactor process item operations

Move the stop operation of the process item to the tail button, and remove the focus state-related code that is no longer used

* fix(process): Fix the layout of the process page and the stop confirmation dialog box

Adjust the spacing condition between CPU and memory display, and only add spacing when both are present

Modify the confirmation dialog for stopping the process, and add a cancel button to provide a better user experience

* refactor(process): Merge duplicate sorting mode removal logic

Combine two separate `removeWhere` calls into one, simplifying the code and enhancing readability
2026-01-22 00:22:23 +08:00
GT610
347d294f6e fix: settings page and SSH virtual keys bottom overflow (#1015)
* fix: Add SafeArea to the page to prevent content from being obscured

Add the SafeArea component to multiple pages to ensure that content is not obscured by the device status bar or navigation bar, thereby enhancing the user experience

* fix(ssh page): Fix the issue of the virtual keyboard area being displayed within the security zone

Wrap the virtual keyboard area within the SafeArea to prevent it from being obscured by the system UI, and remove any unnecessary bottom padding
2026-01-21 10:56:39 +08:00
lollipopkit🏳️‍⚧️
d5e22cbc65 bump: v1297 2026-01-19 09:40:31 +08:00
GT610
7926a4d4a6 fix(ssh page): Fix the issue with calculating the height of the virtual keyboard (#1011) 2026-01-17 21:15:05 +08:00
GT610
39a3e0800b opt: Improve container parsing and error handling (#1001)
* fix(ssh): Modify the return type of execWithPwd to include the output content

Adjust the return type of the `execWithPwd` method to `(int?, String)` so that it can simultaneously return the exit code and output content

Fix the issue in ContainerNotifier where the return result of execWithPwd is not handled correctly

Ensure that server operations (shutdown/restart/suspend) are correctly pending until the command execution is completed

* refactor(container): Change single error handling to multiple error lists

Support the simultaneous display of multiple container operation errors, enhancing error handling capabilities

* fix(container): Adjust the layout width and optimize the handling of text overflow

Adjust the width calculation for the container page layout, changing from subtracting a fixed value to subtracting a smaller value to improve the layout

Add overflow ellipsis processing to the text to prevent anomalies when the text is too long

* Revert "refactor(container): Change single error handling to multiple error lists"

This reverts commit 72aaa173f5.

* feat(container): Add Podman Docker emulation detection function

Add detection for Podman Docker emulation in the container module. When detected, a prompt message will be displayed and users will be advised to switch to Podman settings.

Updated the multilingual translation files to support the new features.

* fix: Fix error handling in SSH client and container operations

Fix the issue where the SSH client does not handle stderr when executing commands

Error handling for an empty client in the container addition operation

Fix the issue where null may be returned during server page operations

* fix(container): Check if client is empty before running the command

When the client is null, directly return an error to avoid null pointer exception

* fix: Revert `stderr` ignore

* fix(container): Detect Podman simulation in advance and optimize error handling

Move the Podman simulation detection to the initial parsing stage to avoid redundant checks

Remove duplicated error handling code and simplify the logic

* fix(container): Fix the error handling logic during container command execution

Increase the inspection of error outputs, including handling situations such as sudo password prompts and Podman not being installed

* refactor(macOS): Remove unused path_provider_foundation plugin
2026-01-17 14:40:44 +08:00
lollipopkit🏳️‍⚧️
cd3c094af0 new: release ipa/app in Actions (#1005)
* new: release ipa/app in Actions
Fixes #770

* opt.

* opt.

* fix

* opt.
2026-01-15 12:10:03 +08:00
lollipopkit🏳️‍⚧️
8589b3b4d7 opt.: add a btn to minimize ai dialog (#1004)
* opt.: add a btn to minimize ai dialog
Fixes #1003

* opt.

* opt.
2026-01-14 15:15:33 +08:00
GT610
7693e30cbf opt: Better performance on server refreshing (#999)
* refactor(server): Replace Future.wait with an explicit list of futures to enhance readability

Refactor the nested map and async functions into explicit for loops and future lists to make the code logic clearer

* fix(server): Fixed the auto-refresh logic and concurrency control issues

- Add `_refreshCompleter` to prevent concurrent refreshes
- Fixed the issue where the status was not updated after the automatic refresh timer was canceled
- Remove the invalid check for `duration == 1`

* refactor(server): Optimize the server refresh logic by filtering out servers that do not need to be refreshed in advance

Move the server filtering logic outside the loop and use the `where` method to filter the servers that need to be refreshed, avoiding repeated condition checks within the loop. This improves code readability and reduces redundant condition checks.

* refactor: Optimize server refresh logic to enhance readability

Break down complex conditional checks into clearer steps, separating the logic for server refresh and rate limiter reset. Replace chained calls with explicit loops to make the code easier to maintain and understand.

* refactor(server): Remove `updateFuture` from `ServerState` and use the `_isRefreshing` flag instead

Simplify the server refresh logic, replace Future state tracking with a boolean flag, and avoid unnecessary state updates

* refactor(server_detail): Extract the setting items as local variables to improve performance

Extract the globally set items that are accessed repeatedly as local variables, reduce unnecessary state retrieval operations, and optimize page performance

* refactor: Rename `_displayCpuIndexSetting` to `_displayCpuIndex` for consistency

* refactor(server): Fix the issue of parallel blocking in server refresh

The original code uses Future.wait to wait for all refresh operations to complete, but in fact, there is no need to wait for the results of these operations. Instead, directly calling ignore() to ignore the results can avoid blocking caused by the slowest server

* fix: Adjust the order of logging and default value settings

Ensure to set the default value after recording the invalid duration warning

* refactor(server): Rename _refreshCompleter to _refreshInProgress to enhance readability

Change the variable name from `_refreshCompleter` to `_refreshInProgress`, so that it more accurately reflects the actual purpose of the variable, which is to indicate whether the refresh operation is in progress

* refactor(server): Remove unnecessary refresh progress status management

Simplify the server refresh logic, remove the unused _refreshInProgress state variable and related Completer handling, making the code more concise and straightforward

* chore: Update dependent package versions

Update the following dependent package versions:
- camera_web has been upgraded from 0.3.5 to 0.3.5+3
- ffi has been upgraded from 2.1.4 to 2.1.5
- hive_ce_flutter is upgraded from 2.3.3 to 2.3.4
- watcher is upgraded from 1.1.4 to 1.2.1

* opt.

---------

Co-authored-by: lollipopkit🏳️‍⚧️ <10864310+lollipopkit@users.noreply.github.com>
2026-01-14 13:47:06 +08:00
lollipopkit🏳️‍⚧️
874d28be12 bump: v1291 2026-01-14 13:32:30 +08:00
GT610
06070c29b9 fix(color-picking): Fix color picking failure and card overflow (#998) 2026-01-11 00:21:48 +08:00
GT610
bb0ada12e6 bump: Update Android build tools and Actions version (#997) 2026-01-10 20:03:37 +08:00
GT610
9ceeaf7cc4 feat(local file page): Display server names for server folders (#996)
* feat(local file page): Display server names for server folders

In the local file list, server folders will display their corresponding server names, enhancing the user experience.

* fix(storage page): Use ref.read instead of ref.watch to fetch the server list

Avoid unnecessary watch operations during construction, reducing potential performance overhead
2026-01-08 18:57:02 +08:00
GT610
29a57ad742 fix(container): Modify container execution commands to prioritize bash or ash (#995) 2026-01-08 09:11:23 +08:00
GT610
2c495a44c3 fix(log): Logging System Improvements and Error Handling Enhancements (#994)
* fix: Added logging to exception handling

Added detailed error logging to exception handling across multiple files, including exception information and stack traces, to facilitate troubleshooting.

* refactor(logging): Standardize logging output methods

Replace existing debugPrint and lprint with Loggers.app.warning to enhance logging consistency and maintainability.

* refactor: Remove redundant debug log prints

Clean up unnecessary log print statements in debug code

* feat(i18n): Added internationalization support for the logging feature
2026-01-07 15:09:22 +08:00
GT610
cc300c141a refactor(sftp): Replace hard-coded path separators with Pfs.seperator (#993)
Unify the use of Pfs.seperator for handling file path separators to enhance cross-platform compatibility.
2026-01-06 23:50:11 +08:00
GT610
26efb8e185 fix: Add input validation and bounds checking to parsing methods (#990)
* fix: Resolved boundary condition issues in string processing

Addressed null and length checks during string splitting across multiple model classes to prevent potential null pointer exceptions and array out-of-bounds errors

* fix: Throw exceptions instead of silently returning when package manager output formats are invalid

Modified the _pacman, _opkg, and _apk parsing methods to throw exceptions when input formats are invalid, rather than silently returning, to prevent potential error handling issues.
2026-01-06 23:47:49 +08:00
GT610
06ed38ff45 fix(container): Fix Podman 5.x Network Traffic Statistics Not Displaying (#991)
* fix(container): Added version parameter to accommodate Podman 5.x network statistics format

Modified the parseStats method to accept a version parameter, handling changes in Podman 5.x's network statistics data structure. When the version is 5.x, network traffic data is retrieved from the RxBytes/TxBytes fields of the Network interface.

* fix(container): Fixed Podman version detection logic to correctly retrieve network statistics

Addressed Podman version number parsing issues and improved version comparison logic to support all 4.x and below versions as well as 5.x and above versions

* fix(container): Resolved display formatting issues for network and disk I/O statistics

Handled default values when NetIO and BlockIO are null, and reformatted display strings to distinguish upstream/downstream traffic and read/write operations.

* fix: Why did I mess up the tag order?
2026-01-06 23:44:54 +08:00
GT610
7c35abe30e fix(cpu): Resolved boundary condition issues when calculating CPU utilization (#988)
Added checks for coreIdx out-of-bounds and totalDelta being zero to prevent array out-of-bounds and division-by-zero errors
2026-01-06 12:48:13 +08:00
lxdklp
78ef181d4a feat: support macOS menubar (#976)
* feat: macOS menubar

* feat: Dynamic NavigateMenuItems

* fix: simplify shortcut config

* fix: Simplify the code

* fix: More suitable tab name
2025-12-10 18:05:30 +08:00
lollipopkit🏳️‍⚧️
3f15caeaf2 new: add copy btn for ask ai (#975) 2025-12-07 17:51:07 +08:00
lollipopkit🏳️‍⚧️
6458e736fa fix: tag switcher ui (#974)
Fixes #964
2025-12-07 17:29:12 +08:00
lollipopkit🏳️‍⚧️
99fda8b747 opt. 2025-12-07 17:19:24 +08:00
lxdklp
c5cbb12ac3 feat: Automatic line wrapping of time (#973) 2025-12-07 17:14:45 +08:00
lollipopkit🏳️‍⚧️
038f0d4d77 chore: bump: v1276 2025-12-06 12:03:10 +08:00
lxdklp
141519d952 fix: SFTP err caused by known host key (#970)
Fixes #965
2025-11-25 10:35:14 +08:00
lxdklp
75d1a59e77 fix: Unable to obtain Windows server information (#963)
* fix: FormatException: Unexpected extension byte (at offset 8) error

* fix: PowerShell script error repair, Windows data parsing repair

* fix: Unable to obtain network card information

* fix: Unable to obtain system startup time

* fix conversation as resolved.
2025-11-22 19:17:40 +08:00
lollipopkit🏳️‍⚧️
ca4e65d7a5 chore: flutter 3.38 2025-11-13 15:24:22 +08:00
Korb
ffda27d057 add: fdroid Russian metadata translation (#947)
* Create ru/short_description.txt

* Create ru/full_description.txt
2025-10-23 02:24:04 +08:00
lollipopkit🏳️‍⚧️
c548b4ef48 fix: container parsing (#948) 2025-10-23 02:21:14 +08:00
lollipopkit🏳️‍⚧️
70040c5840 bump: v1270 2025-10-20 09:32:07 +08:00
lollipopkit🏳️‍⚧️
5272324be6 feat: prompt user on host key verification (#943) 2025-10-20 09:31:20 +08:00
lollipopkit🏳️‍⚧️
8cbb48ed67 feat: support windows clipboard shortcuts (#941)
Fixes #902
2025-10-20 00:56:33 +08:00
lollipopkit🏳️‍⚧️
03720fa322 Merge branch 'main' of github.com:lollipopkit/flutter_server_box 2025-10-20 00:35:07 +08:00
lollipopkit🏳️‍⚧️
0b51719070 fix: synthesize hardware backspace repeat (#940) 2025-10-20 00:34:52 +08:00
lollipopkit🏳️‍⚧️
a84231393d opt.: ask ai hint 2025-10-19 23:38:08 +08:00
lollipopkit🏳️‍⚧️
d6c2cafce7 opt.: ssh disconnection helper (#937) 2025-10-19 13:40:17 +08:00
lollipopkit🏳️‍⚧️
729b76177e feat: ask ai (#936)
* feat: ask ai in ssh terminal
Fixes #934

* new(ask_ai): settings

* fix: app hot reload

* new: l10n

* chore: deps.

* opt.
2025-10-18 01:15:43 +08:00
lollipopkit🏳️‍⚧️
860c11d4a8 bump: v1262 2025-10-10 09:18:38 +08:00
lollipopkit🏳️‍⚧️
bd949288ed fix: code editor tool bar (#933) 2025-10-10 09:14:41 +08:00
lollipopkit🏳️‍⚧️
bb3e3b4848 opt.: no Tag Switcher on desktop (#932) 2025-10-08 21:21:23 +08:00
lollipopkit🏳️‍⚧️
3307fca620 fix: cant sort servers order (#930) 2025-10-08 17:35:07 +08:00
lollipopkit🏳️‍⚧️
da8517bcf7 migrate: riverpod 3 2025-10-08 17:03:13 +08:00
lollipopkit🏳️‍⚧️
f68c4a851b feat: discover local ssh server (#921) 2025-09-19 23:29:01 +08:00
lollipopkit🏳️‍⚧️
17db393c12 bump: v1256 2025-09-15 02:35:42 +08:00
lollipopkit🏳️‍⚧️
275581cfa3 fix: notification permission (#914) 2025-09-14 22:34:01 +08:00
lollipopkit🏳️‍⚧️
d7168ea1ff fix: version code err caused by Flutter (#913) 2025-09-14 14:23:17 +08:00
lollipopkit🏳️‍⚧️
fd2bf08f78 bump: v1253 2025-09-09 13:32:17 +08:00
lollipopkit🏳️‍⚧️
98e13c39cf fix: android channel invoke 2025-09-09 13:30:37 +08:00
lollipopkit🏳️‍⚧️
e70abeef04 bump: v1251 2025-09-09 13:14:01 +08:00
lollipopkit🏳️‍⚧️
194774d6fb opt.: system detect logic to avoid creating useless file (#905) 2025-09-09 13:10:40 +08:00
lollipopkit🏳️‍⚧️
640d61bab9 fix: holding Backspace doesnt work on desktop (#903) 2025-09-08 14:06:35 +08:00
lollipopkit🏳️‍⚧️
7f4cf22cc9 fix: rm camera perm on mac 2025-09-08 12:37:30 +08:00
lollipopkit🏳️‍⚧️
05a927753f feat: stop all servers in noti center (#901) 2025-09-06 14:04:53 +08:00
lollipopkit🏳️‍⚧️
0c7b72fb2c bump: v1246 2025-09-05 12:31:33 +08:00
lollipopkit🏳️‍⚧️
a869b97502 fix: server stat l10n 2025-09-05 00:24:18 +08:00
lollipopkit🏳️‍⚧️
eadd343205 readd: home drawer
Fixes #900
2025-09-05 00:12:41 +08:00
lollipopkit🏳️‍⚧️
1bac986fe0 bug: single server providers should be keepalived (#899) 2025-09-04 23:50:00 +08:00
lollipopkit🏳️‍⚧️
a94be6c2c3 fix: macOS appstore rejection (#893) 2025-09-03 22:19:04 +08:00
lollipopkit🏳️‍⚧️
fc8e9b4bb1 bump: v1241 2025-09-03 09:24:33 +08:00
lollipopkit🏳️‍⚧️
ec4b633889 fix: watchOS app cfg (#890) 2025-09-03 01:41:08 +08:00
lollipopkit🏳️‍⚧️
e51804fa70 new: custom tabs (#889) 2025-09-03 01:05:03 +08:00
lollipopkit🏳️‍⚧️
2466341999 feat: server conn statistics (#888) 2025-09-02 19:41:56 +08:00
lollipopkit🏳️‍⚧️
929061213f refactor: docker status parsing (#886) 2025-09-02 13:22:54 +08:00
lollipopkit🏳️‍⚧️
6b52679942 fix: resolve Docker interface blank issue caused by LateInitializationError (#884) 2025-09-02 12:44:05 +08:00
lollipopkit🏳️‍⚧️
efc0315c93 new: CLAUDE.md 2025-09-01 23:32:44 +08:00
lollipopkit🏳️‍⚧️
8e4c2a7cde fix: fallback to df on incompatible system (#880) 2025-09-01 23:32:20 +08:00
lollipopkit🏳️‍⚧️
4ec7f5895e fix: imported servers from ssh config are the same (#882) 2025-09-01 23:06:58 +08:00
lollipopkit🏳️‍⚧️
ee22cdb55f fix: private key can't be selected in edit page (#879) 2025-09-01 13:05:54 +08:00
lollipopkit🏳️‍⚧️
b1b0d9a18f bump: v1231 2025-09-01 01:19:23 +08:00
lollipopkit🏳️‍⚧️
56e67f4725 fix: sync will refresh the entire app (#877) 2025-09-01 01:18:06 +08:00
lollipopkit🏳️‍⚧️
3b7fdf36fb opt. 2025-08-31 23:59:53 +08:00
lollipopkit🏳️‍⚧️
5291d316a2 fix: ensure unique IDs for bulk server import to prevent overwriting (#875) 2025-08-31 21:20:27 +08:00
lollipopkit🏳️‍⚧️
4c369546da fix: replace String.fromCharCodes with utf8.decode for proper Chinese character handling in JSON import (#874) 2025-08-31 20:06:47 +08:00
lollipopkit🏳️‍⚧️
12a243d139 feat: import servers from ~/.ssh/config (#873) 2025-08-31 19:33:29 +08:00
lollipopkit🏳️‍⚧️
a97b3cf43e opt.: bak pwd is optional (#872) 2025-08-31 11:11:47 +08:00
lollipopkit🏳️‍⚧️
53a7c0d8ff migrate: riverpod + freezed (#870) 2025-08-31 00:55:54 +08:00
lollipopkit🏳️‍⚧️
9cb705f8dd fix: parsing hostname (#865) 2025-08-22 09:18:21 +08:00
lollipopkit🏳️‍⚧️
8270674b7d chore: tests 2025-08-22 00:25:26 +08:00
lxdklp
24fd4b782d fix: GBK decoding fallback (#863)
* fix #757

* fix #757

* apply the code recommendations from sourcery ai

* Make sure raw is non-empty data

* Modified the way to judge gbk, fixed the problem that null cannot throw an error
2025-08-21 23:28:06 +08:00
lollipopkit🏳️‍⚧️
fcb3d7e2b3 bump: v1220 2025-08-18 12:21:52 +08:00
lollipopkit🏳️‍⚧️
f5634d6e88 bump: v1218 2025-08-18 12:08:29 +08:00
lollipopkit🏳️‍⚧️
5497ad83e0 opt.: bio auth settings 2025-08-17 17:56:26 +08:00
dsvf
4a7827f41a Delay bio auth (#642) 2025-08-17 14:06:24 +08:00
lollipopkit🏳️‍⚧️
60671fe461 feat: native widget url settings dialog (#856) 2025-08-16 23:07:19 +08:00
lollipopkit🏳️‍⚧️
bc1b6e5a4a feat: GitHub Gist sync (#854) 2025-08-14 23:21:33 +08:00
lollipopkit🏳️‍⚧️
1d553eccd5 rm: claude pr review 2025-08-13 23:33:03 +08:00
lollipopkit🏳️‍⚧️
68734a9e52 fix: disable command menu doesnt work (#852) 2025-08-13 23:32:22 +08:00
lollipopkit🏳️‍⚧️
ed8a1d18b9 opt.: systemd page (#851) 2025-08-13 22:16:55 +08:00
shamnad-sherief
e4a9875620 fix: Systemd shows nothing (#850) 2025-08-13 20:19:28 +08:00
lollipopkit🏳️‍⚧️
6f9aa2ece9 add: Claude Code GitHub Workflow (#849)
* "Claude PR Assistant workflow"

* "Claude Code Review workflow"
2025-08-13 15:23:30 +08:00
lollipopkit🏳️‍⚧️
13e28675af opt.: watchOS & iOS widget (#847) 2025-08-13 01:44:02 +08:00
lollipopkit🏳️‍⚧️
8c0e0f89d5 fix: term opening on Linux (#845) 2025-08-12 23:55:31 +08:00
lollipopkit🏳️‍⚧️
9b01da5a23 feat: term session mgr (#846) 2025-08-12 23:43:42 +08:00
lollipopkit🏳️‍⚧️
584af5423a bump: v1206 2025-08-09 12:45:45 +08:00
lollipopkit🏳️‍⚧️
95f8e571c1 feat: ability to disable monitoring cmds (#840) 2025-08-09 12:37:30 +08:00
lollipopkit🏳️‍⚧️
9c9648656d fix: macOS ssh term unusable (#838) 2025-08-08 18:59:25 +08:00
lollipopkit🏳️‍⚧️
6880bcc192 opt.: m3 layout breakpoints (#837) 2025-08-08 17:12:13 +08:00
lollipopkit🏳️‍⚧️
3a615449e3 feat: Windows compatibility (#836)
* feat: win compatibility

* fix

* fix: uptime parse

* opt.: linux uptime accuracy

* fix: windows temperature fetching

* opt.

* opt.: powershell exec

* refactor: address PR review feedback and improve code quality

### Major Improvements:
- **Refactored Windows status parsing**: Broke down large `_getWindowsStatus` method into 13 smaller, focused helper methods for better maintainability and readability
- **Extracted system detection logic**: Created dedicated `SystemDetector` helper class to separate OS detection concerns from ServerProvider
- **Improved concurrency handling**: Implemented proper synchronization for server updates using Future-based locks to prevent race conditions

### Bug Fixes:
- **Fixed CPU percentage parsing**: Removed incorrect '*100' multiplication in BSD CPU parsing (values were already percentages)
- **Enhanced memory parsing**: Added validation and error handling to BSD memory fallback parsing with proper logging
- **Improved uptime parsing**: Added support for multiple Windows date formats and robust error handling with validation
- **Fixed division by zero**: Added safety checks in Swap.usedPercent getter

### Code Quality Enhancements:
- **Added comprehensive documentation**: Documented Windows CPU counter limitations and approach
- **Strengthened error handling**: Added detailed logging and validation throughout parsing methods
- **Improved robustness**: Enhanced BSD CPU parsing with percentage validation and warnings
- **Better separation of concerns**: Each parsing method now has single responsibility

### Files Changed:
- `lib/data/helper/system_detector.dart` (new): System detection helper
- `lib/data/model/server/cpu.dart`: Fixed percentage parsing and added validation
- `lib/data/model/server/memory.dart`: Enhanced fallback parsing and division-by-zero protection
- `lib/data/model/server/server_status_update_req.dart`: Refactored into 13 focused parsing methods
- `lib/data/provider/server.dart`: Improved synchronization and extracted system detection

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: parse & shell fn struct

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-08 16:56:36 +08:00
451 changed files with 53560 additions and 6601 deletions

View File

@@ -16,18 +16,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: subosito/flutter-action@v2
with:
channel: 'stable' # or: 'beta', 'dev' or 'master'
channel: 'stable'
- name: Install dependencies
run: flutter pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
- name: Verify formatting
run: dart format --output=none .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
run: dart analyze

View File

@@ -1,20 +0,0 @@
name: 'issue-translator'
on:
issue_comment:
types: [created]
issues:
types: [opened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: usthe/issues-translate-action@v2.7
with:
# not require, default false.
# Decide whether to modify the issue title.
# if true, the robot account @Issues-translate-bot must have modification permissions, invite @Issues-translate-bot to your project or use your custom bot.
IS_MODIFY_TITLE: false
# not require.
# Customize the translation robot prefix message.
CUSTOM_BOT_NOTE: Bot detected the issue body's language is not English, translate it automatically. 👯👭🏻🧑‍🤝‍🧑👫🧑🏿‍🤝‍🧑🏻👩🏾‍🤝‍👨🏿👬🏿

View File

@@ -9,18 +9,24 @@ on:
permissions:
contents: write
env:
APP_NAME: ServerBox
RELEASE_TAG: ${{ github.ref_name }}
jobs:
releaseAndroid:
name: Release android
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.32.2"
flutter-version: "3.38.7"
- uses: actions/setup-java@v4
with:
distribution: "zulu"
@@ -32,50 +38,67 @@ jobs:
- name: Build
run: dart run fl_build -p android
- name: Rename for fdroid
shell: bash
run: |
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_arm.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
mv build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.apk build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
APK_DIR="build/app/outputs/flutter-apk"
shopt -s nullglob
for arch in arm64 arm amd64; do
matches=("$APK_DIR"/"${APP_NAME}"_*_"${arch}".apk)
if [ ${#matches[@]} -ne 1 ]; then
echo "Error: expected 1 APK for ${arch}, found ${#matches[@]}"
echo "APK_DIR: $APK_DIR"
ls -la "$APK_DIR" || true
exit 1
fi
mv "${matches[0]}" "$APK_DIR/${APP_NAME}_${RELEASE_TAG}_${arch}.apk"
done
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_amd64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm64.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_arm.apk
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseLinux:
name: Release linux
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Install dependencies
run: |
sudo apt update
# Basic
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev libvulkan-dev desktop-file-utils wget
sudo apt install -y clang cmake ninja-build pkg-config libgtk-3-dev mesa-utils libvulkan-dev desktop-file-utils wget
# App Specific
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev
# Packaging
sudo wget https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage -O /bin/appimagetool
sudo chmod +x /bin/appimagetool
sudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev libsecret-1-dev
- name: Build
run: |
dart run fl_build -p linux
- name: Rename artifacts
- name: Rename for release
shell: bash
run: |
appimage_name=$(ls dist/*/*.AppImage)
mv $appimage_name ${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
shopt -s nullglob
matches=("${APP_NAME}"_*_amd64.AppImage)
if [ ${#matches[@]} -ne 1 ]; then
echo "Error: expected 1 AppImage, found ${#matches[@]}"
ls -la || true
exit 1
fi
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_amd64.AppImage"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.appimage
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.AppImage
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -84,37 +107,97 @@ jobs:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: dart run fl_build -p windows
- name: Rename for release
shell: bash
run: |
shopt -s nullglob
matches=("${APP_NAME}"_*_windows_amd64.zip)
if [ ${#matches[@]} -ne 1 ]; then
echo "Error: expected 1 zip, found ${#matches[@]}"
ls -la || true
exit 1
fi
mv "${matches[0]}" "${APP_NAME}_${RELEASE_TAG}_windows_amd64.zip"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_windows_amd64.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# releaseApple:
# name: Release ios and macos
# runs-on: macos-latest
# steps:
# - name: Checkout
# uses: actions/checkout@v4
# - name: Install Flutter
# uses: subosito/flutter-action@v2
# with:
# channel: 'stable'
# flutter-version: '3.32.1'
# - name: Build
# run: dart run fl_build -p ios,mac
# - name: Create Release
# uses: softprops/action-gh-release@v2
# with:
# files: |
# ${{ env.APP_NAME }}_universal_macos.zip
# ${{ env.APP_NAME }}_universal.ipa
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseIOS:
name: Release iOS
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: |
dart run fl_build -p ios -- --no-codesign
shopt -s nullglob
IPA_FILES=(build/ios/ipa/*.ipa)
if [ ${#IPA_FILES[@]} -ne 1 ]; then
echo "Error: expected 1 IPA, found ${#IPA_FILES[@]}"
ls -la build/ios/ipa || true
exit 1
fi
IPA_FILE="${IPA_FILES[0]}"
echo "Found IPA: $IPA_FILE"
cp "$IPA_FILE" "${APP_NAME}_${RELEASE_TAG}_ios.ipa"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_ios.ipa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
releaseMacOS:
name: Release macOS
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install Flutter
uses: subosito/flutter-action@v2
- name: Build
run: |
dart run fl_build -p macos -- --no-codesign
- name: Package
run: |
RELEASE_DIR="$GITHUB_WORKSPACE/build/macos/Build/Products/Release"
APP_DIR="$RELEASE_DIR/$APP_NAME.app"
OUT_ZIP="$GITHUB_WORKSPACE/${APP_NAME}_${RELEASE_TAG}_macos.zip"
if [ ! -d "$RELEASE_DIR" ]; then
echo "Error: macOS release directory not found: $RELEASE_DIR"
exit 1
fi
if [ ! -d "$APP_DIR" ]; then
echo "Error: macOS app bundle not found: $APP_DIR"
exit 1
fi
cd "$RELEASE_DIR"
zip -ry "$OUT_ZIP" "$APP_NAME.app"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_macos.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

95
CLAUDE.md Normal file
View File

@@ -0,0 +1,95 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Development
- `flutter run` - Run the app in development mode
- `dart run fl_build -p PLATFORM` - Build the app for specific platform (see fl_build package)
- `dart run build_runner build --delete-conflicting-outputs` - Generate code for models with annotations (json_serializable, freezed, hive, riverpod)
- Every time you change model files, run this command to regenerate code (Hive adapters, Riverpod providers, etc.)
- Generated files include: `*.g.dart`, `*.freezed.dart` files
### Testing
- `flutter test` - Run unit tests
- `flutter test test/battery_test.dart` - Run specific test file
## Architecture
This is a Flutter application for managing Linux servers with the following key architectural components:
### Project Structure
- `lib/core/` - Core utilities, extensions, and routing
- `lib/data/` - Data layer with models, providers, and storage
- `model/` - Data models organized by feature (server, container, ssh, etc.)
- `provider/` - Riverpod providers for state management
- `store/` - Local storage implementations using Hive
- `lib/view/` - UI layer with pages and widgets
- `lib/generated/` - Generated localization files
- `lib/hive/` - Hive adapters for local storage
### Key Technologies
- **State Management**: Riverpod with code generation (riverpod_annotation)
- **Local Storage**: Hive for persistent data with generated adapters
- **SSH/SFTP**: Custom dartssh2 fork for server connections
- **Terminal**: Custom xterm.dart fork for SSH terminal interface
- **Networking**: dio for HTTP requests
- **Charts**: fl_chart for server status visualization
- **Localization**: Flutter's built-in i18n with ARB files
- **Code Generation**: Uses build_runner with json_serializable, freezed, hive_generator, riverpod_generator
### Data Models
- Server management models in `lib/data/model/server/`
- Container/Docker models in `lib/data/model/container/`
- SSH and SFTP models in respective directories
- Most models use freezed for immutability and json_annotation for serialization
### Features
- Server status monitoring (CPU, memory, disk, network)
- SSH terminal with virtual keyboard
- SFTP file browser
- Docker container management
- Process and systemd service management
- Server snippets and custom commands
- Multi-language support (12+ languages)
- Cross-platform support (iOS, Android, macOS, Linux, Windows)
### State Management Pattern
- Uses Riverpod providers for dependency injection and state management
- Uses Freezed for immutable state models
- Providers are organized by feature in `lib/data/provider/`
- State is often persisted using Hive stores in `lib/data/store/`
### Build System
- Uses custom `fl_build` package for cross-platform building
- `make.dart` script handles pre/post build tasks (metadata generation)
- Supports building for multiple platforms with platform-specific configurations
- Many dependencies are custom forks hosted on GitHub (dartssh2, xterm, fl_lib, etc.)
### Important Notes
- **Never run code formatting commands** - The codebase has specific formatting that should not be changed
- **Always run code generation** after modifying models with annotations (freezed, json_serializable, hive, riverpod)
- Generated files (`*.g.dart`, `*.freezed.dart`) should not be manually edited
- AGAIN, NEVER run code formatting commands.
- USE dependency injection via GetIt for services like Stores, Services and etc.
- Generate all l10n files using `flutter gen-l10n` command after modifying ARB files.
- USE `hive_ce` not `hive` package for Hive integration.
- Which no need to config `HiveField` and `HiveType` manually.
- USE widgets and utilities from `fl_lib` package for common functionalities.
- Such as `CustomAppBar`, `context.showRoundDialog`, `Input`, `Btnx.cancelOk`, etc.
- You can use context7 MCP to search `lppcg fl_lib KEYWORD` to find relevant widgets and utilities.
- USE `libL10n` and `l10n` for localization strings.
- `libL10n` is from `fl_lib` package, and `l10n` is from this project.
- Before adding new strings, check if it already exists in `libL10n`.
- Prioritize using strings from `libL10n` to avoid duplication, even if the meaning is not 100% exact, just use the substitution of `libL10n`.
- Split UI into Widget build, Actions, Utils. use `extension on` to achieve this

143
LICENSE
View File

@@ -1,5 +1,5 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
GNU Affero General Public License for more details.
You should have received a copy of the GNU General Public License
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -5,12 +5,12 @@ English | [简体中文](README_zh.md)
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/donate-me-pink"></a>
<img alt="lang" src="https://img.shields.io/badge/lang-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/license-GPLv3-yellow">
<img alt="license" src="https://img.shields.io/badge/license-AGPLv3-yellow">
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center">
A Flutter project which provide charts to display <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> server status and tools to manage server.
A Flutter project which provides charts to display Linux, Unix and Windows server status and tools to manage servers.
<br>
Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>.
</p>
@@ -26,7 +26,7 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
</tr>
</table>
## 📥 Install
## 📥 Installation
|Platform| From|
|--|--|
@@ -36,7 +36,7 @@ Especially thanks to <a href="https://github.com/TerminalStudio/dartssh2">dartss
Please only download pkgs from the source that **you trust**!
## 🔖 Feature
## 🔖 Features
- `Status chart` (CPU, Sensors, GPU...), `SSH` Term, `SFTP`, `Docker & Process & Systemd`, `S.M.A.R.T`...
- Platform specific: `Bio auth``Msg push``Home widget``watchOS App`...
@@ -61,10 +61,12 @@ Before you open an issue, please read the following:
After you read the above, you can open an [issue](https://github.com/lollipopkit/flutter_server_box/issues/new).
## 🧱 Contribution
## 🧱 Contributions
Any positive contribution is welcome.
If I forgot to add your name to the contributors list, please add a comment in the issue or PR you opened to let me know, I will add it as soon as possible.
### Development
1. Setup [Flutter](https://flutter.dev/docs/get-started/install) environment.
@@ -83,4 +85,4 @@ Any positive contribution is welcome.
## 📝 License
`GPL v3 lollipopkit`
`AGPL v3 lollipopkit & all contributors`

View File

@@ -5,12 +5,12 @@
<div align="center">
<a href="https://cdn.lpkt.cn/donate"><img alt="donate" src="https://img.shields.io/badge/捐赠-我-pink"></a>
<img alt="语言" src="https://img.shields.io/badge/语言-dart-cyan">
<img alt="license" src="https://img.shields.io/badge/证书-GPLv3-yellow">
<img alt="license" src="https://img.shields.io/badge/证书-AGPLv3-yellow">
<a href="https://deepwiki.com/lollipopkit/flutter_server_box"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
</div>
<p align="center">
使用 Flutter 开发的 <a href="https://github.com/lollipopkit/flutter_server_box/issues/43">Linux</a> 服务器工具箱,提供服务器状态图表和管理工具。
使用 Flutter 开发的 Linux, Unix, Windows 服务器工具箱,提供服务器状态图表和管理工具。
<br>
特别感谢 <a href="https://github.com/TerminalStudio/dartssh2">dartssh2</a> & <a href="https://github.com/TerminalStudio/xterm.dart">xterm.dart</a>
</p>
@@ -67,6 +67,8 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
任何正面的贡献都欢迎。
如果我忘记在贡献者列表中添加你的名字,请在你打开的 issue 或 PR 中添加评论让我知道,我会尽快添加。
### 开发
1. 安装 [Flutter](https://flutter.dev/docs/get-started/install)
@@ -84,4 +86,4 @@ Linux / Windows | [GitHub](https://github.com/lollipopkit/flutter_server_box/rel
## 📝 协议
`GPL v3 lollipopkit`
`AGPL v3 lollipopkit & 所有贡献者`

View File

@@ -113,7 +113,7 @@ android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def abiVersionCode = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
if (abiVersionCode != null) {
output.versionCodeOverride = variant.versionCode * 10 + abiVersionCode
output.versionCodeOverride = variant.versionCode * 100 + abiVersionCode
}
}
}

View File

@@ -46,6 +46,15 @@
android:name="flutterEmbedding"
android:value="2" />
<activity
android:name=".widget.WidgetConfigureActivity"
android:exported="false"
android:theme="@android:style/Theme.Material.Light.Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widget.HomeWidget"
android:exported="false"

View File

@@ -2,14 +2,32 @@ package tech.lolli.toolbox
import android.app.*
import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.drawable.Icon
import android.os.Build
import android.os.IBinder
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.*
class ForegroundService : Service() {
companion object {
@Volatile
var isRunning: Boolean = false
}
private val chanId = "ForegroundServiceChannel"
private val NOTIFICATION_ID = 1000
private val ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND"
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
private var isFgStarted = false
private val postedIds = mutableSetOf<Int>()
// Stable mapping from session-id -> notification-id to avoid hash collisions
private val notificationIdMap = mutableMapOf<String, Int>()
private val nextNotificationId = java.util.concurrent.atomic.AtomicInteger(2001)
private fun logError(message: String, error: Throwable? = null) {
Log.e("ForegroundService", message, error)
@@ -26,48 +44,51 @@ class ForegroundService : Service() {
override fun onCreate() {
super.onCreate()
Log.d("ForegroundService", "Service onCreate")
isRunning = true
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
try {
// Check notification permission for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Notification permission denied. Stopping service.")
stopForegroundService()
Log.w("ForegroundService", "Notification permission denied. Stopping service gracefully.")
// Don't call stopForegroundService() here as we haven't started foreground yet
stopSelf()
return START_NOT_STICKY
}
if (intent == null) {
Log.w("ForegroundService", "onStartCommand called with null intent")
stopForegroundService()
// Don't call stopForegroundService() here as we haven't started foreground yet
stopSelf()
return START_NOT_STICKY
}
val action = intent.action
Log.d("ForegroundService", "onStartCommand action=$action")
// Create notification before starting foreground
val notification = createNotification()
// Use try-catch for startForeground
try {
startForeground(1, notification)
} catch (e: Exception) {
logError("Failed to start foreground", e)
stopSelf()
return START_NOT_STICKY
}
return when (action) {
"ACTION_STOP_FOREGROUND" -> {
ACTION_STOP_FOREGROUND -> {
// Notify Flutter to stop all connections before stopping service
val stopAllIntent = Intent("tech.lolli.toolbox.STOP_ALL_CONNECTIONS")
sendBroadcast(stopAllIntent)
clearAll()
stopForegroundService()
START_NOT_STICKY
}
ACTION_UPDATE_SESSIONS -> {
val payload = intent.getStringExtra("payload") ?: "{}"
handleUpdateSessions(payload)
START_STICKY
}
else -> {
// Default bring up foreground with placeholder
ensureForeground(createMergedNotification(0, emptyList(), emptyList()))
START_STICKY
}
}
@@ -85,68 +106,205 @@ class ForegroundService : Service() {
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
try {
val manager = getSystemService(NotificationManager::class.java)
if (manager == null) {
Log.e("ForegroundService", "Failed to get NotificationManager")
return
}
val serviceChannel = NotificationChannel(
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
Log.d("ForegroundService", "Notification channel created successfully")
} catch (e: Exception) {
logError("Failed to create notification channel", e)
}
}
}
private fun ensureForeground(notification: Notification) {
try {
// Double-check notification permission before starting foreground service
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
androidx.core.content.ContextCompat.checkSelfPermission(
this, android.Manifest.permission.POST_NOTIFICATIONS
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
Log.w("ForegroundService", "Cannot start foreground service without notification permission")
stopSelf()
return
}
val serviceChannel = NotificationChannel(
chanId,
"ForegroundServiceChannel",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "For foreground service"
}
manager.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(): Notification {
try {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
val deleteIntent = Intent(this, ForegroundService::class.java).apply {
action = "ACTION_STOP_FOREGROUND"
}
val deletePendingIntent = PendingIntent.getService(
this,
0,
deleteIntent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
if (!isFgStarted) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIFICATION_ID, notification)
}
isFgStarted = true
Log.d("ForegroundService", "Foreground service started successfully")
} else {
Notification.Builder(this)
val nm = getSystemService(NotificationManager::class.java)
if (nm != null) {
nm.notify(NOTIFICATION_ID, notification)
} else {
Log.w("ForegroundService", "NotificationManager is null, cannot update notification")
}
}
return builder
.setContentTitle("Server Box")
.setContentText("Running in background")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_delete, "Stop", deletePendingIntent)
.build()
} catch (e: SecurityException) {
logError("Security exception when starting foreground service (likely missing permission)", e)
stopSelf()
} catch (e: Exception) {
logError("Error creating notification", e)
// Return a basic notification as fallback
return Notification.Builder(this)
.setContentTitle("Server Box")
.setSmallIcon(R.mipmap.ic_launcher)
.build()
logError("Failed to start/update foreground", e)
// Don't stop the service for other exceptions, just log them
}
}
private fun createMergedNotification(count: Int, lines: List<String>, sessions: List<SessionItem>): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val stopIntent = Intent(this, ForegroundService::class.java).apply { action = ACTION_STOP_FOREGROUND }
val stopPending = PendingIntent.getService(this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Notification.Builder(this, chanId)
} else {
@Suppress("DEPRECATION")
Notification.Builder(this)
}
// Use the earliest session's start time for chronometer
val earliestStartTime = sessions.minOfOrNull { it.startWhen } ?: System.currentTimeMillis()
val title = when (count) {
0 -> "Server Box"
1 -> sessions.first().title
else -> "SSH sessions: $count active"
}
val contentText = when (count) {
0 -> "Ready for connections"
1 -> {
val session = sessions.first()
"${session.subtitle} · ${session.status}"
}
else -> "Multiple SSH connections active"
}
// For multiple sessions, show details in expanded view
val style = if (count > 1) {
val inbox = Notification.InboxStyle()
val maxLines = 5
val displayLines = if (lines.size > maxLines) {
lines.take(maxLines) + "...and ${lines.size - maxLines} more"
} else {
lines
}
displayLines.forEach { inbox.addLine(it) }
inbox.setBigContentTitle(title)
inbox
} else {
null
}
val notification = builder
.setContentTitle(title)
.setContentText(contentText)
.setSmallIcon(R.mipmap.ic_launcher)
.setWhen(earliestStartTime)
.setUsesChronometer(true)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.addAction(
Notification.Action.Builder(
Icon.createWithResource(this, android.R.drawable.ic_delete),
"Stop All",
stopPending
).build()
)
if (style != null) {
notification.setStyle(style)
}
return notification.build()
}
private fun handleUpdateSessions(payload: String) {
val nm = getSystemService(NotificationManager::class.java)
if (nm == null) {
logError("NotificationManager null")
return
}
val sessions = mutableListOf<SessionItem>()
try {
val obj = JSONObject(payload)
val arr: JSONArray = obj.optJSONArray("sessions") ?: JSONArray()
for (i in 0 until arr.length()) {
val s = arr.optJSONObject(i) ?: continue
val id = s.optString("id")
val title = s.optString("title")
val sub = s.optString("subtitle")
val whenMs = s.optLong("startTimeMs", System.currentTimeMillis())
val status = s.optString("status", "connected")
if (id.isNotEmpty()) {
sessions.add(SessionItem(id, title, sub, whenMs, status))
}
}
} catch (e: Exception) {
logError("Failed to parse payload", e)
}
// Clear if empty
if (sessions.isEmpty()) {
clearAll()
return
}
// Cancel any existing individual notifications (we only show merged notification now)
val toCancel = postedIds.toSet()
toCancel.forEach { nm.cancel(it) }
postedIds.clear()
notificationIdMap.clear()
// Create merged notification content
val summaryLines = sessions.map { "${it.title}: ${it.status}" }
val mergedNotification = createMergedNotification(sessions.size, summaryLines, sessions)
ensureForeground(mergedNotification)
}
private fun clearAll() {
val nm = getSystemService(NotificationManager::class.java)
nm?.cancel(NOTIFICATION_ID)
postedIds.forEach { id -> nm?.cancel(id) }
postedIds.clear()
isFgStarted = false
}
data class SessionItem(
val id: String,
val title: String,
val subtitle: String,
val startWhen: Long,
val status: String,
)
private fun stopForegroundService() {
try {
stopForeground(true)
if (isFgStarted) {
stopForeground(STOP_FOREGROUND_REMOVE)
isFgStarted = false
}
} catch (e: Exception) {
logError("Error stopping foreground", e)
}
@@ -157,5 +315,6 @@ class ForegroundService : Service() {
override fun onDestroy() {
super.onDestroy()
Log.d("ForegroundService", "Service onDestroy")
isRunning = false
}
}
}

View File

@@ -4,6 +4,9 @@ import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.IntentFilter
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterFragmentActivity
@@ -13,20 +16,34 @@ import android.appwidget.AppWidgetManager
import tech.lolli.toolbox.widget.HomeWidget
class MainActivity: FlutterFragmentActivity() {
private lateinit var channel: MethodChannel
private val ACTION_UPDATE_SESSIONS = "tech.lolli.toolbox.ACTION_UPDATE_SESSIONS"
private val ACTION_DISCONNECT_SESSION = "tech.lolli.toolbox.ACTION_DISCONNECT_SESSION"
private val ACTION_STOP_ALL_CONNECTIONS = "tech.lolli.toolbox.STOP_ALL_CONNECTIONS"
private var stopAllReceiver: BroadcastReceiver? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val binaryMessenger = flutterEngine.dartExecutor.binaryMessenger
MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan").apply {
setMethodCallHandler { method, result ->
channel = MethodChannel(binaryMessenger, "tech.lolli.toolbox/main_chan")
channel.setMethodCallHandler { method, result ->
when (method.method) {
"sendToBackground" -> {
moveTaskToBack(true)
result.success(null)
}
"isServiceRunning" -> {
result.success(ForegroundService.isRunning)
}
"startService" -> {
try {
reqPerm()
if (!notificationsAllowed()) {
// Don't start foreground service without notification permission on API 33+
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
return@setMethodCallHandler
}
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
@@ -51,31 +68,138 @@ class MainActivity: FlutterFragmentActivity() {
sendBroadcast(intent)
result.success(null)
}
"updateSessions" -> {
try {
if (!notificationsAllowed()) {
// Avoid starting/continuing service updates when notifications are blocked
result.error("NOTIFICATION_PERMISSION_DENIED", "Notification permission not granted", null)
return@setMethodCallHandler
}
val serviceIntent = Intent(this@MainActivity, ForegroundService::class.java)
serviceIntent.action = ACTION_UPDATE_SESSIONS
serviceIntent.putExtra("payload", method.arguments as String)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
result.success(null)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to update sessions: ${e.message}")
result.error("SERVICE_ERROR", e.message, null)
}
}
else -> {
result.notImplemented()
}
}
}
// Handle intent if launched via notification action
handleActionIntent(intent)
// Register broadcast receiver for stop all connections
setupStopAllReceiver()
}
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
try {
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
// Check if we should show rationale
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) {
android.util.Log.i("MainActivity", "User previously denied notification permission")
}
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
}
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
}
}
private fun notificationsAllowed(): Boolean {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
true
} else {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleActionIntent(intent)
}
private fun handleActionIntent(intent: Intent?) {
if (intent == null) return
when (intent.action) {
ACTION_DISCONNECT_SESSION -> {
val sessionId = intent.getStringExtra("session_id")
if (sessionId != null && ::channel.isInitialized) {
try {
channel.invokeMethod("disconnectSession", mapOf("id" to sessionId))
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to invoke disconnect: ${e.message}")
}
}
}
}
}
private fun reqPerm() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
// Check if we already have the permission to avoid unnecessary prompts
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
try {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
123,
)
} catch (e: Exception) {
// Log error but don't crash
android.util.Log.e("MainActivity", "Failed to request permissions: ${e.message}")
private fun setupStopAllReceiver() {
stopAllReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_STOP_ALL_CONNECTIONS && ::channel.isInitialized) {
try {
channel.invokeMethod("stopAllConnections", null)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to invoke stopAllConnections: ${e.message}")
}
}
}
}
val filter = IntentFilter(ACTION_STOP_ALL_CONNECTIONS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.registerReceiver(this, stopAllReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(stopAllReceiver, filter)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 123) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
android.util.Log.i("MainActivity", "Notification permission granted")
} else {
android.util.Log.w("MainActivity", "Notification permission denied")
// Optionally inform user about the limitation
}
}
}
}
override fun onDestroy() {
super.onDestroy()
stopAllReceiver?.let {
try {
unregisterReceiver(it)
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Failed to unregister receiver: ${e.message}")
}
stopAllReceiver = null
}
}
}

View File

@@ -13,13 +13,24 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import org.json.JSONObject
import org.json.JSONException
import tech.lolli.toolbox.R
import java.net.URL
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.io.FileNotFoundException
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
class HomeWidget : AppWidgetProvider() {
companion object {
private const val TAG = "HomeWidget"
private const val NETWORK_TIMEOUT = 10_000L // 10 seconds
private const val COROUTINE_TIMEOUT = 15_000L // 15 seconds
private val activeUpdates = ConcurrentHashMap<Int, Boolean>()
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
@@ -27,105 +38,184 @@ class HomeWidget : AppWidgetProvider() {
}
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.home_widget)
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
var url = sp.getString("widget_$appWidgetId", null)
if (url.isNullOrEmpty()) {
url = sp.getString("$appWidgetId", null)
}
if (url.isNullOrEmpty()) {
val gUrl = sp.getString("widget_*", null)
url = gUrl
}
if (url.isNullOrEmpty()) {
Log.e("HomeWidget", "URL not found")
}
val intentUpdate = Intent(context, HomeWidget::class.java)
intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = intArrayOf(appWidgetId)
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
var flag = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION_CODES.O <= Build.VERSION.SDK_INT) {
flag = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
}
val pendingUpdate: PendingIntent = PendingIntent.getBroadcast(
context,
appWidgetId,
intentUpdate,
flag)
views.setOnClickPendingIntent(R.id.widget_container, pendingUpdate)
if (url.isNullOrEmpty()) {
views.setTextViewText(R.id.widget_name, "No URL")
// Update the widget to display a message for missing URL
views.setViewVisibility(R.id.error_message, View.VISIBLE)
views.setTextViewText(R.id.error_message, "Please configure the widget URL.")
views.setViewVisibility(R.id.widget_content, View.GONE)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.error_message, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
// Prevent concurrent updates for the same widget
if (activeUpdates.putIfAbsent(appWidgetId, true) == true) {
Log.d(TAG, "Widget $appWidgetId is already updating, skipping")
return
} else {
views.setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
views.setViewVisibility(R.id.widget_net_label, View.VISIBLE)
}
val views = RemoteViews(context.packageName, R.layout.home_widget)
val url = getWidgetUrl(context, appWidgetId)
if (url.isNullOrEmpty()) {
Log.w(TAG, "URL not found for widget $appWidgetId")
showErrorState(views, appWidgetManager, appWidgetId, "Please configure the widget URL.")
activeUpdates.remove(appWidgetId)
return
}
setupClickIntent(context, views, appWidgetId)
showLoadingState(views, appWidgetManager, appWidgetId)
CoroutineScope(Dispatchers.IO).launch {
try {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.getString("name")
val cpu = data.getString("cpu")
val mem = data.getString("mem")
val disk = data.getString("disk")
val net = data.getString("net")
withContext(Dispatchers.Main) {
if (mem.isEmpty() || disk.isEmpty()) {
Log.e("HomeWidget", "Failed to retrieve status: Memory or disk information is empty")
return@withContext
withTimeoutOrNull(COROUTINE_TIMEOUT) {
try {
val serverData = fetchServerData(url)
if (serverData != null) {
withContext(Dispatchers.Main) {
showSuccessState(views, appWidgetManager, appWidgetId, serverData)
}
} else {
withContext(Dispatchers.Main) {
showErrorState(views, appWidgetManager, appWidgetId, "Invalid server data received.")
}
views.setTextViewText(R.id.widget_name, server)
views.setTextViewText(R.id.widget_cpu, cpu)
views.setTextViewText(R.id.widget_mem, mem)
views.setTextViewText(R.id.widget_disk, disk)
views.setTextViewText(R.id.widget_net, net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
views.setTextViewText(R.id.widget_time, timeStr)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
views.setFloat(R.id.widget_mem_label, "setAlpha", 1f)
views.setFloat(R.id.widget_disk_label, "setAlpha", 1f)
views.setFloat(R.id.widget_net_label, "setAlpha", 1f)
views.setFloat(R.id.widget_time, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
} else {
throw FileNotFoundException("HTTP response code: $responseCode")
} catch (e: Exception) {
Log.e(TAG, "Error updating widget $appWidgetId: ${e.message}", e)
withContext(Dispatchers.Main) {
val errorMessage = when (e) {
is SocketTimeoutException -> "Connection timeout. Please check your network."
is IOException -> "Network error. Please check your connection."
is JSONException -> "Invalid data format received from server."
else -> "Failed to retrieve data: ${e.message}"
}
showErrorState(views, appWidgetManager, appWidgetId, errorMessage)
}
}
} catch (e: Exception) {
Log.e("HomeWidget", "Error updating widget: ${e.localizedMessage}", e)
} ?: run {
Log.w(TAG, "Widget update timed out for widget $appWidgetId")
withContext(Dispatchers.Main) {
views.setTextViewText(R.id.widget_name, "Error")
// Update the widget to display a message for data retrieval failure
views.setViewVisibility(R.id.error_message, View.VISIBLE)
views.setTextViewText(R.id.error_message, "Failed to retrieve data.")
views.setViewVisibility(R.id.widget_content, View.GONE)
views.setFloat(R.id.widget_name, "setAlpha", 1f)
views.setFloat(R.id.error_message, "setAlpha", 1f)
appWidgetManager.updateAppWidget(appWidgetId, views)
showErrorState(views, appWidgetManager, appWidgetId, "Update timed out. Please try again.")
}
}
activeUpdates.remove(appWidgetId)
}
}
private fun getWidgetUrl(context: Context, appWidgetId: Int): String? {
val sp = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
return sp.getString("widget_$appWidgetId", null)
?: sp.getString("$appWidgetId", null)
?: sp.getString("widget_*", null)
}
private fun setupClickIntent(context: Context, views: RemoteViews, appWidgetId: Int) {
val intentConfigure = Intent(context, WidgetConfigureActivity::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
}
val flag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingConfigure = PendingIntent.getActivity(context, appWidgetId, intentConfigure, flag)
views.setOnClickPendingIntent(R.id.widget_container, pendingConfigure)
}
private suspend fun fetchServerData(url: String): ServerData? = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
try {
connection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = NETWORK_TIMEOUT.toInt()
readTimeout = NETWORK_TIMEOUT.toInt()
setRequestProperty("User-Agent", "ServerBox-Widget/1.0")
setRequestProperty("Accept", "application/json")
}
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw IOException("HTTP ${connection.responseCode}: ${connection.responseMessage}")
}
val jsonStr = connection.inputStream.bufferedReader().use { it.readText() }
parseServerData(jsonStr)
} finally {
connection?.disconnect()
}
}
private fun parseServerData(jsonStr: String): ServerData? {
return try {
val jsonObject = JSONObject(jsonStr)
val data = jsonObject.getJSONObject("data")
val server = data.optString("name", "Unknown Server")
val cpu = data.optString("cpu", "").takeIf { it.isNotBlank() } ?: "N/A"
val mem = data.optString("mem", "").takeIf { it.isNotBlank() } ?: "N/A"
val disk = data.optString("disk", "").takeIf { it.isNotBlank() } ?: "N/A"
val net = data.optString("net", "").takeIf { it.isNotBlank() } ?: "N/A"
// Return data even if some fields are missing, providing defaults
// Only reject if we can't parse the JSON structure properly
ServerData(server, cpu, mem, disk, net)
} catch (e: JSONException) {
Log.e(TAG, "JSON parsing error: ${e.message}", e)
null
}
}
private fun showLoadingState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
views.apply {
setTextViewText(R.id.widget_name, "Loading...")
setViewVisibility(R.id.error_message, View.GONE)
setViewVisibility(R.id.widget_content, View.VISIBLE)
setViewVisibility(R.id.widget_cpu_label, View.VISIBLE)
setViewVisibility(R.id.widget_mem_label, View.VISIBLE)
setViewVisibility(R.id.widget_disk_label, View.VISIBLE)
setViewVisibility(R.id.widget_net_label, View.VISIBLE)
setViewVisibility(R.id.widget_progress, View.VISIBLE)
setFloat(R.id.widget_name, "setAlpha", 0.7f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun showSuccessState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, data: ServerData) {
views.apply {
setTextViewText(R.id.widget_name, data.name)
setTextViewText(R.id.widget_cpu, data.cpu)
setTextViewText(R.id.widget_mem, data.mem)
setTextViewText(R.id.widget_disk, data.disk)
setTextViewText(R.id.widget_net, data.net)
val timeStr = android.text.format.DateFormat.format("HH:mm", java.util.Date()).toString()
setTextViewText(R.id.widget_time, timeStr)
setViewVisibility(R.id.error_message, View.GONE)
setViewVisibility(R.id.widget_content, View.VISIBLE)
setViewVisibility(R.id.widget_progress, View.GONE)
// Smooth fade-in animation
setFloat(R.id.widget_name, "setAlpha", 1f)
setFloat(R.id.widget_cpu_label, "setAlpha", 1f)
setFloat(R.id.widget_mem_label, "setAlpha", 1f)
setFloat(R.id.widget_disk_label, "setAlpha", 1f)
setFloat(R.id.widget_net_label, "setAlpha", 1f)
setFloat(R.id.widget_time, "setAlpha", 1f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun showErrorState(views: RemoteViews, appWidgetManager: AppWidgetManager, appWidgetId: Int, errorMessage: String) {
views.apply {
setTextViewText(R.id.widget_name, "Error")
setViewVisibility(R.id.error_message, View.VISIBLE)
setTextViewText(R.id.error_message, errorMessage)
setViewVisibility(R.id.widget_content, View.GONE)
setViewVisibility(R.id.widget_progress, View.GONE)
setFloat(R.id.widget_name, "setAlpha", 1f)
setFloat(R.id.error_message, "setAlpha", 1f)
}
appWidgetManager.updateAppWidget(appWidgetId, views)
}
data class ServerData(
val name: String,
val cpu: String,
val mem: String,
val disk: String,
val net: String
)
}

View File

@@ -0,0 +1,82 @@
package tech.lolli.toolbox.widget
import android.app.Activity
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.EditText
import tech.lolli.toolbox.R
class WidgetConfigureActivity : Activity() {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private lateinit var urlEditText: EditText
private lateinit var saveButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.widget_configure)
// 设置结果为取消,以防用户在完成配置前退出
setResult(RESULT_CANCELED)
// 获取 widget ID
val extras = intent.extras
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID
)
}
// 如果没有有效的 widget ID完成 activity
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish()
return
}
// 初始化 UI 元素
urlEditText = findViewById(R.id.url_edit_text)
saveButton = findViewById(R.id.save_button)
// 从 SharedPreferences 加载现有配置
val sp = getSharedPreferences("FlutterSharedPreferences", MODE_PRIVATE)
val existingUrl = sp.getString("widget_$appWidgetId", "")
urlEditText.setText(existingUrl)
// 设置保存按钮点击事件
saveButton.setOnClickListener {
val url = urlEditText.text.toString().trim()
if (url.isEmpty()) {
urlEditText.error = "Please enter a URL"
return@setOnClickListener
}
// 验证 URL 格式
if (!Patterns.WEB_URL.matcher(url).matches()) {
urlEditText.error = "Please enter a valid URL"
return@setOnClickListener
}
// 保存 URL 到 SharedPreferences
val editor = sp.edit()
editor.putString("widget_$appWidgetId", url)
editor.apply()
// 更新 widget 使用 AppWidgetManager
val appWidgetManager = AppWidgetManager.getInstance(this)
val updateIntent = Intent(this, HomeWidget::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
}
sendBroadcast(updateIntent)
// 设置结果并结束 activity
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
}
}
}

View File

@@ -10,14 +10,17 @@
<TextView
android:id="@+id/widget_name"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/widgetText"
android:textSize="23sp"
android:textSize="20sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
android:alpha="0"
android:animateLayoutChanges="true"
android:fadingEdge="horizontal"
android:singleLine="true"
tools:text="Server Name" />
<!-- Wrap the content in a LinearLayout for easy visibility management -->
@@ -27,121 +30,138 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/widget_name"
android:paddingTop="13dp">
android:layout_marginTop="8dp">
<RelativeLayout
android:id="@+id/widget_container_inner"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:paddingTop="13dp"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<LinearLayout
android:id="@+id/widget_cpu_label"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_marginBottom="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:alpha="0"
android:animateLayoutChanges="true">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/speed_24">
</ImageView>
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/speed_24"
android:layout_gravity="center_vertical"
android:contentDescription="CPU usage" />
<TextView
android:id="@+id/widget_cpu"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:singleLine="true"
android:ellipsize = "marquee"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="CPU" />
android:textSize="12sp"
tools:text="CPU: 25.6%" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_mem_label"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_marginBottom="4dp"
android:layout_below="@id/widget_cpu_label"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:alpha="0"
android:animateLayoutChanges="true">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/memory_24">
</ImageView>
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/memory_24"
android:layout_gravity="center_vertical"
android:contentDescription="Memory usage" />
<TextView
android:id="@+id/widget_mem"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Mem" />
android:textSize="12sp"
tools:text="Memory: 4.2GB / 8GB" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_disk_label"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2.7dp"
android:layout_marginBottom="4dp"
android:layout_below="@id/widget_mem_label"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:alpha="0"
android:animateLayoutChanges="true">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/storage_24">
</ImageView>
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/storage_24"
android:layout_gravity="center_vertical"
android:contentDescription="Disk usage" />
<TextView
android:id="@+id/widget_disk"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Disk" />
android:textSize="12sp"
tools:text="Disk: 125GB / 250GB" />
</LinearLayout>
<LinearLayout
android:id="@+id/widget_net_label"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_disk_label"
android:gravity="center_vertical"
android:orientation="horizontal">
android:orientation="horizontal"
android:alpha="0"
android:animateLayoutChanges="true">
<ImageView
android:layout_width="17dp"
android:layout_height="17dp"
android:src="@drawable/net_24">
</ImageView>
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@drawable/net_24"
android:layout_gravity="center_vertical"
android:contentDescription="Network usage" />
<TextView
android:id="@+id/widget_net"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="11dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/widgetSummaryText"
android:textSize="12.7sp"
tools:text="Net" />
android:textSize="12sp"
tools:text="Network: 15MB/s ↓ 8MB/s ↑" />
</LinearLayout>
@@ -149,29 +169,45 @@
</LinearLayout>
<!-- Add a TextView for error messages -->
<!-- Error message display -->
<TextView
android:id="@+id/error_message"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_name"
android:layout_marginTop="8dp"
android:textColor="@color/widgetSummaryText"
android:textSize="12sp"
android:textSize="11sp"
android:visibility="gone"
android:alpha="0"
android:animateLayoutChanges="true"
tools:text="Error message" />
android:lineSpacingMultiplier="1.2"
android:maxLines="3"
android:ellipsize="end"
tools:text="Error message text that might be longer than usual" />
<TextView
android:id="@+id/widget_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:maxLines="2"
android:layout_alignParentEnd="true"
android:maxLines="1"
android:textColor="@color/widgetSummaryText"
android:textSize="11sp"
android:textSize="10sp"
android:alpha="0"
android:animateLayoutChanges="true"
tools:text="UpdateTime" />
android:fontFamily="monospace"
tools:text="12:34" />
<!-- Progress indicator for loading state -->
<ProgressBar
android:id="@+id/widget_progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerInParent="true"
android:visibility="gone"
android:indeterminate="true" />
</RelativeLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Widget URL"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:textColor="@android:color/black" />
<EditText
android:id="@+id/url_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="https://server/status"
android:inputType="textUri"
android:layout_marginBottom="16dp"
android:background="@android:drawable/edit_text"
android:padding="12dp"
android:textColor="@android:color/black"
android:textColorHint="@android:color/darker_gray" />
<Button
android:id="@+id/save_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Save"
android:background="#8b2252"
android:textColor="@android:color/white"
android:padding="12dp" />
</LinearLayout>

View File

@@ -6,6 +6,7 @@
android:minHeight="110dp"
android:updatePeriodMillis="1800001"
android:initialLayout="@layout/home_widget"
android:configure="tech.lolli.toolbox.widget.WidgetConfigureActivity"
android:resizeMode="none"
android:widgetCategory="home_screen">
</appwidget-provider>

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip

View File

@@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.6.0' apply false
id "com.android.application" version '8.9.1' apply false
id "org.jetbrains.kotlin.android" version "2.1.21" apply false
}

6505
coverage/lcov.info Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -1,13 +0,0 @@
variables:
output: dist/
releases:
- name: linux
jobs:
- name: release-linux-deb
package:
platform: linux
target: deb
- name: release-linux-rpm
package:
platform: linux
target: rpm

21
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store

4
docs/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
docs/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

49
docs/README.md Normal file
View File

@@ -0,0 +1,49 @@
# Starlight Starter Kit: Basics
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
```
npm create astro@latest -- --template starlight
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro + Starlight project, you'll see the following folders and files:
```
.
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ └── docs/
│ └── content.config.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
Static assets, like favicons, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [Starlights docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).

131
docs/astro.config.mjs Normal file
View File

@@ -0,0 +1,131 @@
// @ts-check
import { defineConfig } from 'astro/config';
import starlight from '@astrojs/starlight';
// https://astro.build/config
export default defineConfig({
integrations: [
starlight({
title: 'Server Box',
description: 'A comprehensive cross-platform server management application built with Flutter',
defaultLocale: 'root',
locales: {
root: {
label: 'English',
lang: 'en',
},
zh: {
label: '简体中文',
lang: 'zh',
},
de: {
label: 'Deutsch',
lang: 'de',
},
fr: {
label: 'Français',
lang: 'fr',
},
es: {
label: 'Español',
lang: 'es',
},
ja: {
label: '日本語',
lang: 'ja',
},
},
logo: {
src: './src/assets/logo.svg',
},
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/lollipopkit/flutter_server_box' },
],
sidebar: [
{
label: 'Getting Started',
translations: {
zh: '开始使用',
de: 'Erste Schritte',
fr: 'Mise en route',
es: 'Primeros pasos',
ja: 'はじめに',
},
items: [
{ label: 'Introduction', translations: { zh: '介绍', de: 'Einführung', fr: 'Introduction', es: 'Introducción', ja: 'はじめに' }, slug: 'introduction' },
{ label: 'Installation', translations: { zh: '安装', de: 'Installation', fr: 'Installation', es: 'Instalación', ja: 'インストール' }, slug: 'installation' },
{ label: 'Quick Start', translations: { zh: '快速开始', de: 'Schnellstart', fr: 'Démarrage rapide', es: 'Inicio rápido', ja: 'クイックスタート' }, slug: 'quick-start' },
],
},
{
label: 'Platform Features',
translations: {
zh: '平台特性',
de: 'Plattformfunktionen',
fr: 'Fonctionnalités de la plateforme',
es: 'Características de la plataforma',
ja: 'プラットフォーム機能',
},
items: [
{ label: 'Mobile', translations: { zh: '移动端', de: 'Mobil', fr: 'Mobile', es: 'Móvil', ja: 'モバイル' }, slug: 'platforms/mobile' },
{ label: 'Desktop', translations: { zh: '桌面端', de: 'Desktop', fr: 'Bureau', es: 'Escritorio', ja: 'デスクトップ' }, slug: 'platforms/desktop' },
],
},
{
label: 'Advanced',
translations: {
zh: '进阶',
de: 'Fortgeschritten',
fr: 'Avancé',
es: 'Avanzado',
ja: '高度な設定',
},
items: [
{ label: 'Bulk Import Servers', translations: { zh: '批量导入服务器', de: 'Server-Massenimport', fr: 'Importation massive de serveurs', es: 'Importación masiva de servidores', ja: 'サーバーの一括インポート' }, slug: 'advanced/bulk-import' },
{ label: 'Widget Setup', translations: { zh: '小组件设置', de: 'Widget-Einrichtung', fr: 'Configuration du widget', es: 'Configuración de widgets', ja: 'ウィジェット設定' }, slug: 'advanced/widgets' },
{ label: 'Custom Commands', translations: { zh: '自定义命令', de: 'Benutzerdefinierte Befehle', fr: 'Commandes personnalisées', es: 'Comandos personalizados', ja: 'カスタムコマンド' }, slug: 'advanced/custom-commands' },
{ label: 'Custom Logo', translations: { zh: '自定义 Logo', de: 'Benutzerdefiniertes Logo', fr: 'Logo personnalisé', es: 'Logo personalizado', ja: 'カスタムロゴ' }, slug: 'advanced/custom-logo' },
{ label: 'JSON Settings', translations: { zh: 'JSON 设置', de: 'JSON-Einstellungen', fr: 'Paramètres JSON', es: 'Ajustes JSON', ja: 'JSON 設定' }, slug: 'advanced/json-settings' },
{ label: 'Common Issues', translations: { zh: '常见问题', de: 'Häufige Probleme', fr: 'Problèmes courants', es: 'Problemas comunes', ja: 'よくある質問' }, slug: 'advanced/troubleshooting' },
],
},
{
label: 'How It Works',
translations: {
zh: '工作原理',
de: 'Wie es funktioniert',
fr: 'Comment ça marche',
es: 'Cómo funciona',
ja: '仕組み',
},
items: [
{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'principles/architecture' },
{ label: 'SSH Connection', translations: { zh: 'SSH 连接', de: 'SSH-Verbindung', fr: 'Connexion SSH', es: 'Conexión SSH', ja: 'SSH 接続' }, slug: 'principles/ssh' },
{ label: 'Terminal', translations: { zh: '终端', de: 'Terminal', fr: 'Terminal', es: 'Terminal', ja: 'ターミナル' }, slug: 'principles/terminal' },
{ label: 'SFTP', translations: { zh: 'SFTP', de: 'SFTP', fr: 'SFTP', es: 'SFTP', ja: 'SFTP' }, slug: 'principles/sftp' },
{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'principles/state' },
],
},
{
label: 'Development',
translations: {
zh: '开发',
de: 'Entwicklung',
fr: 'Développement',
es: 'Desarrollo',
ja: '開発',
},
items: [
{ label: 'Project Structure', translations: { zh: '项目结构', de: 'Projektstruktur', fr: 'Structure du projet', es: 'Estructura del proyecto', ja: 'プロジェクト構造' }, slug: 'development/structure' },
{ label: 'Architecture', translations: { zh: '架构', de: 'Architektur', fr: 'Architecture', es: 'Arquitectura', ja: 'アーキテクチャ' }, slug: 'development/architecture' },
{ label: 'State Management', translations: { zh: '状态管理', de: 'Zustandsverwaltung', fr: 'Gestion d\'état', es: 'Gestión de estado', ja: '状態管理' }, slug: 'development/state' },
{ label: 'Code Generation', translations: { zh: '代码生成', de: 'Code-Generierung', fr: 'Génération de code', es: 'Generación de código', ja: 'コード生成' }, slug: 'development/codegen' },
{ label: 'Building', translations: { zh: '构建', de: 'Bauen', fr: 'Construction', es: 'Construcción', ja: 'ビルド' }, slug: 'development/building' },
{ label: 'Testing', translations: { zh: '测试', de: 'Testen', fr: 'Tests', es: 'Pruebas', ja: 'テスト' }, slug: 'development/testing' },
],
},
],
customCss: ['./src/styles/custom.css'],
}),
],
});

900
docs/bun.lock Normal file
View File

@@ -0,0 +1,900 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "docs",
"dependencies": {
"@astrojs/starlight": "^0.37.4",
"astro": "^5.6.1",
"sharp": "^0.34.2",
},
},
},
"packages": {
"@astrojs/compiler": ["@astrojs/compiler@2.13.0", "", {}, "sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw=="],
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.7.5", "", {}, "sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA=="],
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.10", "", { "dependencies": { "@astrojs/internal-helpers": "0.7.5", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.19.0", "smol-toml": "^1.5.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-kk4HeYR6AcnzC4QV8iSlOfh+N8TZ3MEStxPyenyCtemqn8IpEATBFMTJcfrNW32dgpt6MY3oCkMM/Tv3/I4G3A=="],
"@astrojs/mdx": ["@astrojs/mdx@4.3.13", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.10", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.15.0", "es-module-lexer": "^1.7.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "piccolore": "^0.1.3", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-IHDHVKz0JfKBy3//52JSiyWv089b7GVSChIXLrlUOoTLWowG3wr2/8hkaEgEyd/vysvNQvGk+QhysXpJW5ve6Q=="],
"@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
"@astrojs/sitemap": ["@astrojs/sitemap@3.7.0", "", { "dependencies": { "sitemap": "^8.0.2", "stream-replace-string": "^2.0.0", "zod": "^3.25.76" } }, "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA=="],
"@astrojs/starlight": ["@astrojs/starlight@0.37.4", "", { "dependencies": { "@astrojs/markdown-remark": "^6.3.1", "@astrojs/mdx": "^4.2.3", "@astrojs/sitemap": "^3.3.0", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "astro-expressive-code": "^0.41.1", "bcp-47": "^2.1.0", "hast-util-from-html": "^2.0.1", "hast-util-select": "^6.0.2", "hast-util-to-string": "^3.0.0", "hastscript": "^9.0.0", "i18next": "^23.11.5", "js-yaml": "^4.1.0", "klona": "^2.0.6", "magic-string": "^0.30.17", "mdast-util-directive": "^3.0.0", "mdast-util-to-markdown": "^2.1.0", "mdast-util-to-string": "^4.0.0", "pagefind": "^1.3.0", "rehype": "^13.0.1", "rehype-format": "^5.0.0", "remark-directive": "^3.0.0", "ultrahtml": "^1.6.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "vfile": "^6.0.2" }, "peerDependencies": { "astro": "^5.5.0" } }, "sha512-ygPGDgRd9nCcNgaYMNN7UeAMAkDOR1ibv3ps3xEz+cuvKG3CRLd19UwdB+Gyz1tbkyfjPWPkFKNhLwNybro8Tw=="],
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
"@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@4.2.0", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@expressive-code/core": ["@expressive-code/core@0.41.6", "", { "dependencies": { "@ctrl/tinycolor": "^4.0.4", "hast-util-select": "^6.0.2", "hast-util-to-html": "^9.0.1", "hast-util-to-text": "^4.0.1", "hastscript": "^9.0.0", "postcss": "^8.4.38", "postcss-nested": "^6.0.1", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1" } }, "sha512-FvJQP+hG0jWi/FLBSmvHInDqWR7jNANp9PUDjdMqSshHb0y7sxx3vHuoOr6SgXjWw+MGLqorZyPQ0aAlHEok6g=="],
"@expressive-code/plugin-frames": ["@expressive-code/plugin-frames@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-d+hkSYXIQot6fmYnOmWAM+7TNWRv/dhfjMsNq+mIZz8Tb4mPHOcgcfZeEM5dV9TDL0ioQNvtcqQNuzA1sRPjxg=="],
"@expressive-code/plugin-shiki": ["@expressive-code/plugin-shiki@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "shiki": "^3.2.2" } }, "sha512-Y6zmKBmsIUtWTzdefqlzm/h9Zz0Rc4gNdt2GTIH7fhHH2I9+lDYCa27BDwuBhjqcos6uK81Aca9dLUC4wzN+ng=="],
"@expressive-code/plugin-text-markers": ["@expressive-code/plugin-text-markers@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6" } }, "sha512-PBFa1wGyYzRExMDzBmAWC6/kdfG1oLn4pLpBeTfIRrALPjcGA/59HP3e7q9J0Smk4pC7U+lWkA2LHR8FYV8U7Q=="],
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
"@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.4.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ=="],
"@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.4.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A=="],
"@pagefind/default-ui": ["@pagefind/default-ui@1.4.0", "", {}, "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ=="],
"@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.4.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q=="],
"@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.4.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw=="],
"@pagefind/linux-x64": ["@pagefind/linux-x64@1.4.0", "", { "os": "linux", "cpu": "x64" }, "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg=="],
"@pagefind/windows-x64": ["@pagefind/windows-x64@1.4.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="],
"@shikijs/core": ["@shikijs/core@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ=="],
"@shikijs/langs": ["@shikijs/langs@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA=="],
"@shikijs/themes": ["@shikijs/themes@3.21.0", "", { "dependencies": { "@shikijs/types": "3.21.0" } }, "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw=="],
"@shikijs/types": ["@shikijs/types@3.21.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
"@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="],
"@types/sax": ["@types/sax@1.2.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"astro": ["astro@5.16.16", "", { "dependencies": { "@astrojs/compiler": "^2.13.0", "@astrojs/internal-helpers": "0.7.5", "@astrojs/markdown-remark": "6.3.10", "@astrojs/telemetry": "3.3.0", "@capsizecss/unpack": "^4.0.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "acorn": "^8.15.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "boxen": "8.0.1", "ci-info": "^4.3.1", "clsx": "^2.1.1", "common-ancestor-path": "^1.0.1", "cookie": "^1.1.1", "cssesc": "^3.0.0", "debug": "^4.4.3", "deterministic-object-hash": "^2.0.2", "devalue": "^5.6.2", "diff": "^8.0.3", "dlv": "^1.1.3", "dset": "^3.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.0", "estree-walker": "^3.0.3", "flattie": "^1.1.1", "fontace": "~0.4.0", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "import-meta-resolve": "^4.2.0", "js-yaml": "^4.1.1", "magic-string": "^0.30.21", "magicast": "^0.5.1", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "p-limit": "^6.2.0", "p-queue": "^8.1.1", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.3", "prompts": "^2.4.2", "rehype": "^13.0.2", "semver": "^7.7.3", "shiki": "^3.21.0", "smol-toml": "^1.6.0", "svgo": "^4.0.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tsconfck": "^3.1.6", "ultrahtml": "^1.6.0", "unifont": "~0.7.3", "unist-util-visit": "^5.0.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", "vite": "^6.4.1", "vitefu": "^1.1.1", "xxhash-wasm": "^1.1.0", "yargs-parser": "^21.1.1", "yocto-spinner": "^0.2.3", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1", "zod-to-ts": "^1.2.0" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "astro.js" } }, "sha512-MFlFvQ84ixaHyqB3uGwMhNHdBLZ3vHawyq3PqzQS2TNWiNfQrxp5ag6S3lX+Cvnh0MUcXX+UnJBPMBHjP1/1ZQ=="],
"astro-expressive-code": ["astro-expressive-code@0.41.6", "", { "dependencies": { "rehype-expressive-code": "^0.41.6" }, "peerDependencies": { "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" } }, "sha512-l47tb1uhmVIebHUkw+HEPtU/av0G4O8Q34g2cbkPvC7/e9ZhANcjUUciKt9Hp6gSVDdIuXBBLwJQn2LkeGMOAw=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="],
"bcp-47": ["bcp-47@2.1.0", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w=="],
"bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
"common-ancestor-path": ["common-ancestor-path@1.0.1", "", {}, "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
"css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-selector-parser": ["css-selector-parser@3.3.0", "", {}, "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="],
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csso": ["csso@5.0.5", "", { "dependencies": { "css-tree": "~2.2.0" } }, "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
"devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
"domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"expressive-code": ["expressive-code@0.41.6", "", { "dependencies": { "@expressive-code/core": "^0.41.6", "@expressive-code/plugin-frames": "^0.41.6", "@expressive-code/plugin-shiki": "^0.41.6", "@expressive-code/plugin-text-markers": "^0.41.6" } }, "sha512-W/5+IQbrpCIM5KGLjO35wlp1NCwDOOVQb+PAvzEoGkW1xjGM807ZGfBKptNWH6UECvt6qgmLyWolCMYKh7eQmA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
"fontace": ["fontace@0.4.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-moThBCItUe2bjZip5PF/iZClpKHGLwMvR79Kp8XpGRBrvoRSnySN4VcILdv3/MJzbhvUA5WeiUXF5o538m5fvg=="],
"fontkitten": ["fontkitten@1.0.2", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-piJxbLnkD9Xcyi7dWJRnqszEURixe7CrF/efBfbffe2DPyabmuIuqraruY8cXTs19QoM8VJzx47BDRVNXETM7Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
"hast-util-embedded": ["hast-util-embedded@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="],
"hast-util-format": ["hast-util-format@1.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-minify-whitespace": "^1.0.0", "hast-util-phrasing": "^3.0.0", "hast-util-whitespace": "^3.0.0", "html-whitespace-sensitive-tag-names": "^3.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA=="],
"hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
"hast-util-has-property": ["hast-util-has-property@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="],
"hast-util-is-body-ok-link": ["hast-util-is-body-ok-link@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="],
"hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
"hast-util-minify-whitespace": ["hast-util-minify-whitespace@1.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-is-element": "^3.0.0", "hast-util-whitespace": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"hast-util-phrasing": ["hast-util-phrasing@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-embedded": "^3.0.0", "hast-util-has-property": "^3.0.0", "hast-util-is-body-ok-link": "^3.0.0", "hast-util-is-element": "^3.0.0" } }, "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="],
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
"hast-util-select": ["hast-util-select@6.0.4", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "bcp-47-match": "^2.0.0", "comma-separated-tokens": "^2.0.0", "css-selector-parser": "^3.0.0", "devlop": "^1.0.0", "direction": "^2.0.0", "hast-util-has-property": "^3.0.0", "hast-util-to-string": "^3.0.0", "hast-util-whitespace": "^3.0.0", "nth-check": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="],
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
"hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"html-whitespace-sensitive-tag-names": ["html-whitespace-sensitive-tag-names@3.0.1", "", {}, "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="],
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="],
"mdast-util-directive": ["mdast-util-directive@3.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-directive": ["micromark-extension-directive@3.0.2", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "parse-entities": "^4.0.0" } }, "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
"p-queue": ["p-queue@8.1.1", "", { "dependencies": { "eventemitter3": "^5.0.1", "p-timeout": "^6.1.2" } }, "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ=="],
"p-timeout": ["p-timeout@6.1.4", "", {}, "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg=="],
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
"pagefind": ["pagefind@1.4.0", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.4.0", "@pagefind/darwin-x64": "1.4.0", "@pagefind/freebsd-x64": "1.4.0", "@pagefind/linux-arm64": "1.4.0", "@pagefind/linux-x64": "1.4.0", "@pagefind/windows-x64": "1.4.0" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"rehype": ["rehype@13.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "unified": "^11.0.0" } }, "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A=="],
"rehype-expressive-code": ["rehype-expressive-code@0.41.6", "", { "dependencies": { "expressive-code": "^0.41.6" } }, "sha512-aBMX8kxPtjmDSFUdZlAWJkMvsQ4ZMASfee90JWIAV8tweltXLzkWC3q++43ToTelI8ac5iC0B3/S/Cl4Ql1y2g=="],
"rehype-format": ["rehype-format@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-format": "^1.0.0" } }, "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ=="],
"rehype-parse": ["rehype-parse@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-html": "^2.0.0", "unified": "^11.0.0" } }, "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag=="],
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
"rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="],
"remark-directive": ["remark-directive@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", "micromark-extension-directive": "^3.0.0", "unified": "^11.0.0" } }, "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-smartypants": ["remark-smartypants@3.0.2", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="],
"retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="],
"retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="],
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
"rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="],
"sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"shiki": ["shiki@3.21.0", "", { "dependencies": { "@shikijs/core": "3.21.0", "@shikijs/engine-javascript": "3.21.0", "@shikijs/engine-oniguruma": "3.21.0", "@shikijs/langs": "3.21.0", "@shikijs/themes": "3.21.0", "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w=="],
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
"sitemap": ["sitemap@8.0.2", "", { "dependencies": { "@types/node": "^17.0.5", "@types/sax": "^1.2.1", "arg": "^5.0.0", "sax": "^1.4.1" }, "bin": { "sitemap": "dist/cli.js" } }, "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ=="],
"smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
"svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="],
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unifont": ["unifont@0.7.3", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-b0GtQzKCyuSHGsfj5vyN8st7muZ6VCI4XD4vFlr7Uy1rlWVYxC3npnfk8MyreHxJYrz1ooLDqDzFe9XqQTlAhA=="],
"unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
"unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="],
"vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="],
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"yocto-spinner": ["yocto-spinner@0.2.3", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ=="],
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
}
}

17
docs/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "docs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/starlight": "^0.37.4",
"astro": "^5.6.1",
"sharp": "^0.34.2"
}
}

1
docs/public/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" d="M81 36 64 0 47 36l-1 2-9-10a6 6 0 0 0-9 9l10 10h-2L0 64l36 17h2L28 91a6 6 0 1 0 9 9l9-10 1 2 17 36 17-36v-2l9 10a6 6 0 1 0 9-9l-9-9 2-1 36-17-36-17-2-1 9-9a6 6 0 1 0-9-9l-9 10v-2Zm-17 2-2 5c-4 8-11 15-19 19l-5 2 5 2c8 4 15 11 19 19l2 5 2-5c4-8 11-15 19-19l5-2-5-2c-8-4-15-11-19-19l-2-5Z" clip-rule="evenodd"/><path d="M118 19a6 6 0 0 0-9-9l-3 3a6 6 0 1 0 9 9l3-3Zm-96 4c-2 2-6 2-9 0l-3-3a6 6 0 1 1 9-9l3 3c3 2 3 6 0 9Zm0 82c-2-2-6-2-9 0l-3 3a6 6 0 1 0 9 9l3-3c3-2 3-6 0-9Zm96 4a6 6 0 0 1-9 9l-3-3a6 6 0 1 1 9-9l3 3Z"/><style>path{fill:#000}@media (prefers-color-scheme:dark){path{fill:#fff}}</style></svg>

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

30
docs/src/assets/logo.svg Normal file
View File

@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<!-- Background Circle -->
<circle cx="32" cy="32" r="30" fill="#02569B"/>
<!-- Terminal Window -->
<rect x="14" y="16" width="36" height="32" rx="3" fill="#fff" opacity="0.9"/>
<!-- Terminal Header -->
<rect x="14" y="16" width="36" height="8" rx="3" fill="#02569B" opacity="0.3"/>
<circle cx="19" cy="20" r="1.5" fill="#ff5f57"/>
<circle cx="24" cy="20" r="1.5" fill="#ffbd2e"/>
<circle cx="29" cy="20" r="1.5" fill="#28c940"/>
<!-- Terminal Prompt -->
<text x="18" y="34" font-family="monospace" font-size="6" fill="#333">></text>
<!-- Terminal Cursor -->
<rect x="23" y="30" width="6" height="6" fill="#02569B" opacity="0.5">
<animate attributeName="opacity" values="0.5;1;0.5" dur="1s" repeatCount="indefinite"/>
</rect>
<!-- Server Icon -->
<g transform="translate(38, 24)">
<rect x="0" y="0" width="8" height="12" rx="1" fill="#02569B"/>
<circle cx="2" cy="3" r="0.8" fill="#28c940"/>
<circle cx="4" cy="3" r="0.8" fill="#28c940"/>
<circle cx="6" cy="3" r="0.8" fill="#28c940"/>
<line x1="1" y1="8" x2="7" y2="8" stroke="#fff" stroke-width="0.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,7 @@
import { defineCollection } from 'astro:content';
import { docsLoader } from '@astrojs/starlight/loaders';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@@ -0,0 +1,83 @@
---
title: Bulk Import Servers
description: Import multiple servers from JSON file
---
Import multiple server configurations at once using a JSON file.
## JSON Format
:::danger[Security Warning]
**Never store plaintext passwords in files!** This JSON example shows a password field for demonstration only, but you should:
- **Prefer SSH keys** (`keyId`) instead of `pwd` - they're more secure
- **Use secret managers** or environment variables if you must use passwords
- **Delete the file immediately** after import - don't leave credentials lying around
- **Add to .gitignore** - never commit credential files to version control
:::
```json
[
{
"name": "My Server",
"ip": "example.com",
"port": 22,
"user": "root",
"pwd": "password",
"keyId": "",
"tags": ["production"],
"autoConnect": false
}
]
```
## Fields
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Display name |
| `ip` | Yes | Domain or IP address |
| `port` | Yes | SSH port (usually 22) |
| `user` | Yes | SSH username |
| `pwd` | No | Password (avoid - use SSH keys instead) |
| `keyId` | No | SSH key name (from Private Keys - recommended) |
| `tags` | No | Organization tags |
| `autoConnect` | No | Auto-connect on startup |
## Import Steps
1. Create JSON file with server configurations
2. Settings → Backup → Bulk Import Servers
3. Select your JSON file
4. Confirm import
## Example
```json
[
{
"name": "Production",
"ip": "prod.example.com",
"port": 22,
"user": "admin",
"keyId": "my-key",
"tags": ["production", "web"]
},
{
"name": "Development",
"ip": "dev.example.com",
"port": 2222,
"user": "dev",
"keyId": "dev-key",
"tags": ["development"]
}
]
```
## Tips
- **Use SSH keys** instead of passwords when possible
- **Test connection** after import
- **Organize with tags** for easier management
- **Delete JSON file** after import
- **Never commit** JSON files with credentials to version control

View File

@@ -0,0 +1,72 @@
---
title: Custom Commands
description: Display custom command output on server page
---
Add custom shell commands to show their output on the server detail page.
## Setup
1. Server settings → Custom Commands
2. Enter commands in JSON format
## Basic Format
```json
{
"Display Name": "shell command"
}
```
**Example:**
```json
{
"Memory": "free -h",
"Disk": "df -h",
"Uptime": "uptime"
}
```
## Viewing Results
After setup, custom commands appear on server detail page and refresh automatically.
## Special Command Names
### server_card_top_right
Display on home page server card (top-right corner):
```json
{
"server_card_top_right": "your-command-here"
}
```
## Tips
**Use absolute paths:**
```json
{"My Script": "/usr/local/bin/my-script.sh"}
```
**Pipe commands:**
```json
{"Top Process": "ps aux | sort -rk 3 | head -5"}
```
**Format output:**
```json
{"CPU Load": "uptime | awk -F'load average:' '{print $2}'"}
```
**Keep commands fast:** Under 5 seconds for best experience
**Limit output:**
```json
{"Logs": "tail -20 /var/log/syslog"}
```
## Security
Commands run with SSH user permissions. Avoid commands that modify system state.

View File

@@ -0,0 +1,54 @@
---
title: Custom Server Logo
description: Use custom images for server cards
---
Display custom logos on server cards using image URLs.
## Setup
1. Server settings → Custom Logo
2. Enter image URL
## URL Placeholders
### {DIST} - Linux Distribution
Auto-replaced with detected distribution:
```
https://example.com/{DIST}.png
```
Becomes: `debian.png`, `ubuntu.png`, `arch.png`, etc.
### {BRIGHT} - Theme
Auto-replaced with current theme:
```
https://example.com/{BRIGHT}.png
```
Becomes: `light.png` or `dark.png`
### Combine Both
```
https://example.com/{DIST}-{BRIGHT}.png
```
Becomes: `debian-light.png`, `ubuntu-dark.png`, etc.
## Tips
- Use PNG or SVG formats
- Recommended size: 64x64 to 128x128 pixels
- Use HTTPS URLs
- Keep file sizes small
## Supported Distributions
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
Full list: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)

View File

@@ -0,0 +1,65 @@
---
title: Hidden Settings (JSON)
description: Access advanced settings via JSON editor
---
Some settings are hidden from the UI but accessible via JSON editor.
## Access
Long-press **Settings** in drawer to open JSON editor.
## Common Hidden Settings
### timeOut
Connection timeout in seconds.
```json
{"timeOut": 10}
```
**Type:** integer | **Default:** 5 | **Range:** 1-60
### recordHistory
Save history (SFTP paths, etc.).
```json
{"recordHistory": true}
```
**Type:** boolean | **Default:** true
### textFactor
Text scaling factor.
```json
{"textFactor": 1.2}
```
**Type:** double | **Default:** 1.0 | **Range:** 0.8-1.5
## Finding More Settings
All settings defined in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
Look for:
```dart
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
```
## ⚠️ Important
**Before editing:**
- **Create backup** - Wrong settings can cause app to not open
- **Edit carefully** - JSON must be valid
- **Change one at a time** - Test each setting
## Recovery
If app won't open after editing:
1. Clear app data (last resort)
2. Reinstall app
3. Restore from backup

View File

@@ -0,0 +1,118 @@
---
title: Common Issues
description: Solutions to common problems
---
## Connection Issues
### SSH Won't Connect
**Symptoms:** Timeout, connection refused, auth failed
**Solutions:**
1. **Verify server type:** Only Unix-like systems supported (Linux, macOS, Android/Termux)
2. **Test manually:** `ssh user@server -p port`
3. **Check firewall:** Port 22 must be open
4. **Verify credentials:** Username and password/key correct
### Frequent Disconnections
**Symptoms:** Terminal disconnects after inactivity
**Solutions:**
1. **Server keep-alive:**
```bash
# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3
```
2. **Disable battery optimization:**
- MIUI: Battery → "No limits"
- Android: Settings → Apps → Disable optimization
- iOS: Enable background refresh
## Input Issues
### Can't Type Certain Characters
**Solution:** Settings → Keyboard Type → Switch to `visiblePassword`
Note: CJK input may not work after this change.
## App Issues
### App Crashes on Startup
**Symptoms:** App won't open, black screen
**Causes:** Corrupted settings, especially from JSON editor
**Solutions:**
1. **Clear app data:**
- Android: Settings → Apps → ServerBox → Clear Data
- iOS: Delete and reinstall
2. **Restore backup:** Import backup created before changing settings
### Backup/Restore Issues
**Backup not working:**
- Check storage space
- Verify app has storage permissions
- Try different location
**Restore fails:**
- Verify backup file integrity
- Check app version compatibility
## Widget Issues
### Widget Not Updating
**iOS:**
- Wait up to 30 minutes for automatic refresh
- Remove and re-add widget
- Check URL ends with `/status`
**Android:**
- Tap widget to force refresh
- Verify widget ID matches configuration in app settings
**watchOS:**
- Restart watch app
- Wait a few minutes after config change
- Verify URL format
### Widget Shows Error
- Verify ServerBox Monitor is running on server
- Test URL in browser
- Check authentication credentials
## Performance Issues
### App is Slow
**Solutions:**
- Reduce refresh rate in settings
- Check network speed
- Disable unused servers
### High Battery Usage
**Solutions:**
- Increase refresh intervals
- Disable background refresh
- Close unused SSH sessions
## Getting Help
If issues persist:
1. **Search GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues
2. **Create New Issue:** Include app version, platform, and steps to reproduce
3. **Check Wiki:** This documentation and GitHub Wiki

View File

@@ -0,0 +1,91 @@
---
title: Home Screen Widgets
description: Add server status widgets to your home screen
---
Requires [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) installed on your servers.
## Prerequisites
Install ServerBox Monitor on your server first. See [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home) for setup instructions.
After installation, your server should have:
- HTTP/HTTPS endpoint
- `/status` API endpoint
- Optional authentication
## URL Format
```
https://your-server.com/status
```
Must end with `/status`.
## iOS Widget
### Setup
1. Long press home screen → Tap **+**
2. Search "ServerBox"
3. Choose widget size
4. Long press widget → **Edit Widget**
5. Enter URL ending with `/status`
### Notes
- Must use HTTPS (except local IPs)
- Max refresh rate: 30 minutes (iOS limit)
- Add multiple widgets for multiple servers
## Android Widget
### Setup
1. Long press home screen → **Widgets**
2. Find "ServerBox" → Add to home screen
3. Note the widget ID number displayed
4. Open ServerBox app → Settings
5. Tap **Config home widget link**
6. Add entry: `Widget ID` = `Status URL`
Example:
- Key: `17`
- Value: `https://my-server.com/status`
7. Tap widget on home screen to refresh
## watchOS Widget
### Setup
1. Open iPhone app → Settings
2. **iOS Settings****Watch app**
3. Tap **Add URL**
4. Enter URL ending with `/status`
5. Wait for watch app to sync
### Notes
- Try restarting watch app if not updating
- Verify phone and watch are connected
## Troubleshooting
### Widget Not Updating
**iOS:** Wait up to 30 minutes, then remove and re-add
**Android:** Tap widget to force refresh, verify ID in settings
**watchOS:** Restart watch app, wait a few minutes
### Widget Shows Error
- Verify ServerBox Monitor is running
- Test URL in browser
- Check URL ends with `/status`
## Security
- **Always use HTTPS** when possible
- **Local IPs only** on trusted networks

View File

@@ -0,0 +1,83 @@
---
title: Massenimport von Servern
description: Importieren Sie mehrere Server aus einer JSON-Datei
---
Importieren Sie mehrere Serverkonfigurationen gleichzeitig mithilfe einer JSON-Datei.
## JSON-Format
:::danger[Sicherheitswarnung]
**Speichern Sie niemals Klartext-Passwörter in Dateien!** Dieses JSON-Beispiel zeigt ein Passwort-Feld nur zur Demonstration, aber Sie sollten:
- **SSH-Schlüssel bevorzugen** (`keyId`) anstelle von `pwd` - diese sind sicherer
- **Passwort-Manager** oder Umgebungsvariablen verwenden, wenn Sie Passwörter verwenden müssen
- **Löschen Sie die Datei sofort** nach dem Import - lassen Sie keine Anmeldedaten herumliegen
- **Fügen Sie sie zur .gitignore hinzu** - checken Sie niemals Anmeldedatendateien in die Versionsverwaltung ein
:::
```json
[
{
"name": "Mein Server",
"ip": "example.com",
"port": 22,
"user": "root",
"pwd": "password",
"keyId": "",
"tags": ["production"],
"autoConnect": false
}
]
```
## Felder
| Feld | Erforderlich | Beschreibung |
|-------|----------|-------------|
| `name` | Ja | Anzeigename |
| `ip` | Ja | Domain oder IP-Adresse |
| `port` | Ja | SSH-Port (normalerweise 22) |
| `user` | Ja | SSH-Benutzername |
| `pwd` | Nein | Passwort (vermeiden - stattdessen SSH-Schlüssel verwenden) |
| `keyId` | Nein | SSH-Schlüsselname (aus Private Keys - empfohlen) |
| `tags` | Nein | Organisations-Tags |
| `autoConnect` | Nein | Automatische Verbindung beim Start |
## Import-Schritte
1. Erstellen Sie eine JSON-Datei mit Serverkonfigurationen
2. Einstellungen → Backup → Server massenhaft importieren
3. Wählen Sie Ihre JSON-Datei aus
4. Bestätigen Sie den Import
## Beispiel
```json
[
{
"name": "Produktion",
"ip": "prod.example.com",
"port": 22,
"user": "admin",
"keyId": "my-key",
"tags": ["production", "web"]
},
{
"name": "Entwicklung",
"ip": "dev.example.com",
"port": 2222,
"user": "dev",
"keyId": "dev-key",
"tags": ["development"]
}
]
```
## Tipps
- **Verwenden Sie SSH-Schlüssel** anstelle von Passwörtern, wann immer möglich
- **Testen Sie die Verbindung** nach dem Import
- **Organisieren Sie mit Tags** für eine einfachere Verwaltung
- **Löschen Sie die JSON-Datei** nach dem Import
- **Checken Sie niemals** JSON-Dateien mit Anmeldedaten in die Versionsverwaltung ein

View File

@@ -0,0 +1,72 @@
---
title: Benutzerdefinierte Befehle
description: Anzeige der Ausgabe benutzerdefinierter Befehle auf der Serverseite
---
Fügen Sie benutzerdefinierte Shell-Befehle hinzu, um deren Ausgabe auf der Server-Detailseite anzuzeigen.
## Einrichtung
1. Servereinstellungen → Benutzerdefinierte Befehle
2. Befehle im JSON-Format eingeben
## Basisformat
```json
{
"Anzeigename": "Shell-Befehl"
}
```
**Beispiel:**
```json
{
"Speicher": "free -h",
"Festplatte": "df -h",
"Laufzeit": "uptime"
}
```
## Ergebnisse anzeigen
Nach der Einrichtung erscheinen benutzerdefinierte Befehle auf der Server-Detailseite und werden automatisch aktualisiert.
## Spezielle Befehlsnamen
### server_card_top_right
Anzeige auf der Serverkarte der Startseite (oben rechts):
```json
{
"server_card_top_right": "Ihr-Befehl-hier"
}
```
## Tipps
**Absolute Pfade verwenden:**
```json
{"Mein Skript": "/usr/local/bin/mein-skript.sh"}
```
**Pipe-Befehle:**
```json
{"Top-Prozess": "ps aux | sort -rk 3 | head -5"}
```
**Ausgabe formatieren:**
```json
{"CPU-Last": "uptime | awk -F'load average:' '{print $2}'"}
```
**Befehle schnell halten:** Unter 5 Sekunden für das beste Erlebnis.
**Ausgabe begrenzen:**
```json
{"Logs": "tail -20 /var/log/syslog"}
```
## Sicherheit
Befehle werden mit den Berechtigungen des SSH-Benutzers ausgeführt. Vermeiden Sie Befehle, die den Systemzustand ändern.

View File

@@ -0,0 +1,54 @@
---
title: Benutzerdefiniertes Server-Logo
description: Verwenden Sie benutzerdefinierte Bilder für Serverkarten
---
Zeigen Sie benutzerdefinierte Logos auf Serverkarten mithilfe von Bild-URLs an.
## Einrichtung
1. Servereinstellungen → Benutzerdefiniertes Logo
2. Bild-URL eingeben
## URL-Platzhalter
### {DIST} - Linux-Distribution
Wird automatisch durch die erkannte Distribution ersetzt:
```
https://example.com/{DIST}.png
```
Wird zu: `debian.png`, `ubuntu.png`, `arch.png`, usw.
### {BRIGHT} - Theme
Wird automatisch durch das aktuelle Theme ersetzt:
```
https://example.com/{BRIGHT}.png
```
Wird zu: `light.png` oder `dark.png`
### Beide kombinieren
```
https://example.com/{DIST}-{BRIGHT}.png
```
Wird zu: `debian-light.png`, `ubuntu-dark.png`, usw.
## Tipps
- Verwenden Sie PNG- oder SVG-Formate
- Empfohlene Größe: 64x64 bis 128x128 Pixel
- Verwenden Sie HTTPS-URLs
- Halten Sie die Dateigrößen gering
## Unterstützte Distributionen
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
Vollständige Liste: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)

View File

@@ -0,0 +1,64 @@
---
title: Versteckte Einstellungen (JSON)
description: Zugriff auf erweiterte Einstellungen über den JSON-Editor
---
Einige Einstellungen sind in der Benutzeroberfläche ausgeblendet, aber über den JSON-Editor zugänglich.
## Zugriff
Halten Sie **Einstellungen** in der Seitenleiste lange gedrückt, um den JSON-Editor zu öffnen.
## Gängige versteckte Einstellungen
### timeOut
Verbindungs-Timeout in Sekunden.
```json
{"timeOut": 10}
```
**Typ:** Integer | **Standard:** 5 | **Bereich:** 1-60
### recordHistory
Verlauf speichern (SFTP-Pfade, usw.).
```json
{"recordHistory": true}
```
**Typ:** Boolean | **Standard:** true
### textFactor
Textskalierungsfaktor.
```json
{"textFactor": 1.2}
```
**Typ:** Double | **Standard:** 1.0 | **Bereich:** 0.8-1.5
## Weitere Einstellungen finden
Alle Einstellungen sind in [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart) definiert.
Suchen Sie nach:
```dart
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
```
## ⚠️ Wichtig
**Vor dem Bearbeiten:**
- **Backup erstellen** - Falsche Einstellungen können dazu führen, dass die App nicht mehr öffnet
- **Sorgfältig bearbeiten** - JSON muss gültig sein
## Wiederherstellung
Wenn die App nach dem Bearbeiten nicht mehr öffnet:
1. App-Daten löschen (letzter Ausweg)
2. App neu installieren
3. Aus Backup wiederherstellen

View File

@@ -0,0 +1,118 @@
---
title: Häufige Probleme
description: Lösungen für gängige Probleme
---
## Verbindungsprobleme
### SSH verbindet nicht
**Symptome:** Timeout, Verbindung abgelehnt, Authentifizierung fehlgeschlagen
**Lösungen:**
1. **Servertyp überprüfen:** Nur Unix-ähnliche Systeme werden unterstützt (Linux, macOS, Android/Termux)
2. **Manuell testen:** `ssh benutzer@server -p port`
3. **Firewall prüfen:** Port 22 muss offen sein
4. **Anmeldedaten prüfen:** Benutzername und Passwort/Schlüssel korrekt
### Häufige Verbindungsabbrüche
**Symptome:** Das Terminal trennt die Verbindung nach Inaktivität
**Lösungen:**
1. **Server Keep-Alive:**
```bash
# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3
```
2. **Akku-Optimierung deaktivieren:**
- MIUI: Akku → "Keine Beschränkungen"
- Android: Einstellungen → Apps → Optimierung deaktivieren
- iOS: Hintergrundaktualisierung aktivieren
## Eingabeprobleme
### Bestimmte Zeichen können nicht getippt werden
**Lösung:** Einstellungen → Tastaturtyp → Wechseln zu `visiblePassword`
Hinweis: CJK-Eingaben funktionieren nach dieser Änderung möglicherweise nicht mehr.
## App-Probleme
### App stürzt beim Start ab
**Symptome:** App öffnet sich nicht, schwarzer Bildschirm
**Ursachen:** Korrupte Einstellungen, insbesondere durch den JSON-Editor
**Lösungen:**
1. **App-Daten löschen:**
- Android: Einstellungen → Apps → ServerBox → Daten löschen
- iOS: Löschen und neu installieren
2. **Backup wiederherstellen:** Importieren Sie ein Backup, das vor der Änderung der Einstellungen erstellt wurde
### Probleme beim Sichern/Wiederherstellen
**Backup funktioniert nicht:**
- Speicherplatz prüfen
- Sicherstellen, dass die App Speicherberechtigungen hat
- Anderen Speicherort versuchen
**Wiederherstellung schlägt fehl:**
- Integrität der Backup-Datei prüfen
- Kompatibilität der App-Version prüfen
## Widget-Probleme
### Widget aktualisiert nicht
**iOS:**
- Bis zu 30 Minuten auf automatische Aktualisierung warten
- Widget entfernen und neu hinzufügen
- Prüfen, ob die URL auf `/status` endet
**Android:**
- Auf das Widget tippen, um die Aktualisierung zu erzwingen
- Sicherstellen, dass die Widget-ID mit der Konfiguration in den App-Einstellungen übereinstimmt
**watchOS:**
- Watch-App neu starten
- Nach Konfigurationsänderung einige Minuten warten
- URL-Format prüfen
### Widget zeigt Fehler
- Sicherstellen, dass der ServerBox Monitor auf dem Server läuft
- URL im Browser testen
- Authentifizierungsdaten prüfen
## Leistungsprobleme
### App ist langsam
**Lösungen:**
- Aktualisierungsrate in den Einstellungen reduzieren
- Netzwerkgeschwindigkeit prüfen
- Nicht verwendete Server deaktivieren
### Hoher Akkuverbrauch
**Lösungen:**
- Aktualisierungsintervalle vergrößern
- Hintergrundaktualisierung deaktivieren
- Nicht verwendete SSH-Sitzungen schließen
## Hilfe erhalten
Wenn die Probleme weiterhin bestehen:
1. **GitHub Issues durchsuchen:** https://github.com/lollipopkit/flutter_server_box/issues
2. **Neues Issue erstellen:** App-Version, Plattform und Schritte zur Reproduktion angeben
3. **Wiki prüfen:** Diese Dokumentation und das GitHub Wiki

View File

@@ -0,0 +1,90 @@
---
title: Startbildschirm-Widgets
description: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu
---
Erfordert [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) auf Ihren Servern installiert.
## Voraussetzungen
Installieren Sie zuerst ServerBox Monitor auf Ihrem Server. Anweisungen zur Einrichtung finden Sie im [ServerBox Monitor Wiki](https://github.com/lollipopkit/server_box_monitor/wiki/Home).
Nach der Installation sollte Ihr Server verfügen über:
- Einen HTTP/HTTPS-Endpunkt
- Einen `/status` API-Endpunkt
- Optionale Authentifizierung
## URL-Format
```
https://ihr-server.com/status
```
Muss auf `/status` enden.
## iOS-Widget
### Einrichtung
1. Startbildschirm lange drücken → Auf **+** tippen
2. Nach "ServerBox" suchen
3. Widget-Größe wählen
4. Widget lange drücken → **Widget bearbeiten**
5. URL eingeben, die auf `/status` endet
### Hinweise
- Muss HTTPS verwenden (außer bei lokalen IPs)
- Maximale Aktualisierungsrate: 30 Minuten (iOS-Limit)
- Fügen Sie mehrere Widgets für mehrere Server hinzu
## Android-Widget
### Einrichtung
1. Startbildschirm lange drücken → **Widgets**
2. "ServerBox" finden → Zum Startbildschirm hinzufügen
3. Notieren Sie sich die angezeigte Widget-ID-Nummer
4. ServerBox-App öffnen → Einstellungen
5. Tippen Sie auf **Config home widget link**
6. Eintrag hinzufügen: `Widget ID` = `Status-URL`
Beispiel:
- Key: `17`
- Value: `https://mein-server.com/status`
7. Tippen Sie auf das Widget auf dem Startbildschirm, um es zu aktualisieren
## watchOS-Widget
### Einrichtung
1. iPhone-App öffnen → Einstellungen
2. **iOS-Einstellungen****Watch-App**
3. Auf **URL hinzufügen** tippen
4. URL eingeben, die auf `/status` endet
5. Warten, bis die Watch-App synchronisiert ist
### Hinweise
- Versuchen Sie, die Watch-App neu zu starten, wenn sie nicht aktualisiert wird
- Sicherstellen, dass Telefon und Watch verbunden sind
## Fehlerbehebung
### Widget aktualisiert nicht
**iOS:** Warten Sie bis zu 30 Minuten, dann entfernen Sie es und fügen es erneut hinzu.
**Android:** Tippen Sie auf das Widget, um die Aktualisierung zu erzwingen, überprüfen Sie die ID in den Einstellungen.
**watchOS:** Starten Sie die Watch-App neu, warten Sie einige Minuten.
### Widget zeigt Fehler an
- Sicherstellen, dass ServerBox Monitor läuft
- URL im Browser testen
- Prüfen, ob die URL auf `/status` endet
## Sicherheit
- **Verwenden Sie immer HTTPS**, wann immer möglich
- **Lokale IPs nur** in vertrauenswürdigen Netzwerken

View File

@@ -0,0 +1,86 @@
---
title: Architektur
description: Architekturmuster und Designentscheidungen
---
Server Box folgt den Prinzipien der Clean Architecture mit einer klaren Trennung zwischen Daten-, Domänen- und Präsentationsschicht.
## Schichtarchitektur
```
┌─────────────────────────────────────┐
│ Präsentationsschicht │
│ (lib/view/page/) │
│ - Seiten, Widgets, Controller │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Business-Logik-Schicht │
│ (lib/data/provider/) │
│ - Riverpod Provider │
│ - Zustandsverwaltung │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Datenschicht │
│ (lib/data/model/, store/) │
│ - Modelle, Speicher, Dienste │
└─────────────────────────────────────┘
```
## Schlüsselmuster
### Zustandsverwaltung: Riverpod
- **Codegenerierung**: Verwendet `riverpod_generator` für typsichere Provider
- **State Notifier**: Für veränderlichen Zustand mit Business-Logik
- **Async Notifier**: Für Lade- und Fehlerzustände
- **Stream Provider**: Für Echtzeitdaten
### Unveränderliche Modelle: Freezed
- Alle Datenmodelle verwenden Freezed für Unveränderlichkeit
- Union-Typen zur Darstellung von Zuständen
- Integrierte JSON-Serialisierung
- CopyWith-Erweiterungen für Aktualisierungen
### Lokale Speicherung: Hive
- **hive_ce**: Community-Edition von Hive
- Keine manuellen `@HiveField` oder `@HiveType` erforderlich
- Typ-Adapter werden automatisch generiert
- Persistenter Key-Value-Speicher
## Dependency Injection
Dienste und Stores werden injiziert über:
1. **Provider**: Stellen Abhängigkeiten der UI zur Verfügung
2. **GetIt**: Service-Locator (wo anwendbar)
3. **Konstruktor-Injektion**: Explizite Abhängigkeiten
## Datenfluss
```
Benutzeraktion → Widget → Provider → Dienst/Store → Modell-Update → UI-Neuaufbau
```
1. Benutzer interagiert mit Widget
2. Widget ruft Provider-Methode auf
3. Provider aktualisiert Zustand über Dienst/Store
4. Zustandsänderung löst Neuaufbau der UI aus
5. Neuer Zustand spiegelt sich im Widget wider
## Eigene Abhängigkeiten
Das Projekt verwendet mehrere eigene Forks zur Funktionserweiterung:
- **dartssh2**: Erweiterte SSH-Funktionen
- **xterm**: Terminal-Emulator mit mobiler Unterstützung
- **fl_lib**: Gemeinsame UI-Komponenten und Dienstprogramme
## Threading
- **Isolates**: Rechenintensive Aufgaben außerhalb des Main-Threads
- **computer-Paket**: Dienstprogramme für Multi-Threading
- **Async/Await**: Nicht-blockierende I/O-Operationen

View File

@@ -0,0 +1,116 @@
---
title: Bauen
description: Bauanleitungen für verschiedene Plattformen
---
Server Box verwendet ein benutzerdefiniertes Build-System (`fl_build`) für plattformübergreifende Builds.
## Voraussetzungen
- Flutter SDK (stabiler Kanal)
- Plattformspezifische Tools (Xcode für iOS, Android Studio für Android)
- Rust-Toolchain (für einige native Abhängigkeiten)
## Entwicklungs-Build
```bash
# Im Entwicklungsmodus ausführen
flutter run
# Auf einem bestimmten Gerät ausführen
flutter run -d <device-id>
```
## Produktions-Build
Das Projekt verwendet `fl_build` zum Bauen:
```bash
# Für eine bestimmte Plattform bauen
dart run fl_build -p <platform>
# Verfügbare Plattformen:
# - ios
# - android
# - macos
# - linux
# - windows
```
## Plattformspezifische Builds
### iOS
```bash
dart run fl_build -p ios
```
Erfordert:
- macOS mit Xcode
- CocoaPods
- Apple Developer Account für die Signierung
### Android
```bash
dart run fl_build -p android
```
Erfordert:
- Android SDK
- Java Development Kit
- Keystore für die Signierung
### macOS
```bash
dart run fl_build -p macos
```
### Linux
```bash
dart run fl_build -p linux
```
### Windows
```bash
dart run fl_build -p windows
```
Erfordert Windows mit Visual Studio.
## Vor/Nach dem Build
Das Skript `make.dart` übernimmt:
- Metadaten-Generierung
- Aktualisierung der Versions-Strings
- Plattformspezifische Konfigurationen
## Fehlerbehebung
### Clean Build
```bash
flutter clean
dart run build_runner build --delete-conflicting-outputs
flutter pub get
```
### Versions-Konflikt
Stellen Sie sicher, dass alle Abhängigkeiten kompatibel sind:
```bash
flutter pub upgrade
```
## Release-Checkliste
1. Version in `pubspec.yaml` aktualisieren
2. Codegenerierung ausführen
3. Tests ausführen
4. Für alle Zielplattformen bauen
5. Auf physischen Geräten testen
6. GitHub-Release erstellen

View File

@@ -0,0 +1,98 @@
---
title: Codegenerierung
description: Verwendung von build_runner für die Codegenerierung
---
Server Box verwendet intensiv Codegenerierung für Modelle, Zustandsverwaltung und Serialisierung.
## Wann sollte die Codegenerierung ausgeführt werden?
Führen Sie sie aus nach der Änderung von:
- Modellen mit `@freezed` Annotation
- Klassen mit `@JsonSerializable`
- Hive-Modellen
- Providern mit `@riverpod`
- Lokalisierungen (ARB-Dateien)
## Codegenerierung ausführen
```bash
# Gesamten Code generieren
dart run build_runner build --delete-conflicting-outputs
# Bereinigen und neu generieren
dart run build_runner build --delete-conflicting-outputs --clean
```
## Generierte Dateien
### Freezed (`*.freezed.dart`)
Unveränderliche Datenmodelle mit Union Types:
```dart
@freezed
class ServerState with _$ServerState {
const factory ServerState.connected() = Connected;
const factory ServerState.disconnected() = Disconnected;
const factory ServerState.error(String message) = Error;
}
```
### JSON-Serialisierung (`*.g.dart`)
Generiert durch `json_serializable`:
```dart
@JsonSerializable()
class Server {
final String id;
final String name;
final String host;
Server({required this.id, required this.name, required this.host});
factory Server.fromJson(Map<String, dynamic> json) =>
_$ServerFromJson(json);
Map<String, dynamic> toJson() => _$ServerToJson(this);
}
```
### Riverpod Provider (`*.g.dart`)
Generiert aus der `@riverpod` Annotation:
```dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
}
```
### Hive-Adapter (`*.g.dart`)
Automatisch generiert für Hive-Modelle (hive_ce):
```dart
@HiveType(typeId: 0)
class ServerModel {
@HiveField(0)
final String id;
}
```
## Generierung der Lokalisierung
```bash
flutter gen-l10n
```
Generiert `lib/generated/l10n/` aus `lib/l10n/*.arb` Dateien.
## Tipps
- Verwenden Sie `--delete-conflicting-outputs`, um Konflikte zu vermeiden.
- Fügen Sie generierte Dateien zur `.gitignore` hinzu.
- Bearbeiten Sie generierte Dateien niemals manuell.

View File

@@ -0,0 +1,115 @@
---
title: Zustandsverwaltung
description: Riverpod-basierte Zustandsverwaltungsmuster
---
Server Box verwendet Riverpod mit Codegenerierung für die Zustandsverwaltung.
## Provider-Typen
### StateProvider
Einfacher Zustand, der gelesen und geschrieben werden kann:
```dart
@riverpod
class Settings extends _$Settings {
@override
SettingsModel build() {
return SettingsModel.defaults();
}
void update(SettingsModel newSettings) {
state = newSettings;
}
}
```
### AsyncNotifierProvider
Zustand, der asynchron mit Lade-/Fehlerzuständen geladen wird:
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
return fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => fetchStatus(server));
}
}
```
### StreamProvider
Echtzeitdaten aus Streams:
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
return cpuService.monitor(server);
}
```
## Zustandsmuster
### Ladezustände
```dart
state.when(
data: (data) => DataWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### Family Provider
Parametrisierte Provider:
```dart
@riverpod
List<Container> containers(ContainersRef ref, Server server) {
return containerService.list(server);
}
```
### Auto-Dispose
Provider, die verworfen werden, wenn sie nicht mehr referenziert werden:
```dart
@Riverpod(keepAlive: false)
class TempState extends _$TempState {
// ...
}
```
## Best Practices
1. **Codegenerierung nutzen**: Immer die `@riverpod` Annotation verwenden.
2. **Provider lokal platzieren**: In der Nähe der Widgets platzieren, die sie nutzen.
3. **Singletons vermeiden**: Stattdessen Provider verwenden.
4. **Korrekt schichten**: UI-Logik von Business-Logik getrennt halten.
## Zustand in Widgets lesen
```dart
class ServerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(serverStatusProvider(server));
return status.when(...);
}
}
```
## Zustand ändern
```dart
ref.read(settingsProvider.notifier).update(newSettings);
```

View File

@@ -0,0 +1,96 @@
---
title: Projektstruktur
description: Verständnis der Server Box Codebasis
---
Das Server Box-Projekt folgt einer modularen Architektur mit einer klaren Trennung der Belange.
## Verzeichnisstruktur
```
lib/
├── core/ # Kern-Dienstprogramme und Erweiterungen
├── data/ # Datenschicht
│ ├── model/ # Datenmodelle nach Funktionen
│ ├── provider/ # Riverpod Provider
│ └── store/ # Lokale Speicherung (Hive)
├── view/ # UI-Schicht
│ ├── page/ # Hauptseiten
│ └── widget/ # Wiederverwendbare Widgets
├── generated/ # Generierte Lokalisierung
├── l10n/ # Lokalisierungs-ARB-Dateien
└── hive/ # Hive-Adapter
```
## Kernschicht (`lib/core/`)
Enthält Dienstprogramme, Erweiterungen und Routing-Konfiguration:
- **Erweiterungen**: Dart-Erweiterungen für gängige Typen
- **Routen**: App-Routing-Konfiguration
- **Dienstprogramme**: Gemeinsame Hilfsfunktionen
## Datenschicht (`lib/data/`)
### Modelle (`lib/data/model/`)
Organisiert nach Funktionen:
- `server/` - Server-Verbindung und Status-Modelle
- `container/` - Docker-Container-Modelle
- `ssh/` - SSH-Sitzungs-Modelle
- `sftp/` - SFTP-Datei-Modelle
- `app/` - App-spezifische Modelle
### Provider (`lib/data/provider/`)
Riverpod Provider für Dependency Injection und Zustandsverwaltung:
- Server Provider
- UI-Zustands-Provider
- Service Provider
### Stores (`lib/data/store/`)
Hive-basierte lokale Speicherung:
- Server-Speicher
- Einstellungs-Speicher
- Cache-Speicher
## UI-Schicht (`lib/view/`)
### Seiten (`lib/view/page/`)
Hauptbildschirme der Anwendung:
- `server/` - Server-Verwaltungsseiten
- `ssh/` - SSH-Terminal-Seiten
- `container/` - Container-Seiten
- `setting/` - Einstellungsseiten
- `storage/` - SFTP-Seiten
- `snippet/` - Snippet-Seiten
### Widgets (`lib/view/widget/`)
Wiederverwendbare UI-Komponenten:
- Server-Karten
- Status-Diagramme
- Eingabe-Komponenten
- Dialoge
## Generierte Dateien
- `lib/generated/l10n/` - Automatisch generierte Lokalisierung
- `*.g.dart` - Generierter Code (json_serializable, freezed, hive, riverpod)
- `*.freezed.dart` - Unveränderliche Freezed-Klassen
## Verzeichnis "packages" (`/packages/`)
Enthält eigene Forks von Abhängigkeiten:
- `dartssh2/` - SSH-Bibliothek
- `xterm/` - Terminal-Emulator
- `fl_lib/` - Gemeinsame Dienstprogramme
- `fl_build/` - Build-System

View File

@@ -0,0 +1,113 @@
---
title: Testen
description: Teststrategien und Ausführung von Tests
---
## Tests ausführen
```bash
# Alle Tests ausführen
flutter test
# Bestimmte Testdatei ausführen
flutter test test/battery_test.dart
# Mit Coverage ausführen
flutter test --coverage
```
## Teststruktur
Tests befinden sich im Verzeichnis `test/` und spiegeln die Struktur von `lib/` wider:
```
test/
├── data/
│ ├── model/
│ └── provider/
├── view/
│ └── widget/
└── test_helpers.dart
```
## Unit-Tests
Geschäftslogik und Datenmodelle testen:
```dart
test('sollte CPU-Prozentsatz berechnen', () {
final cpu = CpuModel(usage: 75.0);
expect(cpu.usagePercentage, '75%');
});
```
## Widget-Tests
UI-Komponenten testen:
```dart
testWidgets('ServerCard zeigt Servernamen an', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: ServerCard(server: testServer),
),
),
);
expect(find.text('Test Server'), findsOneWidget);
});
```
## Provider-Tests
Riverpod Provider testen:
```dart
test('serverStatusProvider gibt Status zurück', () async {
final container = ProviderContainer();
final status = await container.read(serverStatusProvider(testServer).future);
expect(status, isA<StatusModel>());
});
```
## Mocking
Mocks für externe Abhängigkeiten verwenden:
```dart
class MockSshService extends Mock implements SshService {}
test('verbindet zum Server', () async {
final mockSsh = MockSshService();
when(mockSsh.connect(any)).thenAnswer((_) async => true);
// Test mit Mock
});
```
## Integrationstests
Komplette Benutzerabläufe testen (in `integration_test/`):
```dart
testWidgets('Server hinzufügen Ablauf', (tester) async {
await tester.pumpWidget(MyApp());
// Hinzufügen-Button tippen
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// Formular ausfüllen
await tester.enterText(find.byKey(Key('name')), 'Test Server');
// ...
});
```
## Best Practices
1. **Arrange-Act-Assert**: Tests klar strukturieren
2. **Beschreibende Namen**: Testnamen sollten das Verhalten beschreiben
3. **Eine Assertion pro Test**: Tests fokussiert halten
4. **Externe Abhängigkeiten mocken**: Nicht von echten Servern abhängig sein
5. **Grenzfälle testen**: Leere Listen, Null-Werte, usw.

View File

@@ -0,0 +1,46 @@
---
title: Server Box
description: Eine umfassende plattformübergreifende Server-Management-Anwendung
hero:
tagline: Verwalten Sie Ihre Linux-Server von überall aus
actions:
- text: Loslegen
link: /de/introduction/
icon: right-arrow
variant: primary
- text: Auf GitHub ansehen
link: https://github.com/lollipopkit/flutter_server_box
icon: github
variant: minimal
---
import { Card, CardGrid } from '@astrojs/starlight/components';
## Funktionen
<CardGrid stagger>
<Card title="Echtzeit-Überwachung" icon="chart">
Überwachen Sie CPU, Arbeitsspeicher, Festplatte, Netzwerk, GPU und Temperatur mit ansprechenden Echtzeit-Diagrammen.
</Card>
<Card title="SSH-Terminal" icon="terminal">
Voll ausgestattetes SSH-Terminal mit Multi-Tab-Unterstützung und virtueller Tastatur für mobile Geräte.
</Card>
<Card title="SFTP-Dateibrowser" icon="folder">
Verwalten Sie Dateien auf Ihren Servern mit dem integrierten SFTP-Client und dem lokalen Dateibrowser.
</Card>
<Card title="Docker-Verwaltung" icon="box">
Starten, stoppen und überwachen Sie Docker-Container mit einer intuitiven Benutzeroberfläche.
</Card>
<Card title="Plattformübergreifend" icon="device-mobile">
Verfügbar für iOS, Android, macOS, Linux, Windows und watchOS.
</Card>
<Card title="12+ Sprachen" icon="globe">
Vollständige Lokalisierungsunterstützung inklusive Englisch, Chinesisch, Deutsch, Französisch und mehr.
</Card>
</CardGrid>
## Quick-Links
- **Download**: Verfügbar im [App Store](https://apps.apple.com/app/id1586449703), auf [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) und bei [F-Droid](https://f-droid.org/)
- **Dokumentation**: Entdecken Sie die Anleitungen für den Einstieg in die Server Box
- **Support**: Treten Sie unserer Community auf GitHub für Diskussionen und Probleme bei

View File

@@ -0,0 +1,51 @@
---
title: Installation
description: Laden Sie Server Box herunter und installieren Sie es auf Ihrem Gerät
---
Server Box ist für mehrere Plattformen verfügbar. Wählen Sie Ihre bevorzugte Installationsmethode.
## Mobile Apps
### iOS
Laden Sie es aus dem **[App Store](https://apps.apple.com/app/id1586449703)** herunter.
### Android
Wählen Sie Ihre bevorzugte Quelle:
- **[F-Droid](https://f-droid.org/)** Für Benutzer, die reine FOSS-Quellen bevorzugen
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** Für die neueste Version direkt von der Quelle
## Desktop Apps
### macOS
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
Funktionen:
- Native Menüleisten-Integration
- Unterstützung für sowohl Intel als auch Apple Silicon
### Linux
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
Verfügbar als AppImage, deb oder tar.gz Pakete.
### Windows
Herunterladen von den **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
## watchOS
Verfügbar im **[App Store](https://apps.apple.com/app/id1586449703)** als Teil der iOS-App.
## Aus dem Quellcode bauen
Um Server Box aus dem Quellcode zu bauen, lesen Sie den Abschnitt [Bauen](/de/development/building/) in der Entwicklungsdokumentation.
## Versionsinformationen
Besuchen Sie die Seite [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) für die neueste Version und das Änderungsprotokoll.

View File

@@ -0,0 +1,32 @@
---
title: Einführung
description: Erfahren Sie, was Server Box ist und was es kann
---
Server Box ist eine umfassende plattformübergreifende Server-Management-Anwendung, die mit Flutter entwickelt wurde. Sie ermöglicht es Ihnen, Ihre Linux-, Unix- und Windows-Server von überall aus zu überwachen, zu verwalten und zu steuern.
## Was ist Server Box?
Server Box bietet eine einheitliche Oberfläche für Server-Administrationsaufgaben über SSH-Verbindungen. Egal, ob Sie Systemadministrator, Entwickler oder Hobbyist mit eigenen Heimservern sind diese App bietet Ihnen leistungsstarke Server-Management-Tools direkt in Ihrer Tasche.
## Kernfunktionen
- **Echtzeit-Überwachung**: Verfolgen Sie CPU, Arbeitsspeicher, Festplattenbelegung, Netzwerkgeschwindigkeit, GPU-Status und Systemtemperaturen.
- **SSH-Terminal**: Voller Terminalzugriff mit Multi-Tab-Unterstützung und anpassbarem Erscheinungsbild.
- **SFTP-Client**: Durchsuchen und verwalten Sie Dateien auf Ihren Servern.
- **Docker-Verwaltung**: Steuern Sie Container mit Leichtigkeit.
- **Prozess-Management**: Systemprozesse anzeigen und verwalten.
- **Systemd-Dienste**: Systemd-Dienste starten, stoppen und überwachen.
- **Netzwerk-Tools**: iPerf-Tests, Ping und Wake-on-LAN.
- **Snippets**: Benutzerdefinierte Shell-Befehle speichern und ausführen.
## Unterstützte Plattformen
Server Box ist wahrhaft plattformübergreifend:
- **Mobil**: iOS und Android
- **Desktop**: macOS, Linux und Windows
## Lizenz
Dieses Projekt ist unter der AGPL v3 lizenziert. Der Quellcode ist auf [GitHub](https://github.com/lollipopkit/flutter_server_box) verfügbar.

View File

@@ -0,0 +1,80 @@
---
title: Desktop-Funktionen
description: Spezifische Funktionen für macOS, Linux und Windows
---
Server Box bietet auf Desktop-Plattformen zusätzliche Produktivitätsfunktionen.
## macOS
### Menüleisten-Integration
- Schneller Serverstatus in der Menüleiste
- Serverzugriff mit einem Klick
- Kompaktmodus für minimale Ablenkung
- Natives macOS-Menüleistenstyling
### Beständigkeit des Fensterzustands
- Merkt sich Fensterposition und -größe
- Wiederherstellung der vorherigen Sitzung beim Start
- Unterstützung für mehrere Monitore
### Native Funktionen
- **Titelleiste**: Option für benutzerdefinierte oder System-Titelleiste
- **Vollbildmodus**: Dedizierte Serverüberwachung
- **Tastenkombinationen**: macOS-native Tastenkürzel
- **Touch Bar** (unterstützte Geräte): Schnellaktionen
## Linux
### Native Integration
- Unterstützung für System-Tray
- Integration von Desktop-Benachrichtigungen
- Integration der Dateiauswahl
### Fensterverwaltung
- Unterstützung für X11 und Wayland
- Freundlich gegenüber Tiling-Window-Managern
- Option für benutzerdefinierte Fensterdekorationen
## Windows
### Funktionen
- System-Tray-Integration
- Jump List Schnellaktionen
- Native Fenstersteuerung
- Option für Autostart beim Booten
## Plattformübergreifende Desktop-Funktionen
### Tastenkombinationen
- **Cmd/Ctrl + N**: Neuer Server
- **Cmd/Ctrl + W**: Tab schließen
- **Cmd/Ctrl + T**: Neuer Terminal-Tab
- **Cmd/Ctrl + ,**: Einstellungen
### Themes
- Helles Theme
- Dunkles Theme
- AMOLED Theme (reines Schwarz)
- System-Theme (folgt dem Betriebssystem)
### Mehrere Fenster
- Öffnen mehrerer Server in separaten Fenstern
- Tabs in ein neues Fenster ziehen
- Serverstatistiken Seite an Seite vergleichen
### Vorteile gegenüber Mobile
- Größerer Bildschirm für die Überwachung
- Volle Tastatur für das Terminal
- Schnellere Dateioperationen
- Besseres Multitasking

View File

@@ -0,0 +1,77 @@
---
title: Mobile Funktionen
description: Spezifische Funktionen für iOS und Android
---
Server Box bietet mehrere mobile-spezifische Funktionen für iOS- und Android-Geräte.
## Biometrische Authentifizierung
Sichern Sie Ihre Server mit biometrischer Authentifizierung:
- **iOS**: Face ID oder Touch ID
- **Android**: Fingerabdruck-Authentifizierung
Aktivieren Sie dies unter Einstellungen > Sicherheit > Biometrische Authentifizierung.
## Startbildschirm-Widgets
Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm für eine schnelle Überwachung hinzu.
### iOS
- Auf den Startbildschirm lange drücken
- Auf **+** tippen, um ein Widget hinzuzufügen
- Nach "Server Box" suchen
- Widget-Größe wählen:
- Klein: Status eines einzelnen Servers
- Mittel: Mehrere Server
- Groß: Detaillierte Informationen
### Android
- Auf den Startbildschirm lange drücken
- Auf **Widgets** tippen
- "Server Box" finden
- Widget-Typ auswählen
## Hintergrundbetrieb
### Android
Verbindungen im Hintergrund aufrechterhalten:
- Aktivieren unter Einstellungen > Erweitert > Hintergrundbetrieb
- Erfordert Ausschluss von der Akku-Optimierung
- Permanente Benachrichtigungen für aktive Verbindungen
### iOS
Es gelten Hintergrundbeschränkungen:
- Verbindungen können im Hintergrund pausieren
- Schnelle Wiederverbindung bei Rückkehr zur App
- Unterstützung für Hintergrundaktualisierung
## Push-Benachrichtigungen
Erhalten Sie Benachrichtigungen für:
- Server-Offline-Alarme
- Warnungen bei hoher Ressourcenauslastung
- Alarme bei Abschluss von Aufgaben
Konfigurieren unter Einstellungen > Benachrichtigungen.
## Mobile UI-Funktionen
- **Pull to Refresh**: Serverstatus aktualisieren
- **Wischgesten**: Schnelle Serveroperationen
- **Querformat**: Besseres Terminal-Erlebnis
- **Virtuelle Tastatur**: Terminal-Shortcuts
## Datei-Integration
- **Dateien-App (iOS)**: Direkter SFTP-Zugriff aus Dateien
- **Storage Access Framework (Android)**: Dateien mit anderen Apps teilen
- **Dokumentenauswahl**: Einfache Dateiauswahl

View File

@@ -0,0 +1,214 @@
---
title: Architektur-Übersicht
description: High-Level-Anwendungsarchitektur
---
Server Box folgt einer Schichtarchitektur mit klarer Trennung der Belange (Separation of Concerns).
## Architektur-Schichten
```
┌─────────────────────────────────────────────────┐
│ Präsentationsschicht (UI) │
│ lib/view/page/, lib/view/widget/ │
│ - Seiten, Widgets, Controller │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Business-Logik-Schicht │
│ lib/data/provider/ │
│ - Riverpod Provider, State Notifier │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Datenzugriffsschicht │
│ lib/data/store/, lib/data/model/ │
│ - Hive Stores, Datenmodelle │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Externe Integrationsschicht │
│ - SSH (dartssh2), Terminal (xterm), SFTP │
│ - Plattformspezifischer Code (iOS, Android etc.)│
└─────────────────────────────────────────────────┘
```
## Anwendungsgrundlagen
### Haupteinstiegspunkt
`lib/main.dart` initialisiert die App:
```dart
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
```
### Root-Widget
`MyApp` bietet:
- **Theme-Management**: Umschalten zwischen hellem/dunklem Theme
- **Routing-Konfiguration**: Navigationsstruktur
- **Provider Scope**: Wurzel für Dependency Injection
### Startseite
`HomePage` dient als Navigationszentrum:
- **Tab-Interface**: Server, Snippet, Container, SSH
- **Zustandsverwaltung**: Zustand pro Tab
- **Navigation**: Funktionszugriff
## Kernsysteme
### Zustandsverwaltung: Riverpod
**Warum Riverpod?**
- Sicherheit zur Kompilierzeit
- Einfache Testbarkeit
- Keine Abhängigkeit vom Build-Kontext
- Funktioniert plattformübergreifend
**Verwendete Provider-Typen:**
- `StateProvider`: Einfacher veränderlicher Zustand
- `AsyncNotifierProvider`: Lade-/Fehler-/Datenzustände
- `StreamProvider`: Echtzeit-Datenströme
- Future Provider: Einmalige asynchrone Operationen
### Datenpersistenz: Hive CE
**Warum Hive CE?**
- Keine Abhängigkeiten von nativem Code
- Schneller Key-Value-Speicher
- Typsicher durch Codegenerierung
- Keine manuellen Feld-Annotationen erforderlich
**Stores:**
- `SettingStore`: App-Einstellungen
- `ServerStore`: Server-Konfigurationen
- `SnippetStore`: Befehls-Snippets
- `KeyStore`: SSH-Schlüssel
### Immutable Modelle: Freezed
**Vorteile:**
- Unveränderlichkeit zur Kompilierzeit
- Union Types für Zustände
- Integrierte JSON-Serialisierung
- CopyWith-Erweiterungen
## Cross-Plattform-Strategie
### Plugin-System
Flutter-Plugins ermöglichen die Plattformintegration:
| Plattform | Integrationsmethode |
|-----------|--------------------|
| iOS | CocoaPods, Swift/Obj-C |
| Android | Gradle, Kotlin/Java |
| macOS | CocoaPods, Swift |
| Linux | CMake, C++ |
| Windows | CMake, C# |
### Plattformspezifische Funktionen
**Nur iOS:**
- Startbildschirm-Widgets
- Live-Aktivitäten
- Apple Watch Begleit-App
**Nur Android:**
- Hintergrunddienst
- Push-Benachrichtigungen
- Dateisystemzugriff
**Nur Desktop:**
- Menüleisten-Integration
- Mehrere Fenster
- Benutzerdefinierte Titelleiste
## Eigene Abhängigkeiten
### dartssh2 Fork
Erweiterter SSH-Client mit:
- Besserer mobiler Unterstützung
- Verbesserter Fehlerbehandlung
- Leistungsoptimierungen
### xterm.dart Fork
Terminal-Emulator mit:
- Für Mobilgeräte optimiertem Rendering
- Unterstützung für Touch-Gesten
- Integration der virtuellen Tastatur
### fl_lib
Paket mit gemeinsamen Dienstprogrammen:
- Gemeinsame Widgets
- Erweiterungen
- Hilfsfunktionen
## Build-System
### fl_build Paket
Eigenes Build-System für:
- Multi-Plattform-Builds
- Code-Signierung
- Asset-Bündelung
- Versionsverwaltung
### Build-Prozess
```
make.dart (Version) → fl_build (Build) → Plattform-Output
```
1. **Pre-build**: Berechnung der Version aus Git
2. **Build**: Kompilierung für die Zielplattform
3. **Post-build**: Paketierung und Signierung
## Beispiel für den Datenfluss
### Aktualisierung des Serverstatus
```
1. Timer löst aus →
2. Provider ruft Service auf →
3. Service führt SSH-Befehl aus →
4. Antwort wird in Modell geparst →
5. Zustand wird aktualisiert →
6. UI wird mit neuen Daten neu aufgebaut
```
### Ablauf einer Benutzeraktion
```
1. Benutzer tippt auf Schaltfläche →
2. Widget ruft Provider-Methode auf →
3. Provider aktualisiert Zustand →
4. Zustandsänderung löst Neuaufbau aus →
5. Neuer Zustand spiegelt sich in der UI wider
```
## Sicherheitsarchitektur
### Datenschutz
- **Passwörter**: Verschlüsselt mit flutter_secure_storage
- **SSH-Schlüssel**: Verschlüsselt gespeichert
- **Host-Fingerabdrücke**: Sicher gespeichert
- **Sitzungsdaten**: Werden nicht persistiert
### Verbindungssicherheit
- **Host-Key-Verifizierung**: MITM-Erkennung
- **Verschlüsselung**: Standard-SSH-Verschlüsselung
- **Kein Klartext**: Sensible Daten werden niemals im Klartext gespeichert

View File

@@ -0,0 +1,490 @@
---
title: SFTP-System
description: Wie der SFTP-Dateibrowser funktioniert
---
Das SFTP-System bietet Dateimanagement-Funktionen über SSH.
## Architektur
```
┌─────────────────────────────────────────────┐
│ SFTP UI Schicht │
│ - Dateibrowser (remote) │
│ - Dateibrowser (lokal) │
│ - Transfer-Warteschlange │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SFTP-Zustandsverwaltung │
│ - sftpProvider │
│ - Pfadverwaltung │
│ - Operations-Warteschlange │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SFTP-Protokollschicht │
│ - SSH-Subsystem │
│ - Dateioperationen │
│ - Verzeichnisauflistung │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SSH-Transport │
│ - Sicherer Kanal │
│ - Daten-Streaming │
└─────────────────────────────────────────────┘
```
## Verbindungsaufbau
### Erstellung des SFTP-Clients
```dart
Future<SftpClient> createSftpClient(Spi spi) async {
// 1. SSH-Client abrufen (wiederverwenden, falls verfügbar)
final sshClient = await genClient(spi);
// 2. SFTP-Subsystem öffnen
final sftp = await sshClient.openSftp();
return sftp;
}
```
### Wiederverwendung von Verbindungen
SFTP verwendet bestehende SSH-Verbindungen wieder:
```dart
class ServerProvider {
SSHClient? _sshClient;
SftpClient? _sftpClient;
Future<SftpClient> getSftpClient(String spiId) async {
_sftpClient ??= await _sshClient!.openSftp();
return _sftpClient!;
}
}
```
## Dateisystem-Operationen
### Verzeichnisauflistung
```dart
Future<List<SftpFile>> listDirectory(String path) async {
final sftp = await getSftpClient(spiId);
// Verzeichnis auflisten
final files = await sftp.listDir(path);
// Basierend auf Einstellungen sortieren
files.sort((a, b) {
switch (sortOption) {
case SortOption.name:
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
case SortOption.size:
return a.size.compareTo(b.size);
case SortOption.time:
return a.modified.compareTo(b.modified);
}
});
// Ordner zuerst, falls aktiviert
if (showFoldersFirst) {
final dirs = files.where((f) => f.isDirectory);
final regular = files.where((f) => !f.isDirectory);
return [...dirs, ...regular];
}
return files;
}
```
### Dateimetadaten
```dart
class SftpFile {
final String name;
final String path;
final int size; // Bytes
final int modified; // Unix-Zeitstempel
final String permissions; // z.B. "rwxr-xr-x"
final String owner;
final String group;
final bool isDirectory;
final bool isSymlink;
String get sizeFormatted => formatBytes(size);
String get modifiedFormatted => formatDate(modified);
}
```
## Dateioperationen
### Hochladen
```dart
Future<void> uploadFile(
String localPath,
String remotePath,
) async {
final sftp = await getSftpClient(spiId);
// Anfrage erstellen
final req = SftpReq(
spi: spi,
remotePath: remotePath,
localPath: localPath,
type: SftpReqType.upload,
);
// Zur Warteschlange hinzufügen
_transferQueue.add(req);
// Transfer mit Fortschritt ausführen
final file = File(localPath);
final size = await file.length();
final stream = file.openRead();
await sftp.upload(
stream: stream,
toPath: remotePath,
onProgress: (transferred) {
_updateProgress(req, transferred, size);
},
);
// Fertigstellen
_transferQueue.remove(req);
}
```
### Herunterladen
```dart
Future<void> downloadFile(
String remotePath,
String localPath,
) async {
final sftp = await getSftpClient(spiId);
// Lokale Datei erstellen
final file = File(localPath);
final sink = file.openWrite();
// Herunterladen mit Fortschritt
final stat = await sftp.stat(remotePath);
await sftp.download(
fromPath: remotePath,
toSink: sink,
onProgress: (transferred) {
_updateProgress(
SftpReq(...),
transferred,
stat.size,
);
},
);
await sink.close();
}
```
### Berechtigungen bearbeiten
```dart
Future<void> setPermissions(
String path,
String permissions,
) async {
final sftp = await getSftpClient(spiId);
// Berechtigungen parsen (z.B. "rwxr-xr-x" oder "755")
final mode = parsePermissions(permissions);
// Über SSH-Befehl setzen (zuverlässiger als SFTP)
final ssh = await getSshClient(spiId);
await ssh.exec('chmod $mode "$path"');
}
```
## Pfadverwaltung
### Pfadstruktur
```dart
class PathWithPrefix {
final String prefix; // z.B. "/home/user"
final String path; // Relativ oder absolut
String get fullPath {
if (path.startsWith('/')) {
return path; // Absoluter Pfad
}
return '$prefix/$path'; // Relativer Pfad
}
PathWithPrefix cd(String subPath) {
return PathWithPrefix(
prefix: fullPath,
path: subPath,
);
}
}
```
### Navigationsverlauf
```dart
class PathHistory {
final List<String> _history = [];
int _index = -1;
void push(String path) {
// Vorwärtsverlauf entfernen
_history.removeRange(_index + 1, _history.length);
_history.add(path);
_index = _history.length - 1;
}
String? back() {
if (_index > 0) {
_index--;
return _history[_index];
}
return null;
}
String? forward() {
if (_index < _history.length - 1) {
_index++;
return _history[_index];
}
return null;
}
}
```
## Transfersystem
### Transfer-Anfrage
```dart
class SftpReq {
final Spi spi;
final String remotePath;
final String localPath;
final SftpReqType type;
final DateTime createdAt;
int? totalBytes;
int? transferredBytes;
String? error;
}
```
### Fortschrittsverfolgung
```dart
class TransferProgress {
final SftpReq request;
final int total;
final int transferred;
final DateTime startTime;
double get percentage => (transferred / total) * 100;
Duration get elapsed => DateTime.now().difference(startTime);
String get speedFormatted {
final bytesPerSecond = transferred / elapsed.inSeconds;
return formatSpeed(bytesPerSecond);
}
}
```
### Warteschlangen-Management
```dart
class TransferQueue {
final List<SftpReq> _queue = [];
final Map<String, TransferProgress> _progress = {};
int _concurrent = 3; // Max. gleichzeitige Transfers
Future<void> process() async {
final active = _progress.values.where((p) => p.isInProgress);
if (active.length >= _concurrent) return;
final pending = _queue.where((r) => !_progress.containsKey(r.id));
for (final req in pending.take(_concurrent - active.length)) {
_executeTransfer(req);
}
}
Future<void> _executeTransfer(SftpReq req) async {
try {
_progress[req.id] = TransferProgress.inProgress(req);
if (req.type == SftpReqType.upload) {
await uploadFile(req.localPath, req.remotePath);
} else {
await downloadFile(req.remotePath, req.localPath);
}
_progress[req.id] = TransferProgress.completed(req);
} catch (e) {
_progress[req.id] = TransferProgress.failed(req, e);
}
}
}
```
## Lokales Speichermuster
### Download-Cache
Heruntergeladene Dateien werden gespeichert unter:
```dart
String getLocalDownloadPath(String spiId, String remotePath) {
final normalized = remotePath.replaceAll('/', '_');
return 'Paths.file/$spiId/$normalized';
}
```
Beispiel:
- Remote: `/var/log/nginx/access.log`
- spiId: `server-123`
- Lokal: `Paths.file/server-123/_var_log_nginx_access.log`
## Dateibearbeitung
### Bearbeitungs-Workflow
```dart
Future<void> editFile(String path) async {
final sftp = await getSftpClient(spiId);
// 1. Größe prüfen
final stat = await sftp.stat(path);
if (stat.size > editorMaxSize) {
showWarning('Datei zu groß für den integrierten Editor');
return;
}
// 2. Temporär herunterladen
final temp = await downloadToTemp(path);
// 3. Im Editor öffnen
final content = await openEditor(temp.path);
// 4. Zurück hochladen
await uploadFile(temp.path, path);
// 5. Aufräumen
await temp.delete();
}
```
### Integration externer Editoren
```dart
Future<void> editInExternalEditor(String path) async {
final ssh = await getSshClient(spiId);
// Terminal mit Editor öffnen
final editor = getSetting('sftpEditor', 'vim');
await ssh.exec('$editor "$path"');
// Benutzer bearbeitet im Terminal
// Nach dem Speichern die SFTP-Ansicht aktualisieren
}
```
## Fehlerbehandlung
### Berechtigungsfehler
```dart
try {
await sftp.upload(...);
} on SftpPermissionException {
showError('Berechtigung verweigert: ${stat.path}');
showHint('Prüfen Sie Dateiberechtigungen und Eigentümerschaft');
}
```
### Verbindungsfehler
```dart
try {
await sftp.listDir(path);
} on SftpConnectionException {
showError('Verbindung verloren');
await reconnect();
}
```
### Speicherplatzfehler
```dart
try {
await sftp.upload(...);
} on SftpNoSpaceException {
showError('Festplatte auf dem Remote-Server voll');
}
```
## Leistungsoptimierungen
### Verzeichnis-Caching
```dart
class DirectoryCache {
final Map<String, CachedDirectory> _cache = {};
final Duration ttl = Duration(minutes: 5);
Future<List<SftpFile>> list(String path) async {
final cached = _cache[path];
if (cached != null && !cached.isExpired) {
return cached.files;
}
final files = await sftp.listDir(path);
_cache[path] = CachedDirectory(files);
return files;
}
}
```
### Lazy Loading
Für große Verzeichnisse (>1000 Einträge):
```dart
List<SftpFile> loadPage(String path, int page, int pageSize) {
final all = cache[path] ?? [];
final start = page * pageSize;
final end = start + pageSize;
return all.sublist(start, end.clamp(0, all.length));
}
```
### Paginierung
```dart
class PaginatedDirectory {
static const pageSize = 100;
Future<List<SftpFile>> getPage(int page) async {
final offset = page * pageSize;
return await sftp.listDir(
path,
offset: offset,
limit: pageSize,
);
}
}
```

View File

@@ -0,0 +1,305 @@
---
title: SSH-Verbindung
description: Wie SSH-Verbindungen aufgebaut und verwaltet werden
---
Verständnis der SSH-Verbindungen in Server Box.
## Verbindungsablauf
```text
Benutzereingabe → Spi-Konfiguration → genClient() → SSH-Client → Sitzung
```
### Schritt 1: Konfiguration
Das `Spi` (Server Parameter Info) Modell enthält:
```dart
class Spi {
String id; // eindeutige ID / unique identifier
String name; // Servername
String ip; // IP-Adresse
int port; // SSH-Port (Standard 22)
String user; // Benutzername
String? pwd; // Passwort (verschlüsselt)
String? keyId; // SSH-Schlüssel-ID
String? jumpId; // Jump-Server-ID
String? alterUrl; // Alternative URL
}
```
### Schritt 2: Client-Generierung
`genClient(spi)` erstellt den SSH-Client:
```dart
Future<SSHClient> genClient(Spi spi) async {
// 1. Socket aufbauen
var socket = await connect(spi.ip, spi.port);
// 2. Alternative URL versuchen, falls fehlgeschlagen
if (socket == null && spi.alterUrl != null) {
socket = await connect(spi.alterUrl, spi.port);
}
if (socket == null) {
throw ConnectionException('Unable to connect');
}
// 3. Authentifizieren
final client = SSHClient(
socket: socket,
username: spi.user,
onPasswordRequest: () => spi.pwd,
onIdentityRequest: () => loadKey(spi.keyId),
);
// 4. Host-Key verifizieren
await verifyHostKey(client, spi);
return client;
}
```
### Schritt 3: Jump-Server (falls konfiguriert)
Für Jump-Server, rekursive Verbindung:
```dart
if (spi.jumpId != null) {
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
final forwarded = await jumpClient.forwardLocal(
spi.ip,
spi.port,
);
// Über weitergeleiteten Socket verbinden
}
```
## Authentifizierungsmethoden
### Passwort-Authentifizierung
```dart
onPasswordRequest: () => spi.pwd
```
- Passwort verschlüsselt in Hive gespeichert
- Bei Verbindung entschlüsselt
- Zur Verifizierung an den Server gesendet
### Private-Key-Authentifizierung
```dart
onIdentityRequest: () async {
final key = await KeyStore.get(spi.keyId);
return decyptPem(key.pem, key.password);
}
```
**Schlüssel-Ladeprozess:**
1. Verschlüsselten Schlüssel aus `KeyStore` abrufen
2. Passwort entschlüsseln (Biometrie/Eingabeaufforderung)
3. PEM-Format parsen
4. Zeilenenden standardisieren (LF)
5. Zur Authentifizierung zurückgeben
### Tastatur-Interaktiv (Keyboard-Interactive)
```dart
onUserInfoRequest: (instructions) async {
// Challenge-Response handhaben
return responses;
}
```
Unterstützt:
- Passwort-Authentifizierung
- OTP-Token
- Zwei-Faktor-Authentifizierung (2FA)
## Host-Key-Verifizierung
### Warum Host-Keys verifizieren?
Verhindert **Man-in-the-Middle (MITM)** Angriffe, indem sichergestellt wird, dass Sie sich mit demselben Server verbinden.
### Speicherformat
```text
{spi.id}::{keyType}
```
Beispiel:
```text
mein-server::ssh-ed25519
mein-server::ecdsa-sha2-nistp256
```
### Fingerabdruck-Formate
**MD5 Hex:**
```text
aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
```
**Base64:**
```text
SHA256:AbCdEf1234567890...=
```
### Verifizierungsablauf
```dart
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
final key = await client.hostKey;
final keyType = key.type;
final fingerprint = md5Hex(key); // oder base64
final stored = SettingStore.sshKnownHostsFingerprints
['${spi.id}::$keyType'];
if (stored == null) {
// Neuer Host - Benutzer fragen
final trust = await promptUser(
'Unbekannter Host',
'Fingerabdruck: $fingerprint',
);
if (trust) {
SettingStore.sshKnownHostsFingerprints
['${spi.id}::$keyType'] = fingerprint;
}
} else if (stored != fingerprint) {
// Geändert - Benutzer warnen
await warnUser(
'Host-Key geändert!',
'Möglicher MITM-Angriff',
);
}
}
```
## Sitzungsverwaltung
### Verbindungs-Pooling
Aktive Clients werden im `ServerProvider` verwaltet:
```dart
class ServerProvider {
final Map<String, SSHClient> _clients = {};
SSHClient getClient(String spiId) {
return _clients[spiId] ??= connect(spiId);
}
}
```
### Keep-Alive
Verbindung bei Inaktivität aufrechterhalten:
```dart
Timer.periodic(
Duration(seconds: 30),
(_) => client.sendKeepAlive(),
);
```
### Automatische Wiederverbindung
Bei Verbindungsverlust:
```dart
client.onError.listen((error) async {
await Future.delayed(Duration(seconds: 5));
reconnect();
});
```
## Lebenszyklus einer Verbindung
```text
┌─────────────┐
│ Initial │
└──────┬──────┘
│ connect()
┌─────────────┐
│ Verbinden │ ←──┐
└──────┬──────┘ │
│ Erfolg │
↓ │ Fehler (Retry)
┌─────────────┐ │
│ Verbunden │───┘
└──────┬──────┘
┌─────────────┐
│ Aktiv │ ──→ Befehle senden
└──────┬──────┘
↓ (Fehler/Trennung)
┌─────────────┐
│ Getrennt │
└─────────────┘
```
## Fehlerbehandlung
### Verbindungs-Timeout
```dart
try {
await client.connect().timeout(
Duration(seconds: 30),
);
} on TimeoutException {
throw ConnectionException('Verbindungs-Timeout');
}
```
### Authentifizierungsfehler
```dart
onAuthFail: (error) {
if (error.contains('password')) {
return 'Ungültiges Passwort';
} else if (error.contains('key')) {
return 'Ungültiger SSH-Schlüssel';
}
return 'Authentifizierung fehlgeschlagen';
}
```
### Host-Key-Abweichung
```dart
onHostKeyMismatch: (stored, current) {
showSecurityWarning(
'Host-Key hat sich geändert!',
'Möglicher MITM-Angriff',
);
}
```
## Leistungsaspekte
### Wiederverwendung von Verbindungen
- Wiederverwendung von Clients über Funktionen hinweg
- Nicht unnötig trennen/wiederverbinden
- Verbindungs-Pooling für gleichzeitige Operationen
### Optimale Einstellungen
- **Timeout**: 30 Sekunden (anpassbar)
- **Keep-Alive**: Alle 30 Sekunden
- **Wiederholungsverzögerung**: 5 Sekunden
### Netzwerkeffizienz
- Einzelne Verbindung für mehrere Operationen
- Befehle pipelinen, wenn möglich
- Das Öffnen mehrerer Verbindungen vermeiden

View File

@@ -0,0 +1,221 @@
---
title: Zustandsverwaltung
description: Wie der Zustand mit Riverpod verwaltet wird
---
Verständnis der Architektur zur Zustandsverwaltung in Server Box.
## Warum Riverpod?
**Hauptvorteile:**
- **Sicherheit zur Kompilierzeit**: Fehler werden bereits beim Kompilieren abgefangen
- **Kein BuildContext erforderlich**: Zugriff auf den Zustand von überall aus
- **Einfache Testbarkeit**: Provider können leicht isoliert getestet werden
- **Codegenerierung**: Weniger Boilerplate, typsicher
## Provider-Architektur
```
┌─────────────────────────────────────────────┐
│ UI-Schicht (Widgets) │
│ - ConsumerWidget / ConsumerStatefulWidget │
│ - ref.watch() / ref.read() │
└─────────────────────────────────────────────┘
↓ beobachtet (watches)
┌─────────────────────────────────────────────┐
│ Provider-Schicht │
│ - @riverpod Annotationen │
│ - Generierte *.g.dart Dateien │
└─────────────────────────────────────────────┘
↓ nutzt (uses)
┌─────────────────────────────────────────────┐
│ Service- / Store-Schicht │
│ - Business-Logik │
│ - Datenzugriff │
└─────────────────────────────────────────────┘
```
## Verwendete Provider-Typen
### 1. StateProvider (Einfacher Zustand)
Für einfachen, beobachtbaren Zustand:
```dart
@riverpod
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeMode build() {
// Aus Einstellungen laden
return SettingStore.themeMode;
}
void setTheme(ThemeMode mode) {
state = mode;
SettingStore.themeMode = mode; // Persistieren
}
}
```
**Verwendung:**
```dart
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themeNotifierProvider);
return Text('Theme: $theme');
}
}
```
### 2. AsyncNotifierProvider (Asynchroner Zustand)
Für Daten, die asynchron geladen werden:
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
// Initiales Laden
return await fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await fetchStatus(server);
});
}
}
```
**Verwendung:**
```dart
final status = ref.watch(serverStatusProvider(server));
status.when(
data: (data) => StatusWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### 3. StreamProvider (Echtzeit-Daten)
Für kontinuierliche Datenströme:
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
final client = ref.watch(sshClientProvider(server));
final stream = client.monitorCpu();
// Ressourcen freigeben, wenn nicht mehr beobachtet
ref.onDispose(() {
client.stopMonitoring();
});
return stream;
}
```
**Verwendung:**
```dart
final cpu = ref.watch(cpuUsageProvider(server));
cpu.when(
data: (usage) => CpuChart(usage),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error),
)
```
### 4. Family Provider (Parametrisiert)
Provider, die Parameter akzeptieren:
```dart
@riverpod
Future<List<Container>> containers(ContainersRef ref, Server server) async {
final client = await ref.watch(sshClientProvider(server).future);
return await client.listContainers();
}
```
**Verwendung:**
```dart
final containers = ref.watch(containersProvider(server));
// Verschiedene Server = verschiedene gecachte Zustände
final containers2 = ref.watch(containersProvider(server2));
```
## Muster für Zustandsaktualisierungen
### Direkte Zustandsaktualisierung
```dart
ref.read(settingsProvider.notifier).updateTheme(darkMode);
```
### Berechneter Zustand (Computed State)
```dart
@riverpod
int totalServers(TotalServersRef ref) {
final servers = ref.watch(serversProvider);
return servers.length;
}
```
### Abgeleiteter Zustand (Derived State)
```dart
@riverpod
List<Server> onlineServers(OnlineServersRef ref) {
final all = ref.watch(serversProvider);
return all.where((s) => s.isOnline).toList();
}
```
## Server-spezifischer Zustand
### Pro-Server Provider
Jeder Server hat einen isolierten Zustand:
```dart
@riverpod
class ServerProvider extends _$ServerProvider {
@override
ServerState build(Server server) {
return ServerState.disconnected();
}
Future<void> connect() async {
state = ServerState.connecting();
try {
final client = await genClient(server.spi);
state = ServerState.connected(client);
} catch (e) {
state = ServerState.error(e.toString());
}
}
}
```
## Leistungsoptimierungen
- **Provider Keep-Alive**: `@Riverpod(keepAlive: true)` verwenden, um automatische Entsorgung ohne Listener zu verhindern.
- **Selektives Beobachten**: `select` verwenden, um nur bestimmte Teile des Zustands zu beobachten.
- **Provider Caching**: Family Provider cachen Ergebnisse pro Parameter.
## Best Practices
1. **Provider lokal platzieren**: In der Nähe der Widgets, die sie nutzen.
2. **Codegenerierung nutzen**: Immer `@riverpod` verwenden.
3. **Provider fokussiert halten**: Jedes Provider sollte nur eine Aufgabe haben.
4. **Ladezustände behandeln**: AsyncValue-Zustände immer vollständig behandeln.
5. **Ressourcen entsorgen**: `ref.onDispose()` für Aufräumarbeiten nutzen.
6. **Tiefe Provider-Bäume vermeiden**: Den Provider-Graphen flach halten.

View File

@@ -0,0 +1,198 @@
---
title: Terminal-Implementierung
description: Wie das SSH-Terminal intern funktioniert
---
Das SSH-Terminal ist eine der komplexesten Funktionen, aufgebaut auf einem benutzerdefinierten xterm.dart-Fork.
## Architektur-Übersicht
```
┌─────────────────────────────────────────────┐
│ Terminal UI Schicht │
│ - Tab-Management │
│ - Virtuelle Tastatur │
│ - Textauswahl │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ xterm.dart Emulator │
│ - PTY (Pseudo Terminal) │
│ - VT100/ANSI Emulation │
│ - Rendering-Engine │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ SSH-Client-Schicht │
│ - SSH-Sitzung │
│ - Kanalverwaltung │
│ - Daten-Streaming │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Remote-Server │
│ - Shell-Prozess │
│ - Befehlsausführung │
└─────────────────────────────────────────────┘
```
## Lebenszyklus einer Terminal-Sitzung
### 1. Sitzungserstellung
```dart
Future<TerminalSession> createSession(Spi spi) async {
// 1. SSH-Client abrufen
final client = await genClient(spi);
// 2. PTY erstellen
final pty = await client.openPty(
term: 'xterm-256color',
cols: 80,
rows: 24,
);
// 3. Terminal-Emulator initialisieren
final terminal = Terminal(
backend: PtyBackend(pty),
);
// 4. Resize-Handler einrichten
terminal.onResize.listen((size) {
pty.resize(size.cols, size.rows);
});
return TerminalSession(
terminal: terminal,
pty: pty,
client: client,
);
}
```
### 2. Terminal-Emulation
Der xterm.dart-Fork bietet:
**VT100/ANSI Emulation:**
- Cursor-Bewegung
- Farben (256-Farben-Unterstützung)
- Textattribute (fett, unterstrichen, usw.)
- Scroll-Bereiche
- Alternativer Bildschirmpuffer
**Rendering:**
- Zeilenbasiertes Rendering
- Unterstützung für bidirektionalen Text
- Unicode/Emoji Unterstützung
- Optimierte Redraws
### 3. Datenfluss
```
Benutzereingabe
Virtuelle Tastatur / Physische Tastatur
Terminal-Emulator (Taste → Escape-Sequenz)
SSH-Kanal (senden)
Remote PTY
Remote Shell
Befehlsausgabe
SSH-Kanal (empfangen)
Terminal-Emulator (Analyse von ANSI-Codes)
Rendering auf dem Bildschirm
```
## Multi-Tab System
### Tab-Management
Tabs behalten ihren Zustand bei Navigationswechseln bei:
- SSH-Verbindung bleibt aktiv
- Terminalzustand bleibt erhalten
- Scroll-Puffer bleibt bestehen
- Eingabeverlauf bleibt erhalten
## Virtuelle Tastatur
### Plattformspezifische Implementierung
**iOS:**
- UIView-basierte benutzerdefinierte Tastatur
- Umschaltbar mit Tastatur-Button
- Automatisches Ein-/Ausblenden basierend auf dem Fokus
**Android:**
- Benutzerdefinierte Eingabemethode
- Integriert in die Systemtastatur
- Schnellaktionstasten
### Tastatur-Buttons
| Button | Aktion |
|--------|--------|
| **Umschalten** | Systemtastatur ein-/ausblenden |
| **Ctrl** | Ctrl-Modifikator senden |
| **Alt** | Alt-Modifikator senden |
| **SFTP** | Aktuelles Verzeichnis öffnen |
| **Zwischenablage** | Kontextsensitive Kopieren/Einfügen |
| **Snippets** | Snippet ausführen |
## Textauswahl
1. **Langes Drücken**: Auswahlmodus aktivieren
2. **Ziehen**: Auswahl erweitern
3. **Loslassen**: In die Zwischenablage kopieren
## Schriftart und Dimensionen
### Größenberechnung
```dart
class TerminalDimensions {
static Size calculate(double fontSize, Size screenSize) {
final charWidth = fontSize * 0.6; // Monospace-Seitenverhältnis
final charHeight = fontSize * 1.2;
final cols = (screenSize.width / charWidth).floor();
final rows = (screenSize.height / charHeight).floor();
return Size(cols.toDouble(), rows.toDouble());
}
}
```
### Pinch-to-Zoom
```dart
GestureDetector(
onScaleStart: () => _baseFontSize = currentFontSize,
onScaleUpdate: (details) {
final newFontSize = _baseFontSize * details.scale;
resize(newFontSize);
},
)
```
## Farbschema
- **Hell (Light)**: Heller Hintergrund, dunkler Text
- **Dunkel (Dark)**: Dunkler Hintergrund, heller Text
- **AMOLED**: Rein schwarzer Hintergrund
## Leistungsoptimierungen
- **Dirty Rectangle**: Nur geänderte Regionen neu zeichnen
- **Zeilen-Caching**: Gerenderte Zeilen cachen
- **Lazy Scrolling**: Virtuelles Scrollen für lange Puffer
- **Batch-Updates**: Mehrere Schreibvorgänge zusammenfassen
- **Kompression**: Kompression des Scroll-Puffers
- **Debouncing**: Debouncing für schnelle Eingaben

View File

@@ -0,0 +1,45 @@
---
title: Schnellstart
description: In wenigen Minuten mit Server Box loslegen
---
Folgen Sie dieser Schnellstartanleitung, um sich mit Ihrem ersten Server zu verbinden und mit der Überwachung zu beginnen.
## Schritt 1: Einen Server hinzufügen
1. Öffnen Sie Server Box
2. Tippen Sie auf die Schaltfläche **+**, um einen neuen Server hinzuzufügen
3. Geben Sie die Serverinformationen ein:
- **Name**: Ein Anzeigename für Ihren Server
- **Host**: IP-Adresse oder Domainname
- **Port**: SSH-Port (Standard: 22)
- **Benutzer**: SSH-Benutzername
- **Passwort oder Schlüssel**: Authentifizierungsmethode
4. Tippen Sie auf **Speichern**, um den Server hinzuzufügen
## Schritt 2: Verbinden und Überwachen
1. Tippen Sie auf Ihre Serverkarte, um die Verbindung herzustellen
2. Die App baut eine SSH-Verbindung auf
3. Sie sehen den Echtzeit-Status für:
- CPU-Auslastung
- Arbeitsspeicher (RAM) und Swap
- Festplattenbelegung
- Netzwerkgeschwindigkeit
## Schritt 3: Funktionen erkunden
Sobald die Verbindung hergestellt ist, können Sie:
- **Terminal öffnen**: Tippen Sie auf die Terminal-Schaltfläche für vollen SSH-Zugriff
- **Dateien durchsuchen**: Verwenden Sie SFTP, um Dateien zu verwalten
- **Container verwalten**: Docker-Container anzeigen und steuern
- **Prozesse anzeigen**: Laufende Prozesse überprüfen
- **Snippets ausführen**: Gespeicherte Befehle ausführen
## Tipps
- **Biometrische Authentifizierung**: Aktivieren Sie Face ID / Touch ID / Fingerabdruck für schnellen Zugriff (Mobilgerät)
- **Startbildschirm-Widgets**: Fügen Sie Serverstatus-Widgets zu Ihrem Startbildschirm hinzu (iOS/Android)
- **Hintergrundbetrieb**: Halten Sie Verbindungen im Hintergrund aktiv (Android)

View File

@@ -0,0 +1,86 @@
---
title: Architecture
description: Architecture patterns and design decisions
---
Server Box follows clean architecture principles with clear separation between data, domain, and presentation layers.
## Layered Architecture
```
┌─────────────────────────────────────┐
│ Presentation Layer │
│ (lib/view/page/) │
│ - Pages, Widgets, Controllers │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Business Logic Layer │
│ (lib/data/provider/) │
│ - Riverpod Providers │
│ - State Management │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Data Layer │
│ (lib/data/model/, store/) │
│ - Models, Storage, Services │
└─────────────────────────────────────┘
```
## Key Patterns
### State Management: Riverpod
- **Code Generation**: Uses `riverpod_generator` for type-safe providers
- **State Notifiers**: For mutable state with business logic
- **Async Notifiers**: For loading and error states
- **Stream Providers**: For real-time data
### Immutable Models: Freezed
- All data models use Freezed for immutability
- Union types for state representation
- Built-in JSON serialization
- CopyWith extensions for updates
### Local Storage: Hive
- **hive_ce**: Community edition of Hive
- No manual `@HiveField` or `@HiveType` needed
- Type adapters auto-generated
- Persistent key-value storage
## Dependency Injection
Services and stores are injected via:
1. **Providers**: Expose dependencies to UI
2. **GetIt**: Service location (where applicable)
3. **Constructor Injection**: Explicit dependencies
## Data Flow
```
User Action → Widget → Provider → Service/Store → Model Update → UI Rebuild
```
1. User interacts with widget
2. Widget calls provider method
3. Provider updates state via service/store
3. State change triggers UI rebuild
4. New state reflected in widget
## Custom Dependencies
The project uses several custom forks to extend functionality:
- **dartssh2**: Enhanced SSH features
- **xterm**: Terminal emulator with mobile support
- **fl_lib**: Shared UI components and utilities
## Threading
- **Isolates**: Heavy computation off main thread
- **computer package**: Multi-threading utilities
- **Async/Await**: Non-blocking I/O operations

View File

@@ -0,0 +1,116 @@
---
title: Building
description: Build instructions for different platforms
---
Server Box uses a custom build system (`fl_build`) for cross-platform builds.
## Prerequisites
- Flutter SDK (stable channel)
- Platform-specific tools (Xcode for iOS, Android Studio for Android)
- Rust toolchain (for some native dependencies)
## Development Build
```bash
# Run in development mode
flutter run
# Run on specific device
flutter run -d <device-id>
```
## Production Build
The project uses `fl_build` for building:
```bash
# Build for specific platform
dart run fl_build -p <platform>
# Available platforms:
# - ios
# - android
# - macos
# - linux
# - windows
```
## Platform-Specific Builds
### iOS
```bash
dart run fl_build -p ios
```
Requires:
- macOS with Xcode
- CocoaPods
- Apple Developer account for signing
### Android
```bash
dart run fl_build -p android
```
Requires:
- Android SDK
- Java Development Kit
- Keystore for signing
### macOS
```bash
dart run fl_build -p macos
```
### Linux
```bash
dart run fl_build -p linux
```
### Windows
```bash
dart run fl_build -p windows
```
Requires Windows with Visual Studio.
## Pre/Post Build
The `make.dart` script handles:
- Metadata generation
- Version string updates
- Platform-specific configurations
## Troubleshooting
### Clean Build
```bash
flutter clean
dart run build_runner build --delete-conflicting-outputs
flutter pub get
```
### Version Mismatch
Ensure all dependencies are compatible:
```bash
flutter pub upgrade
```
## Release Checklist
1. Update version in `pubspec.yaml`
2. Run code generation
3. Run tests
4. Build for all target platforms
5. Test on physical devices
6. Create GitHub release

View File

@@ -0,0 +1,98 @@
---
title: Code Generation
description: Using build_runner for code generation
---
Server Box heavily uses code generation for models, state management, and serialization.
## When to Run Code Generation
Run after modifying:
- Models with `@freezed` annotation
- Classes with `@JsonSerializable`
- Hive models
- Providers with `@riverpod`
- Localizations (ARB files)
## Running Code Generation
```bash
# Generate all code
dart run build_runner build --delete-conflicting-outputs
# Clean and regenerate
dart run build_runner build --delete-conflicting-outputs --clean
```
## Generated Files
### Freezed (`*.freezed.dart`)
Immutable data models with union types:
```dart
@freezed
class ServerState with _$ServerState {
const factory ServerState.connected() = Connected;
const factory ServerState.disconnected() = Disconnected;
const factory ServerState.error(String message) = Error;
}
```
### JSON Serialization (`*.g.dart`)
Generated from `json_serializable`:
```dart
@JsonSerializable()
class Server {
final String id;
final String name;
final String host;
Server({required this.id, required this.name, required this.host});
factory Server.fromJson(Map<String, dynamic> json) =>
_$ServerFromJson(json);
Map<String, dynamic> toJson() => _$ServerToJson(this);
}
```
### Riverpod Providers (`*.g.dart`)
Generated from `@riverpod` annotation:
```dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
}
```
### Hive Adapters (`*.g.dart`)
Auto-generated for Hive models (hive_ce):
```dart
@HiveType(typeId: 0)
class ServerModel {
@HiveField(0)
final String id;
}
```
## Localization Generation
```bash
flutter gen-l10n
```
Generates `lib/generated/l10n/` from `lib/l10n/*.arb` files.
## Tips
- Use `--delete-conflicting-outputs` to avoid conflicts
- Add generated files to `.gitignore`
- Never manually edit generated files

View File

@@ -0,0 +1,115 @@
---
title: State Management
description: Riverpod-based state management patterns
---
Server Box uses Riverpod with code generation for state management.
## Provider Types
### StateProvider
Simple state that can be read and written:
```dart
@riverpod
class Settings extends _$Settings {
@override
SettingsModel build() {
return SettingsModel.defaults();
}
void update(SettingsModel newSettings) {
state = newSettings;
}
}
```
### AsyncNotifierProvider
State that loads asynchronously with loading/error states:
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
return fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => fetchStatus(server));
}
}
```
### StreamProvider
Real-time data from streams:
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
return cpuService.monitor(server);
}
```
## State Patterns
### Loading States
```dart
state.when(
data: (data) => DataWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### Family Providers
Parameterized providers:
```dart
@riverpod
List<Container> containers(ContainersRef ref, Server server) {
return containerService.list(server);
}
```
### Auto-Dispose
Providers that dispose when no longer referenced:
```dart
@Riverpod(keepAlive: false)
class TempState extends _$TempState {
// ...
}
```
## Best Practices
1. **Use code generation**: Always use `@riverpod` annotation
2. **Co-locate providers**: Place near consuming widgets
3. **Avoid singletons**: Use providers instead
4. **Layer correctly**: Keep UI logic separate from business logic
## Reading State in Widgets
```dart
class ServerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(serverStatusProvider(server));
return status.when(...);
}
}
```
## Modifying State
```dart
ref.read(settingsProvider.notifier).update(newSettings);
```

View File

@@ -0,0 +1,96 @@
---
title: Project Structure
description: Understanding the Server Box codebase
---
The Server Box project follows a modular architecture with clear separation of concerns.
## Directory Structure
```
lib/
├── core/ # Core utilities and extensions
├── data/ # Data layer
│ ├── model/ # Data models by feature
│ ├── provider/ # Riverpod providers
│ └── store/ # Local storage (Hive)
├── view/ # UI layer
│ ├── page/ # Main pages
│ └── widget/ # Reusable widgets
├── generated/ # Generated localization
├── l10n/ # Localization ARB files
└── hive/ # Hive adapters
```
## Core Layer (`lib/core/`)
Contains utilities, extensions, and routing configuration:
- **Extensions**: Dart extensions for common types
- **Routes**: App routing configuration
- **Utils**: Shared utility functions
## Data Layer (`lib/data/`)
### Models (`lib/data/model/`)
Organized by feature:
- `server/` - Server connection and status models
- `container/` - Docker container models
- `ssh/` - SSH session models
- `sftp/` - SFTP file models
- `app/` - App-specific models
### Providers (`lib/data/provider/`)
Riverpod providers for dependency injection and state management:
- Server providers
- UI state providers
- Service providers
### Stores (`lib/data/store/`)
Hive-based local storage:
- Server storage
- Settings storage
- Cache storage
## View Layer (`lib/view/`)
### Pages (`lib/view/page/`)
Main application screens:
- `server/` - Server management pages
- `ssh/` - SSH terminal pages
- `container/` - Container pages
- `setting/` - Settings pages
- `storage/` - SFTP pages
- `snippet/` - Snippet pages
### Widgets (`lib/view/widget/`)
Reusable UI components:
- Server cards
- Status charts
- Input components
- Dialogs
## Generated Files
- `lib/generated/l10n/` - Auto-generated localization
- `*.g.dart` - Generated code (json_serializable, freezed, hive, riverpod)
- `*.freezed.dart` - Freezed immutable classes
## Packages Directory (`/packages/`)
Contains custom forks of dependencies:
- `dartssh2/` - SSH library
- `xterm/` - Terminal emulator
- `fl_lib/` - Shared utilities
- `fl_build/` - Build system

View File

@@ -0,0 +1,113 @@
---
title: Testing
description: Testing strategies and running tests
---
## Running Tests
```bash
# Run all tests
flutter test
# Run specific test file
flutter test test/battery_test.dart
# Run with coverage
flutter test --coverage
```
## Test Structure
Tests are located in the `test/` directory mirroring the lib structure:
```
test/
├── data/
│ ├── model/
│ └── provider/
├── view/
│ └── widget/
└── test_helpers.dart
```
## Unit Tests
Test business logic and data models:
```dart
test('should calculate CPU percentage', () {
final cpu = CpuModel(usage: 75.0);
expect(cpu.usagePercentage, '75%');
});
```
## Widget Tests
Test UI components:
```dart
testWidgets('ServerCard displays server name', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: ServerCard(server: testServer),
),
),
);
expect(find.text('Test Server'), findsOneWidget);
});
```
## Provider Tests
Test Riverpod providers:
```dart
test('serverStatusProvider returns status', () async {
final container = ProviderContainer();
final status = await container.read(serverStatusProvider(testServer).future);
expect(status, isA<StatusModel>());
});
```
## Mocking
Use mocks for external dependencies:
```dart
class MockSshService extends Mock implements SshService {}
test('connects to server', () async {
final mockSsh = MockSshService();
when(mockSsh.connect(any)).thenAnswer((_) async => true);
// Test with mock
});
```
## Integration Tests
Test complete user flows (in `integration_test/`):
```dart
testWidgets('add server flow', (tester) async {
await tester.pumpWidget(MyApp());
// Tap add button
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// Fill form
await tester.enterText(find.byKey(Key('name')), 'Test Server');
// ...
});
```
## Best Practices
1. **Arrange-Act-Assert**: Structure tests clearly
2. **Descriptive names**: Test names should describe behavior
3. **One assertion per test**: Keep tests focused
4. **Mock external deps**: Don't depend on real servers
5. **Test edge cases**: Empty lists, null values, etc.

View File

@@ -0,0 +1,83 @@
---
title: Importación Masiva de Servidores
description: Importar múltiples servidores desde un archivo JSON
---
Importa múltiples configuraciones de servidor a la vez utilizando un archivo JSON.
## Formato JSON
:::danger[Advertencia de Seguridad]
**¡Nunca guardes contraseñas en texto plano en archivos!** Este ejemplo JSON muestra un campo de contraseña solo con fines demostrativos, pero deberías:
- **Preferir claves SSH** (`keyId`) en lugar de `pwd`; son más seguras
- **Usar gestores de secretos** o variables de entorno si debes usar contraseñas
- **Eliminar el archivo inmediatamente** después de la importación; no dejes credenciales tiradas
- **Añadir a .gitignore**: nunca subas archivos de credenciales al control de versiones
:::
```json
[
{
"name": "Mi Servidor",
"ip": "example.com",
"port": 22,
"user": "root",
"pwd": "password",
"keyId": "",
"tags": ["production"],
"autoConnect": false
}
]
```
## Campos
| Campo | Requerido | Descripción |
|-------|-----------|-------------|
| `name` | Sí | Nombre para mostrar |
| `ip` | Sí | Dominio o dirección IP |
| `port` | Sí | Puerto SSH (usualmente 22) |
| `user` | Sí | Usuario SSH |
| `pwd` | No | Contraseña (evitar - usar claves SSH en su lugar) |
| `keyId` | No | Nombre de la clave SSH (de Claves Privadas - recomendado) |
| `tags` | No | Etiquetas de organización |
| `autoConnect` | No | Autoconexión al iniciar |
## Pasos para Importar
1. Crea un archivo JSON con las configuraciones del servidor
2. Ajustes → Copia de seguridad → Importación masiva de servidores
3. Selecciona tu archivo JSON
4. Confirma la importación
## Ejemplo
```json
[
{
"name": "Producción",
"ip": "prod.example.com",
"port": 22,
"user": "admin",
"keyId": "mi-clave",
"tags": ["production", "web"]
},
{
"name": "Desarrollo",
"ip": "dev.example.com",
"port": 2222,
"user": "dev",
"keyId": "dev-clave",
"tags": ["development"]
}
]
```
## Consejos
- **Usa claves SSH** en lugar de contraseñas cuando sea posible
- **Prueba la conexión** después de la importación
- **Organiza con etiquetas** para una gestión más sencilla
- **Elimina el archivo JSON** después de la importación
- **Nunca subas** archivos JSON con credenciales al control de versiones

View File

@@ -0,0 +1,72 @@
---
title: Comandos Personalizados
description: Mostrar la salida de comandos personalizados en la página del servidor
---
Añade comandos shell personalizados para mostrar su salida en la página de detalles del servidor.
## Configuración
1. Ajustes del servidor → Comandos personalizados
2. Introduce los comandos en formato JSON
## Formato Básico
```json
{
"Nombre a mostrar": "comando shell"
}
```
**Ejemplo:**
```json
{
"Memoria": "free -h",
"Disco": "df -h",
"Tiempo de actividad": "uptime"
}
```
## Ver Resultados
Tras la configuración, los comandos personalizados aparecerán en la página de detalles del servidor y se actualizarán automáticamente.
## Nombres de Comando Especiales
### server_card_top_right
Se muestra en la tarjeta del servidor de la página de inicio (esquina superior derecha):
```json
{
"server_card_top_right": "tu-comando-aquí"
}
```
## Consejos
**Usa rutas absolutas:**
```json
{"Mi Script": "/usr/local/bin/mi-script.sh"}
```
**Comandos con tuberías (pipes):**
```json
{"Proceso principal": "ps aux | sort -rk 3 | head -5"}
```
**Formatear salida:**
```json
{"Carga de CPU": "uptime | awk -F'load average:' '{print $2}'"}
```
**Mantén los comandos rápidos:** Menos de 5 segundos para una mejor experiencia.
**Limitar salida:**
```json
{"Logs": "tail -20 /var/log/syslog"}
```
## Seguridad
Los comandos se ejecutan con los permisos del usuario SSH. Evita comandos que modifiquen el estado del sistema.

View File

@@ -0,0 +1,54 @@
---
title: Logo de Servidor Personalizado
description: Usa imágenes personalizadas para las tarjetas de servidor
---
Muestra logos personalizados en las tarjetas de servidor mediante URLs de imagen.
## Configuración
1. Ajustes del servidor → Logo personalizado
2. Introduce la URL de la imagen
## Marcadores de posición de URL
### {DIST} - Distribución Linux
Se reemplaza automáticamente por la distribución detectada:
```
https://ejemplo.com/{DIST}.png
```
Se convierte en: `debian.png`, `ubuntu.png`, `arch.png`, etc.
### {BRIGHT} - Tema
Se reemplaza automáticamente por el tema actual:
```
https://ejemplo.com/{BRIGHT}.png
```
Se convierte en: `light.png` o `dark.png`
### Combinar ambos
```
https://ejemplo.com/{DIST}-{BRIGHT}.png
```
Se convierte en: `debian-light.png`, `ubuntu-dark.png`, etc.
## Consejos
- Usa formatos PNG o SVG
- Tamaño recomendado: de 64x64 a 128x128 píxeles
- Usa URLs HTTPS
- Mantén tamaños de archivo pequeños
## Distribuciones Soportadas
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
Lista completa: [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)

View File

@@ -0,0 +1,64 @@
---
title: Ajustes Ocultos (JSON)
description: Accede a ajustes avanzados mediante el editor JSON
---
Algunos ajustes están ocultos en la interfaz de usuario pero son accesibles a través del editor JSON.
## Acceso
Mantén pulsado **Ajustes** en el menú lateral para abrir el editor JSON.
## Ajustes Ocultos Comunes
### timeOut
Tiempo de espera de conexión en segundos.
```json
{"timeOut": 10}
```
**Tipo:** entero | **Predeterminado:** 5 | **Rango:** 1-60
### recordHistory
Guardar historial (rutas SFTP, etc.).
```json
{"recordHistory": true}
```
**Tipo:** booleano | **Predeterminado:** true
### textFactor
Factor de escala de texto.
```json
{"textFactor": 1.2}
```
**Tipo:** doble | **Predeterminado:** 1.0 | **Rango:** 0.8-1.5
## Encontrar Más Ajustes
Todos los ajustes están definidos en [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
Busca:
```dart
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
```
## ⚠️ Importante
**Antes de editar:**
- **Crea una copia de seguridad**: unos ajustes incorrectos pueden hacer que la app no se abra
- **Edita con cuidado**: el JSON debe ser válido
## Recuperación
Si la aplicación no se abre tras editar:
1. Borra los datos de la aplicación (último recurso)
2. Reinstala la aplicación
3. Restaura desde una copia de seguridad

View File

@@ -0,0 +1,118 @@
---
title: Problemas Comunes
description: Soluciones a problemas frecuentes
---
## Problemas de Conexión
### SSH no conecta
**Síntomas:** Tiempo de espera agotado (timeout), conexión rechazada, fallo de autenticación
**Soluciones:**
1. **Verificar el tipo de servidor:** Solo se admiten sistemas tipo Unix (Linux, macOS, Android/Termux)
2. **Probar manualmente:** `ssh usuario@servidor -p puerto`
3. **Comprobar el cortafuegos:** El puerto 22 debe estar abierto
4. **Verificar credenciales:** Usuario y contraseña/clave correctos
### Desconexiones frecuentes
**Síntomas:** El terminal se desconecta tras un periodo de inactividad
**Soluciones:**
1. **Keep-alive del servidor:**
```bash
# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3
```
2. **Desactivar optimización de batería:**
- MIUI: Batería → "Sin restricciones"
- Android: Ajustes → Aplicaciones → Desactivar optimización
- iOS: Activar actualización en segundo plano
## Problemas de Entrada
### No se pueden escribir ciertos caracteres
**Solución:** Ajustes → Tipo de teclado → Cambiar a `visiblePassword`
Nota: Es posible que la entrada CJK (chino, japonés, coreano) no funcione tras este cambio.
## Problemas de la Aplicación
### La aplicación se cierra al iniciar
**Síntomas:** La aplicación no se abre, pantalla en negro
**Causas:** Ajustes corruptos, especialmente tras usar el editor JSON
**Soluciones:**
1. **Borrar datos de la aplicación:**
- Android: Ajustes → Aplicaciones → ServerBox → Borrar datos
- iOS: Eliminar y reinstalar
2. **Restaurar copia de seguridad:** Importar una copia de seguridad creada antes de cambiar los ajustes
### Problemas con Copia de Seguridad/Restauración
**La copia de seguridad no funciona:**
- Comprobar espacio de almacenamiento
- Verificar que la aplicación tiene permisos de almacenamiento
- Probar una ubicación diferente
**La restauración falla:**
- Verificar la integridad del archivo de copia de seguridad
- Comprobar la compatibilidad de la versión de la aplicación
## Problemas con Widgets
### El Widget no se actualiza
**iOS:**
- Esperar hasta 30 minutos para la actualización automática
- Eliminar y volver a añadir el widget
- Comprobar que la URL termina en `/status`
**Android:**
- Pulsar el widget para forzar la actualización
- Verificar que el ID del widget coincide con la configuración en los ajustes de la aplicación
**watchOS:**
- Reiniciar la aplicación del reloj
- Esperar unos minutos tras cambiar la configuración
- Verificar el formato de la URL
### El Widget muestra un error
- Verificar que ServerBox Monitor se está ejecutando en el servidor
- Probar la URL en un navegador
- Comprobar las credenciales de autenticación
## Problemas de Rendimiento
### La aplicación va lenta
**Soluciones:**
- Reducir la tasa de refresco en los ajustes
- Comprobar la velocidad de la red
- Desactivar servidores no utilizados
### Alto consumo de batería
**Soluciones:**
- Aumentar los intervalos de refresco
- Desactivar la actualización en segundo plano
- Cerrar sesiones SSH no utilizadas
## Obtener Ayuda
Si los problemas persisten:
1. **Buscar en GitHub Issues:** https://github.com/lollipopkit/flutter_server_box/issues
2. **Crear nueva Issue:** Incluir versión de la aplicación, plataforma y pasos para reproducir
3. **Consultar la Wiki:** Esta documentación y la Wiki de GitHub

View File

@@ -0,0 +1,90 @@
---
title: Widgets de Pantalla de Inicio
description: Añade widgets de estado del servidor a tu pantalla de inicio
---
Requiere tener instalado [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) en tus servidores.
## Requisitos Previos
Instala primero ServerBox Monitor en tu servidor. Consulta la [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) para ver las instrucciones de configuración.
Tras la instalación, tu servidor debería tener:
- Un punto de acceso (endpoint) HTTP/HTTPS
- El punto de acceso API `/status`
- Autenticación opcional
## Formato de URL
```
https://tu-servidor.com/status
```
Debe terminar en `/status`.
## Widget de iOS
### Configuración
1. Mantén pulsada la pantalla de inicio → Toca el símbolo **+**
2. Busca "ServerBox"
3. Elige el tamaño del widget
4. Mantén pulsado el widget → **Editar widget**
5. Introduce la URL terminada en `/status`
### Notas
- Debe usar HTTPS (excepto IPs locales)
- Tasa máxima de refresco: 30 minutos (límite de iOS)
- Añade varios widgets para varios servidores
## Widget de Android
### Configuración
1. Mantén pulsada la pantalla de inicio → **Widgets**
2. Busca "ServerBox" → Añadir a la pantalla de inicio
3. Anota el número de ID del widget que aparece
4. Abre la app ServerBox → Ajustes
5. Toca en **Configurar enlace de widget de inicio**
6. Añade la entrada: `Widget ID` = `URL de estado`
Ejemplo:
- Clave (Key): `17`
- Valor (Value): `https://mi-servidor.com/status`
7. Toca el widget en la pantalla de inicio para refrescarlo
## Widget de watchOS
### Configuración
1. Abre la app en el iPhone → Ajustes
2. **Ajustes de iOS****App del Watch**
3. Toca en **Añadir URL**
4. Introduce la URL terminada en `/status`
5. Espera a que la app del reloj se sincronice
### Notas
- Prueba a reiniciar la app del reloj si no se actualiza
- Verifica que el teléfono y el reloj están conectados
## Solución de Problemas
### El Widget no se actualiza
**iOS:** Espera hasta 30 minutos, luego elimínalo y vuelve a añadirlo.
**Android:** Toca el widget para forzar el refresco, verifica el ID en los ajustes.
**watchOS:** Reinicia la app del reloj, espera unos minutos.
### El Widget muestra un error
- Verifica que ServerBox Monitor se está ejecutando
- Prueba la URL en un navegador
- Comprueba que la URL termina en `/status`
## Seguridad
- **Usa siempre HTTPS** cuando sea posible
- **IPs locales solo** en redes de confianza

View File

@@ -0,0 +1,86 @@
---
title: Arquitectura
description: Patrones de arquitectura y decisiones de diseño
---
Server Box sigue los principios de Clean Architecture con una clara separación entre las capas de datos, dominio y presentación.
## Arquitectura por Capas
```
┌─────────────────────────────────────┐
│ Capa de Presentación │
│ (lib/view/page/) │
│ - Páginas, Widgets, Controladores │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Capa de Lógica de Negocio │
│ (lib/data/provider/) │
│ - Riverpod Providers │
│ - Gestión de Estado │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Capa de Datos │
│ (lib/data/model/, store/) │
│ - Modelos, Almacén, Servicios │
└─────────────────────────────────────┘
```
## Patrones Clave
### Gestión de Estado: Riverpod
- **Generación de Código**: Usa `riverpod_generator` para providers con tipado seguro
- **State Notifiers**: Para estados mutables con lógica de negocio
- **Async Notifiers**: Para estados de carga y error
- **Stream Providers**: Para datos en tiempo real
### Modelos Inmutables: Freezed
- Todos los modelos de datos usan Freezed para inmutabilidad
- Tipos Union para representación de estados
- Serialización JSON integrada
- Extensiones CopyWith para actualizaciones
### Almacenamiento Local: Hive
- **hive_ce**: Edición comunitaria de Hive
- No se requiere `@HiveField` o `@HiveType` manual
- Adaptadores de tipo generados automáticamente
- Almacenamiento persistente clave-valor
## Inyección de Dependencias
Los servicios y almacenes se inyectan a través de:
1. **Providers**: Exponen dependencias a la UI
2. **GetIt**: Localizador de servicios (donde sea aplicable)
3. **Inyección en Constructor**: Dependencias explícitas
## Flujo de Datos
```
Acción de Usuario → Widget → Provider → Servicio/Almacén → Actualización de Modelo → Reconstrucción de UI
```
1. El usuario interactúa con el widget
2. El widget llama al método del provider
3. El provider actualiza el estado a través del servicio/almacén
4. El cambio de estado activa la reconstrucción de la UI
5. El nuevo estado se refleja en el widget
## Dependencias Personalizadas
El proyecto utiliza varias ramas (forks) personalizadas para extender la funcionalidad:
- **dartssh2**: Funciones SSH mejoradas
- **xterm**: Emulador de terminal con soporte móvil
- **fl_lib**: Componentes de UI y utilidades compartidas
## Multihilo
- **Isolates**: Computación pesada fuera del hilo principal
- **paquete computer**: Utilidades para multihilo
- **Async/Await**: Operaciones de E/S no bloqueantes

View File

@@ -0,0 +1,116 @@
---
title: Compilación
description: Instrucciones de compilación para diferentes plataformas
---
Server Box utiliza un sistema de compilación personalizado (`fl_build`) para compilaciones multiplataforma.
## Requisitos Previos
- Flutter SDK (canal stable)
- Herramientas específicas de cada plataforma (Xcode para iOS, Android Studio para Android)
- Cadena de herramientas de Rust (para algunas dependencias nativas)
## Compilación de Desarrollo
```bash
# Ejecutar en modo desarrollo
flutter run
# Ejecutar en un dispositivo específico
flutter run -d <id-del-dispositivo>
```
## Compilación de Producción
El proyecto utiliza `fl_build` para compilar:
```bash
# Compilar para una plataforma específica
dart run fl_build -p <plataforma>
# Plataformas disponibles:
# - ios
# - android
# - macos
# - linux
# - windows
```
## Compilaciones Específicas por Plataforma
### iOS
```bash
dart run fl_build -p ios
```
Requiere:
- macOS con Xcode
- CocoaPods
- Cuenta de Apple Developer para la firma
### Android
```bash
dart run fl_build -p android
```
Requiere:
- Android SDK
- Java Development Kit
- Keystore para la firma
### macOS
```bash
dart run fl_build -p macos
```
### Linux
```bash
dart run fl_build -p linux
```
### Windows
```bash
dart run fl_build -p windows
```
Requiere Windows con Visual Studio.
## Pre/Post Compilación
El script `make.dart` se encarga de:
- Generación de metadatos
- Actualización de cadenas de versión
- Configuraciones específicas de plataforma
## Solución de Problemas
### Compilación Limpia
```bash
flutter clean
dart run build_runner build --delete-conflicting-outputs
flutter pub get
```
### Discrepancia de Versión
Asegúrate de que todas las dependencias son compatibles:
```bash
flutter pub upgrade
```
## Lista de Verificación de Lanzamiento
1. Actualizar la versión en `pubspec.yaml`
2. Ejecutar la generación de código
3. Ejecutar las pruebas
4. Compilar para todas las plataformas de destino
5. Probar en dispositivos físicos
6. Crear lanzamiento (release) en GitHub

View File

@@ -0,0 +1,98 @@
---
title: Generación de Código
description: Uso de build_runner para la generación de código
---
Server Box utiliza intensivamente la generación de código para modelos, gestión de estado y serialización.
## Cuándo Ejecutar la Generación de Código
Ejecutar tras modificar:
- Modelos con la anotación `@freezed`
- Clases con `@JsonSerializable`
- Modelos de Hive
- Providers con `@riverpod`
- Localizaciones (archivos ARB)
## Ejecutar la Generación de Código
```bash
# Generar todo el código
dart run build_runner build --delete-conflicting-outputs
# Limpiar y regenerar
dart run build_runner build --delete-conflicting-outputs --clean
```
## Archivos Generados
### Freezed (`*.freezed.dart`)
Modelos de datos inmutables con tipos Union:
```dart
@freezed
class ServerState with _$ServerState {
const factory ServerState.connected() = Connected;
const factory ServerState.disconnected() = Disconnected;
const factory ServerState.error(String message) = Error;
}
```
### Serialización JSON (`*.g.dart`)
Generado por `json_serializable`:
```dart
@JsonSerializable()
class Server {
final String id;
final String name;
final String host;
Server({required this.id, required this.name, required this.host});
factory Server.fromJson(Map<String, dynamic> json) =>
_$ServerFromJson(json);
Map<String, dynamic> toJson() => _$ServerToJson(this);
}
```
### Providers de Riverpod (`*.g.dart`)
Generados a partir de la anotación `@riverpod`:
```dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
}
```
### Adaptadores de Hive (`*.g.dart`)
Auto-generados para modelos de Hive (hive_ce):
```dart
@HiveType(typeId: 0)
class ServerModel {
@HiveField(0)
final String id;
}
```
## Generación de Localización
```bash
flutter gen-l10n
```
Genera `lib/generated/l10n/` a partir de los archivos `lib/l10n/*.arb`.
## Consejos
- Usa `--delete-conflicting-outputs` para evitar conflictos
- Añade los archivos generados al `.gitignore`
- Nunca edites manualmente los archivos generados

View File

@@ -0,0 +1,115 @@
---
title: Gestión de Estado
description: Patrones de gestión de estado basados en Riverpod
---
Server Box utiliza Riverpod con generación de código para la gestión de estado.
## Tipos de Provider
### StateProvider
Estado simple que se puede leer y escribir:
```dart
@riverpod
class Settings extends _$Settings {
@override
SettingsModel build() {
return SettingsModel.defaults();
}
void update(SettingsModel newSettings) {
state = newSettings;
}
}
```
### AsyncNotifierProvider
Estado que se carga de forma asíncrona con estados de carga/error:
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
return fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => fetchStatus(server));
}
}
```
### StreamProvider
Datos en tiempo real desde flujos (streams):
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
return cpuService.monitor(server);
}
```
## Patrones de Estado
### Estados de Carga
```dart
state.when(
data: (data) => DataWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### Family Providers
Providers parametrizados:
```dart
@riverpod
List<Container> containers(ContainersRef ref, Server server) {
return containerService.list(server);
}
```
### Auto-Dispose
Providers que se eliminan cuando ya no están referenciados:
```dart
@Riverpod(keepAlive: false)
class TempState extends _$TempState {
// ...
}
```
## Mejores Prácticas
1. **Usar generación de código**: Usa siempre la anotación `@riverpod`
2. **Co-localizar providers**: Ponlos cerca de los widgets que los consumen
3. **Evitar singletons**: Usa providers en su lugar
4. **Capas correctas**: Mantén la lógica de UI separada de la lógica de negocio
## Leer el Estado en Widgets
```dart
class ServerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(serverStatusProvider(server));
return status.when(...);
}
}
```
## Modificar el Estado
```dart
ref.read(settingsProvider.notifier).update(newSettings);
```

View File

@@ -0,0 +1,96 @@
---
title: Estructura del Proyecto
description: Comprendiendo la base de código de Server Box
---
El proyecto Server Box sigue una arquitectura modular con una clara separación de responsabilidades.
## Estructura de Directorios
```
lib/
├── core/ # Utilidades centrales y extensiones
├── data/ # Capa de datos
│ ├── model/ # Modelos de datos por función
│ ├── provider/ # Riverpod providers
│ └── store/ # Almacenamiento local (Hive)
├── view/ # Capa de UI
│ ├── page/ # Páginas principales
│ └── widget/ # Widgets reutilizables
├── generated/ # Localización generada
├── l10n/ # Archivos ARB de localización
└── hive/ # Adaptadores de Hive
```
## Capa Central (`lib/core/`)
Contiene utilidades, extensiones y configuración de rutas:
- **Extensions**: Extensiones de Dart para tipos comunes
- **Routes**: Configuración de rutas de la app
- **Utils**: Funciones de utilidad compartidas
## Capa de Datos (`lib/data/`)
### Modelos (`lib/data/model/`)
Organizados por función:
- `server/` - Modelos de conexión y estado del servidor
- `container/` - Modelos de contenedores Docker
- `ssh/` - Modelos de sesión SSH
- `sftp/` - Modelos de archivos SFTP
- `app/` - Modelos específicos de la app
### Providers (`lib/data/provider/`)
Providers de Riverpod para inyección de dependencias y gestión de estado:
- Providers de servidor
- Providers de estado de UI
- Providers de servicios
### Almacenes (`lib/data/store/`)
Almacenamiento local basado en Hive:
- Almacén de servidores
- Almacén de ajustes
- Almacén de caché
## Capa de Vista (`lib/view/`)
### Páginas (`lib/view/page/`)
Pantallas principales de la aplicación:
- `server/` - Páginas de gestión de servidores
- `ssh/` - Páginas de terminal SSH
- `container/` - Páginas de contenedores
- `setting/` - Páginas de ajustes
- `storage/` - Páginas de SFTP
- `snippet/` - Páginas de fragmentos (snippets)
### Widgets (`lib/view/widget/`)
Componentes de UI reutilizables:
- Tarjetas de servidor
- Gráficos de estado
- Componentes de entrada
- Diálogos
## Archivos Generados
- `lib/generated/l10n/` - Localización auto-generada
- `*.g.dart` - Código generado (json_serializable, freezed, hive, riverpod)
- `*.freezed.dart` - Clases inmutables de Freezed
## Directorio de Paquetes (`/packages/`)
Contiene ramas (forks) personalizadas de las dependencias:
- `dartssh2/` - Librería SSH
- `xterm/` - Emulador de terminal
- `fl_lib/` - Utilidades compartidas
- `fl_build/` - Sistema de compilación

View File

@@ -0,0 +1,113 @@
---
title: Pruebas
description: Estrategias de prueba y ejecución de pruebas
---
## Ejecución de Pruebas
```bash
# Ejecutar todas las pruebas
flutter test
# Ejecutar un archivo de prueba específico
flutter test test/battery_test.dart
# Ejecutar con cobertura
flutter test --coverage
```
## Estructura de las Pruebas
Las pruebas se encuentran en el directorio `test/` reflejando la estructura de lib:
```
test/
├── data/
│ ├── model/
│ └── provider/
├── view/
│ └── widget/
└── test_helpers.dart
```
## Pruebas Unitarias
Probar la lógica de negocio y los modelos de datos:
```dart
test('debería calcular el porcentaje de CPU', () {
final cpu = CpuModel(usage: 75.0);
expect(cpu.usagePercentage, '75%');
});
```
## Pruebas de Widgets
Probar componentes de la interfaz de usuario (UI):
```dart
testWidgets('ServerCard muestra el nombre del servidor', (tester) async {
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(
home: ServerCard(server: testServer),
),
),
);
expect(find.text('Test Server'), findsOneWidget);
});
```
## Pruebas de Providers
Probar providers de Riverpod:
```dart
test('serverStatusProvider devuelve el estado', () async {
final container = ProviderContainer();
final status = await container.read(serverStatusProvider(testServer).future);
expect(status, isA<StatusModel>());
});
```
## Mocking (Simulaciones)
Utilizar mocks para dependencias externas:
```dart
class MockSshService extends Mock implements SshService {}
test('se conecta al servidor', () async {
final mockSsh = MockSshService();
when(mockSsh.connect(any)).thenAnswer((_) async => true);
// Probar con el mock
});
```
## Pruebas de Integración
Probar flujos de usuario completos (en `integration_test/`):
```dart
testWidgets('flujo de agregar servidor', (tester) async {
await tester.pumpWidget(MyApp());
// Tocar el botón de agregar
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// Completar el formulario
await tester.enterText(find.byKey(Key('name')), 'Test Server');
// ...
});
```
## Buenas Prácticas
1. **Arrange-Act-Assert**: Estructurar las pruebas claramente.
2. **Nombres descriptivos**: Los nombres de las pruebas deben describir el comportamiento.
3. **Una aserción por prueba**: Mantener las pruebas enfocadas.
4. **Simular dependencias externas**: No depender de servidores reales.
5. **Probar casos límite**: Listas vacías, valores nulos, etc.

View File

@@ -0,0 +1,46 @@
---
title: Server Box
description: Una aplicación integral de gestión de servidores multiplataforma
hero:
tagline: Administra tus servidores Linux desde cualquier lugar
actions:
- text: Empezar
link: /es/introduction/
icon: right-arrow
variant: primary
- text: Ver en GitHub
link: https://github.com/lollipopkit/flutter_server_box
icon: github
variant: minimal
---
import { Card, CardGrid } from '@astrojs/starlight/components';
## Características
<CardGrid stagger>
<Card title="Monitoreo en Tiempo Real" icon="chart">
Monitorea CPU, memoria, disco, red, GPU y temperatura con hermosos gráficos en tiempo real.
</Card>
<Card title="Terminal SSH" icon="terminal">
Terminal SSH con todas las funciones, soporte para múltiples pestañas y teclado virtual para dispositivos móviles.
</Card>
<Card title="Navegador de Archivos SFTP" icon="folder">
Administra archivos en tus servidores con el cliente SFTP integrado y el navegador de archivos local.
</Card>
<Card title="Gestión de Docker" icon="box">
Inicia, detén y monitorea contenedores Docker con una interfaz intuitiva.
</Card>
<Card title="Multiplataforma" icon="device-mobile">
Disponible en iOS, Android, macOS, Linux, Windows y watchOS.
</Card>
<Card title="Más de 12 Idiomas" icon="globe">
Soporte completo de localización que incluye inglés, chino, alemán, francés y más.
</Card>
</CardGrid>
## Enlaces Rápidos
- **Descarga**: Disponible en [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) y [F-Droid](https://f-droid.org/)
- **Documentación**: Explora las guías para comenzar con Server Box
- **Soporte**: Únete a nuestra comunidad en GitHub para discusiones y problemas

View File

@@ -0,0 +1,51 @@
---
title: Instalación
description: Descarga e instala Server Box en tu dispositivo
---
Server Box está disponible en múltiples plataformas. Elige tu método de instalación preferido.
## Aplicaciones Móviles
### iOS
Descárgalo desde la **[App Store](https://apps.apple.com/app/id1586449703)**.
### Android
Elige tu fuente preferida:
- **[F-Droid](https://f-droid.org/)** - Para usuarios que prefieren fuentes exclusivamente FOSS (Software Libre y de Código Abierto)
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - Para la última versión directamente desde la fuente
## Aplicaciones de Escritorio
### macOS
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
Características:
- Integración nativa con la barra de menú
- Soporte para Intel y Apple Silicon
### Linux
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
Disponible en paquetes AppImage, deb o tar.gz.
### Windows
Descárgalo desde **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
## watchOS
Disponible en la **[App Store](https://apps.apple.com/app/id1586449703)** como parte de la aplicación para iOS.
## Compilación desde el Código Fuente
Para compilar Server Box desde el código fuente, consulta la sección de [Compilación](/es/development/building/) en la documentación de desarrollo.
## Información de Versión
Consulta la página de [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) para ver la última versión y el registro de cambios.

View File

@@ -0,0 +1,32 @@
---
title: Introducción
description: Aprende qué es Server Box y qué puede hacer
---
Server Box es una aplicación integral de gestión de servidores multiplataforma creada con Flutter. Te permite monitorear, gestionar y controlar tus servidores Linux, Unix y Windows desde cualquier lugar.
## ¿Qué es Server Box?
Server Box proporciona una interfaz unificada para tareas de administración de servidores a través de conexiones SSH. Ya seas un administrador de sistemas, desarrollador o entusiasta con servidores domésticos, esta aplicación pone potentes herramientas de gestión de servidores en tu bolsillo.
## Capacidades Clave
- **Monitoreo en Tiempo Real**: Sigue el uso de CPU, memoria, disco, velocidad de red, estado de GPU y temperaturas del sistema.
- **Terminal SSH**: Acceso total a la terminal con soporte multi-pestaña y apariencia personalizable.
- **Cliente SFTP**: Explora y gestiona archivos en tus servidores.
- **Gestión de Docker**: Controla contenedores con facilidad.
- **Gestión de Procesos**: Visualiza y gestiona procesos del sistema.
- **Servicios Systemd**: Inicia, detén y monitorea servicios systemd.
- **Herramientas de Red**: Pruebas iPerf, ping y Wake-on-LAN.
- **Snippets**: Guarda y ejecuta comandos de shell personalizados.
## Plataformas Soportadas
Server Box es verdaderamente multiplataforma:
- **Móvil**: iOS y Android
- **Escritorio**: macOS, Linux y Windows
## Licencia
Este proyecto está bajo la licencia AGPL v3. El código fuente está disponible en [GitHub](https://github.com/lollipopkit/flutter_server_box).

View File

@@ -0,0 +1,80 @@
---
title: Funciones de Escritorio
description: Funciones específicas para macOS, Linux y Windows
---
Server Box en plataformas de escritorio ofrece funciones de productividad adicionales.
## macOS
### Integración en la Barra de Menús
- Estado rápido del servidor en la barra de menús
- Acceso al servidor con un solo clic
- Modo compacto para una mínima distracción
- Estilo nativo de la barra de menús de macOS
### Persistencia del Estado de la Ventana
- Recuerda la posición y el tamaño de la ventana
- Restaura la sesión anterior al iniciar
- Soporte para múltiples monitores
### Funciones Nativas
- **Barra de título**: Opción de barra de título personalizada o del sistema
- **Modo pantalla completa**: Monitorización dedicada del servidor
- **Atajos de teclado**: Atajos nativos de macOS
- **Touch Bar** (dispositivos compatibles): Acciones rápidas
## Linux
### Integración Nativa
- Soporte para bandeja del sistema (systray)
- Integración con notificaciones de escritorio
- Integración con el selector de archivos
### Gestión de Ventanas
- Soporte para X11 y Wayland
- Compatible con gestores de ventanas en mosaico (tiling)
- Opción de decoraciones de ventana personalizadas
## Windows
### Funciones
- Integración en la bandeja del sistema
- Acciones rápidas en la Jump List
- Controles de ventana nativos
- Opción de inicio automático al arrancar
## Funciones de Escritorio Multiplataforma
### Atajos de Teclado
- **Cmd/Ctrl + N**: Nuevo servidor
- **Cmd/Ctrl + W**: Cerrar pestaña
- **Cmd/Ctrl + T**: Nueva pestaña de terminal
- **Cmd/Ctrl + ,**: Ajustes
### Temas
- Tema claro
- Tema oscuro
- Tema AMOLED (negro puro)
- Tema del sistema (sigue al SO)
### Múltiples Ventanas
- Abrir varios servidores en ventanas separadas
- Arrastrar pestañas a una nueva ventana
- Comparar estadísticas de servidores en paralelo
### Ventajas sobre el Móvil
- Pantalla más grande para monitorización
- Teclado completo para la terminal
- Operaciones de archivos más rápidas
- Mejor multitarea

View File

@@ -0,0 +1,77 @@
---
title: Funciones Móviles
description: Funciones específicas para iOS y Android
---
Server Box proporciona varias funciones específicas para dispositivos móviles iOS y Android.
## Autenticación Biométrica
Asegura tus servidores con autenticación biométrica:
- **iOS**: Face ID o Touch ID
- **Android**: Autenticación por huella dactilar
Actívalo en Ajustes > Seguridad > Autenticación biométrica.
## Widgets de Pantalla de Inicio
Añade widgets de estado del servidor a tu pantalla de inicio para una monitorización rápida.
### iOS
- Mantén pulsada la pantalla de inicio
- Toca en **+** para añadir un widget
- Busca "Server Box"
- Elige el tamaño del widget:
- Pequeño: Estado de un solo servidor
- Mediano: Múltiples servidores
- Grande: Información detallada
### Android
- Mantén pulsada la pantalla de inicio
- Toca en **Widgets**
- Busca "Server Box"
- Selecciona el tipo de widget
## Ejecución en Segundo Plano
### Android
Mantén las conexiones activas en segundo plano:
- Actívalo en Ajustes > Avanzado > Ejecución en segundo plano
- Requiere exclusión de la optimización de batería
- Notificaciones persistentes para conexiones activas
### iOS
Se aplican limitaciones de segundo plano:
- Las conexiones pueden pausarse en segundo plano
- Reconexión rápida al volver a la app
- Soporte para actualización en segundo plano
## Notificaciones Push
Recibe notificaciones para:
- Alertas de servidor fuera de línea
- Avisos de alto uso de recursos
- Alertas de finalización de tareas
Configúralo en Ajustes > Notificaciones.
## Funciones de UI Móvil
- **Deslizar para refrescar**: Actualiza el estado del servidor
- **Acciones de deslizamiento**: Operaciones rápidas de servidor
- **Modo horizontal**: Mejor experiencia de terminal
- **Teclado virtual**: Atajos de terminal
## Integración de Archivos
- **App Archivos (iOS)**: Acceso directo SFTP desde Archivos
- **Storage Access Framework (Android)**: Comparte archivos con otras apps
- **Selector de documentos**: Selección de archivos sencilla

View File

@@ -0,0 +1,214 @@
---
title: Descripción General de la Arquitectura
description: Arquitectura de alto nivel de la aplicación
---
Server Box sigue una arquitectura por capas con una clara separación de responsabilidades.
## Capas de la Arquitectura
```
┌─────────────────────────────────────────────────┐
│ Capa de Presentación (UI) │
│ lib/view/page/, lib/view/widget/ │
│ - Páginas, Widgets, Controladores │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Capa de Lógica de Negocio │
│ lib/data/provider/ │
│ - Riverpod Providers, State Notifiers │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Capa de Acceso a Datos │
│ lib/data/store/, lib/data/model/ │
│ - Hive Stores, Modelos de Datos │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ Capa de Integración Externa │
│ - SSH (dartssh2), Terminal (xterm), SFTP │
│ - Código específico de plataforma (iOS, etc.) │
└─────────────────────────────────────────────────┘
```
## Fundamentos de la Aplicación
### Punto de Entrada Principal
`lib/main.dart` inicializa la aplicación:
```dart
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
```
### Widget Raíz
`MyApp` proporciona:
- **Gestión de Temas**: Cambio entre tema claro/oscuro
- **Configuración de Rutas**: Estructura de navegación
- **Provider Scope**: Raíz para la inyección de dependencias
### Página de Inicio
`HomePage` sirve como núcleo de navegación:
- **Interfaz de Pestañas**: Servidor, Snippet, Contenedor, SSH
- **Gestión de Estado**: Estado por pestaña
- **Navegación**: Acceso a funciones
## Sistemas Principales
### Gestión de Estado: Riverpod
**¿Por qué Riverpod?**
- Seguridad en tiempo de compilación
- Facilidad para realizar pruebas
- Sin dependencia del Build context
- Funciona en todas las plataformas
**Tipos de Provider Utilizados:**
- `StateProvider`: Estado mutable simple
- `AsyncNotifierProvider`: Estados de carga/error/datos
- `StreamProvider`: Flujos de datos en tiempo real
- Future providers: Operaciones asíncronas únicas
### Persistencia de Datos: Hive CE
**¿Por qué Hive CE?**
- Sin dependencias de código nativo
- Almacenamiento clave-valor rápido
- Tipado seguro con generación de código
- Sin necesidad de anotaciones manuales de campos
**Almacenes (Stores):**
- `SettingStore`: Preferencias de la app
- `ServerStore`: Configuraciones de servidores
- `SnippetStore`: Fragmentos de comandos
- `KeyStore`: Claves SSH
### Modelos Inmutables: Freezed
**Beneficios:**
- Inmutabilidad en tiempo de compilación
- Tipos Union para el estado
- Serialización JSON integrada
- Extensiones CopyWith
## Estrategia Multiplataforma
### Sistema de Plugins
Los plugins de Flutter proporcionan la integración con la plataforma:
| Plataforma | Método de Integración |
|------------|-----------------------|
| iOS | CocoaPods, Swift/Obj-C |
| Android | Gradle, Kotlin/Java |
| macOS | CocoaPods, Swift |
| Linux | CMake, C++ |
| Windows | CMake, C# |
### Funciones Específicas por Plataforma
**Solo iOS:**
- Widgets de pantalla de inicio
- Actividades en Directo (Live Activities)
- Compañero de Apple Watch
**Solo Android:**
- Servicio en segundo plano
- Notificaciones push
- Acceso al sistema de archivos
**Solo Escritorio:**
- Integración en la barra de menús
- Múltiples ventanas
- Barra de título personalizada
## Dependencias Personalizadas
### Rama (Fork) de dartssh2
Cliente SSH mejorado con:
- Mejor soporte para móviles
- Gestión de errores mejorada
- Optimizaciones de rendimiento
### Rama (Fork) de xterm.dart
Emulador de terminal con:
- Renderizado optimizado para móviles
- Soporte para gestos táctiles
- Integración con teclado virtual
### fl_lib
Paquete de utilidades compartidas con:
- Widgets comunes
- Extensiones
- Funciones de ayuda
## Sistema de Compilación
### Paquete fl_build
Sistema de compilación personalizado para:
- Compilaciones multiplataforma
- Firma de código
- Empaquetado de recursos (assets)
- Gestión de versiones
### Proceso de Compilación
```
make.dart (versión) → fl_build (compilación) → Salida de plataforma
```
1. **Pre-compilación**: Cálculo de la versión desde Git
2. **Compilación**: Compilar para la plataforma de destino
3. **Post-compilación**: Empaquetado y firma
## Ejemplo de Flujo de Datos
### Actualización del Estado del Servidor
```
1. El temporizador se activa →
2. El Provider llama al servicio →
3. El servicio ejecuta el comando SSH →
4. La respuesta se analiza en el modelo →
5. Se actualiza el estado →
6. La UI se reconstruye con los nuevos datos
```
### Flujo de Acción del Usuario
```
1. El usuario toca un botón →
2. El Widget llama al método del provider →
3. El Provider actualiza el estado →
4. El cambio de estado activa la reconstrucción →
5. El nuevo estado se refleja en la UI
```
## Arquitectura de Seguridad
### Protección de Datos
- **Contraseñas**: Cifradas con flutter_secure_storage
- **Claves SSH**: Cifradas en reposo
- **Huellas de Host**: Almacenadas de forma segura
- **Datos de Sesión**: No se persisten
### Seguridad de Conexión
- **Verificación de Clave de Host**: Detección de MITM
- **Cifrado**: Cifrado SSH estándar
- **Sin Texto Plano**: Los datos sensibles nunca se almacenan en plano

View File

@@ -0,0 +1,490 @@
---
title: Sistema SFTP
description: Cómo funciona el explorador de archivos SFTP
---
El sistema SFTP proporciona capacidades de gestión de archivos sobre SSH.
## Arquitectura
```
┌─────────────────────────────────────────────┐
│ Capa UI de SFTP │
│ - Explorador de archivos (remoto) │
│ - Explorador de archivos (local) │
│ - Cola de transferencia │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Gestión de Estado SFTP │
│ - sftpProvider │
│ - Gestión de rutas │
│ - Cola de operaciones │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Capa de Protocolo SFTP │
│ - Subsistema SSH │
│ - Operaciones de archivos │
│ - Listado de directorios │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Transporte SSH │
│ - Canal seguro │
│ - Streaming de datos │
└─────────────────────────────────────────────┘
```
## Establecimiento de la Conexión
### Creación del Cliente SFTP
```dart
Future<SftpClient> createSftpClient(Spi spi) async {
// 1. Obtener cliente SSH (reutilizar si está disponible)
final sshClient = await genClient(spi);
// 2. Abrir subsistema SFTP
final sftp = await sshClient.openSftp();
return sftp;
}
```
### Reutilización de Conexiones
SFTP reutiliza las conexiones SSH existentes:
```dart
class ServerProvider {
SSHClient? _sshClient;
SftpClient? _sftpClient;
Future<SftpClient> getSftpClient(String spiId) async {
_sftpClient ??= await _sshClient!.openSftp();
return _sftpClient!;
}
}
```
## Operaciones del Sistema de Archivos
### Listado de Directorios
```dart
Future<List<SftpFile>> listDirectory(String path) async {
final sftp = await getSftpClient(spiId);
// Listar directorio
final files = await sftp.listDir(path);
// Ordenar según ajustes
files.sort((a, b) {
switch (sortOption) {
case SortOption.name:
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
case SortOption.size:
return a.size.compareTo(b.size);
case SortOption.time:
return a.modified.compareTo(b.modified);
}
});
// Carpetas primero si está activado
if (showFoldersFirst) {
final dirs = files.where((f) => f.isDirectory);
final regular = files.where((f) => !f.isDirectory);
return [...dirs, ...regular];
}
return files;
}
```
### Metadatos de Archivo
```dart
class SftpFile {
final String name;
final String path;
final int size; // Bytes
final int modified; // Timestamp Unix
final String permissions; // ej., "rwxr-xr-x"
final String owner;
final String group;
final bool isDirectory;
final bool isSymlink;
String get sizeFormatted => formatBytes(size);
String get modifiedFormatted => formatDate(modified);
}
```
## Operaciones de Archivo
### Subida (Upload)
```dart
Future<void> uploadFile(
String localPath,
String remotePath,
) async {
final sftp = await getSftpClient(spiId);
// Crear petición
final req = SftpReq(
spi: spi,
remotePath: remotePath,
localPath: localPath,
type: SftpReqType.upload,
);
// Añadir a la cola
_transferQueue.add(req);
// Ejecutar transferencia con progreso
final file = File(localPath);
final size = await file.length();
final stream = file.openRead();
await sftp.upload(
stream: stream,
toPath: remotePath,
onProgress: (transferred) {
_updateProgress(req, transferred, size);
},
);
// Completar
_transferQueue.remove(req);
}
```
### Descarga (Download)
```dart
Future<void> downloadFile(
String remotePath,
String localPath,
) async {
final sftp = await getSftpClient(spiId);
// Crear archivo local
final file = File(localPath);
final sink = file.openWrite();
// Descargar con progreso
final stat = await sftp.stat(remotePath);
await sftp.download(
fromPath: remotePath,
toSink: sink,
onProgress: (transferred) {
_updateProgress(
SftpReq(...),
transferred,
stat.size,
);
},
);
await sink.close();
}
```
### Edición de Permisos
```dart
Future<void> setPermissions(
String path,
String permissions,
) async {
final sftp = await getSftpClient(spiId);
// Analizar permisos (ej., "rwxr-xr-x" o "755")
final mode = parsePermissions(permissions);
// Establecer vía comando SSH (más fiable que SFTP)
final ssh = await getSshClient(spiId);
await ssh.exec('chmod $mode "$path"');
}
```
## Gestión de Rutas
### Estructura de Rutas
```dart
class PathWithPrefix {
final String prefix; // ej., "/home/user"
final String path; // Relativa o absoluta
String get fullPath {
if (path.startsWith('/')) {
return path; // Ruta absoluta
}
return '$prefix/$path'; // Ruta relativa
}
PathWithPrefix cd(String subPath) {
return PathWithPrefix(
prefix: fullPath,
path: subPath,
);
}
}
```
### Historial de Navegación
```dart
class PathHistory {
final List<String> _history = [];
int _index = -1;
void push(String path) {
// Eliminar historial hacia adelante
_history.removeRange(_index + 1, _history.length);
_history.add(path);
_index = _history.length - 1;
}
String? back() {
if (_index > 0) {
_index--;
return _history[_index];
}
return null;
}
String? forward() {
if (_index < _history.length - 1) {
_index++;
return _history[_index];
}
return null;
}
}
```
## Sistema de Transferencia
### Petición de Transferencia
```dart
class SftpReq {
final Spi spi;
final String remotePath;
final String localPath;
final SftpReqType type;
final DateTime createdAt;
int? totalBytes;
int? transferredBytes;
String? error;
}
```
### Seguimiento de Progreso
```dart
class TransferProgress {
final SftpReq request;
final int total;
final int transferred;
final DateTime startTime;
double get percentage => (transferred / total) * 100;
Duration get elapsed => DateTime.now().difference(startTime);
String get speedFormatted {
final bytesPerSecond = transferred / elapsed.inSeconds;
return formatSpeed(bytesPerSecond);
}
}
```
### Gestión de Colas
```dart
class TransferQueue {
final List<SftpReq> _queue = [];
final Map<String, TransferProgress> _progress = {};
int _concurrent = 3; // Transferencias concurrentes máx.
Future<void> process() async {
final active = _progress.values.where((p) => p.isInProgress);
if (active.length >= _concurrent) return;
final pending = _queue.where((r) => !_progress.containsKey(r.id));
for (final req in pending.take(_concurrent - active.length)) {
_executeTransfer(req);
}
}
Future<void> _executeTransfer(SftpReq req) async {
try {
_progress[req.id] = TransferProgress.inProgress(req);
if (req.type == SftpReqType.upload) {
await uploadFile(req.localPath, req.remotePath);
} else {
await downloadFile(req.remotePath, req.localPath);
}
_progress[req.id] = TransferProgress.completed(req);
} catch (e) {
_progress[req.id] = TransferProgress.failed(req, e);
}
}
}
```
## Patrón de Almacenamiento Local
### Caché de Descargas
Los archivos descargados se guardan en:
```dart
String getLocalDownloadPath(String spiId, String remotePath) {
final normalized = remotePath.replaceAll('/', '_');
return 'Paths.file/$spiId/$normalized';
}
```
Ejemplo:
- Remoto: `/var/log/nginx/access.log`
- spiId: `server-123`
- Local: `Paths.file/server-123/_var_log_nginx_access.log`
## Edición de Archivos
### Flujo de Trabajo de Edición
```dart
Future<void> editFile(String path) async {
final sftp = await getSftpClient(spiId);
// 1. Comprobar tamaño
final stat = await sftp.stat(path);
if (stat.size > editorMaxSize) {
showWarning('Archivo demasiado grande para el editor integrado');
return;
}
// 2. Descargar a temporal
final temp = await downloadToTemp(path);
// 3. Abrir en editor
final content = await openEditor(temp.path);
// 4. Subir de nuevo
await uploadFile(temp.path, path);
// 5. Limpieza
await temp.delete();
}
```
### Integración con Editor Externo
```dart
Future<void> editInExternalEditor(String path) async {
final ssh = await getSshClient(spiId);
// Abrir terminal con editor
final editor = getSetting('sftpEditor', 'vim');
await ssh.exec('$editor "$path"');
// El usuario edita en la terminal
// Tras guardar, refrescar la vista SFTP
}
```
## Gestión de Errores
### Errores de Permiso
```dart
try {
await sftp.upload(...);
} on SftpPermissionException {
showError('Permiso denegado: ${stat.path}');
showHint('Comprueba los permisos y la propiedad del archivo');
}
```
### Erreores de Conexión
```dart
try {
await sftp.listDir(path);
} on SftpConnectionException {
showError('Conexión perdida');
await reconnect();
}
```
### Errores de Espacio
```dart
try {
await sftp.upload(...);
} on SftpNoSpaceException {
showError('Disco lleno en el servidor remoto');
}
```
## Optimizaciones de Rendimiento
### Caché de Directorios
```dart
class DirectoryCache {
final Map<String, CachedDirectory> _cache = {};
final Duration ttl = Duration(minutes: 5);
Future<List<SftpFile>> list(String path) async {
final cached = _cache[path];
if (cached != null && !cached.isExpired) {
return cached.files;
}
final files = await sftp.listDir(path);
_cache[path] = CachedDirectory(files);
return files;
}
}
```
### Carga Perezosa (Lazy Loading)
Para directorios grandes (>1000 elementos):
```dart
List<SftpFile> loadPage(String path, int page, int pageSize) {
final all = cache[path] ?? [];
final start = page * pageSize;
final end = start + pageSize;
return all.sublist(start, end.clamp(0, all.length));
}
```
### Paginación
```dart
class PaginatedDirectory {
static const pageSize = 100;
Future<List<SftpFile>> getPage(int page) async {
final offset = page * pageSize;
return await sftp.listDir(
path,
offset: offset,
limit: pageSize,
);
}
}
```

View File

@@ -0,0 +1,305 @@
---
title: Conexión SSH
description: Cómo se establecen y gestionan las conexiones SSH
---
Entendiendo las conexiones SSH en Server Box.
## Flujo de Conexión
```text
Entrada de Usuario → Configuración Spi → genClient() → Cliente SSH → Sesión
```
### Paso 1: Configuración
El modelo `Spi` (Server Parameter Info) contiene:
```dart
class Spi {
String id; // ID del servidor
String name; // Nombre del servidor
String ip; // Dirección IP
int port; // Puerto SSH (por defecto 22)
String user; // Usuario
String? pwd; // Contraseña (cifrada)
String? keyId; // ID de la clave SSH
String? jumpId; // ID del servidor de salto (Jump server)
String? alterUrl; // URL alternativa
}
```
### Paso 2: Generación del Cliente
`genClient(spi)` crea el cliente SSH:
```dart
Future<SSHClient> genClient(Spi spi) async {
// 1. Establecer socket
var socket = await connect(spi.ip, spi.port);
// 2. Probar URL alternativa si falla
if (socket == null && spi.alterUrl != null) {
socket = await connect(spi.alterUrl, spi.port);
}
if (socket == null) {
throw ConnectionException('Unable to connect');
}
// 3. Autenticar
final client = SSHClient(
socket: socket,
username: spi.user,
onPasswordRequest: () => spi.pwd,
onIdentityRequest: () => loadKey(spi.keyId),
);
// 4. Verificar clave de host
await verifyHostKey(client, spi);
return client;
}
```
### Paso 3: Servidor de Salto (si está configurado)
Para servidores de salto, conexión recursiva:
```dart
if (spi.jumpId != null) {
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
final forwarded = await jumpClient.forwardLocal(
spi.ip,
spi.port,
);
// Conectar a través del socket reenviado
}
```
## Métodos de Autenticación
### Autenticación por Contraseña
```dart
onPasswordRequest: () => spi.pwd
```
- Contraseña almacenada cifrada en Hive
- Descifrada al conectar
- Enviada al servidor para verificación
### Autenticación por Clave Privada
```dart
onIdentityRequest: () async {
final key = await KeyStore.get(spi.keyId);
return decyptPem(key.pem, key.password);
}
```
**Proceso de Carga de Clave:**
1. Recuperar clave cifrada de `KeyStore`
2. Descifrar contraseña (biometría/aviso)
3. Analizar formato PEM
4. Estandarizar finales de línea (LF)
5. Retornar para autenticación
### Interacción por Teclado (Keyboard-Interactive)
```dart
onUserInfoRequest: (instructions) async {
// Gestionar desafío-respuesta
return responses;
}
```
Soporta:
- Autenticación por contraseña
- Tokens OTP
- Autenticación de doble factor (2FA)
## Verificación de Clave de Host
### ¿Por qué verificar las claves de host?
Evita ataques de **Hombre en el Medio (MITM)** asegurando que te conectas al mismo servidor.
### Formato de Almacenamiento
```text
{spi.id}::{keyType}
```
Ejemplo:
```text
mi-servidor::ssh-ed25519
mi-servidor::ecdsa-sha2-nistp256
```
### Formatos de Huella Digital (Fingerprint)
**MD5 Hex:**
```text
aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
```
**Base64:**
```text
SHA256:AbCdEf1234567890...=
```
### Flujo de Verificación
```dart
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
final key = await client.hostKey;
final keyType = key.type;
final fingerprint = md5Hex(key); // o base64
final stored = SettingStore.sshKnownHostsFingerprints
['${spi.id}::$keyType'];
if (stored == null) {
// Nuevo host - preguntar al usuario
final trust = await promptUser(
'Host desconocido',
'Huella: $fingerprint',
);
if (trust) {
SettingStore.sshKnownHostsFingerprints
['${spi.id}::$keyType'] = fingerprint;
}
} else if (stored != fingerprint) {
// Ha cambiado - advertir al usuario
await warnUser(
'¡La clave de host ha cambiado!',
'Posible ataque MITM',
);
}
}
```
## Gestión de Sesiones
### Pool de Conexiones
Clientes activos mantenidos en `ServerProvider`:
```dart
class ServerProvider {
final Map<String, SSHClient> _clients = {};
SSHClient getClient(String spiId) {
return _clients[spiId] ??= connect(spiId);
}
}
```
### Keep-Alive
Mantener la conexión durante la inactividad:
```dart
Timer.periodic(
Duration(seconds: 30),
(_) => client.sendKeepAlive(),
);
```
### Reconexión Automática
Al perder la conexión:
```dart
client.onError.listen((error) async {
await Future.delayed(Duration(seconds: 5));
reconnect();
});
```
## Ciclo de Vida de la Conexión
```text
┌─────────────┐
│ Inicial │
└──────┬──────┘
│ connect()
┌─────────────┐
│ Conectando │ ←──┐
└──────┬──────┘ │
│ éxito │
↓ │ fallo (reintento)
┌─────────────┐ │
│ Conectado │───┘
└──────┬──────┘
┌─────────────┐
│ Activo │ ──→ Enviar comandos
└──────┬──────┘
↓ (error/desconexión)
┌─────────────┐
│ Desconectado│
└─────────────┘
```
## Gestión de Errores
### Tiempo de Espera Agotado (Timeout)
```dart
try {
await client.connect().timeout(
Duration(seconds: 30),
);
} on TimeoutException {
throw ConnectionException('Tiempo de espera de conexión agotado');
}
```
### Fallo de Autenticación
```dart
onAuthFail: (error) {
if (error.contains('password')) {
return 'Contraseña no válida';
} else if (error.contains('key')) {
return 'Clave SSH no válida';
}
return 'Fallo de autenticación';
}
```
### Discrepancia en Clave de Host
```dart
onHostKeyMismatch: (stored, current) {
showSecurityWarning(
'¡La clave de host ha cambiado!',
'Posible ataque MITM',
);
}
```
## Consideraciones de Rendimiento
### Reutilización de Conexiones
- Reutilizar clientes entre funciones
- No desconectar/reconectar innecesariamente
- Pool de conexiones para operaciones concurrentes
### Ajustes Óptimos
- **Timeout**: 30 segundos (ajustable)
- **Keep-alive**: Cada 30 segundos
- **Retraso de reintento**: 5 segundos
### Eficiencia de Red
- Conexión única para múltiples operaciones
- Comandos en tubería (pipeline) cuando sea posible
- Evitar abrir múltiples conexiones

View File

@@ -0,0 +1,167 @@
---
title: Gestión de Estado
description: Cómo se gestiona el estado con Riverpod
---
Entendiendo la arquitectura de gestión de estado en Server Box.
## ¿Por qué Riverpod?
**Beneficios Clave:**
- **Seguridad en tiempo de compilación**: Detecta errores al compilar
- **Sin necesidad de BuildContext**: Accede al estado desde cualquier lugar
- **Facilidad de pruebas**: Sencillo de probar providers de forma aislada
- **Generación de código**: Menos código repetitivo, tipado seguro
## Arquitectura de Providers
```
┌─────────────────────────────────────────────┐
│ Capa UI (Widgets) │
│ - ConsumerWidget / ConsumerStatefulWidget │
│ - ref.watch() / ref.read() │
└─────────────────────────────────────────────┘
↓ observa (watches)
┌─────────────────────────────────────────────┐
│ Capa de Provider │
│ - Anotaciones @riverpod │
│ - Archivos *.g.dart generados │
└─────────────────────────────────────────────┘
↓ usa (uses)
┌─────────────────────────────────────────────┐
│ Capa de Servicio / Store │
│ - Lógica de negocio │
│ - Acceso a datos │
└─────────────────────────────────────────────┘
```
## Tipos de Provider Utilizados
### 1. StateProvider (Estado Simple)
Para estados simples y observables:
```dart
@riverpod
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeMode build() {
// Cargar desde ajustes
return SettingStore.themeMode;
}
void setTheme(ThemeMode mode) {
state = mode;
SettingStore.themeMode = mode; // Persistir
}
}
```
**Uso:**
```dart
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themeNotifierProvider);
return Text('Tema: $theme');
}
}
```
### 2. AsyncNotifierProvider (Estado Asíncrono)
Para datos que se cargan de forma asíncrona:
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
// Carga inicial
return await fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await fetchStatus(server);
});
}
}
```
**Uso:**
```dart
final status = ref.watch(serverStatusProvider(server));
status.when(
data: (data) => StatusWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### 3. StreamProvider (Datos en Tiempo Real)
Para flujos de datos continuos:
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
final client = ref.watch(sshClientProvider(server));
final stream = client.monitorCpu();
// Liberación automática cuando no se observa
ref.onDispose(() {
client.stopMonitoring();
});
return stream;
}
```
**Uso:**
```dart
final cpu = ref.watch(cpuUsageProvider(server));
cpu.when(
data: (usage) => CpuChart(usage),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error),
)
```
### 4. Family Providers (Parametrizados)
Providers que aceptan parámetros:
```dart
@riverpod
Future<List<Container>> containers(ContainersRef ref, Server server) async {
final client = await ref.watch(sshClientProvider(server).future);
return await client.listContainers();
}
```
**Uso:**
```dart
final containers = ref.watch(containersProvider(server));
// Diferentes servidores = diferentes estados en caché
final containers2 = ref.watch(containersProvider(server2));
```
## Optimizaciones de Rendimiento
- **Provider Keep-Alive**: Usa `@Riverpod(keepAlive: true)` para evitar que se destruya automáticamente cuando no haya escuchadores.
- **Observación selectiva**: Usa `select` para observar solo una parte específica del estado.
- **Caché de Providers**: Los Family providers cachean resultados por parámetro.
## Mejores Prácticas
1. **Co-localizar providers**: Colócalos cerca de los widgets que los consumen.
2. **Usar generación de código**: Usa siempre `@riverpod`.
3. **Mantener providers enfocados**: Responsabilidad única.
4. **Gestionar estados de carga**: Maneja siempre los estados de AsyncValue.
5. **Liberar recursos**: Usa `ref.onDispose()` para la limpieza.
6. **Evitar árboles de providers profundos**: Mantén el grafo de providers plano.

View File

@@ -0,0 +1,198 @@
---
title: Implementación de la Terminal
description: Cómo funciona internamente la terminal SSH
---
La terminal SSH es una de las funciones más complejas, construida sobre un fork personalizado de xterm.dart.
## Resumen de la Arquitectura
```
┌─────────────────────────────────────────────┐
│ Capa de UI de la Terminal │
│ - Gestión de pestañas │
│ - Teclado virtual │
│ - Selección de texto │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Emulador xterm.dart │
│ - PTY (Pseudo Terminal) │
│ - Emulación VT100/ANSI │
│ - Motor de renderizado │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Capa de Cliente SSH │
│ - Sesión SSH │
│ - Gestión de canales │
│ - Streaming de datos │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Servidor Remoto │
│ - Proceso de Shell │
│ - Ejecución de comandos │
└─────────────────────────────────────────────┘
```
## Ciclo de Vida de la Sesión de Terminal
### 1. Creación de la Sesión
```dart
Future<TerminalSession> createSession(Spi spi) async {
// 1. Obtener cliente SSH
final client = await genClient(spi);
// 2. Crear PTY
final pty = await client.openPty(
term: 'xterm-256color',
cols: 80,
rows: 24,
);
// 3. Inicializar emulador de terminal
final terminal = Terminal(
backend: PtyBackend(pty),
);
// 4. Configurar manejador de cambio de tamaño
terminal.onResize.listen((size) {
pty.resize(size.cols, size.rows);
});
return TerminalSession(
terminal: terminal,
pty: pty,
client: client,
);
}
```
### 2. Emulación de Terminal
El fork de xterm.dart proporciona:
**Emulación VT100/ANSI:**
- Movimiento del cursor
- Colores (soporte para 256 colores)
- Atributos de texto (negrita, subrayado, etc.)
- Regiones de desplazamiento
- Búfer de pantalla alternativo
**Renderizado:**
- Renderizado basado en líneas
- Soporte para texto bidireccional
- Soporte para Unicode/emoji
- Redibujado optimizado
### 3. Flujo de Datos
```
Entrada del Usuario
Teclado Virtual / Teclado Físico
Emulador de Terminal (tecla → secuencia de escape)
Canal SSH (envío)
PTY Remoto
Shell Remoto
Salida del Comando
Canal SSH (recepción)
Emulador de Terminal (analizar códigos ANSI)
Renderizado en Pantalla
```
## Sistema de Múltiples Pestañas
### Gestión de Pestañas
Las pestañas mantienen su estado durante la navegación:
- La conexión SSH se mantiene activa
- Se preserva el estado de la terminal
- Se mantiene el búfer de desplazamiento
- Se retiene el historial de entrada
## Teclado Virtual
### Implementación Específica por Plataforma
**iOS:**
- Teclado personalizado basado en UIView
- Conmutable con un botón de teclado
- Mostrar/ocultar automáticamente basado en el enfoque
**Android:**
- Método de entrada personalizado
- Integrado con el teclado del sistema
- Botones de acción rápida
### Botones del Teclado
| Botón | Acción |
|--------|--------|
| **Conmutar** | Mostrar/ocultar teclado del sistema |
| **Ctrl** | Enviar modificador Ctrl |
| **Alt** | Enviar modificador Alt |
| **SFTP** | Abrir directorio actual |
| **Portapapeles** | Copiar/Pegar sensible al contexto |
| **Snippets** | Ejecutar fragmento de código |
## Selección de Texto
1. **Pulsación larga**: Entrar en modo selección
2. **Arrastrar**: Extender la selección
3. **Soltar**: Copiar al portapapeles
## Fuente y Dimensiones
### Cálculo de Tamaño
```dart
class TerminalDimensions {
static Size calculate(double fontSize, Size screenSize) {
final charWidth = fontSize * 0.6; // Relación de aspecto monoespaciada
final charHeight = fontSize * 1.2;
final cols = (screenSize.width / charWidth).floor();
final rows = (screenSize.height / charHeight).floor();
return Size(cols.toDouble(), rows.toDouble());
}
}
```
### Pellizcar para Ampliar (Pinch-to-Zoom)
```dart
GestureDetector(
onScaleStart: () => _baseFontSize = currentFontSize,
onScaleUpdate: (details) {
final newFontSize = _baseFontSize * details.scale;
resize(newFontSize);
},
)
```
## Esquema de Colores
- **Claro (Light)**: Fondo claro, texto oscuro
- **Oscuro (Dark)**: Fondo oscuro, texto claro
- **AMOLED**: Fondo negro puro
## Optimizaciones de Rendimiento
- **Dirty rectangle**: Solo redibujar las regiones cambiadas
- **Caché de líneas**: Cachear las líneas renderizadas
- **Desplazamiento perezoso (Lazy scrolling)**: Desplazamiento virtual para búferes largos
- **Actualizaciones por lotes**: Unificar múltiples escrituras
- **Compresión**: Comprimir el búfer de desplazamiento
- **Debouncing**: Antirrebote para entradas rápidas

View File

@@ -0,0 +1,45 @@
---
title: Inicio Rápido
description: Comienza a usar Server Box en cuestión de minutos
---
Sigue esta guía de inicio rápido para conectarte a tu primer servidor y comenzar la monitorización.
## Paso 1: Agregar un Servidor
1. Abre Server Box
2. Toca el botón **+** para agregar un nuevo servidor
3. Completa la información del servidor:
- **Nombre**: Un nombre descriptivo para tu servidor
- **Host**: Dirección IP o nombre de dominio
- **Puerto**: Puerto SSH (por defecto: 22)
- **Usuario**: Nombre de usuario SSH
- **Contraseña o Llave**: Método de autenticación
4. Toca **Guardar** para agregar el servidor
## Paso 2: Conectar y Monitorear
1. Toca en la tarjeta de tu servidor para conectarte
2. La aplicación establecerá una conexión SSH
3. Verás el estado en tiempo real de:
- Uso de CPU
- Memoria (RAM) y Swap
- Uso de disco
- Velocidad de red
## Paso 3: Explorar Funcionalidades
Una vez conectado, puedes:
- **Abrir la Terminal**: Toca el botón de la terminal para obtener acceso SSH completo
- **Explorar Archivos**: Usa SFTP para gestionar archivos
- **Gestionar Contenedores**: Visualiza y controla contenedores Docker
- **Ver Procesos**: Revisa los procesos en ejecución
- **Ejecutar Snippets**: Ejecuta comandos guardados
## Consejos
- **Autenticación Biométrica**: Activa Face ID / Touch ID / Huella dactilar para un acceso rápido (móvil)
- **Widgets en la Pantalla de Inicio**: Agrega widgets de estado del servidor a tu pantalla de inicio (iOS/Android)
- **Ejecución en Segundo Plano**: Mantén las conexiones activas en segundo plano (Android)

View File

@@ -0,0 +1,83 @@
---
title: Importation massive de serveurs
description: Importer plusieurs serveurs à partir d'un fichier JSON
---
Importez plusieurs configurations de serveur en une seule fois à l'aide d'un fichier JSON.
## Format JSON
:::danger[Avertissement de sécurité]
**Ne stockez jamais de mots de passe en clair dans des fichiers !** Cet exemple JSON montre un champ de mot de passe à des fins de démonstration uniquement, mais vous devriez :
- **Préférer les clés SSH** (`keyId`) au lieu de `pwd` - elles sont plus sûres
- **Utiliser des gestionnaires de mots de passe** ou des variables d'environnement si vous devez utiliser des mots de passe
- **Supprimer le fichier immédiatement** après l'importation - ne laissez pas traîner des identifiants
- **Ajouter au .gitignore** - ne validez jamais de fichiers d'identifiants dans le contrôle de version
:::
```json
[
{
"name": "Mon serveur",
"ip": "example.com",
"port": 22,
"user": "root",
"pwd": "password",
"keyId": "",
"tags": ["production"],
"autoConnect": false
}
]
```
## Champs
| Champ | Requis | Description |
|-------|----------|-------------|
| `name` | Oui | Nom d'affichage |
| `ip` | Oui | Domaine ou adresse IP |
| `port` | Oui | Port SSH (généralement 22) |
| `user` | Oui | Nom d'utilisateur SSH |
| `pwd` | Non | Mot de passe (à éviter - utilisez plutôt des clés SSH) |
| `keyId` | Non | Nom de la clé SSH (à partir des clés privées - recommandé) |
| `tags` | Non | Tags d'organisation |
| `autoConnect` | Non | Connexion automatique au démarrage |
## Étapes d'importation
1. Créer un fichier JSON avec les configurations de serveur
2. Paramètres → Sauvegarde → Importation massive de serveurs
3. Sélectionnez votre fichier JSON
4. Confirmez l'importation
## Exemple
```json
[
{
"name": "Production",
"ip": "prod.example.com",
"port": 22,
"user": "admin",
"keyId": "my-key",
"tags": ["production", "web"]
},
{
"name": "Développement",
"ip": "dev.example.com",
"port": 2222,
"user": "dev",
"keyId": "dev-key",
"tags": ["development"]
}
]
```
## Conseils
- **Utilisez des clés SSH** au lieu de mots de passe lorsque cela est possible
- **Testez la connexion** après l'importation
- **Organisez avec des tags** pour une gestion plus facile
- **Supprimez le fichier JSON** après l'importation
- **Ne validez jamais** de fichiers JSON contenant des identifiants dans le contrôle de version

View File

@@ -0,0 +1,72 @@
---
title: Commandes personnalisées
description: Afficher la sortie des commandes personnalisées sur la page du serveur
---
Ajoutez des commandes shell personnalisées pour afficher leur sortie sur la page de détails du serveur.
## Configuration
1. Paramètres du serveur → Commandes personnalisées
2. Entrez les commandes au format JSON
## Format de base
```json
{
"Nom d'affichage": "commande shell"
}
```
**Exemple :**
```json
{
"Mémoire": "free -h",
"Disque": "df -h",
"Uptime": "uptime"
}
```
## Visualisation des résultats
Après la configuration, les commandes personnalisées apparaissent sur la page de détails du serveur et s'actualisent automatiquement.
## Noms de commandes spéciaux
### server_card_top_right
Affichage sur la carte du serveur de la page d'accueil (coin supérieur droit) :
```json
{
"server_card_top_right": "votre-commande-ici"
}
```
## Conseils
**Utilisez des chemins absolus :**
```json
{"Mon script": "/usr/local/bin/mon-script.sh"}
```
**Commandes avec pipe :**
```json
{"Processus principal": "ps aux | sort -rk 3 | head -5"}
```
**Formater la sortie :**
```json
{"Charge CPU": "uptime | awk -F'load average:' '{print $2}'"}
```
**Gardez les commandes rapides :** Moins de 5 secondes pour une meilleure expérience.
**Limiter la sortie :**
```json
{"Logs": "tail -20 /var/log/syslog"}
```
## Sécurité
Les commandes s'exécutent avec les permissions de l'utilisateur SSH. Évitez les commandes qui modifient l'état du système.

View File

@@ -0,0 +1,54 @@
---
title: Logo de serveur personnalisé
description: Utiliser des images personnalisées pour les cartes de serveur
---
Affichez des logos personnalisés sur les cartes de serveur à l'aide d'URL d'images.
## Configuration
1. Paramètres du serveur → Logo personnalisé
2. Entrez l'URL de l'image
## Espaces réservés d'URL
### {DIST} - Distribution Linux
Remplacé automatiquement par la distribution détectée :
```
https://example.com/{DIST}.png
```
Devient : `debian.png`, `ubuntu.png`, `arch.png`, etc.
### {BRIGHT} - Thème
Remplacé automatiquement par le thème actuel :
```
https://example.com/{BRIGHT}.png
```
Devient : `light.png` ou `dark.png`
### Combiner les deux
```
https://example.com/{DIST}-{BRIGHT}.png
```
Devient : `debian-light.png`, `ubuntu-dark.png`, etc.
## Conseils
- Utilisez les formats PNG ou SVG
- Taille recommandée : 64x64 à 128x128 pixels
- Utilisez des URL HTTPS
- Gardez des tailles de fichiers réduites
## Distributions supportées
debian, ubuntu, centos, fedora, opensuse, kali, alpine, arch, rocky, deepin, armbian, wrt
Liste complète : [`dist.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/model/server/dist.dart)

View File

@@ -0,0 +1,64 @@
---
title: Paramètres cachés (JSON)
description: Accéder aux paramètres avancés via l'éditeur JSON
---
Certains paramètres sont masqués de l'interface utilisateur mais accessibles via l'éditeur JSON.
## Accès
Appuyez longuement sur **Paramètres** dans le menu latéral pour ouvrir l'éditeur JSON.
## Paramètres cachés courants
### timeOut
Délai d'attente de connexion en secondes.
```json
{"timeOut": 10}
```
**Type :** entier | **Par défaut :** 5 | **Plage :** 1-60
### recordHistory
Enregistrer l'historique (chemins SFTP, etc.).
```json
{"recordHistory": true}
```
**Type :** booléen | **Par défaut :** true
### textFactor
Facteur de mise à l'échelle du texte.
```json
{"textFactor": 1.2}
```
**Type :** double | **Par défaut :** 1.0 | **Plage :** 0.8-1.5
## Trouver plus de paramètres
Tous les paramètres sont définis dans [`setting.dart`](https://github.com/lollipopkit/flutter_server_box/blob/main/lib/data/store/setting.dart).
Recherchez :
```dart
late final settingName = StoreProperty(box, 'settingKey', defaultValue);
```
## ⚠️ Important
**Avant d'éditer :**
- **Créer une sauvegarde** - De mauvais paramètres peuvent empêcher l'ouverture de l'application
- **Éditer avec soin** - Le JSON doit être valide
## Récupération
Si l'application ne s'ouvre plus après l'édition :
1. Effacer les données de l'application (dernier recours)
2. Réinstaller l'application
3. Restaurer à partir d'une sauvegarde

View File

@@ -0,0 +1,118 @@
---
title: Problèmes courants
description: Solutions aux problèmes fréquents
---
## Problèmes de connexion
### SSH ne se connecte pas
**Symptômes :** Délai d'attente (timeout), connexion refusée, échec d'authentification
**Solutions :**
1. **Vérifier le type de serveur :** Seuls les systèmes de type Unix sont supportés (Linux, macOS, Android/Termux)
2. **Tester manuellement :** `ssh utilisateur@serveur -p port`
3. **Vérifier le pare-feu :** Le port 22 doit être ouvert
4. **Vérifier les identifiants :** Nom d'utilisateur et mot de passe/clé corrects
### Déconnexions fréquentes
**Symptômes :** Le terminal se déconnecte après une période d'inactivité
**Solutions :**
1. **Keep-alive du serveur :**
```bash
# /etc/ssh/sshd_config
ClientAliveInterval 60
ClientAliveCountMax 3
```
2. **Désactiver l'optimisation de la batterie :**
- MIUI : Batterie → "Pas de restrictions"
- Android : Paramètres → Applications → Désactiver l'optimisation
- iOS : Activer l'actualisation en arrière-plan
## Problèmes de saisie
### Impossible de taper certains caractères
**Solution :** Paramètres → Type de clavier → Passer à `visiblePassword`
Note : La saisie CJK (Chinois, Japonais, Coréen) peut ne pas fonctionner après ce changement.
## Problèmes de l'application
### L'application plante au démarrage
**Symptômes :** L'application ne s'ouvre pas, écran noir
**Causes :** Paramètres corrompus, particulièrement via l'éditeur JSON
**Solutions :**
1. **Effacer les données de l'application :**
- Android : Paramètres → Applications → ServerBox → Effacer les données
- iOS : Supprimer et réinstaller
2. **Restaurer une sauvegarde :** Importer une sauvegarde créée avant de modifier les paramètres
### Problèmes de sauvegarde/restauration
**La sauvegarde ne fonctionne pas :**
- Vérifier l'espace de stockage
- Vérifier que l'application a les permissions de stockage
- Essayer un autre emplacement
**La restauration échoue :**
- Vérifier l'intégrité du fichier de sauvegarde
- Vérifier la compatibilité de la version de l'application
## Problèmes de Widget
### Le widget ne se met pas à jour
**iOS :**
- Attendre jusqu'à 30 minutes pour le rafraîchissement automatique
- Supprimer et rajouter le widget
- Vérifier que l'URL se termine par `/status`
**Android :**
- Appuyer sur le widget pour forcer le rafraîchissement
- Vérifier que l'ID du widget correspond à la configuration dans les paramètres de l'application
**watchOS :**
- Redémarrer l'application sur la montre
- Attendre quelques minutes après un changement de configuration
- Vérifier le format de l'URL
### Le widget affiche une erreur
- Vérifier que ServerBox Monitor fonctionne sur le serveur
- Tester l'URL dans un navigateur
- Vérifier les identifiants d'authentification
## Problèmes de performance
### L'application est lente
**Solutions :**
- Réduire la fréquence de rafraîchissement dans les paramètres
- Vérifier la vitesse du réseau
- Désactiver les serveurs inutilisés
### Utilisation élevée de la batterie
**Solutions :**
- Augmenter les intervalles de rafraîchissement
- Désactiver le rafraîchissement en arrière-plan
- Fermer les sessions SSH inutilisées
## Obtenir de l'aide
Si les problèmes persistent :
1. **Rechercher dans les Issues GitHub :** https://github.com/lollipopkit/flutter_server_box/issues
2. **Créer une nouvelle Issue :** Inclure la version de l'application, la plateforme et les étapes pour reproduire le problème
3. **Consulter le Wiki :** Cette documentation et le Wiki GitHub

View File

@@ -0,0 +1,90 @@
---
title: Widgets de l'écran d'accueil
description: Ajoutez des widgets d'état du serveur à votre écran d'accueil
---
Nécessite l'installation de [ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor) sur vos serveurs.
## Prérequis
Installez d'abord ServerBox Monitor sur votre serveur. Consultez le [Wiki de ServerBox Monitor](https://github.com/lollipopkit/server_box_monitor/wiki/Home) pour les instructions de configuration.
Après l'installation, votre serveur doit avoir :
- Un point de terminaison HTTP/HTTPS
- Un point de terminaison API `/status`
- Une authentification facultative
## Format de l'URL
```
https://votre-serveur.com/status
```
Doit se terminer par `/status`.
## Widget iOS
### Configuration
1. Appuyez longuement sur l'écran d'accueil → Appuyez sur **+**
2. Recherchez "ServerBox"
3. Choisissez la taille du widget
4. Appuyez longuement sur le widget → **Modifier le widget**
5. Entrez l'URL se terminant par `/status`
### Notes
- Doit utiliser HTTPS (sauf pour les adresses IP locales)
- Taux de rafraîchissement maximal : 30 minutes (limite iOS)
- Ajoutez plusieurs widgets pour plusieurs serveurs
## Widget Android
### Configuration
1. Appuyez longuement sur l'écran d'accueil → **Widgets**
2. Trouvez "ServerBox" → Ajoutez à l'écran d'accueil
3. Notez le numéro d'ID du widget affiché
4. Ouvrez l'application ServerBox → Paramètres
5. Appuyez sur **Configurer le lien du widget d'accueil**
6. Ajoutez l'entrée : `Widget ID` = `URL d'état`
Exemple :
- Clé : `17`
- Valeur : `https://mon-serveur.com/status`
7. Appuyez sur le widget sur l'écran d'accueil pour le rafraîchir
## Widget watchOS
### Configuration
1. Ouvrez l'application iPhone → Paramètres
2. **Paramètres iOS****Application Watch**
3. Appuyez sur **Ajouter une URL**
4. Entrez l'URL se terminant par `/status`
5. Attendez que l'application de la montre se synchronise
### Notes
- Essayez de redémarrer l'application de la montre si elle ne se met pas à jour
- Vérifiez que le téléphone et la montre sont connectés
## Dépannage
### Le widget ne se met pas à jour
**iOS :** Attendez jusqu'à 30 minutes, puis supprimez et rajoutez-le.
**Android :** Appuyez sur le widget pour forcer le rafraîchissement, vérifiez l'ID dans les paramètres.
**watchOS :** Redémarrez l'application de la montre, attendez quelques minutes.
### Le widget affiche une erreur
- Vérifiez que ServerBox Monitor fonctionne
- Testez l'URL dans un navigateur
- Vérifiez que l'URL se termine par `/status`
## Sécurité
- **Utilisez toujours HTTPS** si possible
- **Adresses IP locales uniquement** sur les réseaux de confiance

View File

@@ -0,0 +1,86 @@
---
title: Architecture
description: Modèles d'architecture et décisions de conception
---
Server Box suit les principes de la Clean Architecture avec une séparation claire entre les couches de données, de domaine et de présentation.
## Architecture en couches
```
┌─────────────────────────────────────┐
│ Couche Présentation │
│ (lib/view/page/) │
│ - Pages, Widgets, Contrôleurs │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Couche Logique Métier │
│ (lib/data/provider/) │
│ - Providers Riverpod │
│ - Gestion de l'état │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Couche Données │
│ (lib/data/model/, store/) │
│ - Modèles, Stockage, Services │
└─────────────────────────────────────┘
```
## Modèles clés
### Gestion de l'état : Riverpod
- **Génération de code** : Utilise `riverpod_generator` pour des providers type-safe
- **State Notifiers** : Pour un état mutable avec une logique métier
- **Async Notifiers** : Pour les états de chargement et d'erreur
- **Stream Providers** : Pour les données en temps réel
### Modèles immuables : Freezed
- Tous les modèles de données utilisent Freezed pour l'immuabilité
- Types Union pour la représentation de l'état
- Sérialisation JSON intégrée
- Extensions CopyWith pour les mises à jour
### Stockage local : Hive
- **hive_ce** : Édition communautaire de Hive
- Pas de `@HiveField` ou `@HiveType` manuel requis
- Adaptateurs de type auto-générés
- Stockage clé-valeur persistant
## Injection de dépendances
Les services et les stores sont injectés via :
1. **Providers** : Exposer les dépendances à l'UI
2. **GetIt** : Localisation de services (le cas échéant)
3. **Injection par constructeur** : Dépendances explicites
## Flux de données
```
Action Utilisateur → Widget → Provider → Service/Store → Mise à jour Modèle → Reconstruction UI
```
1. L'utilisateur interagit avec le widget
2. Le widget appelle une méthode du provider
3. Le provider met à jour l'état via le service/store
4. Le changement d'état déclenche la reconstruction de l'UI
5. Le nouvel état est reflété dans le widget
## Dépendances personnalisées
Le projet utilise plusieurs forks personnalisés pour étendre les fonctionnalités :
- **dartssh2** : Fonctionnalités SSH améliorées
- **xterm** : Émulateur de terminal avec support mobile
- **fl_lib** : Composants UI et utilitaires partagés
## Threading (Multi-processus)
- **Isolates** : Calculs lourds hors du thread principal
- **paquet computer** : Utilitaires multi-threading
- **Async/Await** : Opérations d'E/S non bloquantes

View File

@@ -0,0 +1,116 @@
---
title: Construction (Building)
description: Instructions de construction pour différentes plateformes
---
Server Box utilise un système de construction personnalisé (`fl_build`) pour les constructions multiplateformes.
## Prérequis
- Flutter SDK (canal stable)
- Outils spécifiques à la plateforme (Xcode pour iOS, Android Studio pour Android)
- Chaîne d'outils Rust (pour certaines dépendances natives)
## Construction pour le développement
```bash
# Exécuter en mode développement
flutter run
# Exécuter sur un appareil spécifique
flutter run -d <id-appareil>
```
## Construction pour la production
Le projet utilise `fl_build` pour la construction :
```bash
# Construire pour une plateforme spécifique
dart run fl_build -p <plateforme>
# Plateformes disponibles :
# - ios
# - android
# - macos
# - linux
# - windows
```
## Constructions spécifiques aux plateformes
### iOS
```bash
dart run fl_build -p ios
```
Nécessite :
- macOS avec Xcode
- CocoaPods
- Compte Apple Developer pour la signature
### Android
```bash
dart run fl_build -p android
```
Nécessite :
- Android SDK
- Java Development Kit
- Keystore pour la signature
### macOS
```bash
dart run fl_build -p macos
```
### Linux
```bash
dart run fl_build -p linux
```
### Windows
```bash
dart run fl_build -p windows
```
Nécessite Windows avec Visual Studio.
## Pré/Post Construction
Le script `make.dart` gère :
- La génération des métadonnées
- Les mises à jour de la chaîne de version
- Les configurations spécifiques aux plateformes
## Dépannage
### Nettoyage de la construction (Clean Build)
```bash
flutter clean
dart run build_runner build --delete-conflicting-outputs
flutter pub get
```
### Incompatibilité de version
Assurez-vous que toutes les dépendances sont compatibles :
```bash
flutter pub upgrade
```
## Liste de contrôle de publication (Release Checklist)
1. Mettre à jour la version dans `pubspec.yaml`
2. Exécuter la génération de code
3. Exécuter les tests
4. Construire pour toutes les plateformes cibles
5. Tester sur des appareils physiques
6. Créer une version (release) GitHub

View File

@@ -0,0 +1,98 @@
---
title: Génération de code
description: Utiliser build_runner pour la génération de code
---
Server Box utilise intensivement la génération de code pour les modèles, la gestion de l'état et la sérialisation.
## Quand exécuter la génération de code
À exécuter après avoir modifié :
- Des modèles avec l'annotation `@freezed`
- Des classes avec `@JsonSerializable`
- Des modèles Hive
- Des providers avec `@riverpod`
- Des localisations (fichiers ARB)
## Exécuter la génération de code
```bash
# Générer tout le code
dart run build_runner build --delete-conflicting-outputs
# Nettoyer et régénérer
dart run build_runner build --delete-conflicting-outputs --clean
```
## Fichiers générés
### Freezed (`*.freezed.dart`)
Modèles de données immuables avec types Union :
```dart
@freezed
class ServerState with _$ServerState {
const factory ServerState.connected() = Connected;
const factory ServerState.disconnected() = Disconnected;
const factory ServerState.error(String message) = Error;
}
```
### Sérialisation JSON (`*.g.dart`)
Généré à partir de `json_serializable` :
```dart
@JsonSerializable()
class Server {
final String id;
final String name;
final String host;
Server({required this.id, required this.name, required this.host});
factory Server.fromJson(Map<String, dynamic> json) =>
_$ServerFromJson(json);
Map<String, dynamic> toJson() => _$ServerToJson(this);
}
```
### Providers Riverpod (`*.g.dart`)
Généré à partir de l'annotation `@riverpod` :
```dart
@riverpod
class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
}
```
### Adaptateurs Hive (`*.g.dart`)
Auto-générés pour les modèles Hive (hive_ce) :
```dart
@HiveType(typeId: 0)
class ServerModel {
@HiveField(0)
final String id;
}
```
## Génération de localisation
```bash
flutter gen-l10n
```
Génère `lib/generated/l10n/` à partir des fichiers `lib/l10n/*.arb`.
## Conseils
- Utilisez `--delete-conflicting-outputs` pour éviter les conflits
- Ajoutez les fichiers générés au `.gitignore`
- Ne modifiez jamais manuellement les fichiers générés

View File

@@ -0,0 +1,115 @@
---
title: Gestion de l'état
description: Modèles de gestion de l'état basés sur Riverpod
---
Server Box utilise Riverpod avec la génération de code pour la gestion de l'état.
## Types de Provider
### StateProvider
État simple qui peut être lu et écrit :
```dart
@riverpod
class Settings extends _$Settings {
@override
SettingsModel build() {
return SettingsModel.defaults();
}
void update(SettingsModel newSettings) {
state = newSettings;
}
}
```
### AsyncNotifierProvider
État qui se charge de manière asynchrone avec des états de chargement/erreur :
```dart
@riverpod
class ServerStatus extends _$ServerStatus {
@override
Future<StatusModel> build(Server server) async {
return fetchStatus(server);
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => fetchStatus(server));
}
}
```
### StreamProvider
Données en temps réel provenant de flux (streams) :
```dart
@riverpod
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
return cpuService.monitor(server);
}
```
## Modèles d'état
### États de chargement
```dart
state.when(
data: (data) => DataWidget(data),
loading: () => LoadingWidget(),
error: (error, stack) => ErrorWidget(error),
)
```
### Family Providers
Providers paramétrés :
```dart
@riverpod
List<Container> containers(ContainersRef ref, Server server) {
return containerService.list(server);
}
```
### Auto-Dispose
Providers qui se détruisent lorsqu'ils ne sont plus référencés :
```dart
@Riverpod(keepAlive: false)
class TempState extends _$TempState {
// ...
}
```
## Bonnes pratiques
1. **Utiliser la génération de code** : Utilisez toujours l'annotation `@riverpod`
2. **Co-localiser les providers** : Placez-les près des widgets qui les consomment
3. **Éviter les singletons** : Utilisez des providers à la place
4. **Couches correctes** : Gardez la logique UI séparée de la logique métier
## Lire l'état dans les Widgets
```dart
class ServerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final status = ref.watch(serverStatusProvider(server));
return status.when(...);
}
}
```
## Modifier l'état
```dart
ref.read(settingsProvider.notifier).update(newSettings);
```

Some files were not shown because too many files have changed in this diff Show More