mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2026-02-14 20:24:29 +01:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79e53c419e | ||
|
|
6d10054791 | ||
|
|
f9425948ca | ||
|
|
89c1d66181 | ||
|
|
a2922f9b0f | ||
|
|
67f091b2dc | ||
|
|
360baf2d75 | ||
|
|
d5e15574d7 | ||
|
|
d2cf9f5dbd | ||
|
|
2d1e0a7edd | ||
|
|
64dc00475a | ||
|
|
6338c6ce6b | ||
|
|
9281a578e7 | ||
|
|
d5e1d89394 | ||
|
|
71e757fe13 | ||
|
|
a0a62acdbc | ||
|
|
9ac866644c | ||
|
|
e226fec03d | ||
|
|
7d47c9d673 | ||
|
|
e49b31ed25 | ||
|
|
87d7feb823 | ||
|
|
84a1bd5519 | ||
|
|
f47d1e7141 | ||
|
|
d14e97485f | ||
|
|
347d294f6e | ||
|
|
d5e22cbc65 | ||
|
|
7926a4d4a6 | ||
|
|
39a3e0800b | ||
|
|
cd3c094af0 | ||
|
|
8589b3b4d7 | ||
|
|
7693e30cbf |
3
.github/workflows/analysis.yml
vendored
3
.github/workflows/analysis.yml
vendored
@@ -19,6 +19,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -29,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
# Consider passing '--fatal-infos' for slightly stricter analysis.
|
# Consider passing '--fatal-infos' for slightly stricter analysis.
|
||||||
- name: Analyze project source
|
- name: Analyze project source
|
||||||
run: dart analyze
|
run: flutter analyze lib test
|
||||||
|
|
||||||
# Your project will need to have tests in test/ and a dependency on
|
# Your project will need to have tests in test/ and a dependency on
|
||||||
# package:test for this step to succeed. Note that Flutter projects will
|
# package:test for this step to succeed. Note that Flutter projects will
|
||||||
|
|||||||
66
.github/workflows/claude.yml
vendored
66
.github/workflows/claude.yml
vendored
@@ -1,66 +0,0 @@
|
|||||||
name: Claude Code
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
issues:
|
|
||||||
types: [opened, assigned]
|
|
||||||
pull_request_review:
|
|
||||||
types: [submitted]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude:
|
|
||||||
if: |
|
|
||||||
github.actor == 'lollipopkit' && (
|
|
||||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
||||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
||||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
||||||
)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
actions: read # Required for Claude to read CI results on PRs
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code
|
|
||||||
id: claude
|
|
||||||
uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
||||||
|
|
||||||
# This is an optional setting that allows Claude to read CI results on PRs
|
|
||||||
additional_permissions: |
|
|
||||||
actions: read
|
|
||||||
|
|
||||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
|
||||||
# model: "claude-opus-4-1-20250805"
|
|
||||||
|
|
||||||
# Optional: Customize the trigger phrase (default: @claude)
|
|
||||||
# trigger_phrase: "/claude"
|
|
||||||
|
|
||||||
# Optional: Trigger when specific user is assigned to an issue
|
|
||||||
# assignee_trigger: "claude-bot"
|
|
||||||
|
|
||||||
# Optional: Allow Claude to run specific commands
|
|
||||||
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
|
||||||
|
|
||||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
|
||||||
# custom_instructions: |
|
|
||||||
# Follow our coding standards
|
|
||||||
# Ensure all new code has tests
|
|
||||||
# Use TypeScript for new files
|
|
||||||
|
|
||||||
# Optional: Custom environment variables for Claude
|
|
||||||
# claude_env: |
|
|
||||||
# NODE_ENV: test
|
|
||||||
|
|
||||||
20
.github/workflows/issue-translator.yml
vendored
20
.github/workflows/issue-translator.yml
vendored
@@ -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. 👯👭🏻🧑🤝🧑👫🧑🏿🤝🧑🏻👩🏾🤝👨🏿👬🏿
|
|
||||||
149
.github/workflows/release.yml
vendored
149
.github/workflows/release.yml
vendored
@@ -9,10 +9,9 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
# Set by fl_build
|
env:
|
||||||
# env:
|
APP_NAME: ServerBox
|
||||||
# APP_NAME: ServerBox
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
# BUILD_NUMBER: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
releaseAndroid:
|
releaseAndroid:
|
||||||
@@ -21,11 +20,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
flutter-version: "3.38.0"
|
flutter-version: "3.38.7"
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "zulu"
|
distribution: "zulu"
|
||||||
@@ -37,17 +38,28 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: dart run fl_build -p android
|
run: dart run fl_build -p android
|
||||||
- name: Rename for fdroid
|
- name: Rename for fdroid
|
||||||
|
shell: bash
|
||||||
run: |
|
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
|
APK_DIR="build/app/outputs/flutter-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
|
shopt -s nullglob
|
||||||
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
|
|
||||||
|
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
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_v1.0.${{ env.BUILD_NUMBER }}_arm64.apk
|
build/app/outputs/flutter-apk/${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_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 }}_${{ env.RELEASE_TAG }}_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 }}_amd64.apk
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -57,6 +69,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -69,11 +83,22 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
dart run fl_build -p linux
|
dart run fl_build -p linux
|
||||||
|
- name: Rename for release
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
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
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_amd64.AppImage
|
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_amd64.AppImage
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
@@ -83,32 +108,96 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
- name: Install Flutter
|
- name: Install Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dart run fl_build -p windows
|
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
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
${{ env.APP_NAME }}_${{ env.BUILD_NUMBER }}_windows_amd64.zip
|
${{ env.APP_NAME }}_${{ env.RELEASE_TAG }}_windows_amd64.zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# releaseApple:
|
releaseIOS:
|
||||||
# name: Release ios and macos
|
name: Release iOS
|
||||||
# runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
# steps:
|
steps:
|
||||||
# - name: Checkout
|
- name: Checkout
|
||||||
# uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
# - name: Install Flutter
|
with:
|
||||||
# uses: subosito/flutter-action@v2
|
submodules: recursive
|
||||||
# - name: Build
|
- name: Install Flutter
|
||||||
# run: dart run fl_build -p ios
|
uses: subosito/flutter-action@v2
|
||||||
# - name: Create Release
|
- name: Build
|
||||||
# uses: softprops/action-gh-release@v2
|
run: |
|
||||||
# with:
|
dart run fl_build -p ios -- --no-codesign
|
||||||
# files: |
|
shopt -s nullglob
|
||||||
# ${{ env.APP_NAME }}_universal.ipa
|
IPA_FILES=(build/ios/ipa/*.ipa)
|
||||||
# env:
|
if [ ${#IPA_FILES[@]} -ne 1 ]; then
|
||||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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 }}
|
||||||
|
|||||||
20
.gitmodules
vendored
Normal file
20
.gitmodules
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[submodule "dartssh2"]
|
||||||
|
path = packages/dartssh2
|
||||||
|
url = https://github.com/lollipopkit/dartssh2
|
||||||
|
branch = master
|
||||||
|
[submodule "xterm"]
|
||||||
|
path = packages/xterm
|
||||||
|
url = https://github.com/lollipopkit/xterm.dart
|
||||||
|
branch = master
|
||||||
|
[submodule "fl_lib"]
|
||||||
|
path = packages/fl_lib
|
||||||
|
url = https://github.com/lollipopkit/fl_lib
|
||||||
|
branch = main
|
||||||
|
[submodule "fl_build"]
|
||||||
|
path = packages/fl_build
|
||||||
|
url = https://github.com/lppcg/fl_build.git
|
||||||
|
branch = main
|
||||||
|
[submodule "server_box_monitor"]
|
||||||
|
path = packages/server_box_monitor
|
||||||
|
url = https://github.com/lollipopkit/server_box_monitor
|
||||||
|
branch = main
|
||||||
21
docs/.gitignore
vendored
Normal file
21
docs/.gitignore
vendored
Normal 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
4
docs/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
docs/.vscode/launch.json
vendored
Normal file
11
docs/.vscode/launch.json
vendored
Normal 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
49
docs/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Starlight Starter Kit: Basics
|
||||||
|
|
||||||
|
[](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 [Starlight’s 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
131
docs/astro.config.mjs
Normal 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
900
docs/bun.lock
Normal 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
17
docs/package.json
Normal 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
1
docs/public/favicon.svg
Normal 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 |
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
30
docs/src/assets/logo.svg
Normal file
30
docs/src/assets/logo.svg
Normal 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 |
7
docs/src/content.config.ts
Normal file
7
docs/src/content.config.ts
Normal 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() }),
|
||||||
|
};
|
||||||
83
docs/src/content/docs/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/advanced/bulk-import.md
Normal 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
|
||||||
72
docs/src/content/docs/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/advanced/custom-commands.md
Normal 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.
|
||||||
54
docs/src/content/docs/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/advanced/custom-logo.md
Normal 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)
|
||||||
65
docs/src/content/docs/advanced/json-settings.md
Normal file
65
docs/src/content/docs/advanced/json-settings.md
Normal 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
|
||||||
118
docs/src/content/docs/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/advanced/troubleshooting.md
Normal 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
|
||||||
91
docs/src/content/docs/advanced/widgets.md
Normal file
91
docs/src/content/docs/advanced/widgets.md
Normal 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
|
||||||
83
docs/src/content/docs/de/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/de/advanced/bulk-import.md
Normal 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
|
||||||
72
docs/src/content/docs/de/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/de/advanced/custom-commands.md
Normal 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.
|
||||||
54
docs/src/content/docs/de/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/de/advanced/custom-logo.md
Normal 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)
|
||||||
64
docs/src/content/docs/de/advanced/json-settings.md
Normal file
64
docs/src/content/docs/de/advanced/json-settings.md
Normal 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
|
||||||
118
docs/src/content/docs/de/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/de/advanced/troubleshooting.md
Normal 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
|
||||||
90
docs/src/content/docs/de/advanced/widgets.md
Normal file
90
docs/src/content/docs/de/advanced/widgets.md
Normal 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
|
||||||
86
docs/src/content/docs/de/development/architecture.md
Normal file
86
docs/src/content/docs/de/development/architecture.md
Normal 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
|
||||||
116
docs/src/content/docs/de/development/building.md
Normal file
116
docs/src/content/docs/de/development/building.md
Normal 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
|
||||||
98
docs/src/content/docs/de/development/codegen.md
Normal file
98
docs/src/content/docs/de/development/codegen.md
Normal 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.
|
||||||
115
docs/src/content/docs/de/development/state.md
Normal file
115
docs/src/content/docs/de/development/state.md
Normal 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);
|
||||||
|
```
|
||||||
96
docs/src/content/docs/de/development/structure.md
Normal file
96
docs/src/content/docs/de/development/structure.md
Normal 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
|
||||||
113
docs/src/content/docs/de/development/testing.md
Normal file
113
docs/src/content/docs/de/development/testing.md
Normal 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.
|
||||||
46
docs/src/content/docs/de/index.mdx
Normal file
46
docs/src/content/docs/de/index.mdx
Normal 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
|
||||||
51
docs/src/content/docs/de/installation.mdx
Normal file
51
docs/src/content/docs/de/installation.mdx
Normal 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.
|
||||||
32
docs/src/content/docs/de/introduction.mdx
Normal file
32
docs/src/content/docs/de/introduction.mdx
Normal 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.
|
||||||
80
docs/src/content/docs/de/platforms/desktop.md
Normal file
80
docs/src/content/docs/de/platforms/desktop.md
Normal 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
|
||||||
77
docs/src/content/docs/de/platforms/mobile.md
Normal file
77
docs/src/content/docs/de/platforms/mobile.md
Normal 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
|
||||||
214
docs/src/content/docs/de/principles/architecture.md
Normal file
214
docs/src/content/docs/de/principles/architecture.md
Normal 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
|
||||||
490
docs/src/content/docs/de/principles/sftp.md
Normal file
490
docs/src/content/docs/de/principles/sftp.md
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
305
docs/src/content/docs/de/principles/ssh.md
Normal file
305
docs/src/content/docs/de/principles/ssh.md
Normal 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
|
||||||
221
docs/src/content/docs/de/principles/state.md
Normal file
221
docs/src/content/docs/de/principles/state.md
Normal 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.
|
||||||
198
docs/src/content/docs/de/principles/terminal.md
Normal file
198
docs/src/content/docs/de/principles/terminal.md
Normal 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
|
||||||
45
docs/src/content/docs/de/quick-start.mdx
Normal file
45
docs/src/content/docs/de/quick-start.mdx
Normal 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)
|
||||||
86
docs/src/content/docs/development/architecture.md
Normal file
86
docs/src/content/docs/development/architecture.md
Normal 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
|
||||||
116
docs/src/content/docs/development/building.md
Normal file
116
docs/src/content/docs/development/building.md
Normal 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
|
||||||
98
docs/src/content/docs/development/codegen.md
Normal file
98
docs/src/content/docs/development/codegen.md
Normal 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
|
||||||
115
docs/src/content/docs/development/state.md
Normal file
115
docs/src/content/docs/development/state.md
Normal 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);
|
||||||
|
```
|
||||||
96
docs/src/content/docs/development/structure.md
Normal file
96
docs/src/content/docs/development/structure.md
Normal 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
|
||||||
113
docs/src/content/docs/development/testing.md
Normal file
113
docs/src/content/docs/development/testing.md
Normal 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.
|
||||||
83
docs/src/content/docs/es/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/es/advanced/bulk-import.md
Normal 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
|
||||||
72
docs/src/content/docs/es/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/es/advanced/custom-commands.md
Normal 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.
|
||||||
54
docs/src/content/docs/es/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/es/advanced/custom-logo.md
Normal 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)
|
||||||
64
docs/src/content/docs/es/advanced/json-settings.md
Normal file
64
docs/src/content/docs/es/advanced/json-settings.md
Normal 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
|
||||||
118
docs/src/content/docs/es/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/es/advanced/troubleshooting.md
Normal 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
|
||||||
90
docs/src/content/docs/es/advanced/widgets.md
Normal file
90
docs/src/content/docs/es/advanced/widgets.md
Normal 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
|
||||||
86
docs/src/content/docs/es/development/architecture.md
Normal file
86
docs/src/content/docs/es/development/architecture.md
Normal 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
|
||||||
116
docs/src/content/docs/es/development/building.md
Normal file
116
docs/src/content/docs/es/development/building.md
Normal 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
|
||||||
98
docs/src/content/docs/es/development/codegen.md
Normal file
98
docs/src/content/docs/es/development/codegen.md
Normal 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
|
||||||
115
docs/src/content/docs/es/development/state.md
Normal file
115
docs/src/content/docs/es/development/state.md
Normal 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);
|
||||||
|
```
|
||||||
96
docs/src/content/docs/es/development/structure.md
Normal file
96
docs/src/content/docs/es/development/structure.md
Normal 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
|
||||||
113
docs/src/content/docs/es/development/testing.md
Normal file
113
docs/src/content/docs/es/development/testing.md
Normal 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.
|
||||||
46
docs/src/content/docs/es/index.mdx
Normal file
46
docs/src/content/docs/es/index.mdx
Normal 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
|
||||||
51
docs/src/content/docs/es/installation.mdx
Normal file
51
docs/src/content/docs/es/installation.mdx
Normal 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.
|
||||||
32
docs/src/content/docs/es/introduction.mdx
Normal file
32
docs/src/content/docs/es/introduction.mdx
Normal 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).
|
||||||
80
docs/src/content/docs/es/platforms/desktop.md
Normal file
80
docs/src/content/docs/es/platforms/desktop.md
Normal 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
|
||||||
77
docs/src/content/docs/es/platforms/mobile.md
Normal file
77
docs/src/content/docs/es/platforms/mobile.md
Normal 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
|
||||||
214
docs/src/content/docs/es/principles/architecture.md
Normal file
214
docs/src/content/docs/es/principles/architecture.md
Normal 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
|
||||||
490
docs/src/content/docs/es/principles/sftp.md
Normal file
490
docs/src/content/docs/es/principles/sftp.md
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
305
docs/src/content/docs/es/principles/ssh.md
Normal file
305
docs/src/content/docs/es/principles/ssh.md
Normal 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
|
||||||
167
docs/src/content/docs/es/principles/state.md
Normal file
167
docs/src/content/docs/es/principles/state.md
Normal 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.
|
||||||
198
docs/src/content/docs/es/principles/terminal.md
Normal file
198
docs/src/content/docs/es/principles/terminal.md
Normal 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
|
||||||
45
docs/src/content/docs/es/quick-start.mdx
Normal file
45
docs/src/content/docs/es/quick-start.mdx
Normal 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)
|
||||||
83
docs/src/content/docs/fr/advanced/bulk-import.md
Normal file
83
docs/src/content/docs/fr/advanced/bulk-import.md
Normal 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
|
||||||
72
docs/src/content/docs/fr/advanced/custom-commands.md
Normal file
72
docs/src/content/docs/fr/advanced/custom-commands.md
Normal 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.
|
||||||
54
docs/src/content/docs/fr/advanced/custom-logo.md
Normal file
54
docs/src/content/docs/fr/advanced/custom-logo.md
Normal 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)
|
||||||
64
docs/src/content/docs/fr/advanced/json-settings.md
Normal file
64
docs/src/content/docs/fr/advanced/json-settings.md
Normal 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
|
||||||
118
docs/src/content/docs/fr/advanced/troubleshooting.md
Normal file
118
docs/src/content/docs/fr/advanced/troubleshooting.md
Normal 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
|
||||||
90
docs/src/content/docs/fr/advanced/widgets.md
Normal file
90
docs/src/content/docs/fr/advanced/widgets.md
Normal 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
|
||||||
86
docs/src/content/docs/fr/development/architecture.md
Normal file
86
docs/src/content/docs/fr/development/architecture.md
Normal 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
|
||||||
116
docs/src/content/docs/fr/development/building.md
Normal file
116
docs/src/content/docs/fr/development/building.md
Normal 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
|
||||||
98
docs/src/content/docs/fr/development/codegen.md
Normal file
98
docs/src/content/docs/fr/development/codegen.md
Normal 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
|
||||||
115
docs/src/content/docs/fr/development/state.md
Normal file
115
docs/src/content/docs/fr/development/state.md
Normal 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);
|
||||||
|
```
|
||||||
96
docs/src/content/docs/fr/development/structure.md
Normal file
96
docs/src/content/docs/fr/development/structure.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
title: Structure du projet
|
||||||
|
description: Comprendre la base de code de Server Box
|
||||||
|
---
|
||||||
|
|
||||||
|
Le projet Server Box suit une architecture modulaire avec une séparation claire des préoccupations.
|
||||||
|
|
||||||
|
## Structure des répertoires
|
||||||
|
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── core/ # Utilitaires de base et extensions
|
||||||
|
├── data/ # Couche de données
|
||||||
|
│ ├── model/ # Modèles de données par fonctionnalité
|
||||||
|
│ ├── provider/ # Providers Riverpod
|
||||||
|
│ └── store/ # Stockage local (Hive)
|
||||||
|
├── view/ # Couche UI
|
||||||
|
│ ├── page/ # Pages principales
|
||||||
|
│ └── widget/ # Widgets réutilisables
|
||||||
|
├── generated/ # Localisation générée
|
||||||
|
├── l10n/ # Fichiers ARB de localisation
|
||||||
|
└── hive/ # Adaptateurs Hive
|
||||||
|
```
|
||||||
|
|
||||||
|
## Couche Core (`lib/core/`)
|
||||||
|
|
||||||
|
Contient les utilitaires, les extensions et la configuration du routage :
|
||||||
|
|
||||||
|
- **Extensions** : Extensions Dart pour les types courants
|
||||||
|
- **Routes** : Configuration du routage de l'application
|
||||||
|
- **Utils** : Fonctions utilitaires partagées
|
||||||
|
|
||||||
|
## Couche Données (`lib/data/`)
|
||||||
|
|
||||||
|
### Modèles (`lib/data/model/`)
|
||||||
|
|
||||||
|
Organisés par fonctionnalité :
|
||||||
|
|
||||||
|
- `server/` - Modèles de connexion et d'état du serveur
|
||||||
|
- `container/` - Modèles de conteneurs Docker
|
||||||
|
- `ssh/` - Modèles de session SSH
|
||||||
|
- `sftp/` - Modèles de fichiers SFTP
|
||||||
|
- `app/` - Modèles spécifiques à l'application
|
||||||
|
|
||||||
|
### Providers (`lib/data/provider/`)
|
||||||
|
|
||||||
|
Providers Riverpod pour l'injection de dépendances et la gestion de l'état :
|
||||||
|
|
||||||
|
- Providers de serveur
|
||||||
|
- Providers d'état de l'UI
|
||||||
|
- Providers de service
|
||||||
|
|
||||||
|
### Stores (`lib/data/store/`)
|
||||||
|
|
||||||
|
Stockage local basé sur Hive :
|
||||||
|
|
||||||
|
- Stockage des serveurs
|
||||||
|
- Stockage des paramètres
|
||||||
|
- Stockage du cache
|
||||||
|
|
||||||
|
## Couche Vue (`lib/view/`)
|
||||||
|
|
||||||
|
### Pages (`lib/view/page/`)
|
||||||
|
|
||||||
|
Écrans principaux de l'application :
|
||||||
|
|
||||||
|
- `server/` - Pages de gestion des serveurs
|
||||||
|
- `ssh/` - Pages de terminal SSH
|
||||||
|
- `container/` - Pages de conteneurs
|
||||||
|
- `setting/` - Pages de paramètres
|
||||||
|
- `storage/` - Pages SFTP
|
||||||
|
- `snippet/` - Pages d'extraits de code (snippets)
|
||||||
|
|
||||||
|
### Widgets (`lib/view/widget/`)
|
||||||
|
|
||||||
|
Composants UI réutilisables :
|
||||||
|
|
||||||
|
- Cartes de serveur
|
||||||
|
- Graphiques d'état
|
||||||
|
- Composants de saisie (input)
|
||||||
|
- Dialogues
|
||||||
|
|
||||||
|
## Fichiers générés
|
||||||
|
|
||||||
|
- `lib/generated/l10n/` - Localisation auto-générée
|
||||||
|
- `*.g.dart` - Code généré (json_serializable, freezed, hive, riverpod)
|
||||||
|
- `*.freezed.dart` - Classes immuables Freezed
|
||||||
|
|
||||||
|
## Répertoire Packages (`/packages/`)
|
||||||
|
|
||||||
|
Contient les forks personnalisés des dépendances :
|
||||||
|
|
||||||
|
- `dartssh2/` - Bibliothèque SSH
|
||||||
|
- `xterm/` - Émulateur de terminal
|
||||||
|
- `fl_lib/` - Utilitaires partagés
|
||||||
|
- `fl_build/` - Système de construction
|
||||||
113
docs/src/content/docs/fr/development/testing.md
Normal file
113
docs/src/content/docs/fr/development/testing.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
title: Tests
|
||||||
|
description: Stratégies de test et exécution des tests
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exécuter les tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exécuter tous les tests
|
||||||
|
flutter test
|
||||||
|
|
||||||
|
# Exécuter un fichier de test spécifique
|
||||||
|
flutter test test/battery_test.dart
|
||||||
|
|
||||||
|
# Exécuter avec couverture de code
|
||||||
|
flutter test --coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure des tests
|
||||||
|
|
||||||
|
Les tests sont situés dans le répertoire `test/`, reflétant la structure de `lib/` :
|
||||||
|
|
||||||
|
```
|
||||||
|
test/
|
||||||
|
├── data/
|
||||||
|
│ ├── model/
|
||||||
|
│ └── provider/
|
||||||
|
├── view/
|
||||||
|
│ └── widget/
|
||||||
|
└── test_helpers.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests unitaires
|
||||||
|
|
||||||
|
Tester la logique métier et les modèles de données :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('devrait calculer le pourcentage du CPU', () {
|
||||||
|
final cpu = CpuModel(usage: 75.0);
|
||||||
|
expect(cpu.usagePercentage, '75%');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests de widgets
|
||||||
|
|
||||||
|
Tester les composants UI :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
testWidgets('ServerCard affiche le nom du serveur', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
child: MaterialApp(
|
||||||
|
home: ServerCard(server: testServer),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Test Server'), findsOneWidget);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests de providers
|
||||||
|
|
||||||
|
Tester les providers Riverpod :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('serverStatusProvider retourne le statut', () async {
|
||||||
|
final container = ProviderContainer();
|
||||||
|
final status = await container.read(serverStatusProvider(testServer).future);
|
||||||
|
expect(status, isA<StatusModel>());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking (Simulations)
|
||||||
|
|
||||||
|
Utiliser des mocks pour les dépendances externes :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class MockSshService extends Mock implements SshService {}
|
||||||
|
|
||||||
|
test('se connecte au serveur', () async {
|
||||||
|
final mockSsh = MockSshService();
|
||||||
|
when(mockSsh.connect(any)).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
// Tester avec le mock
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests d'intégration
|
||||||
|
|
||||||
|
Tester des flux utilisateurs complets (dans `integration_test/`) :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
testWidgets('flux d\'ajout de serveur', (tester) async {
|
||||||
|
await tester.pumpWidget(MyApp());
|
||||||
|
|
||||||
|
// Appuyer sur le bouton d'ajout
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Remplir le formulaire
|
||||||
|
await tester.enterText(find.byKey(Key('name')), 'Test Server');
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
1. **Arrange-Act-Assert** : Structurer les tests clairement
|
||||||
|
2. **Noms descriptifs** : Les noms de tests doivent décrire le comportement
|
||||||
|
3. **Une assertion par test** : Garder les tests focalisés
|
||||||
|
4. **Mocker les dépendances externes** : Ne pas dépendre de serveurs réels
|
||||||
|
5. **Tester les cas limites** : Listes vides, valeurs nulles, etc.
|
||||||
46
docs/src/content/docs/fr/index.mdx
Normal file
46
docs/src/content/docs/fr/index.mdx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: Server Box
|
||||||
|
description: Une application complète de gestion de serveurs multiplateforme
|
||||||
|
hero:
|
||||||
|
tagline: Gérez vos serveurs Linux de n'importe où
|
||||||
|
actions:
|
||||||
|
- text: Commencer
|
||||||
|
link: /fr/introduction/
|
||||||
|
icon: right-arrow
|
||||||
|
variant: primary
|
||||||
|
- text: Voir sur GitHub
|
||||||
|
link: https://github.com/lollipopkit/flutter_server_box
|
||||||
|
icon: github
|
||||||
|
variant: minimal
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
<CardGrid stagger>
|
||||||
|
<Card title="Surveillance en temps réel" icon="chart">
|
||||||
|
Surveillez le CPU, la mémoire, le disque, le réseau, le GPU et la température avec de magnifiques graphiques en temps réel.
|
||||||
|
</Card>
|
||||||
|
<Card title="Terminal SSH" icon="terminal">
|
||||||
|
Terminal SSH complet avec support multi-onglets et clavier virtuel pour les appareils mobiles.
|
||||||
|
</Card>
|
||||||
|
<Card title="Navigateur de fichiers SFTP" icon="folder">
|
||||||
|
Gérez les fichiers sur vos serveurs avec le client SFTP intégré et le navigateur de fichiers local.
|
||||||
|
</Card>
|
||||||
|
<Card title="Gestion Docker" icon="box">
|
||||||
|
Démarrez, arrêtez et surveillez les conteneurs Docker avec une interface intuitive.
|
||||||
|
</Card>
|
||||||
|
<Card title="Multiplateforme" icon="device-mobile">
|
||||||
|
Disponible sur iOS, Android, macOS, Linux, Windows et watchOS.
|
||||||
|
</Card>
|
||||||
|
<Card title="Plus de 12 langues" icon="globe">
|
||||||
|
Support complet de localisation incluant l'anglais, le chinois, l'allemand, le français et plus encore.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
## Liens rapides
|
||||||
|
|
||||||
|
- **Téléchargement**: Disponible sur l'[App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases) et [F-Droid](https://f-droid.org/)
|
||||||
|
- **Documentation**: Explorez les guides pour commencer avec Server Box
|
||||||
|
- **Support**: Rejoignez notre communauté sur GitHub pour des discussions et des problèmes
|
||||||
51
docs/src/content/docs/fr/installation.mdx
Normal file
51
docs/src/content/docs/fr/installation.mdx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
description: Téléchargez et installez Server Box sur votre appareil
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box est disponible sur plusieurs plateformes. Choisissez votre méthode d'installation préférée.
|
||||||
|
|
||||||
|
## Applications Mobiles
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
Téléchargez depuis l'**[App Store](https://apps.apple.com/app/id1586449703)**.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Choisissez votre source préférée :
|
||||||
|
|
||||||
|
- **[F-Droid](https://f-droid.org/)** - Pour les utilisateurs qui préfèrent les sources exclusivement FOSS
|
||||||
|
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - Pour la dernière version directement depuis la source
|
||||||
|
|
||||||
|
## Applications de Bureau
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
Téléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
Caractéristiques :
|
||||||
|
- Intégration native de la barre de menus
|
||||||
|
- Prise en charge d'Intel et d'Apple Silicon
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Téléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
Disponible en packages AppImage, deb ou tar.gz.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Téléchargez depuis les **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
## watchOS
|
||||||
|
|
||||||
|
Disponible sur l'**[App Store](https://apps.apple.com/app/id1586449703)** en tant que partie de l'application iOS.
|
||||||
|
|
||||||
|
## Construction à partir des sources
|
||||||
|
|
||||||
|
Pour construire Server Box à partir des sources, consultez la section [Construction](/fr/development/building/) dans la documentation de développement.
|
||||||
|
|
||||||
|
## Informations sur la version
|
||||||
|
|
||||||
|
Consultez la page [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) pour la dernière version et le journal des modifications (changelog).
|
||||||
32
docs/src/content/docs/fr/introduction.mdx
Normal file
32
docs/src/content/docs/fr/introduction.mdx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Découvrez ce qu'est Server Box et ce qu'il peut faire
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box est une application complète de gestion de serveur multiplateforme construite avec Flutter. Elle vous permet de surveiller, gérer et contrôler vos serveurs Linux, Unix et Windows de n'importe où.
|
||||||
|
|
||||||
|
## Qu'est-ce que Server Box ?
|
||||||
|
|
||||||
|
Server Box fournit une interface unifiée pour les tâches d'administration de serveur via des connexions SSH. Que vous soyez un administrateur système, un développeur ou un passionné gérant des serveurs domestiques, cette application met de puissants outils de gestion de serveur dans votre poche.
|
||||||
|
|
||||||
|
## Capacités clés
|
||||||
|
|
||||||
|
- **Surveillance en temps réel** : Suivez le processeur (CPU), la mémoire, l'utilisation du disque, la vitesse du réseau, l'état du GPU et les températures du système.
|
||||||
|
- **Terminal SSH** : Accès complet au terminal avec prise en charge multi-onglets et apparence personnalisable.
|
||||||
|
- **Client SFTP** : Parcourez et gérez les fichiers sur vos serveurs.
|
||||||
|
- **Gestion Docker** : Contrôlez les conteneurs en toute simplicité.
|
||||||
|
- **Gestion des processus** : Visualisez et gérez les processus système.
|
||||||
|
- **Services Systemd** : Démarrez, arrêtez et surveillez les services systemd.
|
||||||
|
- **Outils réseau** : Tests iPerf, ping et Wake-on-LAN.
|
||||||
|
- **Snippets** : Enregistrez et exécutez des commandes shell personnalisées.
|
||||||
|
|
||||||
|
## Plateformes supportées
|
||||||
|
|
||||||
|
Server Box est véritablement multiplateforme :
|
||||||
|
|
||||||
|
- **Mobile** : iOS et Android
|
||||||
|
- **Bureau** : macOS, Linux et Windows
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Ce projet est sous licence AGPL v3. Le code source est disponible sur [GitHub](https://github.com/lollipopkit/flutter_server_box).
|
||||||
80
docs/src/content/docs/fr/platforms/desktop.md
Normal file
80
docs/src/content/docs/fr/platforms/desktop.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Fonctionnalités de bureau
|
||||||
|
description: Fonctionnalités spécifiques à macOS, Linux et Windows
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box sur les plateformes de bureau offre des fonctionnalités de productivité supplémentaires.
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
### Intégration de la barre de menus
|
||||||
|
|
||||||
|
- État rapide du serveur dans la barre de menus
|
||||||
|
- Accès au serveur en un clic
|
||||||
|
- Mode compact pour une distraction minimale
|
||||||
|
- Style de barre de menus natif macOS
|
||||||
|
|
||||||
|
### Persistance de l'état des fenêtres
|
||||||
|
|
||||||
|
- Mémorise la position et la taille de la fenêtre
|
||||||
|
- Restaurer la session précédente au lancement
|
||||||
|
- Prise en charge de plusieurs écrans
|
||||||
|
|
||||||
|
### Fonctionnalités natives
|
||||||
|
|
||||||
|
- **Barre de titre** : Option de barre de titre personnalisée ou système
|
||||||
|
- **Mode plein écran** : Surveillance dédiée du serveur
|
||||||
|
- **Raccourcis clavier** : Raccourcis natifs macOS
|
||||||
|
- **Touch Bar** (appareils compatibles) : Actions rapides
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### Intégration native
|
||||||
|
|
||||||
|
- Prise en charge de la zone de notification (systray)
|
||||||
|
- Intégration des notifications de bureau
|
||||||
|
- Intégration du sélecteur de fichiers
|
||||||
|
|
||||||
|
### Gestion des fenêtres
|
||||||
|
|
||||||
|
- Prise en charge de X11 et Wayland
|
||||||
|
- Compatible avec les gestionnaires de fenêtres à tuiles (tiling)
|
||||||
|
- Option de décorations de fenêtre personnalisées
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
### Fonctionnalités
|
||||||
|
|
||||||
|
- Intégration de la zone de notification (systray)
|
||||||
|
- Actions rapides via la Jump List
|
||||||
|
- Contrôles de fenêtre natifs
|
||||||
|
- Option de démarrage automatique au boot
|
||||||
|
|
||||||
|
## Fonctionnalités de bureau multiplateformes
|
||||||
|
|
||||||
|
### Raccourcis clavier
|
||||||
|
|
||||||
|
- **Cmd/Ctrl + N** : Nouveau serveur
|
||||||
|
- **Cmd/Ctrl + W** : Fermer l'onglet
|
||||||
|
- **Cmd/Ctrl + T** : Nouvel onglet de terminal
|
||||||
|
- **Cmd/Ctrl + ,** : Paramètres
|
||||||
|
|
||||||
|
### Thèmes
|
||||||
|
|
||||||
|
- Thème clair
|
||||||
|
- Thème sombre
|
||||||
|
- Thème AMOLED (noir pur)
|
||||||
|
- Thème système (suit l'OS)
|
||||||
|
|
||||||
|
### Fenêtres multiples
|
||||||
|
|
||||||
|
- Ouvrir plusieurs serveurs dans des fenêtres séparées
|
||||||
|
- Faire glisser des onglets vers une nouvelle fenêtre
|
||||||
|
- Comparer les statistiques des serveurs côte à côte
|
||||||
|
|
||||||
|
### Avantages par rapport au mobile
|
||||||
|
|
||||||
|
- Écran plus grand pour la surveillance
|
||||||
|
- Clavier complet pour le terminal
|
||||||
|
- Opérations de fichiers plus rapides
|
||||||
|
- Meilleur multitâche
|
||||||
77
docs/src/content/docs/fr/platforms/mobile.md
Normal file
77
docs/src/content/docs/fr/platforms/mobile.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
title: Fonctionnalités mobiles
|
||||||
|
description: Fonctionnalités spécifiques à iOS et Android
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box offre plusieurs fonctionnalités spécifiques aux mobiles pour les appareils iOS et Android.
|
||||||
|
|
||||||
|
## Authentification biométrique
|
||||||
|
|
||||||
|
Sécurisez vos serveurs avec l'authentification biométrique :
|
||||||
|
|
||||||
|
- **iOS** : Face ID ou Touch ID
|
||||||
|
- **Android** : Authentification par empreinte digitale
|
||||||
|
|
||||||
|
Activez-la dans Paramètres > Sécurité > Authentification biométrique.
|
||||||
|
|
||||||
|
## Widgets de l'écran d'accueil
|
||||||
|
|
||||||
|
Ajoutez des widgets d'état du serveur à votre écran d'accueil pour une surveillance rapide.
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
- Appui long sur l'écran d'accueil
|
||||||
|
- Appuyez sur **+** pour ajouter un widget
|
||||||
|
- Recherchez "Server Box"
|
||||||
|
- Choisissez la taille du widget :
|
||||||
|
- Petit : État d'un seul serveur
|
||||||
|
- Moyen : Plusieurs serveurs
|
||||||
|
- Grand : Informations détaillées
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
- Appui long sur l'écran d'accueil
|
||||||
|
- Appuyez sur **Widgets**
|
||||||
|
- Trouvez "Server Box"
|
||||||
|
- Sélectionnez le type de widget
|
||||||
|
|
||||||
|
## Fonctionnement en arrière-plan
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Maintenir les connexions actives en arrière-plan :
|
||||||
|
|
||||||
|
- Activer dans Paramètres > Avancé > Fonctionnement en arrière-plan
|
||||||
|
- Nécessite l'exclusion de l'optimisation de la batterie
|
||||||
|
- Notifications persistantes pour les connexions actives
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
Des limitations en arrière-plan s'appliquent :
|
||||||
|
|
||||||
|
- Les connexions peuvent se mettre en pause en arrière-plan
|
||||||
|
- Reconnexion rapide au retour dans l'application
|
||||||
|
- Prise en charge de l'actualisation en arrière-plan
|
||||||
|
|
||||||
|
## Notifications Push
|
||||||
|
|
||||||
|
Recevez des notifications pour :
|
||||||
|
|
||||||
|
- Alertes de serveur hors ligne
|
||||||
|
- Avertissements d'utilisation élevée des ressources
|
||||||
|
- Alertes de fin de tâche
|
||||||
|
|
||||||
|
Configurez dans Paramètres > Notifications.
|
||||||
|
|
||||||
|
## Fonctionnalités de l'interface mobile
|
||||||
|
|
||||||
|
- **Tirer pour rafraîchir** : Mettre à jour l'état du serveur
|
||||||
|
- **Actions de glissement** : Opérations rapides sur le serveur
|
||||||
|
- **Mode paysage** : Meilleure expérience du terminal
|
||||||
|
- **Clavier virtuel** : Raccourcis pour le terminal
|
||||||
|
|
||||||
|
## Intégration de fichiers
|
||||||
|
|
||||||
|
- **Application Fichiers (iOS)** : Accès direct SFTP depuis Fichiers
|
||||||
|
- **Storage Access Framework (Android)** : Partager des fichiers avec d'autres applications
|
||||||
|
- **Sélecteur de documents** : Sélection de fichiers facile
|
||||||
214
docs/src/content/docs/fr/principles/architecture.md
Normal file
214
docs/src/content/docs/fr/principles/architecture.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
title: Présentation de l'architecture
|
||||||
|
description: Architecture de haut niveau de l'application
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box suit une architecture en couches avec une séparation claire des préoccupations.
|
||||||
|
|
||||||
|
## Couches architecturales
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Couche de présentation (UI) │
|
||||||
|
│ lib/view/page/, lib/view/widget/ │
|
||||||
|
│ - Pages, Widgets, Contrôleurs │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Couche logique métier │
|
||||||
|
│ lib/data/provider/ │
|
||||||
|
│ - Riverpod Providers, State Notifiers │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Couche d'accès aux données │
|
||||||
|
│ lib/data/store/, lib/data/model/ │
|
||||||
|
│ - Hive Stores, Modèles de données │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Couche d'intégration externe │
|
||||||
|
│ - SSH (dartssh2), Terminal (xterm), SFTP │
|
||||||
|
│ - Code spécifique à la plateforme (iOS, etc.) │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fondations de l'application
|
||||||
|
|
||||||
|
### Point d'entrée principal
|
||||||
|
|
||||||
|
`lib/main.dart` initialise l'application :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void main() {
|
||||||
|
runApp(
|
||||||
|
ProviderScope(
|
||||||
|
child: MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget racine
|
||||||
|
|
||||||
|
`MyApp` fournit :
|
||||||
|
- **Gestion des thèmes** : Commutation entre thèmes clair/sombre
|
||||||
|
- **Configuration du routage** : Structure de navigation
|
||||||
|
- **Provider Scope** : Racine de l'injection de dépendances
|
||||||
|
|
||||||
|
### Page d'accueil
|
||||||
|
|
||||||
|
`HomePage` sert de plaque tournante pour la navigation :
|
||||||
|
- **Interface par onglets** : Serveur, Snippet, Conteneur, SSH
|
||||||
|
- **Gestion de l'état** : État par onglet
|
||||||
|
- **Navigation** : Accès aux fonctionnalités
|
||||||
|
|
||||||
|
## Systèmes de base
|
||||||
|
|
||||||
|
### Gestion de l'état : Riverpod
|
||||||
|
|
||||||
|
**Pourquoi Riverpod ?**
|
||||||
|
- Sécurité au moment de la compilation
|
||||||
|
- Tests faciles
|
||||||
|
- Pas de dépendance au Build context
|
||||||
|
- Fonctionne sur toutes les plateformes
|
||||||
|
|
||||||
|
**Types de Provider utilisés :**
|
||||||
|
- `StateProvider` : État mutable simple
|
||||||
|
- `AsyncNotifierProvider` : États de chargement/erreur/données
|
||||||
|
- `StreamProvider` : Flux de données en temps réel
|
||||||
|
- Future providers : Opérations asynchrones uniques
|
||||||
|
|
||||||
|
### Persistance des données : Hive CE
|
||||||
|
|
||||||
|
**Pourquoi Hive CE ?**
|
||||||
|
- Pas de dépendances de code natif
|
||||||
|
- Stockage clé-valeur rapide
|
||||||
|
- Type-safe avec génération de code
|
||||||
|
- Pas d'annotations de champs manuelles requises
|
||||||
|
|
||||||
|
**Stores :**
|
||||||
|
- `SettingStore` : Préférences de l'application
|
||||||
|
- `ServerStore` : Configurations de serveur
|
||||||
|
- `SnippetStore` : Extraits de commande
|
||||||
|
- `KeyStore` : Clés SSH
|
||||||
|
|
||||||
|
### Modèles immuables : Freezed
|
||||||
|
|
||||||
|
**Avantages :**
|
||||||
|
- Immuabilité au moment de la compilation
|
||||||
|
- Types Union pour l'état
|
||||||
|
- Sérialisation JSON intégrée
|
||||||
|
- Extensions CopyWith
|
||||||
|
|
||||||
|
## Stratégie multiplateforme
|
||||||
|
|
||||||
|
### Système de plugins
|
||||||
|
|
||||||
|
Les plugins Flutter permettent l'intégration avec les plateformes :
|
||||||
|
|
||||||
|
| Plateforme | Méthode d'intégration |
|
||||||
|
|------------|----------------------|
|
||||||
|
| iOS | CocoaPods, Swift/Obj-C |
|
||||||
|
| Android | Gradle, Kotlin/Java |
|
||||||
|
| macOS | CocoaPods, Swift |
|
||||||
|
| Linux | CMake, C++ |
|
||||||
|
| Windows | CMake, C# |
|
||||||
|
|
||||||
|
### Fonctionnalités spécifiques aux plateformes
|
||||||
|
|
||||||
|
**iOS uniquement :**
|
||||||
|
- Widgets de l'écran d'accueil
|
||||||
|
- Activités en direct (Live Activities)
|
||||||
|
- Compagnon Apple Watch
|
||||||
|
|
||||||
|
**Android uniquement :**
|
||||||
|
- Service en arrière-plan
|
||||||
|
- Notifications push
|
||||||
|
- Accès au système de fichiers
|
||||||
|
|
||||||
|
**Bureau uniquement :**
|
||||||
|
- Intégration de la barre de menus
|
||||||
|
- Fenêtres multiples
|
||||||
|
- Barre de titre personnalisée
|
||||||
|
|
||||||
|
## Dépendances personnalisées
|
||||||
|
|
||||||
|
### Fork de dartssh2
|
||||||
|
|
||||||
|
Client SSH amélioré avec :
|
||||||
|
- Meilleur support mobile
|
||||||
|
- Gestion des erreurs améliorée
|
||||||
|
- Optimisations de performance
|
||||||
|
|
||||||
|
### Fork de xterm.dart
|
||||||
|
|
||||||
|
Émulateur de terminal avec :
|
||||||
|
- Rendu optimisé pour le mobile
|
||||||
|
- Support des gestes tactiles
|
||||||
|
- Intégration du clavier virtuel
|
||||||
|
|
||||||
|
### fl_lib
|
||||||
|
|
||||||
|
Paquet d'utilitaires partagés avec :
|
||||||
|
- Widgets communs
|
||||||
|
- Extensions
|
||||||
|
- Fonctions d'aide
|
||||||
|
|
||||||
|
## Système de construction
|
||||||
|
|
||||||
|
### Paquet fl_build
|
||||||
|
|
||||||
|
Système de construction personnalisé pour :
|
||||||
|
- Constructions multiplateformes
|
||||||
|
- Signature de code
|
||||||
|
- Regroupement des ressources (assets)
|
||||||
|
- Gestion des versions
|
||||||
|
|
||||||
|
### Processus de construction
|
||||||
|
|
||||||
|
```
|
||||||
|
make.dart (version) → fl_build (build) → Sortie plateforme
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Pré-construction** : Calculer la version à partir de Git
|
||||||
|
2. **Construction** : Compiler pour la plateforme cible
|
||||||
|
3. **Post-construction** : Paqueter et signer
|
||||||
|
|
||||||
|
## Exemple de flux de données
|
||||||
|
|
||||||
|
### Mise à jour de l'état du serveur
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Le minuteur se déclenche →
|
||||||
|
2. Le Provider appelle le service →
|
||||||
|
3. Le service exécute la commande SSH →
|
||||||
|
4. La réponse est analysée en modèle →
|
||||||
|
5. L'état est mis à jour →
|
||||||
|
6. L'UI se reconstruit avec les nouvelles données
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux d'action utilisateur
|
||||||
|
|
||||||
|
```
|
||||||
|
1. L'utilisateur appuie sur un bouton →
|
||||||
|
2. Le Widget appelle une méthode du provider →
|
||||||
|
3. Le Provider met à jour l'état →
|
||||||
|
4. Le changement d'état déclenche la reconstruction →
|
||||||
|
5. Le nouvel état est reflété dans l'UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture de sécurité
|
||||||
|
|
||||||
|
### Protection des données
|
||||||
|
|
||||||
|
- **Mots de passe** : Chiffrés avec flutter_secure_storage
|
||||||
|
- **Clés SSH** : Chiffrées au repos
|
||||||
|
- **Empreintes d'hôte** : Stockées de manière sécurisée
|
||||||
|
- **Données de session** : Non persistées
|
||||||
|
|
||||||
|
### Sécurité de la connexion
|
||||||
|
|
||||||
|
- **Vérification de la clé d'hôte** : Détection MITM (homme du milieu)
|
||||||
|
- **Chiffrement** : Chiffrement SSH standard
|
||||||
|
- **Pas de texte clair** : Les données sensibles ne sont jamais stockées en clair
|
||||||
490
docs/src/content/docs/fr/principles/sftp.md
Normal file
490
docs/src/content/docs/fr/principles/sftp.md
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
---
|
||||||
|
title: Système SFTP
|
||||||
|
description: Comment fonctionne le navigateur de fichiers SFTP
|
||||||
|
---
|
||||||
|
|
||||||
|
Le système SFTP fournit des capacités de gestion de fichiers via SSH.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche UI SFTP │
|
||||||
|
│ - Navigateur de fichiers (distant) │
|
||||||
|
│ - Navigateur de fichiers (local) │
|
||||||
|
│ - File d'attente de transfert │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Gestion de l'état SFTP │
|
||||||
|
│ - sftpProvider │
|
||||||
|
│ - Gestion des chemins │
|
||||||
|
│ - File d'attente d'opérations │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche protocole SFTP │
|
||||||
|
│ - Sous-système SSH │
|
||||||
|
│ - Opérations sur les fichiers │
|
||||||
|
│ - Liste des répertoires │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Transport SSH │
|
||||||
|
│ - Canal sécurisé │
|
||||||
|
│ - Streaming de données │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Établissement de la connexion
|
||||||
|
|
||||||
|
### Création du client SFTP
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<SftpClient> createSftpClient(Spi spi) async {
|
||||||
|
// 1. Obtenir le client SSH (réutiliser si disponible)
|
||||||
|
final sshClient = await genClient(spi);
|
||||||
|
|
||||||
|
// 2. Ouvrir le sous-système SFTP
|
||||||
|
final sftp = await sshClient.openSftp();
|
||||||
|
|
||||||
|
return sftp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Réutilisation de la connexion
|
||||||
|
|
||||||
|
SFTP réutilise les connexions SSH existantes :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ServerProvider {
|
||||||
|
SSHClient? _sshClient;
|
||||||
|
SftpClient? _sftpClient;
|
||||||
|
|
||||||
|
Future<SftpClient> getSftpClient(String spiId) async {
|
||||||
|
_sftpClient ??= await _sshClient!.openSftp();
|
||||||
|
return _sftpClient!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Opérations du système de fichiers
|
||||||
|
|
||||||
|
### Liste des répertoires
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<List<SftpFile>> listDirectory(String path) async {
|
||||||
|
final sftp = await getSftpClient(spiId);
|
||||||
|
|
||||||
|
// Lister le répertoire
|
||||||
|
final files = await sftp.listDir(path);
|
||||||
|
|
||||||
|
// Trier selon les paramètres
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dossiers en premier si activé
|
||||||
|
if (showFoldersFirst) {
|
||||||
|
final dirs = files.where((f) => f.isDirectory);
|
||||||
|
final regular = files.where((f) => !f.isDirectory);
|
||||||
|
return [...dirs, ...regular];
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Métadonnées de fichiers
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class SftpFile {
|
||||||
|
final String name;
|
||||||
|
final String path;
|
||||||
|
final int size; // Octets
|
||||||
|
final int modified; // Horodatage Unix
|
||||||
|
final String permissions; // ex: "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);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Opérations sur les fichiers
|
||||||
|
|
||||||
|
### Téléversement (Upload)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> uploadFile(
|
||||||
|
String localPath,
|
||||||
|
String remotePath,
|
||||||
|
) async {
|
||||||
|
final sftp = await getSftpClient(spiId);
|
||||||
|
|
||||||
|
// Créer la requête
|
||||||
|
final req = SftpReq(
|
||||||
|
spi: spi,
|
||||||
|
remotePath: remotePath,
|
||||||
|
localPath: localPath,
|
||||||
|
type: SftpReqType.upload,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ajouter à la file d'attente
|
||||||
|
_transferQueue.add(req);
|
||||||
|
|
||||||
|
// Exécuter le transfert avec progression
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Terminé
|
||||||
|
_transferQueue.remove(req);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Téléchargement (Download)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> downloadFile(
|
||||||
|
String remotePath,
|
||||||
|
String localPath,
|
||||||
|
) async {
|
||||||
|
final sftp = await getSftpClient(spiId);
|
||||||
|
|
||||||
|
// Créer le fichier local
|
||||||
|
final file = File(localPath);
|
||||||
|
final sink = file.openWrite();
|
||||||
|
|
||||||
|
// Télécharger avec progression
|
||||||
|
final stat = await sftp.stat(remotePath);
|
||||||
|
|
||||||
|
await sftp.download(
|
||||||
|
fromPath: remotePath,
|
||||||
|
toSink: sink,
|
||||||
|
onProgress: (transferred) {
|
||||||
|
_updateProgress(
|
||||||
|
SftpReq(...),
|
||||||
|
transferred,
|
||||||
|
stat.size,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await sink.close();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Édition des permissions
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> setPermissions(
|
||||||
|
String path,
|
||||||
|
String permissions,
|
||||||
|
) async {
|
||||||
|
final sftp = await getSftpClient(spiId);
|
||||||
|
|
||||||
|
// Analyser les permissions (ex: "rwxr-xr-x" ou "755")
|
||||||
|
final mode = parsePermissions(permissions);
|
||||||
|
|
||||||
|
// Définir via commande SSH (plus fiable que SFTP)
|
||||||
|
final ssh = await getSshClient(spiId);
|
||||||
|
await ssh.exec('chmod $mode "$path"');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des chemins
|
||||||
|
|
||||||
|
### Structure de chemin
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class PathWithPrefix {
|
||||||
|
final String prefix; // ex: "/home/user"
|
||||||
|
final String path; // Relatif ou absolu
|
||||||
|
|
||||||
|
String get fullPath {
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
return path; // Chemin absolu
|
||||||
|
}
|
||||||
|
return '$prefix/$path'; // Chemin relatif
|
||||||
|
}
|
||||||
|
|
||||||
|
PathWithPrefix cd(String subPath) {
|
||||||
|
return PathWithPrefix(
|
||||||
|
prefix: fullPath,
|
||||||
|
path: subPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Historique de navigation
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class PathHistory {
|
||||||
|
final List<String> _history = [];
|
||||||
|
int _index = -1;
|
||||||
|
|
||||||
|
void push(String path) {
|
||||||
|
// Supprimer l'historique suivant
|
||||||
|
_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Système de transfert
|
||||||
|
|
||||||
|
### Requête de transfert
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class SftpReq {
|
||||||
|
final Spi spi;
|
||||||
|
final String remotePath;
|
||||||
|
final String localPath;
|
||||||
|
final SftpReqType type;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
int? totalBytes;
|
||||||
|
int? transferredBytes;
|
||||||
|
String? error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Suivi de progression
|
||||||
|
|
||||||
|
```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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestion de la file d'attente
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class TransferQueue {
|
||||||
|
final List<SftpReq> _queue = [];
|
||||||
|
final Map<String, TransferProgress> _progress = {};
|
||||||
|
int _concurrent = 3; // Nombre max de transferts simultanés
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modèle de stockage local
|
||||||
|
|
||||||
|
### Cache de téléchargement
|
||||||
|
|
||||||
|
Fichiers téléchargés stockés sur :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
String getLocalDownloadPath(String spiId, String remotePath) {
|
||||||
|
final normalized = remotePath.replaceAll('/', '_');
|
||||||
|
return 'Paths.file/$spiId/$normalized';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
- Distant : `/var/log/nginx/access.log`
|
||||||
|
- spiId : `server-123`
|
||||||
|
- Local : `Paths.file/server-123/_var_log_nginx_access.log`
|
||||||
|
|
||||||
|
## Édition de fichiers
|
||||||
|
|
||||||
|
### Flux d'édition
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> editFile(String path) async {
|
||||||
|
final sftp = await getSftpClient(spiId);
|
||||||
|
|
||||||
|
// 1. Vérifier la taille
|
||||||
|
final stat = await sftp.stat(path);
|
||||||
|
if (stat.size > editorMaxSize) {
|
||||||
|
showWarning('Fichier trop volumineux pour l\'éditeur intégré');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Télécharger vers dossier temp
|
||||||
|
final temp = await downloadToTemp(path);
|
||||||
|
|
||||||
|
// 3. Ouvrir dans l'éditeur
|
||||||
|
final content = await openEditor(temp.path);
|
||||||
|
|
||||||
|
// 4. Téléverser en retour
|
||||||
|
await uploadFile(temp.path, path);
|
||||||
|
|
||||||
|
// 5. Nettoyage
|
||||||
|
await temp.delete();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intégration d'un éditeur externe
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> editInExternalEditor(String path) async {
|
||||||
|
final ssh = await getSshClient(spiId);
|
||||||
|
|
||||||
|
// Ouvrir le terminal avec l'éditeur
|
||||||
|
final editor = getSetting('sftpEditor', 'vim');
|
||||||
|
await ssh.exec('$editor "$path"');
|
||||||
|
|
||||||
|
// L'utilisateur édite dans le terminal
|
||||||
|
// Après sauvegarde, rafraîchir la vue SFTP
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des erreurs
|
||||||
|
|
||||||
|
### Erreurs de permission
|
||||||
|
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
await sftp.upload(...);
|
||||||
|
} on SftpPermissionException {
|
||||||
|
showError('Permission refusée : ${stat.path}');
|
||||||
|
showHint('Vérifiez les permissions et la propriété du fichier');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreurs de connexion
|
||||||
|
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
await sftp.listDir(path);
|
||||||
|
} on SftpConnectionException {
|
||||||
|
showError('Connexion perdue');
|
||||||
|
await reconnect();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erreurs d'espace disque
|
||||||
|
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
await sftp.upload(...);
|
||||||
|
} on SftpNoSpaceException {
|
||||||
|
showError('Disque plein sur le serveur distant');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimisations de performance
|
||||||
|
|
||||||
|
### Cache de répertoire
|
||||||
|
|
||||||
|
```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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chargement différé (Lazy Loading)
|
||||||
|
|
||||||
|
Pour les répertoires volumineux (>1000 éléments) :
|
||||||
|
|
||||||
|
```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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
```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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
305
docs/src/content/docs/fr/principles/ssh.md
Normal file
305
docs/src/content/docs/fr/principles/ssh.md
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
---
|
||||||
|
title: Connexion SSH
|
||||||
|
description: Comment les connexions SSH sont établies et gérées
|
||||||
|
---
|
||||||
|
|
||||||
|
Comprendre les connexions SSH dans Server Box.
|
||||||
|
|
||||||
|
## Flux de connexion
|
||||||
|
|
||||||
|
```text
|
||||||
|
Entrée utilisateur → Configuration Spi → genClient() → Client SSH → Session
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 1 : Configuration
|
||||||
|
|
||||||
|
Le modèle `Spi` (Server Parameter Info) contient :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class Spi {
|
||||||
|
String id; // Identifiant unique
|
||||||
|
String name; // Nom du serveur
|
||||||
|
String ip; // Adresse IP
|
||||||
|
int port; // Port SSH (par défaut 22)
|
||||||
|
String user; // Nom d'utilisateur
|
||||||
|
String? pwd; // Mot de passe (chiffré)
|
||||||
|
String? keyId; // ID de la clé SSH
|
||||||
|
String? jumpId; // ID du serveur de rebond (Jump server)
|
||||||
|
String? alterUrl; // URL alternative
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 2 : Génération du client
|
||||||
|
|
||||||
|
`genClient(spi)` crée le client SSH :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<SSHClient> genClient(Spi spi) async {
|
||||||
|
// 1. Établir le socket
|
||||||
|
var socket = await connect(spi.ip, spi.port);
|
||||||
|
|
||||||
|
// 2. Essayer l'URL alternative en cas d'échec
|
||||||
|
if (socket == null && spi.alterUrl != null) {
|
||||||
|
socket = await connect(spi.alterUrl, spi.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socket == null) {
|
||||||
|
throw ConnectionException('Unable to connect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Authentifier
|
||||||
|
final client = SSHClient(
|
||||||
|
socket: socket,
|
||||||
|
username: spi.user,
|
||||||
|
onPasswordRequest: () => spi.pwd,
|
||||||
|
onIdentityRequest: () => loadKey(spi.keyId),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Vérifier la clé d'hôte
|
||||||
|
await verifyHostKey(client, spi);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Étape 3 : Serveur de rebond (si configuré)
|
||||||
|
|
||||||
|
Pour les serveurs de rebond, connexion récursive :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
if (spi.jumpId != null) {
|
||||||
|
final jumpClient = await genClient(getJumpSpi(spi.jumpId));
|
||||||
|
final forwarded = await jumpClient.forwardLocal(
|
||||||
|
spi.ip,
|
||||||
|
spi.port,
|
||||||
|
);
|
||||||
|
// Se connecter via le socket transféré
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Méthodes d'authentification
|
||||||
|
|
||||||
|
### Authentification par mot de passe
|
||||||
|
|
||||||
|
```dart
|
||||||
|
onPasswordRequest: () => spi.pwd
|
||||||
|
```
|
||||||
|
|
||||||
|
- Mot de passe stocké chiffré dans Hive
|
||||||
|
- Déchiffré lors de la connexion
|
||||||
|
- Envoyé au serveur pour vérification
|
||||||
|
|
||||||
|
### Authentification par clé privée
|
||||||
|
|
||||||
|
```dart
|
||||||
|
onIdentityRequest: () async {
|
||||||
|
final key = await KeyStore.get(spi.keyId);
|
||||||
|
return decyptPem(key.pem, key.password);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processus de chargement de la clé :**
|
||||||
|
1. Récupérer la clé chiffrée depuis `KeyStore`
|
||||||
|
2. Déchiffrer le mot de passe (biométrie/invite)
|
||||||
|
3. Analyser le format PEM
|
||||||
|
4. Standardiser les fins de ligne (LF)
|
||||||
|
5. Retourner pour l'authentification
|
||||||
|
|
||||||
|
### Keyboard-Interactive
|
||||||
|
|
||||||
|
```dart
|
||||||
|
onUserInfoRequest: (instructions) async {
|
||||||
|
// Gérer le challenge-response
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Supporte :
|
||||||
|
- L'authentification par mot de passe
|
||||||
|
- Les jetons OTP
|
||||||
|
- L'authentification à deux facteurs (2FA)
|
||||||
|
|
||||||
|
## Vérification de la clé d'hôte
|
||||||
|
|
||||||
|
### Pourquoi vérifier les clés d'hôte ?
|
||||||
|
|
||||||
|
Empêche les attaques de type **Man-in-the-Middle (MITM)** en s'assurant que vous vous connectez au même serveur.
|
||||||
|
|
||||||
|
### Format de stockage
|
||||||
|
|
||||||
|
```text
|
||||||
|
{spi.id}::{keyType}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
```text
|
||||||
|
mon-serveur::ssh-ed25519
|
||||||
|
mon-serveur::ecdsa-sha2-nistp256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Formats d'empreinte
|
||||||
|
|
||||||
|
**MD5 Hex :**
|
||||||
|
```text
|
||||||
|
aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99
|
||||||
|
```
|
||||||
|
|
||||||
|
**Base64 :**
|
||||||
|
```text
|
||||||
|
SHA256:AbCdEf1234567890...=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flux de vérification
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> verifyHostKey(SSHClient client, Spi spi) async {
|
||||||
|
final key = await client.hostKey;
|
||||||
|
final keyType = key.type;
|
||||||
|
final fingerprint = md5Hex(key); // ou base64
|
||||||
|
|
||||||
|
final stored = SettingStore.sshKnownHostsFingerprints
|
||||||
|
['${spi.id}::$keyType'];
|
||||||
|
|
||||||
|
if (stored == null) {
|
||||||
|
// Nouvel hôte - inviter l'utilisateur
|
||||||
|
final trust = await promptUser(
|
||||||
|
'Hôte inconnu',
|
||||||
|
'Empreinte : $fingerprint',
|
||||||
|
);
|
||||||
|
if (trust) {
|
||||||
|
SettingStore.sshKnownHostsFingerprints
|
||||||
|
['${spi.id}::$keyType'] = fingerprint;
|
||||||
|
}
|
||||||
|
} else if (stored != fingerprint) {
|
||||||
|
// Modifié - avertir l'utilisateur
|
||||||
|
await warnUser(
|
||||||
|
'La clé d\'hôte a changé !',
|
||||||
|
'Attaque MITM possible',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des sessions
|
||||||
|
|
||||||
|
### Mise en commun des connexions (Pooling)
|
||||||
|
|
||||||
|
Clients actifs maintenus dans `ServerProvider` :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class ServerProvider {
|
||||||
|
final Map<String, SSHClient> _clients = {};
|
||||||
|
|
||||||
|
SSHClient getClient(String spiId) {
|
||||||
|
return _clients[spiId] ??= connect(spiId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep-Alive
|
||||||
|
|
||||||
|
Maintenir la connexion pendant l'inactivité :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Timer.periodic(
|
||||||
|
Duration(seconds: 30),
|
||||||
|
(_) => client.sendKeepAlive(),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reconnexion automatique
|
||||||
|
|
||||||
|
En cas de perte de connexion :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
client.onError.listen((error) async {
|
||||||
|
await Future.delayed(Duration(seconds: 5));
|
||||||
|
reconnect();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cycle de vie de la connexion
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────┐
|
||||||
|
│ Initial │
|
||||||
|
└──────┬──────┘
|
||||||
|
│ connect()
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ Connexion │ ←──┐
|
||||||
|
└──────┬──────┘ │
|
||||||
|
│ succès │
|
||||||
|
↓ │ échec (retry)
|
||||||
|
┌─────────────┐ │
|
||||||
|
│ Connecté │───┘
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌─────────────┐
|
||||||
|
│ Actif │ ──→ Envoyer des commandes
|
||||||
|
└──────┬──────┘
|
||||||
|
│
|
||||||
|
↓ (erreur/déconnexion)
|
||||||
|
┌─────────────┐
|
||||||
|
│ Déconnecté │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gestion des erreurs
|
||||||
|
|
||||||
|
### Délai d'attente de connexion (Timeout)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
await client.connect().timeout(
|
||||||
|
Duration(seconds: 30),
|
||||||
|
);
|
||||||
|
} on TimeoutException {
|
||||||
|
throw ConnectionException('Délai d\'attente de connexion dépassé');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Échec d'authentification
|
||||||
|
|
||||||
|
```dart
|
||||||
|
onAuthFail: (error) {
|
||||||
|
if (error.contains('password')) {
|
||||||
|
return 'Mot de passe invalide';
|
||||||
|
} else if (error.contains('key')) {
|
||||||
|
return 'Clé SSH invalide';
|
||||||
|
}
|
||||||
|
return 'Authentification échouée';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discordance de clé d'hôte
|
||||||
|
|
||||||
|
```dart
|
||||||
|
onHostKeyMismatch: (stored, current) {
|
||||||
|
showSecurityWarning(
|
||||||
|
'La clé d\'hôte a changé !',
|
||||||
|
'Attaque MITM possible',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Considérations de performance
|
||||||
|
|
||||||
|
### Réutilisation de la connexion
|
||||||
|
|
||||||
|
- Réutiliser les clients entre les fonctionnalités
|
||||||
|
- Ne pas déconnecter/reconnecter inutilement
|
||||||
|
- Mutualiser les connexions pour les opérations simultanées
|
||||||
|
|
||||||
|
### Paramètres optimaux
|
||||||
|
|
||||||
|
- **Timeout** : 30 secondes (ajustable)
|
||||||
|
- **Keep-alive** : Toutes les 30 secondes
|
||||||
|
- **Délai de relecture** : 5 secondes
|
||||||
|
|
||||||
|
### Efficacité du réseau
|
||||||
|
|
||||||
|
- Connexion unique pour plusieurs opérations
|
||||||
|
- Commandes en pipeline si possible
|
||||||
|
- Éviter d'ouvrir plusieurs connexions
|
||||||
167
docs/src/content/docs/fr/principles/state.md
Normal file
167
docs/src/content/docs/fr/principles/state.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
title: Gestion de l'état
|
||||||
|
description: Comment l'état est géré avec Riverpod
|
||||||
|
---
|
||||||
|
|
||||||
|
Comprendre l'architecture de gestion de l'état dans Server Box.
|
||||||
|
|
||||||
|
## Pourquoi Riverpod ?
|
||||||
|
|
||||||
|
**Avantages clés :**
|
||||||
|
- **Sécurité à la compilation** : Capture les erreurs lors de la compilation
|
||||||
|
- **Pas de BuildContext requis** : Accès à l'état n'importe où
|
||||||
|
- **Tests faciles** : Simple de tester les providers de manière isolée
|
||||||
|
- **Génération de code** : Moins de code répétitif, type-safe
|
||||||
|
|
||||||
|
## Architecture des Providers
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche UI (Widgets) │
|
||||||
|
│ - ConsumerWidget / ConsumerStatefulWidget │
|
||||||
|
│ - ref.watch() / ref.read() │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓ observe (watches)
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche Provider │
|
||||||
|
│ - Annotations @riverpod │
|
||||||
|
│ - Fichiers *.g.dart générés │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓ utilise (uses)
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche Service / Store │
|
||||||
|
│ - Logique métier │
|
||||||
|
│ - Accès aux données │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Types de Providers utilisés
|
||||||
|
|
||||||
|
### 1. StateProvider (État simple)
|
||||||
|
|
||||||
|
Pour un état simple et observable :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
class ThemeNotifier extends _$ThemeNotifier {
|
||||||
|
@override
|
||||||
|
ThemeMode build() {
|
||||||
|
// Charger depuis les paramètres
|
||||||
|
return SettingStore.themeMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTheme(ThemeMode mode) {
|
||||||
|
state = mode;
|
||||||
|
SettingStore.themeMode = mode; // Persister
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utilisation :**
|
||||||
|
```dart
|
||||||
|
class MyWidget extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final theme = ref.watch(themeNotifierProvider);
|
||||||
|
return Text('Thème : $theme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. AsyncNotifierProvider (État asynchrone)
|
||||||
|
|
||||||
|
Pour les données qui se chargent de manière asynchrone :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
class ServerStatus extends _$ServerStatus {
|
||||||
|
@override
|
||||||
|
Future<StatusModel> build(Server server) async {
|
||||||
|
// Chargement initial
|
||||||
|
return await fetchStatus(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refresh() async {
|
||||||
|
state = const AsyncValue.loading();
|
||||||
|
state = await AsyncValue.guard(() async {
|
||||||
|
return await fetchStatus(server);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utilisation :**
|
||||||
|
```dart
|
||||||
|
final status = ref.watch(serverStatusProvider(server));
|
||||||
|
|
||||||
|
status.when(
|
||||||
|
data: (data) => StatusWidget(data),
|
||||||
|
loading: () => LoadingWidget(),
|
||||||
|
error: (error, stack) => ErrorWidget(error),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. StreamProvider (Données en temps réel)
|
||||||
|
|
||||||
|
Pour les flux de données continus :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
Stream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) {
|
||||||
|
final client = ref.watch(sshClientProvider(server));
|
||||||
|
final stream = client.monitorCpu();
|
||||||
|
|
||||||
|
// Libération automatique des ressources quand non observé
|
||||||
|
ref.onDispose(() {
|
||||||
|
client.stopMonitoring();
|
||||||
|
});
|
||||||
|
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utilisation :**
|
||||||
|
```dart
|
||||||
|
final cpu = ref.watch(cpuUsageProvider(server));
|
||||||
|
|
||||||
|
cpu.when(
|
||||||
|
data: (usage) => CpuChart(usage),
|
||||||
|
loading: () => CircularProgressIndicator(),
|
||||||
|
error: (error, stack) => ErrorWidget(error),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Family Providers (Paramétrés)
|
||||||
|
|
||||||
|
Providers qui acceptent des paramètres :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
@riverpod
|
||||||
|
Future<List<Container>> containers(ContainersRef ref, Server server) async {
|
||||||
|
final client = await ref.watch(sshClientProvider(server).future);
|
||||||
|
return await client.listContainers();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Utilisation :**
|
||||||
|
```dart
|
||||||
|
final containers = ref.watch(containersProvider(server));
|
||||||
|
|
||||||
|
// Différents serveurs = différents états mis en cache
|
||||||
|
final containers2 = ref.watch(containersProvider(server2));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optimisations de performance
|
||||||
|
|
||||||
|
- **Provider Keep-Alive** : Utilisez `@Riverpod(keepAlive: true)` pour empêcher la destruction automatique quand il n'y a plus d'écouteurs.
|
||||||
|
- **Observation sélective** : Utilisez `select` pour n'observer qu'une partie spécifique de l'état.
|
||||||
|
- **Mise en cache des Providers** : Les Family providers mettent en cache les résultats par paramètre.
|
||||||
|
|
||||||
|
## Bonnes pratiques
|
||||||
|
|
||||||
|
1. **Co-localiser les providers** : Placez-les près des widgets qui les consomment.
|
||||||
|
2. **Utiliser la génération de code** : Utilisez toujours `@riverpod`.
|
||||||
|
3. **Garder les providers focalisés** : Responsabilité unique.
|
||||||
|
4. **Gérer les états de chargement** : Gérez toujours les états AsyncValue.
|
||||||
|
5. **Libérer les ressources** : Utilisez `ref.onDispose()` pour le nettoyage.
|
||||||
|
6. **Éviter les arbres de providers profonds** : Gardez le graphe des providers plat.
|
||||||
198
docs/src/content/docs/fr/principles/terminal.md
Normal file
198
docs/src/content/docs/fr/principles/terminal.md
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
---
|
||||||
|
title: Implémentation du terminal
|
||||||
|
description: Comment le terminal SSH fonctionne en interne
|
||||||
|
---
|
||||||
|
|
||||||
|
Le terminal SSH est l'une des fonctionnalités les plus complexes, basée sur un fork personnalisé de xterm.dart.
|
||||||
|
|
||||||
|
## Présentation de l'architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche UI du terminal │
|
||||||
|
│ - Gestion des onglets │
|
||||||
|
│ - Clavier virtuel │
|
||||||
|
│ - Sélection de texte │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Émulateur xterm.dart │
|
||||||
|
│ - PTY (Pseudo Terminal) │
|
||||||
|
│ - Émulation VT100/ANSI │
|
||||||
|
│ - Moteur de rendu │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Couche client SSH │
|
||||||
|
│ - Session SSH │
|
||||||
|
│ - Gestion des canaux │
|
||||||
|
│ - Streaming de données │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Serveur distant │
|
||||||
|
│ - Processus Shell │
|
||||||
|
│ - Exécution de commandes │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cycle de vie d'une session de terminal
|
||||||
|
|
||||||
|
### 1. Création de la session
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<TerminalSession> createSession(Spi spi) async {
|
||||||
|
// 1. Obtenir le client SSH
|
||||||
|
final client = await genClient(spi);
|
||||||
|
|
||||||
|
// 2. Créer le PTY
|
||||||
|
final pty = await client.openPty(
|
||||||
|
term: 'xterm-256color',
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Initialiser l'émulateur de terminal
|
||||||
|
final terminal = Terminal(
|
||||||
|
backend: PtyBackend(pty),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Configurer le gestionnaire de redimensionnement
|
||||||
|
terminal.onResize.listen((size) {
|
||||||
|
pty.resize(size.cols, size.rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
return TerminalSession(
|
||||||
|
terminal: terminal,
|
||||||
|
pty: pty,
|
||||||
|
client: client,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Émulation de terminal
|
||||||
|
|
||||||
|
Le fork xterm.dart fournit :
|
||||||
|
|
||||||
|
**Émulation VT100/ANSI :**
|
||||||
|
- Mouvement du curseur
|
||||||
|
- Couleurs (support 256 couleurs)
|
||||||
|
- Attributs de texte (gras, souligné, etc.)
|
||||||
|
- Régions de défilement
|
||||||
|
- Tampon d'écran alterné
|
||||||
|
|
||||||
|
**Rendu :**
|
||||||
|
- Rendu basé sur les lignes
|
||||||
|
- Support du texte bidirectionnel
|
||||||
|
- Support Unicode/emoji
|
||||||
|
- Redessins optimisés
|
||||||
|
|
||||||
|
### 3. Flux de données
|
||||||
|
|
||||||
|
```
|
||||||
|
Entrée utilisateur
|
||||||
|
↓
|
||||||
|
Clavier virtuel / Clavier physique
|
||||||
|
↓
|
||||||
|
Émulateur de terminal (touche → séquence d'échappement)
|
||||||
|
↓
|
||||||
|
Canal SSH (envoi)
|
||||||
|
↓
|
||||||
|
PTY distant
|
||||||
|
↓
|
||||||
|
Shell distant
|
||||||
|
↓
|
||||||
|
Sortie de commande
|
||||||
|
↓
|
||||||
|
Canal SSH (réception)
|
||||||
|
↓
|
||||||
|
Émulateur de terminal (analyse des codes ANSI)
|
||||||
|
↓
|
||||||
|
Rendu à l'écran
|
||||||
|
```
|
||||||
|
|
||||||
|
## Système multi-onglets
|
||||||
|
|
||||||
|
### Gestion des onglets
|
||||||
|
|
||||||
|
Les onglets maintiennent leur état lors de la navigation :
|
||||||
|
- Connexion SSH maintenue active
|
||||||
|
- État du terminal préservé
|
||||||
|
- Tampon de défilement conservé
|
||||||
|
- Historique de saisie retenu
|
||||||
|
|
||||||
|
## Clavier virtuel
|
||||||
|
|
||||||
|
### Implémentation spécifique à la plateforme
|
||||||
|
|
||||||
|
**iOS :**
|
||||||
|
- Clavier personnalisé basé sur UIView
|
||||||
|
- Basculable avec un bouton clavier
|
||||||
|
- Affichage/masquage automatique basé sur le focus
|
||||||
|
|
||||||
|
**Android :**
|
||||||
|
- Méthode de saisie personnalisée
|
||||||
|
- Intégré au clavier système
|
||||||
|
- Boutons d'action rapide
|
||||||
|
|
||||||
|
### Boutons du clavier
|
||||||
|
|
||||||
|
| Bouton | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| **Basculer** | Afficher/masquer le clavier système |
|
||||||
|
| **Ctrl** | Envoyer le modificateur Ctrl |
|
||||||
|
| **Alt** | Envoyer le modificateur Alt |
|
||||||
|
| **SFTP** | Ouvrir le répertoire courant |
|
||||||
|
| **Presse-papiers** | Copier/Coller contextuel |
|
||||||
|
| **Snippets** | Exécuter un extrait de code |
|
||||||
|
|
||||||
|
## Sélection de texte
|
||||||
|
|
||||||
|
1. **Appui long** : Entrer en mode sélection
|
||||||
|
2. **Glisser** : Étendre la sélection
|
||||||
|
3. **Relâcher** : Copier dans le presse-papiers
|
||||||
|
|
||||||
|
## Police et dimensions
|
||||||
|
|
||||||
|
### Calcul de la taille
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class TerminalDimensions {
|
||||||
|
static Size calculate(double fontSize, Size screenSize) {
|
||||||
|
final charWidth = fontSize * 0.6; // Ratio d'aspect monospace
|
||||||
|
final charHeight = fontSize * 1.2;
|
||||||
|
|
||||||
|
final cols = (screenSize.width / charWidth).floor();
|
||||||
|
final rows = (screenSize.height / charHeight).floor();
|
||||||
|
|
||||||
|
return Size(cols.toDouble(), rows.toDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pincer pour zoomer (Pinch-to-Zoom)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
GestureDetector(
|
||||||
|
onScaleStart: () => _baseFontSize = currentFontSize,
|
||||||
|
onScaleUpdate: (details) {
|
||||||
|
final newFontSize = _baseFontSize * details.scale;
|
||||||
|
resize(newFontSize);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schéma de couleurs
|
||||||
|
|
||||||
|
- **Clair (Light)** : Fond clair, texte sombre
|
||||||
|
- **Sombre (Dark)** : Fond sombre, texte clair
|
||||||
|
- **AMOLED** : Fond noir pur
|
||||||
|
|
||||||
|
## Optimisations de performance
|
||||||
|
|
||||||
|
- **Dirty rectangle** : Ne redessiner que les régions modifiées
|
||||||
|
- **Mise en cache des lignes** : Mettre en cache les lignes rendues
|
||||||
|
- **Défilement paresseux (Lazy scrolling)** : Défilement virtuel pour les longs tampons
|
||||||
|
- **Mises à jour par lots** : Fusionner plusieurs écritures
|
||||||
|
- **Compression** : Compresser le tampon de défilement
|
||||||
|
- **Anti-rebond (Debouncing)** : Anti-rebond pour les saisies rapides
|
||||||
45
docs/src/content/docs/fr/quick-start.mdx
Normal file
45
docs/src/content/docs/fr/quick-start.mdx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: Démarrage Rapide
|
||||||
|
description: Soyez opérationnel avec Server Box en quelques minutes
|
||||||
|
---
|
||||||
|
|
||||||
|
Suivez ce guide de démarrage rapide pour vous connecter à votre premier serveur et commencer la surveillance.
|
||||||
|
|
||||||
|
## Étape 1 : Ajouter un serveur
|
||||||
|
|
||||||
|
1. Ouvrez Server Box
|
||||||
|
2. Appuyez sur le bouton **+** pour ajouter un nouveau serveur
|
||||||
|
3. Remplissez les informations du serveur :
|
||||||
|
- **Nom** : Un nom convivial pour votre serveur
|
||||||
|
- **Hôte** : Adresse IP ou nom de domaine
|
||||||
|
- **Port** : Port SSH (par défaut : 22)
|
||||||
|
- **Utilisateur** : Nom d'utilisateur SSH
|
||||||
|
- **Mot de passe ou Clé** : Méthode d'authentification
|
||||||
|
|
||||||
|
4. Appuyez sur **Enregistrer** pour ajouter le serveur
|
||||||
|
|
||||||
|
## Étape 2 : Connecter et surveiller
|
||||||
|
|
||||||
|
1. Appuyez sur la carte de votre serveur pour vous connecter
|
||||||
|
2. L'application établira une connexion SSH
|
||||||
|
3. Vous verrez le statut en temps réel pour :
|
||||||
|
- L'utilisation du processeur (CPU)
|
||||||
|
- La mémoire (RAM) et le Swap
|
||||||
|
- L'utilisation du disque
|
||||||
|
- La vitesse du réseau
|
||||||
|
|
||||||
|
## Étape 3 : Explorer les fonctionnalités
|
||||||
|
|
||||||
|
Une fois connecté, vous pouvez :
|
||||||
|
|
||||||
|
- **Ouvrir le terminal** : Appuyez sur le bouton du terminal pour un accès SSH complet
|
||||||
|
- **Parcourir les fichiers** : Utilisez SFTP pour gérer les fichiers
|
||||||
|
- **Gérer les conteneurs** : Visualisez et contrôlez les conteneurs Docker
|
||||||
|
- **Afficher les processus** : Vérifiez les processus en cours d'exécution
|
||||||
|
- **Exécuter des snippets** : Exécutez des commandes enregistrées
|
||||||
|
|
||||||
|
## Conseils
|
||||||
|
|
||||||
|
- **Authentification biométrique** : Activez Face ID / Touch ID / Empreinte digitale pour un accès rapide (mobile)
|
||||||
|
- **Widgets de l'écran d'accueil** : Ajoutez des widgets d'état du serveur à votre écran d'accueil (iOS/Android)
|
||||||
|
- **Fonctionnement en arrière-plan** : Maintenez les connexions actives en arrière-plan (Android)
|
||||||
46
docs/src/content/docs/index.mdx
Normal file
46
docs/src/content/docs/index.mdx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: Server Box
|
||||||
|
description: A comprehensive cross-platform server management application
|
||||||
|
hero:
|
||||||
|
tagline: Manage your Linux servers from anywhere
|
||||||
|
actions:
|
||||||
|
- text: Get Started
|
||||||
|
link: /introduction/
|
||||||
|
icon: right-arrow
|
||||||
|
variant: primary
|
||||||
|
- text: View on GitHub
|
||||||
|
link: https://github.com/lollipopkit/flutter_server_box
|
||||||
|
icon: github
|
||||||
|
variant: minimal
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
<CardGrid stagger>
|
||||||
|
<Card title="Real-time Monitoring" icon="chart">
|
||||||
|
Monitor CPU, memory, disk, network, GPU, and temperature with beautiful real-time charts.
|
||||||
|
</Card>
|
||||||
|
<Card title="SSH Terminal" icon="terminal">
|
||||||
|
Full-featured SSH terminal with multi-tab support and virtual keyboard for mobile devices.
|
||||||
|
</Card>
|
||||||
|
<Card title="SFTP File Browser" icon="folder">
|
||||||
|
Manage files on your servers with the built-in SFTP client and local file browser.
|
||||||
|
</Card>
|
||||||
|
<Card title="Docker Management" icon="box">
|
||||||
|
Start, stop, and monitor Docker containers with an intuitive interface.
|
||||||
|
</Card>
|
||||||
|
<Card title="Cross-Platform" icon="device-mobile">
|
||||||
|
Available on iOS, Android, macOS, Linux, Windows, and watchOS.
|
||||||
|
</Card>
|
||||||
|
<Card title="12+ Languages" icon="globe">
|
||||||
|
Full localization support including English, Chinese, German, French, and more.
|
||||||
|
</Card>
|
||||||
|
</CardGrid>
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- **Download**: Available on [App Store](https://apps.apple.com/app/id1586449703), [GitHub](https://github.com/lollipopkit/flutter_server_box/releases), and [F-Droid](https://f-droid.org/)
|
||||||
|
- **Documentation**: Explore the guides to get started with Server Box
|
||||||
|
- **Support**: Join our community on GitHub for discussions and issues
|
||||||
51
docs/src/content/docs/installation.mdx
Normal file
51
docs/src/content/docs/installation.mdx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
description: Download and install Server Box on your device
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box is available on multiple platforms. Choose your preferred method of installation.
|
||||||
|
|
||||||
|
## Mobile Apps
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
|
||||||
|
Download from the **[App Store](https://apps.apple.com/app/id1586449703)**.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
Choose your preferred source:
|
||||||
|
|
||||||
|
- **[F-Droid](https://f-droid.org/)** - For users who prefer FOSS-only sources
|
||||||
|
- **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)** - For the latest version directly from the source
|
||||||
|
|
||||||
|
## Desktop Apps
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
Download from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Native menu bar integration
|
||||||
|
- Support for both Intel and Apple Silicon
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
|
||||||
|
Download from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
Available as AppImage, deb, or tar.gz packages.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
Download from **[GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases)**.
|
||||||
|
|
||||||
|
## watchOS
|
||||||
|
|
||||||
|
Available on the **[App Store](https://apps.apple.com/app/id1586449703)** as part of the iOS app.
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
To build Server Box from source, see the [Building](/development/building) section in the Development documentation.
|
||||||
|
|
||||||
|
## Version Information
|
||||||
|
|
||||||
|
Check the [GitHub Releases](https://github.com/lollipopkit/flutter_server_box/releases) page for the latest version and changelog.
|
||||||
32
docs/src/content/docs/introduction.mdx
Normal file
32
docs/src/content/docs/introduction.mdx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
title: Introduction
|
||||||
|
description: Learn what Server Box is and what it can do
|
||||||
|
---
|
||||||
|
|
||||||
|
Server Box is a comprehensive cross-platform server management application built with Flutter. It allows you to monitor, manage, and control your Linux, Unix, and Windows servers from anywhere.
|
||||||
|
|
||||||
|
## What is Server Box?
|
||||||
|
|
||||||
|
Server Box provides a unified interface for server administration tasks through SSH connections. Whether you're a system administrator, developer, or hobbyist running home servers, this app puts powerful server management tools in your pocket.
|
||||||
|
|
||||||
|
## Key Capabilities
|
||||||
|
|
||||||
|
- **Real-time Monitoring**: Track CPU, memory, disk usage, network speed, GPU status, and system temperatures
|
||||||
|
- **SSH Terminal**: Full terminal access with multi-tab support and customizable appearance
|
||||||
|
- **SFTP Client**: Browse and manage files on your servers
|
||||||
|
- **Docker Management**: Control containers with ease
|
||||||
|
- **Process Management**: View and manage system processes
|
||||||
|
- **Systemd Services**: Start, stop, and monitor systemd services
|
||||||
|
- **Network Tools**: iPerf testing, ping, and Wake-on-LAN
|
||||||
|
- **Snippets**: Save and execute custom shell commands
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
Server Box is truly cross-platform:
|
||||||
|
|
||||||
|
- **Mobile**: iOS and Android
|
||||||
|
- **Desktop**: macOS, Linux, and Windows
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under AGPL v3. Source code is available on [GitHub](https://github.com/lollipopkit/flutter_server_box).
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user