Full Code of tiny-craft/tiny-rdm for AI

main a87f8f319be6 cached
306 files
1.3 MB
359.8k tokens
840 symbols
1 requests
Download .txt
Showing preview only (1,417K chars total). Download the full file or copy to clipboard to get everything.
Repository: tiny-craft/tiny-rdm
Branch: main
Commit: a87f8f319be6
Files: 306
Total size: 1.3 MB

Directory structure:
gitextract_sgnqk2bp/

├── .github/
│   ├── CONTRIBUTING.md
│   ├── CONTRIBUTING_zh.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docker-publish.yml
│       ├── release-linux-webkit2-41.yaml
│       ├── release-linux.yaml
│       ├── release-macos.yaml
│       └── release-windows.yaml
├── .gitignore
├── .prettierignore
├── Dockerfile
├── LICENSE
├── README.md
├── README_es.md
├── README_fr.md
├── README_ja.md
├── README_ko.md
├── README_pt.md
├── README_ru.md
├── README_tr.md
├── README_tw.md
├── README_zh.md
├── backend/
│   ├── api/
│   │   ├── auth.go
│   │   ├── browser_api.go
│   │   ├── cli_api.go
│   │   ├── connection_api.go
│   │   ├── monitor_api.go
│   │   ├── preferences_api.go
│   │   ├── pubsub_api.go
│   │   ├── router.go
│   │   ├── system_api.go
│   │   └── websocket_hub.go
│   ├── consts/
│   │   ├── app_name_desktop.go
│   │   ├── app_name_web.go
│   │   └── default_config.go
│   ├── services/
│   │   ├── browser_service.go
│   │   ├── cli_service.go
│   │   ├── connection_service.go
│   │   ├── connection_service_web.go
│   │   ├── ga_service.go
│   │   ├── monitor_service.go
│   │   ├── platform_desktop.go
│   │   ├── platform_web.go
│   │   ├── preferences_service.go
│   │   ├── pubsub_service.go
│   │   └── system_service.go
│   ├── storage/
│   │   ├── connections.go
│   │   ├── local_storage.go
│   │   └── preferences.go
│   ├── types/
│   │   ├── connection.go
│   │   ├── js_resp.go
│   │   ├── preferences.go
│   │   ├── redis_wrapper.go
│   │   └── view_type.go
│   └── utils/
│       ├── coll/
│       │   └── set.go
│       ├── constraints.go
│       ├── convert/
│       │   ├── base64_convert.go
│       │   ├── binary_convert.go
│       │   ├── bitset_convert.go
│       │   ├── brotli_convert.go
│       │   ├── cmd_convert.go
│       │   ├── common.go
│       │   ├── common_nonwindows.go
│       │   ├── common_windows.go
│       │   ├── convert.go
│       │   ├── deflate_convert.go
│       │   ├── gzip_convert.go
│       │   ├── hex_convert.go
│       │   ├── json_convert.go
│       │   ├── lz4_convert.go
│       │   ├── msgpack_convert.go
│       │   ├── php_convert.go
│       │   ├── pickle_convert.go
│       │   ├── unicode_json_convert.go
│       │   ├── xml_convert.go
│       │   ├── yaml_convert.go
│       │   └── zstd_convert.go
│       ├── map/
│       │   └── map_util.go
│       ├── math/
│       │   └── math_util.go
│       ├── proxy/
│       │   └── http.go
│       ├── redis/
│       │   └── log_hook.go
│       ├── slice/
│       │   └── slice_util.go
│       └── string/
│           ├── any_convert.go
│           ├── common.go
│           ├── json_formatter.go
│           └── key_convert.go
├── build/
│   ├── README.md
│   ├── darwin/
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── dmg/
│   │   ├── background.tiff
│   │   ├── fix-app
│   │   └── fix-app_zh
│   ├── linux/
│   │   └── tiny-rdm_0.0.0_amd64/
│   │       ├── DEBIAN/
│   │       │   └── control
│   │       └── usr/
│   │           ├── local/
│   │           │   └── bin/
│   │           │       └── .gitkeep
│   │           └── share/
│   │               └── applications/
│   │                   └── tiny-rdm.desktop
│   └── windows/
│       ├── info.json
│       ├── installer/
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── docker/
│   ├── entrypoint.sh
│   └── nginx.conf
├── docker-compose.yml
├── docs/
│   └── index.html
├── frontend/
│   ├── .prettierrc
│   ├── README.md
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── AppContent.vue
│   │   ├── assets/
│   │   │   └── fonts/
│   │   │       └── OFL.txt
│   │   ├── components/
│   │   │   ├── LoginPage.vue
│   │   │   ├── common/
│   │   │   │   ├── AutoRefreshForm.vue
│   │   │   │   ├── DropdownSelector.vue
│   │   │   │   ├── EditableTableColumn.vue
│   │   │   │   ├── EditableTableRow.vue
│   │   │   │   ├── FileOpenInput.vue
│   │   │   │   ├── FileSaveInput.vue
│   │   │   │   ├── IconButton.vue
│   │   │   │   ├── RedisTypeSelector.vue
│   │   │   │   ├── RedisTypeTag.vue
│   │   │   │   ├── ResizeableWrapper.vue
│   │   │   │   ├── SwitchButton.vue
│   │   │   │   ├── ToolbarControlWidget.vue
│   │   │   │   └── TtlInput.vue
│   │   │   ├── content/
│   │   │   │   ├── ContentLogPane.vue
│   │   │   │   ├── ContentPane.vue
│   │   │   │   ├── ContentServerPane.vue
│   │   │   │   └── ContentValueTab.vue
│   │   │   ├── content_value/
│   │   │   │   ├── ContentCli.vue
│   │   │   │   ├── ContentEditor.vue
│   │   │   │   ├── ContentEntryEditor.vue
│   │   │   │   ├── ContentMonitor.vue
│   │   │   │   ├── ContentPubsub.vue
│   │   │   │   ├── ContentSearchInput.vue
│   │   │   │   ├── ContentServerStatus.vue
│   │   │   │   ├── ContentSlog.vue
│   │   │   │   ├── ContentToolbar.vue
│   │   │   │   ├── ContentValueHash.vue
│   │   │   │   ├── ContentValueJson.vue
│   │   │   │   ├── ContentValueList.vue
│   │   │   │   ├── ContentValueSet.vue
│   │   │   │   ├── ContentValueStream.vue
│   │   │   │   ├── ContentValueString.vue
│   │   │   │   ├── ContentValueWrapper.vue
│   │   │   │   ├── ContentValueZSet.vue
│   │   │   │   └── FormatSelector.vue
│   │   │   ├── dialogs/
│   │   │   │   ├── AboutDialog.vue
│   │   │   │   ├── AddFieldsDialog.vue
│   │   │   │   ├── ConnectionDialog.vue
│   │   │   │   ├── DecoderDialog.vue
│   │   │   │   ├── DeleteKeyDialog.vue
│   │   │   │   ├── ExportKeyDialog.vue
│   │   │   │   ├── FlushDbDialog.vue
│   │   │   │   ├── GroupDialog.vue
│   │   │   │   ├── ImportKeyDialog.vue
│   │   │   │   ├── KeyFilterDialog.vue
│   │   │   │   ├── NewKeyDialog.vue
│   │   │   │   ├── PreferencesDialog.vue
│   │   │   │   ├── RenameKeyDialog.vue
│   │   │   │   └── SetTtlDialog.vue
│   │   │   ├── icons/
│   │   │   │   ├── Add.vue
│   │   │   │   ├── AddGroup.vue
│   │   │   │   ├── AddLink.vue
│   │   │   │   ├── AlignCenter.vue
│   │   │   │   ├── AlignLeft.vue
│   │   │   │   ├── Binary.vue
│   │   │   │   ├── Bottom.vue
│   │   │   │   ├── Checkbox.vue
│   │   │   │   ├── Checked.vue
│   │   │   │   ├── Clear.vue
│   │   │   │   ├── Close.vue
│   │   │   │   ├── Cluster.vue
│   │   │   │   ├── Code.vue
│   │   │   │   ├── Config.vue
│   │   │   │   ├── Connect.vue
│   │   │   │   ├── Conversion.vue
│   │   │   │   ├── Copy.vue
│   │   │   │   ├── CopyLink.vue
│   │   │   │   ├── Database.vue
│   │   │   │   ├── Delete.vue
│   │   │   │   ├── Detail.vue
│   │   │   │   ├── Down.vue
│   │   │   │   ├── Edit.vue
│   │   │   │   ├── EditFile.vue
│   │   │   │   ├── Export.vue
│   │   │   │   ├── Filter.vue
│   │   │   │   ├── Folder.vue
│   │   │   │   ├── FullScreen.vue
│   │   │   │   ├── Github.vue
│   │   │   │   ├── Help.vue
│   │   │   │   ├── Import.vue
│   │   │   │   ├── Key.vue
│   │   │   │   ├── Lang.vue
│   │   │   │   ├── Layer.vue
│   │   │   │   ├── ListView.vue
│   │   │   │   ├── LoadAll.vue
│   │   │   │   ├── LoadList.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   ├── Log.vue
│   │   │   │   ├── Logout.vue
│   │   │   │   ├── Monitor.vue
│   │   │   │   ├── Moon.vue
│   │   │   │   ├── More.vue
│   │   │   │   ├── OffScreen.vue
│   │   │   │   ├── Pause.vue
│   │   │   │   ├── Pin.vue
│   │   │   │   ├── Play.vue
│   │   │   │   ├── Plus.vue
│   │   │   │   ├── Publish.vue
│   │   │   │   ├── QRCode.vue
│   │   │   │   ├── Record.vue
│   │   │   │   ├── Refresh.vue
│   │   │   │   ├── Save.vue
│   │   │   │   ├── Search.vue
│   │   │   │   ├── Server.vue
│   │   │   │   ├── Sort.vue
│   │   │   │   ├── SpellCheck.vue
│   │   │   │   ├── Status.vue
│   │   │   │   ├── Structure.vue
│   │   │   │   ├── Subscribe.vue
│   │   │   │   ├── Sun.vue
│   │   │   │   ├── Terminal.vue
│   │   │   │   ├── ThemeAuto.vue
│   │   │   │   ├── Timer.vue
│   │   │   │   ├── TreeView.vue
│   │   │   │   ├── Twitter.vue
│   │   │   │   ├── Unlink.vue
│   │   │   │   ├── Update.vue
│   │   │   │   ├── WindowClose.vue
│   │   │   │   ├── WindowMax.vue
│   │   │   │   ├── WindowMin.vue
│   │   │   │   └── WindowRestore.vue
│   │   │   ├── new_value/
│   │   │   │   ├── AddHashValue.vue
│   │   │   │   ├── AddListValue.vue
│   │   │   │   ├── AddZSetValue.vue
│   │   │   │   ├── NewHashValue.vue
│   │   │   │   ├── NewJsonValue.vue
│   │   │   │   ├── NewListValue.vue
│   │   │   │   ├── NewSetValue.vue
│   │   │   │   ├── NewStreamValue.vue
│   │   │   │   ├── NewStringValue.vue
│   │   │   │   └── NewZSetValue.vue
│   │   │   └── sidebar/
│   │   │       ├── BrowserPane.vue
│   │   │       ├── BrowserTree.vue
│   │   │       ├── ConnectionPane.vue
│   │   │       ├── ConnectionTree.vue
│   │   │       ├── ConnectionTreeItem.vue
│   │   │       └── Ribbon.vue
│   │   ├── consts/
│   │   │   ├── browser_tab_type.js
│   │   │   ├── connection_type.js
│   │   │   ├── key_view_type.js
│   │   │   ├── localstorage_key.js
│   │   │   ├── support_redis_type.js
│   │   │   ├── text_align_type.js
│   │   │   ├── tree_context_menu.js
│   │   │   └── value_view_type.js
│   │   ├── langs/
│   │   │   ├── en-us.json
│   │   │   ├── es-es.json
│   │   │   ├── fr-fr.json
│   │   │   ├── index.js
│   │   │   ├── ja-jp.json
│   │   │   ├── ko-kr.json
│   │   │   ├── pt-br.json
│   │   │   ├── ru-ru.json
│   │   │   ├── tr-tr.json
│   │   │   ├── zh-cn.json
│   │   │   └── zh-tw.json
│   │   ├── main.js
│   │   ├── objects/
│   │   │   ├── redisDatabaseItem.js
│   │   │   ├── redisNodeItem.js
│   │   │   ├── redisServerState.js
│   │   │   └── tabItem.js
│   │   ├── stores/
│   │   │   ├── browser.js
│   │   │   ├── connections.js
│   │   │   ├── dialog.js
│   │   │   ├── preferences.js
│   │   │   └── tab.js
│   │   ├── styles/
│   │   │   ├── content.scss
│   │   │   └── style.scss
│   │   └── utils/
│   │       ├── analytics.js
│   │       ├── api.js
│   │       ├── byte_convert.js
│   │       ├── chart.js
│   │       ├── date.js
│   │       ├── decoder_cmd.js
│   │       ├── discrete.js
│   │       ├── extra_theme.js
│   │       ├── glob_pattern.js
│   │       ├── i18n.js
│   │       ├── key_convert.js
│   │       ├── monaco.js
│   │       ├── platform.js
│   │       ├── promise.js
│   │       ├── render.js
│   │       ├── rgb.js
│   │       ├── theme.js
│   │       ├── version.js
│   │       ├── wails_runtime.js
│   │       └── websocket.js
│   └── vite.config.js
├── go.mod
├── go.sum
├── main.go
├── main_web.go
└── wails.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/CONTRIBUTING.md
================================================
## Tiny RDM Contribute Guide

### Multi-language Contributions

#### Adding New Language

1. New file: Add a new JSON file in the [frontend/src/langs](../frontend/src/langs/), with the file naming format is "
   {language}-{region}.json", e.g. English is "en-us.json", simplified Chinese is "zh-cn.json". Highly recommended to duplicate the [en-us.json](../frontend/src/langs/en-us.json) file and rename it.
2. Fill content: Refer to [en-us.json](../frontend/src/langs/en-us.json), or duplicate the file and modify the language content.
3. Update codes: Edit[frontend/src/langs/index.js](.../frontend/src/langs/index.js), import the new language data inside.
    ```javascript
    import en from './en-us'
    // import your new localize file 'zh-cn' here
    import zh from './zh-cn'
    
    export const lang = {
        en,
        // export new language data 'zh' here
        zh,
    }
   ```
4. Submit review once there are no issues with the translation context in the application. (learn how to submit)

### Code Submission`(To be completed)`

#### Pull Request Title
The format of PR's title like "<type>: <description>"
- type: PR type
- description: PR description

PR type list below:

| type     | description                                        |
|----------|----------------------------------------------------|
| revert   | Revert a commit                                    |
| feat     | New features                                       |
| perf     | Performance improvements                           |
| fix      | Fix any bugs                                       |
| style    | Style updates                                      |
| docs     | Document updates                                   |
| refactor | Code refactors                                     |
| chore    | Some chores                                        |
| ci       | Automation process configuration or script updates |


================================================
FILE: .github/CONTRIBUTING_zh.md
================================================
## Tiny RDM 代码贡献指南

### 多国语言贡献

#### 增加新的语言
1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件,文件名格式为“{语言}-{地区}.json”,如英文为“en-us.json”,简体中文为“zh-cn.json”,建议直接复制[en-us.json](../frontend/src/langs/en-us.json)文件进行改名。
2. 填充内容:参考[en-us.json](../frontend/src/langs/en-us.json),或者直接克隆一份文件,对语言部分内容进行修改。
3. 代码修改:在[frontend/src/langs/index.js](.../frontend/src/langs/index.js)文件内导入新增的语言数据
    ```javascript
    import en from './en-us'
    // import your new localize file 'zh-cn' here
    import zh from './zh-cn'
    
    export const lang = {
        en,
        // export new language data 'zh' here
        zh,
    }
    ```
4. 检查应用中对应翻译语境无问题后,可提交审核([查看如何提交](#pull_request))

### 代码提交`(待完善)`

#### PR提交规范
PR提交格式为“<type>: <description>”
- type: 提交类型
- description: 提交内容描述

其中提交类型如下:

| 提交类型     | 类型描述         |
|----------|--------------|
| revert   | 回退某个commit提交 |
| feat     | 新功能/新特性      |
| perf     | 功能、体验等方面的优化  |
| fix      | 修复问题         |
| style    | 样式相关修改       |
| docs     | 文档更新         |
| refactor | 代码重构         |
| chore    | 杂项修改         |
| ci       | 自动化流程配置或脚本修改 |



================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: '[BUG]'
labels: ''
assignees: ''
---

**Tiny RDM Version**
What version of Tiny RDM are you using?

**OS Version**
Which OS and version you launch? (Mac/Windows/Linux)

**Redis Version**
Which version of Redis are you using?

**Describe the bug**
A clear and concise description of what the bug is.

Steps to Reproduce:

1.
2.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE]'
---


================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Release Docker Image
run-name: ${{ github.event.release.tag_name || github.event.inputs.tag || 'manual' }}

on:
  release:
    types: [ published ]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag'
        required: true
        default: '1.0.0'

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: tiny-craft/tiny-rdm

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }}
            type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }}
            type=raw,value=latest

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Determine version
        id: version
        run: |
          if [ "${{ github.event_name }}" = "release" ]; then
            echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
          else
            echo "value=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
          fi

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          platforms: linux/amd64,linux/arm64
          provenance: false
          sbom: false
          build-args: |
            APP_VERSION=${{ steps.version.outputs.value }}


================================================
FILE: .github/workflows/release-linux-webkit2-41.yaml
================================================
name: Release Linux App
run-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}

on:
  release:
    types: [ published ]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag'
        required: true
        default: '1.0.0'

jobs:
  release:
    name: Release Linux App
    runs-on: ubuntu-24.04
    strategy:
      matrix:
        platform:
          - linux/amd64

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Normalise platform tag
        id: normalise_platform
        shell: bash
        run: |
          tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
          echo "tag=$tag" >> "$GITHUB_OUTPUT"

      - name: Normalise platform arch
        id: normalise_platform_arch
        run: |
           if [ "${{ matrix.platform }}" == "linux/amd64" ]; then
             echo "arch=x86_64" >> "$GITHUB_OUTPUT"
           elif [ "${{ matrix.platform }}" == "linux/aarch64" ]; then
             echo "arch=aarch64" >> "$GITHUB_OUTPUT"
           fi

      - name: Normalise version tag
        id: normalise_version
        shell: bash
        run: |
          if [ "${{ github.event.release.tag_name }}" == "" ]; then
            version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          else
            version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          fi

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: stable

      - name: Install wails
        shell: bash
        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest

      - name: Install Ubuntu prerequisites
        shell: bash
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libfuse-dev libfuse2

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 22

      - name: Build frontend assets
        shell: bash
        run: |
          npm install -g npm@9
          jq '.info.productVersion = "${{ steps.normalise_version.outputs.version }}"' wails.json > tmp.json
          mv tmp.json wails.json
          cd frontend
          jq '.version = "${{ steps.normalise_version.outputs.version }}"' package.json > tmp.json
          mv tmp.json package.json
          npm install

      - name: Build wails app for Linux
        shell: bash
        run: |
          CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
          -ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.LINUX_GA_SECRET }}" \
          -tags webkit2_41 \
          -o tiny-rdm

      - name: Setup control template
        shell: bash
        run: |
          content=$(cat build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control)
          name=$(jq -r '.name' wails.json | tr -d ' ' | tr '[:upper:]' '[:lower:]')
          content=$(echo "$content" | sed -e "s/{{.Name}}/$name/g")
          content=$(echo "$content" | sed -e "s/{{.Info.ProductVersion}}/$(jq -r '.info.productVersion' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Author.Name}}/$(jq -r '.author.name' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Author.Email}}/$(jq -r '.author.email' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.libwebkit2gtk.PackageName}}/libwebkit2gtk-4.1-0/g")
          echo $content
          echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control

      - name: Setup app template
        shell: bash
        run: |
          content=$(cat build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop)
          content=$(echo "$content" | sed -e "s/{{.Info.ProductName}}/$(jq -r '.info.productName' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
          echo $content
          echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop

      - name: Package up deb file
        shell: bash
        run: |
          mv build/bin/tiny-rdm build/linux/tiny-rdm_0.0.0_amd64/usr/local/bin/
          cd build/linux
          mv tiny-rdm_0.0.0_amd64 "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
          sed -i 's/0.0.0/${{ steps.normalise_version.outputs.version }}/g' "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64/DEBIAN/control"
          dpkg-deb --build -Zxz "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"

      - name: Rename deb
        working-directory: ./build/linux
        run: mv "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64.deb" "tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}_webkit2_41.deb"

      - name: Upload release asset
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.normalise_version.outputs.version }}
          files: |
            ./build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}_webkit2_41.deb
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-linux.yaml
================================================
name: Release Linux App
run-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}

on:
  release:
    types: [ published ]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag'
        required: true
        default: '1.0.0'

jobs:
  release:
    name: Release Linux App
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        platform:
          - linux/amd64

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Normalise platform tag
        id: normalise_platform
        shell: bash
        run: |
          tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g')
          echo "tag=$tag" >> "$GITHUB_OUTPUT"

      - name: Normalise platform arch
        id: normalise_platform_arch
        run: |
           if [ "${{ matrix.platform }}" == "linux/amd64" ]; then
             echo "arch=x86_64" >> "$GITHUB_OUTPUT"
           elif [ "${{ matrix.platform }}" == "linux/aarch64" ]; then
             echo "arch=aarch64" >> "$GITHUB_OUTPUT"
           fi

      - name: Normalise version tag
        id: normalise_version
        shell: bash
        run: |
          if [ "${{ github.event.release.tag_name }}" == "" ]; then
            version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          else
            version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          fi

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: stable

      - name: Install wails
        shell: bash
        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest

      - name: Install Ubuntu prerequisites
        shell: bash
        run: |
          sudo apt-get update
          sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libfuse-dev libfuse2

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 22

      - name: Build frontend assets
        shell: bash
        run: |
          npm install -g npm@9
          jq '.info.productVersion = "${{ steps.normalise_version.outputs.version }}"' wails.json > tmp.json
          mv tmp.json wails.json
          cd frontend
          jq '.version = "${{ steps.normalise_version.outputs.version }}"' package.json > tmp.json
          mv tmp.json package.json
          npm install

      - name: Build wails app for Linux
        shell: bash
        run: |
          CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
          -ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.LINUX_GA_SECRET }}" \
          -o tiny-rdm

      - name: Setup control template
        shell: bash
        run: |
          content=$(cat build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control)
          name=$(jq -r '.name' wails.json | tr -d ' ' | tr '[:upper:]' '[:lower:]')
          content=$(echo "$content" | sed -e "s/{{.Name}}/$name/g")
          content=$(echo "$content" | sed -e "s/{{.Info.ProductVersion}}/$(jq -r '.info.productVersion' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Author.Name}}/$(jq -r '.author.name' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Author.Email}}/$(jq -r '.author.email' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.libwebkit2gtk.PackageName}}/libwebkit2gtk-4.0-37/g")
          echo $content
          echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control

      - name: Setup app template
        shell: bash
        run: |
          content=$(cat build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop)
          content=$(echo "$content" | sed -e "s/{{.Info.ProductName}}/$(jq -r '.info.productName' wails.json)/g")
          content=$(echo "$content" | sed -e "s/{{.Info.Comments}}/$(jq -r '.info.comments' wails.json)/g")
          echo $content
          echo "$content" > build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop

      - name: Package up deb file
        shell: bash
        run: |
          mv build/bin/tiny-rdm build/linux/tiny-rdm_0.0.0_amd64/usr/local/bin/
          cd build/linux
          mv tiny-rdm_0.0.0_amd64 "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"
          sed -i 's/0.0.0/${{ steps.normalise_version.outputs.version }}/g' "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64/DEBIAN/control"
          dpkg-deb --build -Zxz "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"

      - name: Package up appimage file
        run: |
          curl https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-${{ steps.normalise_platform_arch.outputs.arch }}.AppImage \
                -o linuxdeploy \
                -L
          chmod u+x linuxdeploy

          ./linuxdeploy --appdir AppDir

          pushd AppDir
          # Copy WebKit files.
          find /usr/lib* -name WebKitNetworkProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
          find /usr/lib* -name WebKitWebProcess -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
          find /usr/lib* -name libwebkit2gtkinjectedbundle.so -exec mkdir -p $(dirname '{}') \; -exec cp --parents '{}' "." \; || true
          popd


          mkdir -p AppDir/usr/share/icons/hicolor/512x512/apps
          build_dir="build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64"

          cp -r $build_dir/usr/share/icons/hicolor/512x512/apps/tiny-rdm.png AppDir/usr/share/icons/hicolor/512x512/apps/
          cp $build_dir/usr/local/bin/tiny-rdm AppDir/usr/bin/


          sed -i 's#/usr/local/bin/tiny-rdm#tiny-rdm#g' $build_dir/usr/share/applications/tiny-rdm.desktop

          curl -o linuxdeploy-plugin-gtk.sh "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"

          sed -i '/XDG_DATA_DIRS/a export WEBKIT_DISABLE_COMPOSITING_MODE=1' linuxdeploy-plugin-gtk.sh
          chmod +x linuxdeploy-plugin-gtk.sh

          curl -o AppDir/AppRun https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${{ steps.normalise_platform_arch.outputs.arch }} -L

          ./linuxdeploy --appdir AppDir \
             --output=appimage \
             --plugin=gtk \
             -e $build_dir/usr/local/bin/tiny-rdm \
             -d $build_dir/usr/share/applications/tiny-rdm.desktop

      - name: Rename deb
        working-directory: ./build/linux
        run: mv "tiny-rdm_${{ steps.normalise_version.outputs.version }}_amd64.deb" "tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb"

      - name: Rename appimage
        run: mv Tiny_RDM-${{ steps.normalise_platform_arch.outputs.arch }}.AppImage "tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.AppImage"

      - name: Upload release asset
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.normalise_version.outputs.version }}
          files: |
            ./build/linux/tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.deb
            tiny-rdm_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.AppImage
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-macos.yaml
================================================
name: Release macOS App
run-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}

on:
  release:
    types: [ published ]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag'
        required: true
        default: '1.0.0'

jobs:
  release:
    name: Release macOS App
    runs-on: macos-latest # We can cross compile but need to be on macOS to notarise
    strategy:
      matrix:
        platform:
          - darwin/amd64
          - darwin/arm64
    #          - darwin/universal
    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Normalise platform tag
        id: normalise_platform
        shell: bash
        run: |
          tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/darwin/mac/g' -e 's/amd64/intel/g')
          echo "tag=$tag" >> "$GITHUB_OUTPUT"

      - name: Normalise version tag
        id: normalise_version
        shell: bash
        run: |
          if [ "${{ github.event.release.tag_name }}" == "" ]; then
            version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          else
            version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          fi

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: stable

      #      - name: Install gon for macOS notarisation
      #        shell: bash
      #        run: wget https://github.com/mitchellh/gon/releases/download/v0.2.5/gon_macos.zip && unzip gon_macos.zip && mv gon /usr/local/bin
      #
      #      - name: Import code signing certificate from Github Secrets
      #        uses: Apple-Actions/import-codesign-certs@v1
      #        with:
      #          p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }}
      #          p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}

      - name: Install wails
        shell: bash
        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 22

      - name: Build frontend assets
        shell: bash
        run: |
          npm install -g npm@9
          jq '.info.productVersion = "${{ steps.normalise_version.outputs.version }}"' wails.json > tmp.json
          mv tmp.json wails.json
          cd frontend
          jq '.version = "${{ steps.normalise_version.outputs.version }}"' package.json > tmp.json
          mv tmp.json package.json
          npm install

      - name: Build wails app for macOS
        shell: bash
        run: |
          CGO_ENABLED=1 wails build -platform ${{ matrix.platform }} \
          -ldflags "-X main.version=${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.MAC_GA_SECRET }}"

      #      - name: Notarise macOS app + create dmg
      #        shell: bash
      #        run: gon -log-level=info gon.config.json
      #        env:
      #          AC_USERNAME: ${{ secrets.AC_USERNAME }}
      #          AC_PASSWORD: ${{ secrets.AC_PASSWORD }}

      - name: Checkout create-image
        uses: actions/checkout@v2
        with:
          repository: create-dmg/create-dmg
          path: ./build/create-dmg
          ref: master

      - name: Build macOS DMG
        shell: bash
        working-directory: ./build
        run: |
          mv bin/tinyrdm.app "bin/Tiny RDM.app"
          ./create-dmg/create-dmg \
            --no-internet-enable \
            --volname "Tiny RDM" \
            --volicon "bin/Tiny RDM.app/Contents/Resources/iconfile.icns" \
            --background "dmg/background.tiff" \
            --text-size 12 \
            --window-pos 400 400 \
            --window-size 660 450 \
            --icon-size 80 \
            --icon "Tiny RDM.app" 180 180 \
            --hide-extension "Tiny RDM.app" \
            --app-drop-link 480 180 \
            --add-file "Repair" "dmg/fix-app" 230 290 \
            --add-file "损坏修复" "dmg/fix-app_zh" 430 290 \
            "bin/TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" \
            "bin"

      - name: Rename dmg
        working-directory: ./build/bin
        run: mv "TinyRDM-${{ steps.normalise_platform.outputs.tag }}.dmg" "TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg"

      - name: Upload release asset (DMG Package)
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.normalise_version.outputs.version }}
          files: ./build/bin/TinyRDM_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.dmg
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/release-windows.yaml
================================================
name: Release Windows App
run-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}

on:
  release:
    types: [ published ]
  workflow_dispatch:
    inputs:
      tag:
        description: 'Version tag'
        required: true
        default: '1.0.0'

jobs:
  release:
    name: Release Windows App
    runs-on: windows-latest
    strategy:
      matrix:
        platform:
          - windows/amd64
          - windows/arm64
    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Normalise platform tag
        id: normalise_platform
        shell: bash
        run: |
          tag=$(echo ${{ matrix.platform }} | sed -e 's/\//_/g' -e 's/amd64/x64/g')
          echo "tag=$tag" >> "$GITHUB_OUTPUT"

      - name: Normalise platform name
        id: normalise_platform_name
        shell: bash
        run: |
          pname=$(echo "${{ matrix.platform }}" | sed 's/windows\///g')
          echo "pname=$pname" >> "$GITHUB_OUTPUT"

      - name: Normalise version tag
        id: normalise_version
        shell: bash
        run: |
          if [ "${{ github.event.release.tag_name }}" == "" ]; then
            version=$(echo ${{ github.event.inputs.tag }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          else
            version=$(echo ${{ github.event.release.tag_name }} | sed -e 's/v//g')
            echo "version=$version" >> "$GITHUB_OUTPUT"
          fi

      - name: Setup Go
        uses: actions/setup-go@v6
        with:
          go-version: stable

      - name: Install chocolatey
        uses: crazy-max/ghaction-chocolatey@v2
        with:
          args: install nsis jq

      - name: Install wails
        shell: bash
        run: go install github.com/wailsapp/wails/v2/cmd/wails@latest

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 22

      - name: Build frontend assets
        shell: bash
        run: |
          npm install -g npm@9
          jq '.info.productVersion = "${{ steps.normalise_version.outputs.version }}"' wails.json > tmp.json
          mv tmp.json wails.json
          cd frontend
          jq '.version = "${{ steps.normalise_version.outputs.version }}"' package.json > tmp.json
          mv tmp.json package.json
          npm install

      - name: Build Windows portable app
        shell: bash
        run: |
          CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
          -webview2 embed \
          -ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.WINDOWS_GA_SECRET }}"

      - name: Compress portable binary
        working-directory: ./build/bin
        run: Compress-Archive "Tiny RDM.exe" "TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip"

      - name: Upload release asset (Portable)
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.normalise_version.outputs.version }}
          files: ./build/bin/TinyRDM_Portable_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.zip
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Build Windows NSIS installer
        shell: bash
        run: |
          export PATH="/c/Program Files (x86)/NSIS:$PATH"
          which makensis && echo "makensis found" || echo "makensis NOT found"
          CGO_ENABLED=1 wails build -clean -platform ${{ matrix.platform }} \
          -nsis -webview2 embed \
          -ldflags "-X main.version=v${{ steps.normalise_version.outputs.version }} -X main.gaMeasurementID=${{ secrets.GA_MEASUREMENT_ID }} -X main.gaSecretKey=${{ secrets.WINDOWS_GA_SECRET }}"

      - name: Sign the installer
        uses: dlemstra/code-sign-action@v1
        with:
          certificate: ${{ secrets.WIN_SIGNING_CERT }}
          password: ${{ secrets.WIN_SIGNING_CERT_PASSWORD }}
          folder: ./build/bin

      - name: Rename installer
        working-directory: ./build/bin
        run: Rename-Item -Path "tinyrdm-${{ steps.normalise_platform_name.outputs.pname }}-installer.exe" -NewName "TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe"

      - name: Upload release asset (Installer)
        uses: softprops/action-gh-release@v1
        with:
          tag_name: v${{ steps.normalise_version.outputs.version }}
          files: ./build/bin/TinyRDM_Setup_${{ steps.normalise_version.outputs.version }}_${{ steps.normalise_platform.outputs.tag }}.exe
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
build/bin
node_modules
frontend/dist
frontend/wailsjs
frontend/package.json.md5
design/
.vscode
.idea
test


================================================
FILE: .prettierignore
================================================
/frontend/wailsjs/**


================================================
FILE: Dockerfile
================================================
# ============================================================
# Stage 1: Build frontend
# ============================================================
FROM --platform=linux/amd64 node:22-alpine AS frontend-builder

WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --ignore-scripts
COPY frontend/ ./
ENV NODE_OPTIONS=--max-old-space-size=4096
ENV VITE_WEB=true
RUN npm run build

# ============================================================
# Stage 2: Build Go backend (web mode)
# ============================================================
FROM golang:1.25-alpine AS backend-builder

WORKDIR /app
COPY go.mod go.sum ./
ENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct
RUN GOFLAGS="-mod=mod" go mod download

COPY backend/ ./backend/
COPY main_web.go ./

ARG APP_VERSION=1.0.0
RUN CGO_ENABLED=0 GOOS=linux GOFLAGS="-mod=mod" go build -tags web -ldflags "-s -w -X main.version=${APP_VERSION}" -o /app/tinyrdm-server .

# ============================================================
# Stage 3: Runtime (nginx + Go backend)
# ============================================================
FROM alpine:3.21

RUN apk add --no-cache ca-certificates tzdata nginx \
    && rm -rf /var/cache/apk/* /tmp/*

# Frontend static files
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html

# Nginx config
COPY docker/nginx.conf /etc/nginx/http.d/default.conf

# Go backend binary
WORKDIR /app
COPY --from=backend-builder /app/tinyrdm-server .
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

EXPOSE 8086

ENV PORT=8088
ENV GIN_MODE=release
ENV XDG_CONFIG_HOME=/app

ENTRYPOINT ["/entrypoint.sh"]


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) <year>  <name of author>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    <program>  Copyright (C) <year>  <name of author>
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.


================================================
FILE: README.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><strong>English</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)
[![Discord](https://img.shields.io/discord/1170373259133456434?label=Discord&color=5865F2)](https://discord.gg/VTFbBMGjWh)
[![X](https://img.shields.io/badge/Twitter-black?logo=x&logoColor=white)](https://twitter.com/Lykin53448)

<strong>Tiny RDM is a modern lightweight cross-platform Redis desktop manager available for Mac, Windows, and Linux. It also provides a web version that can be deployed via Docker.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Feature

* Super lightweight, built on Webview2, without embedded browsers (Thanks
  to [Wails](https://github.com/wailsapp/wails)).
* Provides visually and user-friendly UI, light and dark themes (Thanks to [Naive UI](https://github.com/tusen-ai/naive-ui)
  and [IconPark](https://iconpark.oceanengine.com)).
* Multi-language support ([Need more languages ? Click here to contribute](.github/CONTRIBUTING.md)).
* Better connection management: supports SSH Tunnel/SSL/Sentinel Mode/Cluster Mode/HTTP proxy/SOCKS5 proxy.
* Visualize key value operations, CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams.
* Support multiple data viewing format and decode/decompression methods.
* Use SCAN for segmented loading, making it easy to list millions of keys.
* Logs list for command operation history.
* Provides command-line mode.
* Provides slow logs list.
* Segmented loading and querying for List/Hash/Set/Sorted Set.
* Provide value decode/decompression for List/Hash/Set/Sorted Set.
* Integrate with Monaco Editor
* Support real-time commands monitoring.
* Support import/export data.
* Support publish/subscribe.
* Support import/export connection profile.
* Custom data encoder and decoder for value display ([Here are the instructions](https://tinyrdm.com/guide/custom-decoder/)).

## Installation

Available to download for free from [here](https://github.com/tiny-craft/tiny-rdm/releases).

> If you can't open it after installation on macOS, exec the following command then reopen:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Build Guidelines

### Prerequisites

* Go (latest version)
* Node.js >= 20
* NPM >= 9

### Install Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Pull the Code

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Build Frontend

```bash
npm install --prefix ./frontend
```

or

```bash
cd frontend
npm install
```

### Compile and Run

```bash
wails dev
```

## Docker Deployment

In addition to the desktop client, Tiny RDM also provides a web version that can be quickly deployed via Docker.

### Using Docker Compose (Recommended)

Create a `docker-compose.yml` file:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Start the service:

```bash
docker compose up -d
```

Once started, visit `http://localhost:8086` and log in with the credentials configured above.

### Using Docker Command

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `ADMIN_USERNAME` | Login username | - |
| `ADMIN_PASSWORD` | Login password | - |

## About

### Wechat Official Account

<img src="docs/images/wechat_official.png" alt="wechat" width="360" />

### Sponsor

If this project helpful for you, feel free to buy me a cup of coffee ☕️.

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Thanks

Thanks to the following service providers for hosting sponsorship

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_es.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <strong>Español</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM es un gestor Redis moderno, ligero y multiplataforma, disponible para Mac, Windows y Linux. También ofrece una versión web que se puede desplegar mediante Docker.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Características

* Ultra ligero, basado en Webview2, sin navegador integrado (Gracias a [Wails](https://github.com/wailsapp/wails))
* Interfaz visual y fácil de usar, temas claro y oscuro (Gracias a [Naive UI](https://github.com/tusen-ai/naive-ui) e [IconPark](https://iconpark.oceanengine.com))
* Soporte multilingüe ([¿Necesitas más idiomas? Haz clic aquí para contribuir](.github/CONTRIBUTING.md))
* Gestión mejorada de conexiones: túnel SSH/SSL/modo Sentinel/modo Cluster/proxy HTTP/proxy SOCKS5
* Visualización de operaciones clave-valor, soporte CRUD para List, Hash, String, Set, Sorted Set y Stream
* Soporte de múltiples formatos de visualización y métodos de decodificación/descompresión
* Carga segmentada con SCAN para listar fácilmente millones de claves
* Lista de registros del historial de comandos
* Modo línea de comandos
* Lista de registros lentos
* Carga segmentada y consultas para List/Hash/Set/Sorted Set
* Decodificación/descompresión de valores para List/Hash/Set/Sorted Set
* Integración con Monaco Editor
* Monitoreo de comandos en tiempo real
* Importación/exportación de datos
* Publicación/suscripción
* Importación/exportación de perfiles de conexión
* Codificador y decodificador de datos personalizados para la visualización de valores ([Instrucciones aquí](https://tinyrdm.com/guide/custom-decoder/))

## Instalación

Disponible para descargar gratis [aquí](https://github.com/tiny-craft/tiny-rdm/releases).

> Si no puedes abrirlo después de la instalación en macOS, ejecuta el siguiente comando y vuelve a abrirlo:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Guía de compilación

### Requisitos previos

* Go (última versión)
* Node.js >= 20
* NPM >= 9

### Instalar Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Obtener el código

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Compilar el frontend

```bash
npm install --prefix ./frontend
```

o

```bash
cd frontend
npm install
```

### Compilar y ejecutar

```bash
wails dev
```

## Despliegue con Docker

Además del cliente de escritorio, Tiny RDM también ofrece una versión web que se puede desplegar rápidamente con Docker.

### Usando Docker Compose (recomendado)

Crea un archivo `docker-compose.yml`:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Inicia el servicio:

```bash
docker compose up -d
```

Una vez iniciado, visita `http://localhost:8086` e inicia sesión con las credenciales configuradas arriba.

### Usando el comando Docker

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Variables de entorno

| Variable | Descripción | Valor por defecto |
|----------|-------------|-------------------|
| `ADMIN_USERNAME` | Nombre de usuario | - |
| `ADMIN_PASSWORD` | Contraseña | - |

## Acerca de

### Patrocinar

Si este proyecto te resulta útil, no dudes en invitar al autor a un café ☕️

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Agradecimientos

Gracias a los siguientes proveedores de servicios por el patrocinio de alojamiento

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_fr.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <strong>Français</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM est un gestionnaire Redis moderne, léger et multiplateforme, disponible pour Mac, Windows et Linux. Une version web déployable via Docker est également proposée.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Fonctionnalités

* Ultra léger, basé sur Webview2, sans navigateur intégré (Merci à [Wails](https://github.com/wailsapp/wails))
* Interface visuelle et conviviale, thèmes clair et sombre (Merci à [Naive UI](https://github.com/tusen-ai/naive-ui) et [IconPark](https://iconpark.oceanengine.com))
* Support multilingue ([Besoin de plus de langues ? Cliquez ici pour contribuer](.github/CONTRIBUTING.md))
* Gestion améliorée des connexions : tunnel SSH/SSL/mode Sentinelle/mode Cluster/proxy HTTP/proxy SOCKS5
* Visualisation des opérations clé-valeur, support CRUD pour List, Hash, String, Set, Sorted Set et Stream
* Support de multiples formats d'affichage et méthodes de décodage/décompression
* Chargement segmenté avec SCAN pour lister facilement des millions de clés
* Liste des journaux d'historique des commandes
* Mode ligne de commande
* Liste des journaux lents
* Chargement segmenté et requêtes pour List/Hash/Set/Sorted Set
* Décodage/décompression des valeurs pour List/Hash/Set/Sorted Set
* Intégration de Monaco Editor
* Surveillance des commandes en temps réel
* Import/export de données
* Publication/abonnement
* Import/export de profils de connexion
* Encodeur et décodeur de données personnalisés pour l'affichage des valeurs ([Instructions ici](https://tinyrdm.com/guide/custom-decoder/))

## Installation

Disponible en téléchargement gratuit [ici](https://github.com/tiny-craft/tiny-rdm/releases).

> Si vous ne pouvez pas l'ouvrir après l'installation sur macOS, exécutez la commande suivante puis relancez :
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Guide de compilation

### Prérequis

* Go (dernière version)
* Node.js >= 20
* NPM >= 9

### Installer Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Récupérer le code

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Compiler le frontend

```bash
npm install --prefix ./frontend
```

ou

```bash
cd frontend
npm install
```

### Compiler et exécuter

```bash
wails dev
```

## Déploiement Docker

En plus du client de bureau, Tiny RDM propose une version web déployable rapidement via Docker.

### Avec Docker Compose (recommandé)

Créez un fichier `docker-compose.yml` :

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Démarrez le service :

```bash
docker compose up -d
```

Une fois démarré, accédez à `http://localhost:8086` et connectez-vous avec les identifiants configurés ci-dessus.

### Avec la commande Docker

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Variables d'environnement

| Variable | Description | Valeur par défaut |
|----------|-------------|-------------------|
| `ADMIN_USERNAME` | Nom d'utilisateur | - |
| `ADMIN_PASSWORD` | Mot de passe | - |

## À propos

### Sponsor

Si ce projet vous est utile, n'hésitez pas à offrir un café ☕️

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Remerciements

Merci aux fournisseurs de services suivants pour le parrainage d'hébergement

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_ja.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <strong>日本語</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)
[![Discord](https://img.shields.io/discord/1170373259133456434?label=Discord&color=5865F2)](https://discord.gg/VTFbBMGjWh)
[![X](https://img.shields.io/badge/Twitter-black?logo=x&logoColor=white)](https://twitter.com/Lykin53448)

<strong>Tiny RDMは、Mac、Windows、Linuxで利用可能な、モダンで軽量なクロスプラットフォームのRedisデスクトップマネージャーです。Docker経由でデプロイ可能なWeb版も提供しています。</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## 特徴

* 超軽量、Webview2をベースにしており、埋め込みブラウザなし([Wails](https://github.com/wailsapp/wails)に感謝)。
* 視覚的でユーザーフレンドリーなUI、ライトとダークテーマを提供([Naive UI](https://github.com/tusen-ai/naive-ui)と[IconPark](https://iconpark.oceanengine.com)に感謝)。
* 多言語サポート([もっと多くの言語が必要ですか?ここをクリックして貢献してください](.github/CONTRIBUTING.md))。
* より良い接続管理:SSHトンネル/SSL/センチネルモード/クラスターモード/HTTPプロキシ/SOCKS5プロキシをサポート。
* キー値操作の可視化、リスト、ハッシュ、文字列、セット、ソートセット、ストリームのCRUDサポート。
* 複数のデータ表示形式とデコード/解凍方法をサポート。
* SCANを使用してセグメント化された読み込みを行い、数百万のキーを簡単にリスト化。
* コマンド操作履歴のログリスト。
* コマンドラインモードを提供。
* スローログリストを提供。
* リスト/ハッシュ/セット/ソートセットのセグメント化された読み込みとクエリ。
* リスト/ハッシュ/セット/ソートセットの値のデコード/解凍を提供。
* Monaco Editorと統合。
* リアルタイムコマンド監視をサポート。
* データのインポート/エクスポートをサポート。
* パブリッシュ/サブスクライブをサポート。
* 接続プロファイルのインポート/エクスポートをサポート。
* 値表示のためのカスタムデータエンコーダーとデコーダーをサポート([こちらが手順です](https://tinyrdm.com/guide/custom-decoder/))。

## インストール

[こちら](https://github.com/tiny-craft/tiny-rdm/releases)から無料でダウンロードできます。

> macOSにインストール後に開けない場合、**信頼されていない**または**ゴミ箱に移動**というエラーが表示された場合は、以下のコマンドを実行してから再度開いてください:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## ビルドガイドライン

### 前提条件

* Go(最新バージョン)
* Node.js >= 20
* NPM >= 9

### Wailsのインストール

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### コードの取得

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### フロントエンドのビルド

```bash
npm install --prefix ./frontend
```

または

```bash
cd frontend
npm install
```

### コンパイルと実行

```bash
wails dev
```

## Dockerデプロイ

デスクトップクライアントに加えて、Tiny RDMはDockerで素早くデプロイできるWeb版も提供しています。

### Docker Composeを使用(推奨)

`docker-compose.yml` ファイルを作成します:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

サービスを起動します:

```bash
docker compose up -d
```

起動後、`http://localhost:8086` にアクセスし、上記で設定したユーザー名とパスワードでログインしてください。

### Dockerコマンドを使用

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### 環境変数の説明

| 変数 | 説明 | デフォルト値 |
|------|------|-------------|
| `ADMIN_USERNAME` | ログインユーザー名 | - |
| `ADMIN_PASSWORD` | ログインパスワード | - |

## について

### Wechat公式アカウント

<img src="docs/images/wechat_official.png" alt="wechat" width="360" />

### スポンサー

このプロジェクトが役立つ場合は、コーヒーを一杯おごってください ☕️。

* Wechatスポンサー

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### 謝辞

以下のサービスプロバイダーによるホスティングスポンサーシップに感謝いたします

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_ko.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <strong>한국어</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM은 Mac, Windows, Linux에서 사용할 수 있는 현대적이고 가벼운 크로스 플랫폼 Redis 데스크톱 관리자입니다. Docker를 통해 배포할 수 있는 웹 버전도 제공합니다.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## 기능

* 초경량, Webview2 기반으로 내장 브라우저 없음 ([Wails](https://github.com/wailsapp/wails) 감사합니다)
* 시각적이고 사용자 친화적인 UI, 라이트/다크 테마 제공 ([Naive UI](https://github.com/tusen-ai/naive-ui) 및 [IconPark](https://iconpark.oceanengine.com) 감사합니다)
* 다국어 지원 ([더 많은 언어가 필요하신가요? 여기를 클릭하여 기여하세요](.github/CONTRIBUTING.md))
* 향상된 연결 관리: SSH 터널/SSL/센티널 모드/클러스터 모드/HTTP 프록시/SOCKS5 프록시 지원
* 키-값 작업 시각화, List, Hash, String, Set, Sorted Set, Stream의 CRUD 지원
* 다양한 데이터 보기 형식 및 디코딩/압축 해제 방법 지원
* SCAN을 사용한 분할 로딩으로 수백만 개의 키를 쉽게 나열
* 명령 실행 이력 로그 목록
* 명령줄 모드 제공
* 슬로우 로그 목록 제공
* List/Hash/Set/Sorted Set의 분할 로딩 및 쿼리
* List/Hash/Set/Sorted Set 값의 디코딩/압축 해제 제공
* Monaco Editor 통합
* 실시간 명령 모니터링 지원
* 데이터 가져오기/내보내기 지원
* 발행/구독 지원
* 연결 프로필 가져오기/내보내기 지원
* 값 표시를 위한 사용자 정의 데이터 인코더 및 디코더 ([사용 방법](https://tinyrdm.com/guide/custom-decoder/))

## 설치

[여기](https://github.com/tiny-craft/tiny-rdm/releases)에서 무료로 다운로드할 수 있습니다.

> macOS에서 설치 후 열 수 없는 경우, 다음 명령을 실행한 후 다시 열어주세요:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## 빌드 가이드

### 사전 요구 사항

* Go (최신 버전)
* Node.js >= 20
* NPM >= 9

### Wails 설치

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### 코드 가져오기

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### 프론트엔드 빌드

```bash
npm install --prefix ./frontend
```

또는

```bash
cd frontend
npm install
```

### 컴파일 및 실행

```bash
wails dev
```

## Docker 배포

데스크톱 클라이언트 외에도 Tiny RDM은 Docker를 통해 빠르게 배포할 수 있는 웹 버전을 제공합니다.

### Docker Compose 사용 (권장)

`docker-compose.yml` 파일을 생성합니다:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

서비스를 시작합니다:

```bash
docker compose up -d
```

시작 후 `http://localhost:8086`에 접속하여 위에서 설정한 사용자 이름과 비밀번호로 로그인하세요.

### Docker 명령 사용

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### 환경 변수

| 변수 | 설명 | 기본값 |
|------|------|--------|
| `ADMIN_USERNAME` | 로그인 사용자 이름 | - |
| `ADMIN_PASSWORD` | 로그인 비밀번호 | - |

## 소개

### 스폰서

이 프로젝트가 도움이 되셨다면 커피 한 잔 사주세요 ☕️

* Wechat 후원

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### 감사

호스팅 후원을 제공해 주신 다음 서비스 제공업체에 감사드립니다

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_pt.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <strong>Português (BR)</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM é um gerenciador Redis moderno, leve e multiplataforma, disponível para Mac, Windows e Linux. Também oferece uma versão web que pode ser implantada via Docker.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Funcionalidades

* Ultra leve, baseado em Webview2, sem navegador embutido (Graças ao [Wails](https://github.com/wailsapp/wails))
* Interface visual e amigável, temas claro e escuro (Graças ao [Naive UI](https://github.com/tusen-ai/naive-ui) e [IconPark](https://iconpark.oceanengine.com))
* Suporte multilíngue ([Precisa de mais idiomas? Clique aqui para contribuir](.github/CONTRIBUTING.md))
* Gerenciamento aprimorado de conexões: túnel SSH/SSL/modo Sentinel/modo Cluster/proxy HTTP/proxy SOCKS5
* Visualização de operações chave-valor, suporte CRUD para List, Hash, String, Set, Sorted Set e Stream
* Suporte a múltiplos formatos de visualização e métodos de decodificação/descompressão
* Carregamento segmentado com SCAN para listar facilmente milhões de chaves
* Lista de logs do histórico de comandos
* Modo linha de comando
* Lista de logs lentos
* Carregamento segmentado e consultas para List/Hash/Set/Sorted Set
* Decodificação/descompressão de valores para List/Hash/Set/Sorted Set
* Integração com Monaco Editor
* Monitoramento de comandos em tempo real
* Importação/exportação de dados
* Publicação/assinatura
* Importação/exportação de perfis de conexão
* Codificador e decodificador de dados personalizados para exibição de valores ([Instruções aqui](https://tinyrdm.com/guide/custom-decoder/))

## Instalação

Disponível para download gratuito [aqui](https://github.com/tiny-craft/tiny-rdm/releases).

> Se não conseguir abrir após a instalação no macOS, execute o seguinte comando e reabra:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Guia de compilação

### Pré-requisitos

* Go (versão mais recente)
* Node.js >= 20
* NPM >= 9

### Instalar Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Obter o código

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Compilar o frontend

```bash
npm install --prefix ./frontend
```

ou

```bash
cd frontend
npm install
```

### Compilar e executar

```bash
wails dev
```

## Implantação com Docker

Além do cliente desktop, o Tiny RDM também oferece uma versão web que pode ser implantada rapidamente via Docker.

### Usando Docker Compose (recomendado)

Crie um arquivo `docker-compose.yml`:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Inicie o serviço:

```bash
docker compose up -d
```

Após iniciar, acesse `http://localhost:8086` e faça login com as credenciais configuradas acima.

### Usando o comando Docker

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Variáveis de ambiente

| Variável | Descrição | Padrão |
|----------|-----------|--------|
| `ADMIN_USERNAME` | Nome de usuário | - |
| `ADMIN_PASSWORD` | Senha | - |

## Sobre

### Patrocinar

Se este projeto foi útil para você, sinta-se à vontade para pagar um café ☕️

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Agradecimentos

Agradecemos aos seguintes provedores de serviços pelo patrocínio de hospedagem

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_ru.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <strong>Русский</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM — современный легковесный кроссплатформенный менеджер Redis для Mac, Windows и Linux. Также доступна веб-версия с возможностью развёртывания через Docker.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Возможности

* Сверхлёгкий, на базе Webview2, без встроенного браузера (Спасибо [Wails](https://github.com/wailsapp/wails))
* Визуально приятный и удобный интерфейс, светлая и тёмная темы (Спасибо [Naive UI](https://github.com/tusen-ai/naive-ui) и [IconPark](https://iconpark.oceanengine.com))
* Поддержка нескольких языков ([Нужно больше языков? Нажмите здесь, чтобы помочь](.github/CONTRIBUTING.md))
* Улучшенное управление подключениями: SSH-туннель/SSL/режим Sentinel/режим Cluster/HTTP-прокси/SOCKS5-прокси
* Визуализация операций с ключами, поддержка CRUD для List, Hash, String, Set, Sorted Set и Stream
* Поддержка множества форматов отображения и методов декодирования/распаковки
* Сегментированная загрузка через SCAN для удобной работы с миллионами ключей
* Журнал истории выполненных команд
* Режим командной строки
* Список медленных запросов
* Сегментированная загрузка и запросы для List/Hash/Set/Sorted Set
* Декодирование/распаковка значений для List/Hash/Set/Sorted Set
* Интеграция с Monaco Editor
* Мониторинг команд в реальном времени
* Импорт/экспорт данных
* Публикация/подписка
* Импорт/экспорт профилей подключений
* Пользовательские кодировщики и декодировщики для отображения значений ([Инструкция](https://tinyrdm.com/guide/custom-decoder/))

## Установка

Доступно для бесплатного скачивания [здесь](https://github.com/tiny-craft/tiny-rdm/releases).

> Если после установки на macOS приложение не открывается, выполните следующую команду и попробуйте снова:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Руководство по сборке

### Требования

* Go (последняя версия)
* Node.js >= 20
* NPM >= 9

### Установка Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Получение кода

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Сборка фронтенда

```bash
npm install --prefix ./frontend
```

или

```bash
cd frontend
npm install
```

### Компиляция и запуск

```bash
wails dev
```

## Развёртывание через Docker

Помимо десктопного клиента, Tiny RDM предоставляет веб-версию, которую можно быстро развернуть через Docker.

### С помощью Docker Compose (рекомендуется)

Создайте файл `docker-compose.yml`:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Запустите сервис:

```bash
docker compose up -d
```

После запуска откройте `http://localhost:8086` и войдите с указанными выше учётными данными.

### С помощью команды Docker

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Переменные окружения

| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `ADMIN_USERNAME` | Имя пользователя | - |
| `ADMIN_PASSWORD` | Пароль | - |

## О проекте

### Спонсорство

Если этот проект оказался полезным, угостите автора чашкой кофе ☕️

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Благодарности

Благодарим следующих поставщиков услуг за спонсорство хостинга

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_tr.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <strong>Türkçe</strong></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM, Mac, Windows ve Linux için kullanılabilen modern, hafif ve çapraz platform bir Redis masaüstü yöneticisidir. Docker ile dağıtılabilen bir web sürümü de sunmaktadır.</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## Özellikler

* Ultra hafif, Webview2 tabanlı, gömülü tarayıcı yok ([Wails](https://github.com/wailsapp/wails)'e teşekkürler)
* Görsel ve kullanıcı dostu arayüz, açık ve koyu tema desteği ([Naive UI](https://github.com/tusen-ai/naive-ui) ve [IconPark](https://iconpark.oceanengine.com)'a teşekkürler)
* Çoklu dil desteği ([Daha fazla dil mi gerekiyor? Katkıda bulunmak için tıklayın](.github/CONTRIBUTING.md))
* Gelişmiş bağlantı yönetimi: SSH Tüneli/SSL/Sentinel Modu/Cluster Modu/HTTP proxy/SOCKS5 proxy desteği
* Anahtar-değer işlemlerinin görselleştirilmesi, List, Hash, String, Set, Sorted Set ve Stream için CRUD desteği
* Çoklu veri görüntüleme formatı ve çözme/sıkıştırma açma yöntemleri desteği
* SCAN ile segmentli yükleme, milyonlarca anahtarı kolayca listeleme
* Komut işlem geçmişi günlük listesi
* Komut satırı modu
* Yavaş günlük listesi
* List/Hash/Set/Sorted Set için segmentli yükleme ve sorgulama
* List/Hash/Set/Sorted Set değerleri için çözme/sıkıştırma açma
* Monaco Editor entegrasyonu
* Gerçek zamanlı komut izleme desteği
* Veri içe/dışa aktarma desteği
* Yayınla/abone ol desteği
* Bağlantı profili içe/dışa aktarma desteği
* Değer görüntüleme için özel veri kodlayıcı ve çözücü ([Talimatlar burada](https://tinyrdm.com/guide/custom-decoder/))

## Kurulum

[Buradan](https://github.com/tiny-craft/tiny-rdm/releases) ücretsiz olarak indirilebilir.

> macOS'ta kurulumdan sonra açamıyorsanız, aşağıdaki komutu çalıştırıp tekrar açın:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## Derleme Kılavuzu

### Gereksinimler

* Go (en son sürüm)
* Node.js >= 20
* NPM >= 9

### Wails Kurulumu

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### Kodu Çekme

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### Frontend Derleme

```bash
npm install --prefix ./frontend
```

veya

```bash
cd frontend
npm install
```

### Derleme ve Çalıştırma

```bash
wails dev
```

## Docker ile Dağıtım

Masaüstü istemcisinin yanı sıra, Tiny RDM Docker ile hızlıca dağıtılabilen bir web sürümü de sunmaktadır.

### Docker Compose Kullanımı (önerilen)

Bir `docker-compose.yml` dosyası oluşturun:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

Servisi başlatın:

```bash
docker compose up -d
```

Başlatıldıktan sonra `http://localhost:8086` adresini ziyaret edin ve yukarıda yapılandırılan kimlik bilgileriyle giriş yapın.

### Docker Komutu Kullanımı

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### Ortam Değişkenleri

| Değişken | Açıklama | Varsayılan |
|----------|----------|------------|
| `ADMIN_USERNAME` | Giriş kullanıcı adı | - |
| `ADMIN_PASSWORD` | Giriş şifresi | - |

## Hakkında

### Sponsor

Bu proje işinize yaradıysa, bir kahve ısmarlayabilirsiniz ☕️

* Wechat Sponsor

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### Teşekkürler

Barındırma sponsorluğu sağlayan aşağıdaki hizmet sağlayıcılara teşekkür ederiz

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_tw.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_zh.md">简体中文</a> | <strong>繁體中文</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<strong>Tiny RDM 是一款現代化輕量級的跨平台 Redis 桌面管理工具,支援 Mac、Windows 和 Linux,同時提供 Web 版本,可透過 Docker 快速部署</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en.png">
 <img alt="screenshot" src="screenshots/dark_en.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_en2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_en2.png">
 <img alt="screenshot" src="screenshots/dark_en2.png">
</picture>

## 功能特性

* 極度輕量,基於 Webview2,無內嵌瀏覽器(感謝 [Wails](https://github.com/wailsapp/wails))
* 介面精美易用,提供淺色/深色主題(感謝 [Naive UI](https://github.com/tusen-ai/naive-ui) 和 [IconPark](https://iconpark.oceanengine.com))
* 多國語言支援([需要更多語言支援?點此貢獻](.github/CONTRIBUTING.md))
* 更好的連線管理:支援 SSH 隧道/SSL/哨兵模式/叢集模式/HTTP 代理/SOCKS5 代理
* 視覺化鍵值操作,支援 List、Hash、String、Set、Sorted Set 和 Stream 的 CRUD
* 支援多種資料檢視格式及轉碼/解壓方式
* 採用 SCAN 分段載入,可輕鬆處理數百萬鍵列表
* 操作命令執行日誌展示
* 提供命令列模式
* 提供慢日誌展示
* List/Hash/Set/Sorted Set 的分段載入和查詢
* List/Hash/Set/Sorted Set 值的轉碼顯示
* 內建高級編輯器 Monaco Editor
* 支援命令即時監控
* 支援匯入/匯出資料
* 支援發布訂閱
* 支援匯入/匯出連線設定
* 自訂資料展示編碼/解碼([操作指引](https://tinyrdm.com/guide/custom-decoder/))

## 安裝

提供 Mac、Windows 和 Linux 安裝包,可[免費下載](https://github.com/tiny-craft/tiny-rdm/releases)。

> 如果在 macOS 上安裝後無法開啟,出現**不受信任**或**移到垃圾桶**的錯誤,執行以下命令後再啟動即可:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## 建置專案

### 環境需求

* Go(最新版本)
* Node.js >= 20
* NPM >= 9

### 安裝 Wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### 取得程式碼

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### 建置前端

```bash
npm install --prefix ./frontend
```

或

```bash
cd frontend
npm install
```

### 編譯並執行

```bash
wails dev
```

## Docker 部署

除桌面客戶端外,Tiny RDM 還提供 Web 版本,可透過 Docker 快速部署。

### 使用 Docker Compose(推薦)

建立 `docker-compose.yml` 檔案:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

啟動服務:

```bash
docker compose up -d
```

啟動後造訪 `http://localhost:8086`,使用上方設定的帳號密碼登入。

### 使用 Docker 命令

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### 環境變數說明

| 變數 | 說明 | 預設值 |
|------|------|--------|
| `ADMIN_USERNAME` | 登入帳號 | - |
| `ADMIN_PASSWORD` | 登入密碼 | - |

## 關於

### 贊助

如果此專案對您有幫助,歡迎請作者喝杯咖啡 ☕️

* 微信贊賞

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### 感謝

感謝以下服務商提供主機贊助

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: README_zh.md
================================================
<div align="center">
<a href="https://github.com/tiny-craft/tiny-rdm/"><img src="build/appicon.png" width="120"/></a>
</div>
<h1 align="center">Tiny RDM</h1>
<h4 align="center"><a href="/">English</a> | <strong>简体中文</strong> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tw.md">繁體中文</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ja.md">日本語</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ko.md">한국어</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_fr.md">Français</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_es.md">Español</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_pt.md">Português (BR)</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_ru.md">Русский</a> | <a href="https://github.com/tiny-craft/tiny-rdm/blob/main/README_tr.md">Türkçe</a></h4>
<div align="center">

[![License](https://img.shields.io/github/license/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/blob/main/LICENSE)
[![GitHub release](https://img.shields.io/github/release/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/releases)
![GitHub All Releases](https://img.shields.io/github/downloads/tiny-craft/tiny-rdm/total)
[![GitHub stars](https://img.shields.io/github/stars/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/tiny-craft/tiny-rdm)](https://github.com/tiny-craft/tiny-rdm/fork)

<str>一个现代化轻量级的跨平台Redis桌面客户端,支持Mac、Windows和Linux,同时提供Web版本,可通过Docker快速部署</strong>
</div>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_zh.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_zh.png">
 <img alt="screenshot" src="screenshots/dark_zh.png">
</picture>

<picture>
 <source media="(prefers-color-scheme: dark)" srcset="screenshots/dark_zh2.png">
 <source media="(prefers-color-scheme: light)" srcset="screenshots/light_zh2.png">
 <img alt="screenshot" src="screenshots/dark_zh2.png">
</picture>

## 功能特性

* 极度轻量,基于Webview2,无内嵌浏览器(感谢[Wails](https://github.com/wailsapp/wails))
* 界面精美易用,提供浅色/深色主题(感谢[Naive UI](https://github.com/tusen-ai/naive-ui)
  和 [IconPark](https://iconpark.oceanengine.com))
* 多国语言支持:英文/中文([需要更多语言支持?点我贡献语言](.github/CONTRIBUTING_zh.md))
* 更好用的连接管理:支持SSH隧道/SSL/哨兵模式/集群模式/HTTP代理/SOCKS5代理
* 可视化键值操作,增删查改一应俱全
* 支持多种数据查看格式以及转码/解压方式
* 采用SCAN分段加载,可轻松处理数百万键列表
* 操作命令执行日志展示
* 提供命令行操作
* 提供慢日志展示
* List/Hash/Set/Sorted Set的分段加载和查询
* List/Hash/Set/Sorted Set值的转码显示
* 内置高级编辑器Monaco Editor
* 支持命令实时监控
* 支持导入/导出数据
* 支持发布订阅
* 支持导入/导出连接配置
* 自定义数据展示编码/解码([这是操作指引](https://tinyrdm.com/zh/guide/custom-decoder/))

## 安装

提供Mac、Windows和Linux安装包,可[免费下载](https://github.com/tiny-craft/tiny-rdm/releases)。

> 如果在macOS上安装后无法打开,报错**不受信任**或者**移到垃圾箱**,执行下面命令后再启动即可:
> ``` shell
>  sudo xattr -d com.apple.quarantine /Applications/Tiny\ RDM.app
> ```

## 构建客户端

### 运行环境要求

* Go(最新版本)
* Node.js >= 20
* NPM >= 9

### 安装wails

```bash
go install github.com/wailsapp/wails/v2/cmd/wails@latest
```

### 拉取代码

```bash
git clone https://github.com/tiny-craft/tiny-rdm --depth=1
```

### 构建前端代码

```bash
npm install --prefix ./frontend
```

或者

```bash
cd frontend
npm install
```

### 编译运行开发版本

```bash
wails dev
```

## Docker 部署

除桌面客户端外,Tiny RDM 还提供 Web 版本,可通过 Docker 快速部署。

### 使用 Docker Compose(推荐)

创建 `docker-compose.yml` 文件:

```yaml
services:
  tinyrdm:
    image: ghcr.io/tiny-craft/tiny-rdm:latest
    container_name: tinyrdm
    restart: unless-stopped
    ports:
      - "8086:8086"
    environment:
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=tinyrdm
    volumes:
      - ./data:/app/tinyrdm
```

启动服务:

```bash
docker compose up -d
```

启动后访问 `http://localhost:8086`,使用上面配置的用户名密码登录。

### 使用 Docker 命令

```bash
docker run -d --name tinyrdm \
  -p 8086:8086 \
  -e ADMIN_USERNAME=admin \
  -e ADMIN_PASSWORD=tinyrdm \
  -v ./data:/app/tinyrdm \
  ghcr.io/tiny-craft/tiny-rdm:latest
```

### 环境变量说明

| 变量 | 说明 | 默认值 |
|------|------|--------|
| `ADMIN_USERNAME` | 登录用户名 | - |
| `ADMIN_PASSWORD` | 登录密码 | - |

## 关于

如果你也同为独立开发者(团队),喜欢开源,或者对Tiny Craft的相关产品感兴趣,可以关注微信公众号或者加入QQ群,探讨心得,反馈意见,交个朋友。

### 微信公众号(用户交流微信群)

我会不定期更新一些关于独立开发的思考和感悟,以及独立产品的介绍,欢迎扫码关注~👏

<img src="docs/images/wechat_official.png" alt="wechat" width="360" />

### B站官方账号

<img src="docs/images/bilibili_official.png" alt="bilibili" width="360" />

### 独立开发互助QQ群

```
831077639
```

### 赞助

该项目完全为爱发电,如果对你有所帮助,可以请作者喝杯咖啡 ☕️

* 微信赞赏

<img src="docs/images/wechat_sponsor.jpg" alt="wechat" width="200" />

### 感谢

感谢以下服务商提供主机赞助

[![Powered by NotiDC](docs/images/notidc_logo.png)](https://www.notidc.com/ "Powered by NotiDC")


================================================
FILE: backend/api/auth.go
================================================
//go:build web

package api

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

// Auth configuration
var (
	authEnabled  bool
	authUsername string
	authPassword string
	jwtSecret    []byte
	sessionTTL   = 24 * time.Hour
)

// Rate limiter for login attempts
type rateLimiter struct {
	mu          sync.Mutex
	attempts    map[string][]time.Time // ip -> timestamps
	maxRate     int                    // max attempts per window
	window      time.Duration
	maxEntries  int // max tracked IPs to prevent memory exhaustion
	lastCleanup time.Time
}

var loginLimiter = &rateLimiter{
	attempts:    make(map[string][]time.Time),
	maxRate:     5,
	window:      time.Minute,
	maxEntries:  10000,
	lastCleanup: time.Now(),
}

func (rl *rateLimiter) allow(ip string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	cutoff := now.Add(-rl.window)

	// Periodic full cleanup every 5 minutes to prevent memory leak
	if now.Sub(rl.lastCleanup) > 5*time.Minute {
		for k, times := range rl.attempts {
			valid := times[:0]
			for _, t := range times {
				if t.After(cutoff) {
					valid = append(valid, t)
				}
			}
			if len(valid) == 0 {
				delete(rl.attempts, k)
			} else {
				rl.attempts[k] = valid
			}
		}
		rl.lastCleanup = now
	}

	// Hard cap on tracked IPs to prevent memory exhaustion from distributed attacks
	if len(rl.attempts) >= rl.maxEntries {
		if _, exists := rl.attempts[ip]; !exists {
			// Too many tracked IPs, reject new ones as a safety measure
			return false
		}
	}

	// Clean old entries for this IP
	times := rl.attempts[ip]
	valid := times[:0]
	for _, t := range times {
		if t.After(cutoff) {
			valid = append(valid, t)
		}
	}
	rl.attempts[ip] = valid

	if len(valid) >= rl.maxRate {
		return false
	}

	rl.attempts[ip] = append(valid, now)
	return true
}

// InitAuth reads auth config from environment variables
func InitAuth() {
	authUsername = os.Getenv("ADMIN_USERNAME")
	authPassword = os.Getenv("ADMIN_PASSWORD")
	authEnabled = authUsername != "" && authPassword != ""

	// Generate random JWT secret on each startup
	secret := make([]byte, 32)
	rand.Read(secret)
	jwtSecret = secret

	if ttl := os.Getenv("SESSION_TTL"); ttl != "" {
		if d, err := time.ParseDuration(ttl); err == nil {
			sessionTTL = d
		}
	}

	if authEnabled {
		fmt.Printf("Auth enabled for user: %s\n", authUsername)
	} else {
		fmt.Println("Auth disabled (set ADMIN_USERNAME and ADMIN_PASSWORD to enable)")
	}
}

// IsAuthEnabled returns whether authentication is enabled
func IsAuthEnabled() bool {
	return authEnabled
}

// Simple JWT-like token: header.payload.signature (HMAC-SHA256)
type tokenPayload struct {
	User string `json:"u"`
	Exp  int64  `json:"e"`
	IP   string `json:"ip"`
}

func generateToken(username, ip string) (string, time.Time) {
	exp := time.Now().Add(sessionTTL)
	payload := tokenPayload{User: username, Exp: exp.Unix(), IP: ip}
	data, _ := json.Marshal(payload)
	encoded := hex.EncodeToString(data)

	mac := hmac.New(sha256.New, jwtSecret)
	mac.Write(data)
	sig := hex.EncodeToString(mac.Sum(nil))

	return encoded + "." + sig, exp
}

func validateToken(token, ip string) bool {
	parts := strings.SplitN(token, ".", 2)
	if len(parts) != 2 {
		return false
	}

	data, err := hex.DecodeString(parts[0])
	if err != nil {
		return false
	}

	// Verify signature
	mac := hmac.New(sha256.New, jwtSecret)
	mac.Write(data)
	expectedSig := hex.EncodeToString(mac.Sum(nil))
	if !hmac.Equal([]byte(parts[1]), []byte(expectedSig)) {
		return false
	}

	// Parse and validate payload
	var payload tokenPayload
	if err := json.Unmarshal(data, &payload); err != nil {
		return false
	}

	if time.Now().Unix() > payload.Exp {
		return false
	}

	// IP binding
	if payload.IP != ip {
		return false
	}

	return true
}

func getClientIP(c *gin.Context) string {
	// Cloudflare
	if ip := c.GetHeader("CF-Connecting-IP"); ip != "" {
		return ip
	}
	if ip := c.GetHeader("X-Real-IP"); ip != "" {
		return ip
	}
	if ip := c.GetHeader("X-Forwarded-For"); ip != "" {
		return strings.Split(ip, ",")[0]
	}
	return c.ClientIP()
}

// AuthMiddleware protects API routes
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		if !authEnabled {
			c.Next()
			return
		}

		// Get token from cookie
		token, err := c.Cookie("rdm_token")
		if err != nil || !validateToken(token, getClientIP(c)) {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
				"success": false,
				"msg":     "unauthorized",
			})
			return
		}

		c.Next()
	}
}

// SecurityHeaders adds security headers to all responses
func SecurityHeaders() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Header("X-Content-Type-Options", "nosniff")
		c.Header("X-Frame-Options", "SAMEORIGIN")
		c.Header("X-XSS-Protection", "1; mode=block")
		c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
		c.Header("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://static.cloudflareinsights.com https://analytics.tinycraft.cc; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss: https://static.cloudflareinsights.com https://analytics.tinycraft.cc; font-src 'self' data:;")
		c.Next()
	}
}

// registerAuthRoutes registers login/logout/status endpoints
func registerAuthRoutes(r *gin.Engine) {
	r.POST("/api/auth/login", handleLogin)
	r.POST("/api/auth/logout", handleLogout)
	r.GET("/api/auth/status", handleAuthStatus)
}

func handleLogin(c *gin.Context) {
	ip := getClientIP(c)

	// Rate limiting
	if !loginLimiter.allow(ip) {
		c.JSON(http.StatusTooManyRequests, gin.H{
			"success": false,
			"msg":     "too many login attempts, please try again later",
		})
		return
	}

	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"success": false, "msg": "invalid request"})
		return
	}

	// Constant-time comparison to prevent timing attacks
	userOK := hmac.Equal([]byte(req.Username), []byte(authUsername))
	passOK := hmac.Equal([]byte(req.Password), []byte(authPassword))

	if !userOK || !passOK {
		// Delay to slow down brute force
		time.Sleep(500 * time.Millisecond)
		c.JSON(http.StatusUnauthorized, gin.H{"success": false, "msg": "invalid credentials"})
		return
	}

	token, exp := generateToken(req.Username, ip)

	// Set httpOnly, secure cookie
	secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
	c.SetSameSite(http.SameSiteStrictMode)
	c.SetCookie("rdm_token", token, int(sessionTTL.Seconds()), "/", "", secure, true)

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"data": gin.H{
			"expires": exp.Unix(),
		},
	})
}

func handleLogout(c *gin.Context) {
	c.SetSameSite(http.SameSiteStrictMode)
	c.SetCookie("rdm_token", "", -1, "/", "", false, true)
	c.JSON(http.StatusOK, gin.H{"success": true})
}

func handleAuthStatus(c *gin.Context) {
	if !authEnabled {
		c.JSON(http.StatusOK, gin.H{
			"success": true,
			"data":    gin.H{"enabled": false, "authenticated": true},
		})
		return
	}

	token, err := c.Cookie("rdm_token")
	authenticated := err == nil && validateToken(token, getClientIP(c))

	c.JSON(http.StatusOK, gin.H{
		"success": true,
		"data":    gin.H{"enabled": true, "authenticated": authenticated},
	})
}


================================================
FILE: backend/api/browser_api.go
================================================
//go:build web

package api

import (
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerBrowserRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/browser")

	g.POST("/open-connection", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().OpenConnection(req.Name))
	})

	g.POST("/close-connection", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().CloseConnection(req.Name))
	})

	g.POST("/open-database", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().OpenDatabase(req.Server, req.DB))
	})

	g.POST("/server-info", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().ServerInfo(req.Name))
	})

	g.POST("/load-next-keys", func(c *gin.Context) {
		var req struct {
			Server     string `json:"server"`
			DB         int    `json:"db"`
			Match      string `json:"match"`
			KeyType    string `json:"keyType"`
			ExactMatch bool   `json:"exactMatch"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().LoadNextKeys(req.Server, req.DB, req.Match, req.KeyType, req.ExactMatch))
	})

	g.POST("/load-next-all-keys", func(c *gin.Context) {
		var req struct {
			Server     string `json:"server"`
			DB         int    `json:"db"`
			Match      string `json:"match"`
			KeyType    string `json:"keyType"`
			ExactMatch bool   `json:"exactMatch"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().LoadNextAllKeys(req.Server, req.DB, req.Match, req.KeyType, req.ExactMatch))
	})

	g.POST("/load-all-keys", func(c *gin.Context) {
		var req struct {
			Server     string `json:"server"`
			DB         int    `json:"db"`
			Match      string `json:"match"`
			KeyType    string `json:"keyType"`
			ExactMatch bool   `json:"exactMatch"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().LoadAllKeys(req.Server, req.DB, req.Match, req.KeyType, req.ExactMatch))
	})

	g.POST("/get-key-type", func(c *gin.Context) {
		var param types.KeySummaryParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetKeyType(param))
	})

	g.POST("/get-key-summary", func(c *gin.Context) {
		var param types.KeySummaryParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetKeySummary(param))
	})

	g.POST("/get-key-detail", func(c *gin.Context) {
		var param types.KeyDetailParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetKeyDetail(param))
	})

	g.POST("/convert-value", func(c *gin.Context) {
		var req struct {
			Value  any    `json:"value"`
			Decode string `json:"decode"`
			Format string `json:"format"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().ConvertValue(req.Value, req.Decode, req.Format))
	})

	g.POST("/set-key-value", func(c *gin.Context) {
		var param types.SetKeyParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().SetKeyValue(param))
	})

	g.POST("/get-hash-value", func(c *gin.Context) {
		var param types.GetHashParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetHashValue(param))
	})

	g.POST("/set-hash-value", func(c *gin.Context) {
		var param types.SetHashParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().SetHashValue(param))
	})

	g.POST("/add-hash-field", func(c *gin.Context) {
		var req struct {
			Server     string `json:"server"`
			DB         int    `json:"db"`
			Key        any    `json:"key"`
			Action     int    `json:"action"`
			FieldItems []any  `json:"fieldItems"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().AddHashField(req.Server, req.DB, req.Key, req.Action, req.FieldItems))
	})

	g.POST("/add-list-item", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
			Key    any    `json:"key"`
			Action int    `json:"action"`
			Items  []any  `json:"items"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().AddListItem(req.Server, req.DB, req.Key, req.Action, req.Items))
	})

	g.POST("/set-list-item", func(c *gin.Context) {
		var param types.SetListParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().SetListItem(param))
	})

	g.POST("/set-set-item", func(c *gin.Context) {
		var req struct {
			Server  string `json:"server"`
			DB      int    `json:"db"`
			Key     any    `json:"key"`
			Remove  bool   `json:"remove"`
			Members []any  `json:"members"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().SetSetItem(req.Server, req.DB, req.Key, req.Remove, req.Members))
	})

	g.POST("/update-set-item", func(c *gin.Context) {
		var param types.SetSetParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().UpdateSetItem(param))
	})

	g.POST("/update-zset-value", func(c *gin.Context) {
		var param types.SetZSetParam
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().UpdateZSetValue(param))
	})

	g.POST("/add-zset-value", func(c *gin.Context) {
		var req struct {
			Server     string             `json:"server"`
			DB         int                `json:"db"`
			Key        any                `json:"key"`
			Action     int                `json:"action"`
			ValueScore map[string]float64 `json:"valueScore"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().AddZSetValue(req.Server, req.DB, req.Key, req.Action, req.ValueScore))
	})

	g.POST("/add-stream-value", func(c *gin.Context) {
		var req struct {
			Server     string `json:"server"`
			DB         int    `json:"db"`
			Key        any    `json:"key"`
			ID         string `json:"id"`
			FieldItems []any  `json:"fieldItems"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().AddStreamValue(req.Server, req.DB, req.Key, req.ID, req.FieldItems))
	})

	g.POST("/remove-stream-values", func(c *gin.Context) {
		var req struct {
			Server string   `json:"server"`
			DB     int      `json:"db"`
			Key    any      `json:"key"`
			IDs    []string `json:"ids"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().RemoveStreamValues(req.Server, req.DB, req.Key, req.IDs))
	})

	g.POST("/set-key-ttl", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
			Key    any    `json:"key"`
			TTL    int64  `json:"ttl"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().SetKeyTTL(req.Server, req.DB, req.Key, req.TTL))
	})

	g.POST("/batch-set-ttl", func(c *gin.Context) {
		var req struct {
			Server   string `json:"server"`
			DB       int    `json:"db"`
			Keys     []any  `json:"keys"`
			TTL      int64  `json:"ttl"`
			SerialNo string `json:"serialNo"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().BatchSetTTL(req.Server, req.DB, req.Keys, req.TTL, req.SerialNo))
	})

	g.POST("/delete-key", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
			Key    any    `json:"key"`
			Async  bool   `json:"async"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().DeleteKey(req.Server, req.DB, req.Key, req.Async))
	})

	g.POST("/delete-keys", func(c *gin.Context) {
		var req struct {
			Server   string `json:"server"`
			DB       int    `json:"db"`
			Keys     []any  `json:"keys"`
			SerialNo string `json:"serialNo"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().DeleteKeys(req.Server, req.DB, req.Keys, req.SerialNo))
	})

	g.POST("/delete-keys-by-pattern", func(c *gin.Context) {
		var req struct {
			Server  string `json:"server"`
			DB      int    `json:"db"`
			Pattern string `json:"pattern"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().DeleteKeysByPattern(req.Server, req.DB, req.Pattern))
	})

	g.POST("/rename-key", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
			Key    string `json:"key"`
			NewKey string `json:"newKey"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().RenameKey(req.Server, req.DB, req.Key, req.NewKey))
	})

	g.POST("/export-key", func(c *gin.Context) {
		var req struct {
			Server        string `json:"server"`
			DB            int    `json:"db"`
			Keys          []any  `json:"keys"`
			Path          string `json:"path"`
			IncludeExpire bool   `json:"includeExpire"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().ExportKey(req.Server, req.DB, req.Keys, req.Path, req.IncludeExpire))
	})

	g.POST("/import-csv", func(c *gin.Context) {
		var req struct {
			Server   string `json:"server"`
			DB       int    `json:"db"`
			Path     string `json:"path"`
			Conflict int    `json:"conflict"`
			TTL      int64  `json:"ttl"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().ImportCSV(req.Server, req.DB, req.Path, req.Conflict, req.TTL))
	})

	g.POST("/flush-db", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
			Async  bool   `json:"async"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().FlushDB(req.Server, req.DB, req.Async))
	})

	g.POST("/get-slow-logs", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			Num    int64  `json:"num"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetSlowLogs(req.Server, req.Num))
	})

	g.POST("/get-client-list", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Browser().GetClientList(req.Server))
	})

	g.POST("/get-cmd-history", func(c *gin.Context) {
		var req struct {
			PageNo   int `json:"pageNo"`
			PageSize int `json:"pageSize"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		if req.PageSize <= 0 {
			req.PageSize = 50
		}
		c.JSON(http.StatusOK, services.Browser().GetCmdHistory(req.PageNo, req.PageSize))
	})

	g.POST("/clean-cmd-history", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Browser().CleanCmdHistory())
	})
}


================================================
FILE: backend/api/cli_api.go
================================================
//go:build web

package api

import (
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerCLIRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/cli")

	g.POST("/start", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
			DB     int    `json:"db"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Cli().StartCli(req.Server, req.DB))
	})

	g.POST("/close", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Cli().CloseCli(req.Server))
	})

	// CLI input is handled via WebSocket - the frontend sends
	// {"event": "cmd:input:<server>", "data": "<command>"} over WS
}


================================================
FILE: backend/api/connection_api.go
================================================
//go:build web

package api

import (
	"fmt"
	"io"
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerConnectionRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/connection")

	g.GET("/list", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Connection().ListConnection())
	})

	g.GET("/get", func(c *gin.Context) {
		name := c.Query("name")
		c.JSON(http.StatusOK, services.Connection().GetConnection(name))
	})

	g.POST("/save", func(c *gin.Context) {
		var req struct {
			Name   string                 `json:"name"`
			Param  types.ConnectionConfig `json:"param"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().SaveConnection(req.Name, req.Param))
	})

	g.POST("/save-sorted", func(c *gin.Context) {
		var req struct {
			Conns []types.Connection `json:"conns"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().SaveSortedConnection(req.Conns))
	})

	g.POST("/test", func(c *gin.Context) {
		var param types.ConnectionConfig
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().TestConnection(param))
	})

	g.DELETE("/delete", func(c *gin.Context) {
		name := c.Query("name")
		c.JSON(http.StatusOK, services.Connection().DeleteConnection(name))
	})

	g.POST("/group/create", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().CreateGroup(req.Name))
	})

	g.POST("/group/rename", func(c *gin.Context) {
		var req struct {
			Name    string `json:"name"`
			NewName string `json:"newName"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().RenameGroup(req.Name, req.NewName))
	})

	g.DELETE("/group/delete", func(c *gin.Context) {
		name := c.Query("name")
		includeConn := c.Query("includeConn") == "true"
		c.JSON(http.StatusOK, services.Connection().DeleteGroup(name, includeConn))
	})

	g.POST("/save-last-db", func(c *gin.Context) {
		var req struct {
			Name string `json:"name"`
			DB   int    `json:"db"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().SaveLastDB(req.Name, req.DB))
	})

	g.POST("/save-refresh-interval", func(c *gin.Context) {
		var req struct {
			Name     string `json:"name"`
			Interval int    `json:"interval"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().SaveRefreshInterval(req.Name, req.Interval))
	})

	g.POST("/export", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Connection().ExportConnections())
	})

	g.POST("/import", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Connection().ImportConnections())
	})

	// Web-specific: download connections as zip file
	g.GET("/export-download", func(c *gin.Context) {
		data, filename, err := services.Connection().ExportConnectionsToBytes()
		if err != nil {
			c.JSON(http.StatusInternalServerError, types.JSResp{Msg: "export failed"})
			return
		}
		c.Header("Content-Disposition", "attachment; filename="+filename)
		c.Header("Content-Type", "application/zip")
		c.Header("Content-Length", fmt.Sprintf("%d", len(data)))
		c.Data(http.StatusOK, "application/zip", data)
	})

	// Web-specific: import connections from uploaded zip file
	g.POST("/import-upload", func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid file"})
			return
		}
		src, err := file.Open()
		if err != nil {
			c.JSON(http.StatusInternalServerError, types.JSResp{Msg: "failed to read file"})
			return
		}
		defer src.Close()

		data, err := io.ReadAll(src)
		if err != nil {
			c.JSON(http.StatusInternalServerError, types.JSResp{Msg: "failed to read file"})
			return
		}

		resp := services.Connection().ImportConnectionsFromBytes(data)
		c.JSON(http.StatusOK, resp)
	})

	g.POST("/list-sentinel-masters", func(c *gin.Context) {
		var param types.ConnectionConfig
		if err := c.ShouldBindJSON(&param); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().ListSentinelMasters(param))
	})

	g.POST("/parse-url", func(c *gin.Context) {
		var req struct {
			URL string `json:"url"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Connection().ParseConnectURL(req.URL))
	})
}


================================================
FILE: backend/api/monitor_api.go
================================================
//go:build web

package api

import (
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerMonitorRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/monitor")

	g.POST("/start", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Monitor().StartMonitor(req.Server))
	})

	g.POST("/stop", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Monitor().StopMonitor(req.Server))
	})

	g.POST("/export-log", func(c *gin.Context) {
		var req struct {
			Logs []string `json:"logs"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Monitor().ExportLog(req.Logs))
	})
}


================================================
FILE: backend/api/preferences_api.go
================================================
//go:build web

package api

import (
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerPreferencesRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/preferences")

	g.GET("/get", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().GetPreferences())
	})

	g.POST("/set", func(c *gin.Context) {
		var pf types.Preferences
		if err := c.ShouldBindJSON(&pf); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Preferences().SetPreferences(pf))
	})

	g.POST("/update", func(c *gin.Context) {
		var value map[string]any
		if err := c.ShouldBindJSON(&value); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Preferences().UpdatePreferences(value))
	})

	g.POST("/restore", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().RestorePreferences())
	})

	g.GET("/font-list", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().GetFontList())
	})

	g.GET("/buildin-decoder", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().GetBuildInDecoder())
	})

	g.GET("/check-update", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().CheckForUpdate())
	})
}


================================================
FILE: backend/api/pubsub_api.go
================================================
//go:build web

package api

import (
	"net/http"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

func registerPubsubRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/pubsub")

	g.POST("/publish", func(c *gin.Context) {
		var req struct {
			Server  string `json:"server"`
			Channel string `json:"channel"`
			Payload string `json:"payload"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Pubsub().Publish(req.Server, req.Channel, req.Payload))
	})

	g.POST("/subscribe", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Pubsub().StartSubscribe(req.Server))
	})

	g.POST("/unsubscribe", func(c *gin.Context) {
		var req struct {
			Server string `json:"server"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid request"})
			return
		}
		c.JSON(http.StatusOK, services.Pubsub().StopSubscribe(req.Server))
	})
}


================================================
FILE: backend/api/router.go
================================================
//go:build web

package api

import (
	"log"
	"net/http"
	"strings"
	"tinyrdm/backend/services"

	"github.com/gin-gonic/gin"
)

// maxRequestBodySize limits request body to 10MB to prevent memory exhaustion
const maxRequestBodySize = 10 << 20 // 10MB

// SetupRouter creates the Gin router with all API routes and static file serving
func SetupRouter() *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	r := gin.Default()

	// Request body size limit
	r.Use(func(c *gin.Context) {
		c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxRequestBodySize)
		c.Next()
	})

	// Security headers
	r.Use(SecurityHeaders())

	// CORS - validate origin for cross-origin requests
	r.Use(func(c *gin.Context) {
		origin := c.GetHeader("Origin")
		if origin != "" {
			if isSameOrigin(c, origin) {
				c.Header("Access-Control-Allow-Origin", origin)
				c.Header("Access-Control-Allow-Credentials", "true")
				c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
				c.Header("Access-Control-Allow-Headers", "Content-Type, X-Requested-With")
			} else {
				log.Printf("[cors] blocked origin=%s host=%s", origin, getRequestHost(c))
				c.AbortWithStatus(http.StatusForbidden)
				return
			}
		}
		if c.Request.Method == "OPTIONS" {
			c.AbortWithStatus(204)
			return
		}
		c.Next()
	})

	// CSRF protection for state-changing requests
	r.Use(csrfProtection())

	// Public routes (no auth required)
	registerAuthRoutes(r)
	r.GET("/api/preferences/version", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.Preferences().GetAppVersion())
	})

	// WebSocket endpoint (auth checked via cookie + origin)
	r.GET("/ws", wsAuthCheck(), Hub().HandleWebSocket)

	// Protected API routes
	api := r.Group("/api")
	api.Use(AuthMiddleware())
	registerConnectionRoutes(api)
	registerBrowserRoutes(api)
	registerCLIRoutes(api)
	registerMonitorRoutes(api)
	registerPubsubRoutes(api)
	registerPreferencesRoutes(api)
	registerSystemRoutes(api)

	return r
}

// getRequestHost returns the effective host, considering reverse proxy headers
func getRequestHost(c *gin.Context) string {
	if fwdHost := c.GetHeader("X-Forwarded-Host"); fwdHost != "" {
		return fwdHost
	}
	return c.Request.Host
}

// stripPort removes port from host string ("example.com:8088" -> "example.com")
func stripPort(host string) string {
	if idx := strings.LastIndex(host, ":"); idx >= 0 {
		// Make sure it's not part of IPv6 address
		if !strings.Contains(host, "]") || strings.LastIndex(host, "]") < idx {
			return host[:idx]
		}
	}
	return host
}

// extractOriginHost extracts hostname from Origin header value
func extractOriginHost(origin string) string {
	host := origin
	if idx := strings.Index(host, "://"); idx >= 0 {
		host = host[idx+3:]
	}
	host = strings.TrimRight(host, "/")
	return host
}

// isSameOrigin checks if the Origin header matches the request host.
// Compares hostnames only (ignoring port) to support reverse proxy scenarios
// where the external port differs from the internal port.
func isSameOrigin(c *gin.Context, origin string) bool {
	originHost := stripPort(extractOriginHost(origin))
	requestHost := stripPort(getRequestHost(c))
	return originHost == requestHost
}

// csrfProtection validates Origin/Referer for state-changing requests
func csrfProtection() gin.HandlerFunc {
	return func(c *gin.Context) {
		method := c.Request.Method
		if method == "GET" || method == "HEAD" || method == "OPTIONS" {
			c.Next()
			return
		}

		// Check Origin header first
		origin := c.GetHeader("Origin")
		if origin != "" {
			if !isSameOrigin(c, origin) {
				log.Printf("[csrf] blocked origin=%s host=%s", origin, getRequestHost(c))
				c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"success": false, "msg": "cross-origin request blocked"})
				return
			}
			c.Next()
			return
		}

		// Fallback: check Referer
		referer := c.GetHeader("Referer")
		if referer != "" {
			refererHost := extractOriginHost(referer)
			if slashIdx := strings.Index(refererHost, "/"); slashIdx >= 0 {
				refererHost = refererHost[:slashIdx]
			}
			requestHost := stripPort(getRequestHost(c))
			if stripPort(refererHost) != requestHost {
				log.Printf("[csrf] blocked referer=%s host=%s", referer, getRequestHost(c))
				c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"success": false, "msg": "cross-origin request blocked"})
				return
			}
		}

		c.Next()
	}
}

// wsAuthCheck validates auth and origin for WebSocket connections
func wsAuthCheck() gin.HandlerFunc {
	return func(c *gin.Context) {
		origin := c.GetHeader("Origin")
		if origin != "" {
			if !isSameOrigin(c, origin) {
				log.Printf("[ws] blocked origin=%s host=%s", origin, getRequestHost(c))
				c.AbortWithStatus(http.StatusForbidden)
				return
			}
		}

		if !IsAuthEnabled() {
			c.Next()
			return
		}
		token, err := c.Cookie("rdm_token")
		if err != nil || !validateToken(token, getClientIP(c)) {
			c.AbortWithStatus(http.StatusUnauthorized)
			return
		}
		c.Next()
	}
}


================================================
FILE: backend/api/system_api.go
================================================
//go:build web

package api

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"tinyrdm/backend/services"
	"tinyrdm/backend/types"

	"github.com/gin-gonic/gin"
)

// safeTempPath validates that a path is within the OS temp directory.
// Prevents directory traversal attacks.
func safeTempPath(reqPath string) (string, error) {
	tmpDir := os.TempDir()
	cleaned := filepath.Clean(reqPath)
	abs, err := filepath.Abs(cleaned)
	if err != nil {
		return "", fmt.Errorf("invalid path")
	}
	// Ensure the resolved path is within tmpDir
	if !strings.HasPrefix(abs, filepath.Clean(tmpDir)+string(os.PathSeparator)) && abs != filepath.Clean(tmpDir) {
		return "", fmt.Errorf("access denied")
	}
	return abs, nil
}

// sanitizeFilename removes path separators and dangerous characters from filename
func sanitizeFilename(name string) string {
	// Take only the base name to strip any directory components
	name = filepath.Base(name)
	// Remove any remaining path separators (extra safety)
	name = strings.ReplaceAll(name, "..", "")
	name = strings.ReplaceAll(name, "/", "")
	name = strings.ReplaceAll(name, "\\", "")
	if name == "" || name == "." {
		name = "upload"
	}
	return name
}

func registerSystemRoutes(rg *gin.RouterGroup) {
	g := rg.Group("/system")

	g.GET("/info", func(c *gin.Context) {
		c.JSON(http.StatusOK, services.System().Info())
	})

	// Web replacement for native file dialog - select file
	g.POST("/select-file", func(c *gin.Context) {
		file, err := c.FormFile("file")
		if err != nil {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "invalid file upload"})
			return
		}

		// Sanitize filename to prevent path traversal
		safeName := sanitizeFilename(file.Filename)
		tmpDir := os.TempDir()
		dst := filepath.Join(tmpDir, safeName)

		if err := c.SaveUploadedFile(file, dst); err != nil {
			c.JSON(http.StatusInternalServerError, types.JSResp{Msg: "failed to save file"})
			return
		}

		c.JSON(http.StatusOK, types.JSResp{
			Success: true,
			Data: map[string]any{
				"path": dst,
			},
		})
	})

	// Web replacement for native file dialog - download file
	g.GET("/download", func(c *gin.Context) {
		reqPath := c.Query("path")
		if reqPath == "" {
			c.JSON(http.StatusBadRequest, types.JSResp{Msg: "path is required"})
			return
		}

		// Validate path is within temp directory only
		safePath, err := safeTempPath(reqPath)
		if err != nil {
			c.JSON(http.StatusForbidden, types.JSResp{Msg: "access denied"})
			return
		}

		file, err := os.Open(safePath)
		if err != nil {
			c.JSON(http.StatusNotFound, types.JSResp{Msg: "file not found"})
			return
		}
		defer file.Close()

		stat, err := file.Stat()
		if err != nil {
			c.JSON(http.StatusInternalServerError, types.JSResp{Msg: "failed to read file"})
			return
		}

		c.Header("Content-Disposition", "attachment; filename="+filepath.Base(safePath))
		c.Header("Content-Type", "application/octet-stream")
		c.Header("Content-Length", fmt.Sprintf("%d", stat.Size()))
		io.Copy(c.Writer, file)
	})
}


================================================
FILE: backend/api/websocket_hub.go
================================================
//go:build web

package api

import (
	"encoding/json"
	"log"
	"net/http"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

const (
	// wsMaxMessageSize limits incoming WebSocket messages to 1MB
	wsMaxMessageSize = 1 << 20
	// wsWriteWait is the time allowed to write a message
	wsWriteWait = 10 * time.Second
	// wsMaxClients limits concurrent WebSocket connections
	wsMaxClients = 50
)

// WSMessage represents a WebSocket message
type WSMessage struct {
	Event string `json:"event"`
	Data  any    `json:"data"`
}

// WSHub manages all WebSocket connections
type WSHub struct {
	clients map[*websocket.Conn]bool
	mutex   sync.RWMutex
}

var hub *WSHub
var onceHub sync.Once

func Hub() *WSHub {
	if hub == nil {
		onceHub.Do(func() {
			hub = &WSHub{
				clients: make(map[*websocket.Conn]bool),
			}
		})
	}
	return hub
}

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		// Origin validation is handled by wsAuthCheck middleware
		// Allow all here to avoid double-checking
		return true
	},
}

// Emit sends an event to all connected WebSocket clients
func (h *WSHub) Emit(event string, data any) {
	msg := WSMessage{Event: event, Data: data}
	jsonData, err := json.Marshal(msg)
	if err != nil {
		return
	}

	h.mutex.RLock()
	defer h.mutex.RUnlock()
	for conn := range h.clients {
		conn.SetWriteDeadline(time.Now().Add(wsWriteWait))
		if err := conn.WriteMessage(websocket.TextMessage, jsonData); err != nil {
			log.Printf("ws write error: %v", err)
		}
	}
}

// HandleWebSocket handles WebSocket upgrade and connection lifecycle
func (h *WSHub) HandleWebSocket(c *gin.Context) {
	// Check max clients
	h.mutex.RLock()
	clientCount := len(h.clients)
	h.mutex.RUnlock()
	if clientCount >= wsMaxClients {
		c.JSON(http.StatusServiceUnavailable, gin.H{"msg": "too many connections"})
		return
	}

	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Printf("ws upgrade error: %v", err)
		return
	}

	// Set read limits to prevent oversized messages
	conn.SetReadLimit(wsMaxMessageSize)

	h.mutex.Lock()
	h.clients[conn] = true
	h.mutex.Unlock()

	defer func() {
		h.mutex.Lock()
		delete(h.clients, conn)
		h.mutex.Unlock()
		conn.Close()
	}()

	// read loop - handle incoming messages (e.g. CLI input)
	for {
		_, message, err := conn.ReadMessage()
		if err != nil {
			break
		}
		var msg WSMessage
		if err := json.Unmarshal(message, &msg); err != nil {
			continue
		}
		h.handleIncoming(msg)
	}
}

// handleIncoming processes messages from the client
func (h *WSHub) handleIncoming(msg WSMessage) {
	// dispatch CLI input events etc.
	if handler, ok := incomingHandlers[msg.Event]; ok {
		handler(msg.Data)
	}
}

var incomingHandlers = map[string]func(data any){}

// RegisterHandler registers a handler for incoming WebSocket events
func RegisterHandler(event string, handler func(data any)) {
	incomingHandlers[event] = handler
}


================================================
FILE: backend/consts/app_name_desktop.go
================================================
//go:build !web

package consts

const APP_DATA_FOLDER = "TinyRDM"


================================================
FILE: backend/consts/app_name_web.go
================================================
//go:build web

package consts

const APP_DATA_FOLDER = "tinyrdm"


================================================
FILE: backend/consts/default_config.go
================================================
package consts

const DEFAULT_FONT_SIZE = 14
const DEFAULT_ASIDE_WIDTH = 300
const DEFAULT_WINDOW_WIDTH = 1024
const DEFAULT_WINDOW_HEIGHT = 768
const MIN_WINDOW_WIDTH = 960
const MIN_WINDOW_HEIGHT = 640
const DEFAULT_LOAD_SIZE = 10000
const DEFAULT_SCAN_SIZE = 3000


================================================
FILE: backend/services/browser_service.go
================================================
package services

import (
	"context"
	"encoding/csv"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"net/url"
	"os"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
	"tinyrdm/backend/consts"
	"tinyrdm/backend/types"
	"tinyrdm/backend/utils/coll"
	convutil "tinyrdm/backend/utils/convert"
	maputil "tinyrdm/backend/utils/map"
	redis2 "tinyrdm/backend/utils/redis"
	sliceutil "tinyrdm/backend/utils/slice"
	strutil "tinyrdm/backend/utils/string"

	"github.com/redis/go-redis/v9"
)

type slowLogItem struct {
	Timestamp int64  `json:"timestamp"`
	Client    string `json:"client"`
	Addr      string `json:"addr"`
	Cmd       string `json:"cmd"`
	Cost      int64  `json:"cost"`
}

type entryCursor struct {
	DB      int
	Type    string
	Key     string
	Pattern string
	Cursor  uint64
	XLast   string // last stream pos
}

type connectionItem struct {
	client      redis.UniversalClient
	ctx         context.Context
	cancelFunc  context.CancelFunc
	cursor      map[int]uint64      // current cursor of databases
	entryCursor map[int]entryCursor // current entry cursor of databases
	stepSize    int64
	db          int // current database index
}

type browserService struct {
	ctx        context.Context
	connMap    map[string]*connectionItem
	cmdHistory []cmdHistoryItem
	mutex      sync.Mutex
}

var browser *browserService
var onceBrowser sync.Once

func Browser() *browserService {
	if browser == nil {
		onceBrowser.Do(func() {
			browser = &browserService{
				connMap: map[string]*connectionItem{},
			}
		})
	}
	return browser
}

func (b *browserService) Start(ctx context.Context) {
	b.ctx = ctx
}

func (b *browserService) Stop() {
	for _, item := range b.connMap {
		if item.client != nil {
			if item.cancelFunc != nil {
				item.cancelFunc()
			}
			item.client.Close()
		}
	}
	b.connMap = map[string]*connectionItem{}
}

// OpenConnection open redis server connection
func (b *browserService) OpenConnection(name string) (resp types.JSResp) {
	// get connection config
	selConn := Connection().getConnection(name)
	// correct last database index
	lastDB := selConn.LastDB
	if selConn.DBFilterType == "show" && !slices.Contains(selConn.DBFilterList, lastDB) {
		lastDB = selConn.DBFilterList[0]
	} else if selConn.DBFilterType == "hide" && slices.Contains(selConn.DBFilterList, lastDB) {
		lastDB = selConn.DBFilterList[0]
	}

	item, db, err := b.getRedisClient2(name, lastDB)
	if err != nil {
		resp.Msg = err.Error()
		return
	}
	if lastDB != db {
		lastDB = db
	}

	client, ctx := item.client, item.ctx
	var totaldb int
	if selConn.DBFilterType == "" || selConn.DBFilterType == "none" {
		// get total databases
		if config, err := client.ConfigGet(ctx, "databases").Result(); err == nil {
			if total, err := strconv.Atoi(config["databases"]); err == nil {
				totaldb = total
			}
		}
	}

	// parse all db, response content like below
	var dbs []types.ConnectionDB
	var clusterKeyCount int64
	cluster, isCluster := client.(*redis.ClusterClient)
	if isCluster {
		var keyCount atomic.Int64
		err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
			if size, serr := cli.DBSize(ctx).Result(); serr != nil {
				return serr
			} else {
				keyCount.Add(size)
			}
			return nil
		})
		if err != nil {
			resp.Msg = "get db size error:" + err.Error()
			return
		}
		clusterKeyCount = keyCount.Load()

		// only one database in cluster mode
		dbs = []types.ConnectionDB{
			{
				Name:    "db0",
				Index:   0,
				MaxKeys: int(clusterKeyCount),
			},
		}
	} else {
		// get database info
		var res string
		info := map[string]map[string]string{}
		if res, err = client.Info(ctx, "keyspace").Result(); err != nil {
			//resp.Msg = "get server info fail:" + err.Error()
			//return
		} else {
			info = b.parseInfo(res)
		}

		if totaldb <= 0 {
			// cannot retrieve the database count by "CONFIG GET databases", try to get max index from keyspace
			keyspace := info["Keyspace"]
			var db, maxDB int
			for dbName := range keyspace {
				if db, err = strconv.Atoi(strings.TrimLeft(dbName, "db")); err == nil {
					if maxDB < db {
						maxDB = db
					}
				}
			}
			totaldb = maxDB + 1
		}

		queryDB := func(idx int) types.ConnectionDB {
			dbName := "db" + strconv.Itoa(idx)
			dbInfoStr := info["Keyspace"][dbName]
			var alias string
			if selConn.Alias != nil {
				alias = selConn.Alias[idx]
			}
			if len(dbInfoStr) > 0 {
				dbInfo := b.parseDBItemInfo(dbInfoStr)
				return types.ConnectionDB{
					Name:    dbName,
					Alias:   alias,
					Index:   idx,
					MaxKeys: dbInfo["keys"],
					Expires: dbInfo["expires"],
					AvgTTL:  dbInfo["avg_ttl"],
				}
			} else {
				return types.ConnectionDB{
					Name:  dbName,
					Alias: alias,
					Index: idx,
				}
			}
		}

		switch selConn.DBFilterType {
		case "show":
			filterList := sliceutil.Unique(selConn.DBFilterList)
			for _, idx := range filterList {
				dbs = append(dbs, queryDB(idx))
			}
		case "hide":
			hiddenList := coll.NewSet(selConn.DBFilterList...)
			for idx := 0; idx < totaldb; idx++ {
				if !hiddenList.Contains(idx) {
					dbs = append(dbs, queryDB(idx))
				}
			}
		default:
			for idx := 0; idx < totaldb; idx++ {
				dbs = append(dbs, queryDB(idx))
			}
		}
	}

	// get redis server version
	var version string
	if res, err := client.Info(ctx, "server").Result(); err == nil || errors.Is(err, redis.Nil) {
		info := b.parseInfo(res)
		serverInfo := maputil.Get(info, "Server", map[string]string{})
		version = maputil.Get(serverInfo, "redis_version", "1.0.0")
	}

	resp.Success = true
	resp.Data = map[string]any{
		"db":      dbs,
		"view":    selConn.KeyView,
		"lastDB":  selConn.LastDB,
		"version": version,
	}
	return
}

// CloseConnection close redis server connection
func (b *browserService) CloseConnection(name string) (resp types.JSResp) {
	if item, ok := b.connMap[name]; ok {
		delete(b.connMap, name)
		if item.cancelFunc != nil {
			item.cancelFunc()
		}
		if item.client != nil {
			item.client.Close()
		}
	}
	resp.Success = true
	return
}

func (b *browserService) createRedisClient(ctx context.Context, selConn types.ConnectionConfig) (client redis.UniversalClient, err error) {
	hook := redis2.NewHook(selConn.Name, func(cmd string, cost int64) {
		now := time.Now()
		//last := strings.LastIndex(cmd, ":")
		//if last != -1 {
		//	cmd = cmd[:last]
		//}
		b.cmdHistory = append(b.cmdHistory, cmdHistoryItem{
			Timestamp: now.UnixMilli(),
			Server:    selConn.Name,
			Cmd:       cmd,
			Cost:      cost,
		})
	})

	client, err = Connection().createRedisClient(selConn)
	if err != nil {
		err = fmt.Errorf("create conenction error: %s", err.Error())
		return
	}

	_ = client.Do(ctx, "CLIENT", "SETNAME", url.QueryEscape(selConn.Name)).Err()
	// add hook to each node in cluster mode
	if cluster, ok := client.(*redis.ClusterClient); ok {
		err = cluster.ForEachShard(ctx, func(ctx context.Context, cli *redis.Client) error {
			cli.AddHook(hook)
			return nil
		})
		if err != nil {
			err = fmt.Errorf("get cluster nodes error: %s", err.Error())
			return
		}
	} else {
		client.AddHook(hook)
	}

	if _, err = client.Ping(ctx).Result(); err != nil && !errors.Is(err, redis.Nil) {
		err = errors.New("can not connect to redis server:" + err.Error())
		return
	}
	return
}

// get a redis client from local cache or create a new one
// if db >= 0, it will also switch to target database index
func (b *browserService) getRedisClient(server string, db int) (item *connectionItem, err error) {
	b.mutex.Lock()
	defer b.mutex.Unlock()

	var ok bool
	var client redis.UniversalClient
	if item, ok = b.connMap[server]; ok {
		if item.db == db || db < 0 {
			// return without switch database directly
			return
		}

		// close previous connection if database is not the same
		if item.cancelFunc != nil {
			item.cancelFunc()
		}
		item.client.Close()
		delete(b.connMap, server)
	}

	// recreate new connection after switch database
	selConn := Connection().getConnection(server)
	if selConn == nil {
		err = fmt.Errorf("no match connection \"%s\"", server)
		delete(b.connMap, server)
		return
	}

	ctx, cancelFunc := context.WithCancel(b.ctx)
	b.connMap[server] = &connectionItem{
		ctx:        ctx,
		cancelFunc: cancelFunc,
	}
	var connConfig = selConn.ConnectionConfig
	connConfig.LastDB = db
	client, err = b.createRedisClient(ctx, connConfig)
	if err != nil {
		delete(b.connMap, server)
		return
	}
	item = &connectionItem{
		client:      client,
		ctx:         ctx,
		cancelFunc:  cancelFunc,
		cursor:      map[int]uint64{},
		entryCursor: map[int]entryCursor{},
		stepSize:    int64(selConn.LoadSize),
		db:          db,
	}
	if item.stepSize <= 0 {
		item.stepSize = consts.DEFAULT_LOAD_SIZE
	}
	b.connMap[server] = item
	return
}

// get redis client and try to reset selected database when not exists
func (b *browserService) getRedisClient2(server string, db int) (item *connectionItem, selecetdDB int, err error) {
	selecetdDB = db
	item, err = b.getRedisClient(server, db)
	if err != nil {
		if strings.Contains(err.Error(), "DB index is out of range") && db != 0 {
			if item, err = b.getRedisClient(server, 0); err != nil {
				item = nil
			} else {
				selecetdDB = 0
			}
		}
	}
	return
}

// load current database size
func (b *browserService) loadDBSize(ctx context.Context, client redis.UniversalClient) int64 {
	keyCount, _ := client.DBSize(ctx).Result()
	return keyCount
}

// save current scan cursor
func (b *browserService) setClientCursor(server string, db int, cursor uint64) {
	if _, ok := b.connMap[server]; ok {
		if cursor == 0 {
			delete(b.connMap[server].cursor, db)
		} else {
			b.connMap[server].cursor[db] = cursor
		}
	}
}

// parse command response content which use "redis info"
// # Keyspace\r\ndb0:keys=2,expires=1,avg_ttl=1877111749\r\ndb1:keys=33,expires=0,avg_ttl=0\r\ndb3:keys=17,expires=0,avg_ttl=0\r\ndb5:keys=3,expires=0,avg_ttl=0\r\n
func (b *browserService) parseInfo(info string) map[string]map[string]string {
	parsedInfo := map[string]map[string]string{}
	lines := strings.Split(info, "\r\n")
	if len(lines) > 0 {
		var subInfo map[string]string
		for _, line := range lines {
			if strings.HasPrefix(line, "#") {
				subInfo = map[string]string{}
				parsedInfo[strings.TrimSpace(strings.TrimLeft(line, "#"))] = subInfo
			} else {
				items := strings.SplitN(line, ":", 2)
				if len(items) < 2 {
					continue
				}
				subInfo[items[0]] = items[1]
			}
		}
	}
	return parsedInfo
}

// parse db item value, content format like below
// keys=2,expires=1,avg_ttl=1877111749
func (b *browserService) parseDBItemInfo(info string) map[string]int {
	ret := map[string]int{}
	items := strings.Split(info, ",")
	for _, item := range items {
		kv := strings.SplitN(item, "=", 2)
		if len(kv) > 1 {
			ret[kv[0]], _ = strconv.Atoi(kv[1])
		}
	}
	return ret
}

// ServerInfo get server info
func (b *browserService) ServerInfo(name string) (resp types.JSResp) {
	item, err := b.getRedisClient(name, -1)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	// get database info
	res, err := client.Info(ctx).Result()
	if err != nil {
		resp.Msg = "get server info fail:" + err.Error()
		return
	}

	resp.Success = true
	resp.Data = b.parseInfo(res)
	return
}

// OpenDatabase open select database, and list all keys
// @param path contain connection name and db name
func (b *browserService) OpenDatabase(server string, db int) (resp types.JSResp) {
	b.setClientCursor(server, db, 0)

	item, err := b.getRedisClient(server, db)
	if err != nil {
		resp.Msg = err.Error()
		return
	}
	client, ctx := item.client, item.ctx
	maxKeys := b.loadDBSize(ctx, client)

	resp.Success = true
	resp.Data = map[string]any{
		"maxKeys": maxKeys,
	}
	return
}

// scan keys
// @return loaded keys
// @return next cursor
// @return scan error
func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalClient, match, keyType string, cursor uint64, count int64) ([]any, uint64, error) {
	var err error
	filterType := len(keyType) > 0
	scanSize := int64(Preferences().GetScanSize())
	// define sub scan function
	scan := func(ctx context.Context, cli redis.UniversalClient, count int64, appendFunc func(k []any)) error {
		var loadedKey []string
		var scanCount int64
		for {
			if filterType {
				loadedKey, cursor, err = cli.ScanType(ctx, cursor, match, scanSize, keyType).Result()
			} else {
				loadedKey, cursor, err = cli.Scan(ctx, cursor, match, scanSize).Result()
			}
			if err != nil {
				return err
			} else {
				ks := sliceutil.Map(loadedKey, func(i int) any {
					return strutil.EncodeRedisKey(loadedKey[i])
				})
				scanCount += int64(len(ks))
				appendFunc(ks)
			}

			if (count > 0 && scanCount > count) || cursor == 0 {
				break
			}
		}
		return nil
	}

	keys := make([]any, 0)
	if cluster, ok := client.(*redis.ClusterClient); ok {
		// cluster mode
		var mutex sync.Mutex
		var totalMaster int64
		cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
			totalMaster += 1
			return nil
		})
		partCount := count / max(totalMaster, 1)
		err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
			// FIXME: BUG? can not fully load in cluster mode? maybe remove the shared "cursor"
			return scan(ctx, cli, partCount, func(k []any) {
				mutex.Lock()
				keys = append(keys, k...)
				mutex.Unlock()
			})
		})
	} else {
		err = scan(ctx, client, count, func(k []any) {
			keys = append(keys, k...)
		})
	}
	if err != nil {
		return keys, cursor, err
	}
	return keys, cursor, nil
}

// check if key exists
func (b *browserService) existsKey(ctx context.Context, client redis.UniversalClient, key, keyType string) bool {
	var keyExists atomic.Bool
	if cluster, ok := client.(*redis.ClusterClient); ok {
		// cluster mode
		cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
			if n := cli.Exists(ctx, key).Val(); n > 0 {
				if len(keyType) <= 0 || strings.ToLower(keyType) == cli.Type(ctx, key).Val() {
					keyExists.Store(true)
				}
			}
			return nil
		})
	} else {
		if n := client.Exists(ctx, key).Val(); n > 0 {
			if len(keyType) <= 0 || strings.ToLower(keyType) == client.Type(ctx, key).Val() {
				keyExists.Store(true)
			}
		}
	}
	return keyExists.Load()
}

// LoadNextKeys load next key from saved cursor
func (b *browserService) LoadNextKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
	item, err := b.getRedisClient(server, db)
	if err != nil {
		resp.Msg = err.Error()
		return
	}
	if match == "*" {
		exactMatch = false
	}

	client, ctx, count := item.client, item.ctx, item.stepSize
	var matchKeys []any
	var maxKeys int64
	cursor := item.cursor[db]
	fullScan := match == "*" || match == ""
	if exactMatch && !fullScan {
		if b.existsKey(ctx, client, match, keyType) {
			matchKeys = []any{match}
			maxKeys = 1
		}
		b.setClientCursor(server, db, 0)
	} else {
		matchKeys, cursor, err = b.scanKeys(ctx, client, match, keyType, cursor, count)
		if err != nil {
			resp.Msg = err.Error()
			return
		}
		b.setClientCursor(server, db, cursor)
		if fullScan {
			maxKeys = b.loadDBSize(ctx, client)
		} else {
			maxKeys = int64(len(matchKeys))
		}
	}

	resp.Success = true
	resp.Data = map[string]any{
		"keys":    matchKeys,
		"end":     cursor == 0,
		"maxKeys": maxKeys,
	}
	return
}

// LoadNextAllKeys load next all keys
func (b *browserService) LoadNextAllKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
	item, err := b.getRedisClient(server, db)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	var matchKeys []any
	var maxKeys int64
	fullScan := match == "*" || match == ""
	if exactMatch && !fullScan {
		if b.existsKey(ctx, client, match, keyType) {
			matchKeys = []any{match}
			maxKeys = 1
		}
	} else {
		cursor := item.cursor[db]
		matchKeys, _, err = b.scanKeys(ctx, client, match, keyType, cursor, 0)
		if err != nil {
			resp.Msg = err.Error()
			return
		}
		b.setClientCursor(server, db, 0)
		if fullScan {
			maxKeys = b.loadDBSize(ctx, client)
		} else {
			maxKeys = int64(len(matchKeys))
		}
	}

	resp.Success = true
	resp.Data = map[string]any{
		"keys":    matchKeys,
		"maxKeys": maxKeys,
	}
	return
}

// LoadAllKeys load all keys
func (b *browserService) LoadAllKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
	item, err := b.getRedisClient(server, db)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	var matchKeys []any
	fullScan := match == "*" || match == ""
	if exactMatch && !fullScan {
		if b.existsKey(ctx, client, match, keyType) {
			matchKeys = []any{match}
		}
	} else {
		matchKeys, _, err = b.scanKeys(ctx, client, match, keyType, 0, 0)
		if err != nil {
			resp.Msg = err.Error()
			return
		}
	}

	resp.Success = true
	resp.Data = map[string]any{
		"keys": matchKeys,
	}
	return
}

func (b *browserService) GetKeyType(param types.KeySummaryParam) (resp types.JSResp) {
	item, err := b.getRedisClient(param.Server, param.DB)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	key := strutil.DecodeRedisKey(param.Key)
	var keyType string
	keyType, err = client.Type(ctx, key).Result()
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	if keyType == "none" {
		resp.Msg = "key not exists"
		return
	}

	var data types.KeySummary
	switch keyType {
	case "ReJSON-RL":
		data.Type = "JSON"
	default:
		data.Type = strings.ToLower(keyType)
	}

	resp.Success = true
	resp.Data = data
	return
}

// GetKeySummary get key summary info
func (b *browserService) GetKeySummary(param types.KeySummaryParam) (resp types.JSResp) {
	item, err := b.getRedisClient(param.Server, param.DB)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	key := strutil.DecodeRedisKey(param.Key)

	pipe := client.Pipeline()
	typeVal := pipe.Type(ctx, key)
	ttlVal := pipe.TTL(ctx, key)
	_, err = pipe.Exec(ctx)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	if typeVal.Err() != nil {
		resp.Msg = typeVal.Err().Error()
		return
	}
	size, _ := client.MemoryUsage(ctx, key, 0).Result()
	data := types.KeySummary{
		Type: typeVal.Val(),
		Size: size,
	}
	if data.Type == "none" {
		resp.Msg = "key not exists"
		return
	}

	if ttlVal.Err() != nil {
		data.TTL = -1
	} else {
		if ttlVal.Val() < 0 {
			data.TTL = -1
		} else {
			data.TTL = int64(ttlVal.Val().Seconds())
		}
	}

	switch data.Type {
	case "string":
		data.Length, err = client.StrLen(ctx, key).Result()
	case "list":
		data.Length, err = client.LLen(ctx, key).Result()
	case "hash":
		data.Length, err = client.HLen(ctx, key).Result()
	case "set":
		data.Length, err = client.SCard(ctx, key).Result()
	case "zset":
		data.Length, err = client.ZCard(ctx, key).Result()
	case "stream":
		data.Length, err = client.XLen(ctx, key).Result()
	case "ReJSON-RL":
		data.Type = "JSON"
		data.Length = 0
	default:
		err = errors.New("unknown key type")
	}

	if err != nil {
		resp.Msg = err.Error()
		return
	}

	resp.Success = true
	resp.Data = data
	return
}

// GetKeyDetail get key detail
func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (resp types.JSResp) {
	item, err := b.getRedisClient(param.Server, param.DB)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx, entryCors := item.client, item.ctx, item.entryCursor
	key := strutil.DecodeRedisKey(param.Key)
	var keyType string
	keyType, err = client.Type(ctx, key).Result()
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	if keyType == "none" {
		resp.Msg = "key not exists"
		return
	}
	var doConvert bool
	if (len(param.Decode) > 0 && param.Decode != types.DECODE_NONE) ||
		(len(param.Format) > 0 && param.Format != types.FORMAT_RAW) {
		doConvert = true
	}

	var data types.KeyDetail
	data.KeyType = strings.ToLower(keyType)
	//var cursor uint64
	matchPattern := param.MatchPattern
	if len(matchPattern) <= 0 {
		matchPattern = "*"
	}

	// define get entry cursor function
	getEntryCursor := func() (uint64, string, bool) {
		if entry, ok := entryCors[param.DB]; !ok || entry.Key != key || entry.Pattern != matchPattern {
			// not the same key or match pattern, reset cursor
			entry = entryCursor{
				DB:      param.DB,
				Key:     key,
				Pattern: matchPattern,
				Cursor:  0,
			}
			entryCors[param.DB] = entry
			return 0, "", true
		} else {
			return entry.Cursor, entry.XLast, false
		}
	}
	// define set entry cursor function
	setEntryCursor := func(cursor uint64) {
		entryCors[param.DB] = entryCursor{
			DB:      param.DB,
			Type:    "",
			Key:     key,
			Pattern: matchPattern,
			Cursor:  cursor,
		}
	}
	// define set last stream pos function
	setEntryXLast := func(last string) {
		entryCors[param.DB] = entryCursor{
			DB:      param.DB,
			Type:    "",
			Key:     key,
			Pattern: matchPattern,
			XLast:   last,
		}
	}

	decoder := Preferences().GetDecoder()

	switch data.KeyType {
	case "string":
		var str string
		str, err = client.Get(ctx, key).Result()
		data.Value = strutil.EncodeRedisKey(str)
		//data.Value, data.Decode, data.Format = convutil.ConvertTo(str, param.Decode, param.Format, decoder)

	case "list":
		loadListHandle := func() ([]types.ListEntryItem, bool, bool, error) {
			var loadVal []string
			var cursor uint64
			var reset bool
			var subErr error
			doFilter := matchPattern != "*"
			if param.Full || doFilter {
				// load all
				cursor, reset = 0, true
				loadVal, subErr = client.LRange(ctx, key, 0, -1).Result()
			} else {
				if param.Reset {
					cursor, reset = 0, true
				} else {
					cursor, _, reset = getEntryCursor()
				}
				scanSize := int64(Preferences().GetScanSize())
				loadVal, subErr = client.LRange(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result()
				cursor = cursor + uint64(scanSize)
				if len(loadVal) < int(scanSize) {
					cursor = 0
				}
			}
			setEntryCursor(cursor)

			items := make([]types.ListEntryItem, 0, len(loadVal))
			for _, val := range loadVal {
				if doFilter && !strings.Contains(val, param.MatchPattern) {
					continue
				}
				items = append(items, types.ListEntryItem{
					Index: len(items),
					Value: strutil.EncodeRedisKey(val),
				})
				if doConvert {
					if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
						items[len(items)-1].DisplayValue = dv
					}
				}
			}
			if subErr != nil {
				return items, reset, false, subErr
			}
			return items, reset, cursor == 0, nil
		}

		data.Value, data.Reset, data.End, err = loadListHandle()
		data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format
		if err != nil {
			resp.Msg = err.Error()
			return
		}

	case "hash":
		if !strings.HasPrefix(matchPattern, "*") {
			matchPattern = "*" + matchPattern
		}
		if !strings.HasSuffix(matchPattern, "*") {
			matchPattern = matchPattern + "*"
		}
		loadHashHandle := func() ([]types.HashEntryItem, bool, bool, error) {
			var items []types.HashEntryItem
			var loadedVal []string
			var cursor uint64
			var reset bool
			var subErr error
			scanSize := int64(Preferences().GetScanSize())
			if param.Full || matchPattern != "*" {
				// load all
				cursor, reset = 0, true
				items = []types.HashEntryItem{}
				for {
					loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result()
					if subErr != nil {
						return nil, reset, false, subErr
					}
					for i := 0; i < len(loadedVal); i += 2 {
						items = append(items, types.HashEntryItem{
							Key:   loadedVal[i],
							Value: strutil.EncodeRedisKey(loadedVal[i+1]),
						})
						if doConvert {
							if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format, decoder); dv != loadedVal[i+1] {
								items[len(items)-1].DisplayValue = dv
							}
						}
					}
					if cursor == 0 {
						break
					}
				}
			} else {
				if param.Reset {
					cursor, reset = 0, true
				} else {
					cursor, _, reset = getEntryCursor()
				}
				loadedVal, cursor, subErr = client.HScan(ctx, key, cursor, matchPattern, scanSize).Result()
				if subErr != nil {
					return nil, reset, false, subErr
				}
				loadedLen := len(loadedVal)
				items = make([]types.HashEntryItem, loadedLen/2)
				for i := 0; i < loadedLen; i += 2 {
					items[i/2].Key = loadedVal[i]
					items[i/2].Value = strutil.EncodeRedisKey(loadedVal[i+1])
					if doConvert {
						if dv, _, _ := convutil.ConvertTo(loadedVal[i+1], param.Decode, param.Format, decoder); dv != loadedVal[i+1] {
							items[i/2].DisplayValue = dv
						}
					}
				}
			}
			setEntryCursor(cursor)
			return items, reset, cursor == 0, nil
		}

		data.Value, data.Reset, data.End, err = loadHashHandle()
		data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format
		if err != nil {
			resp.Msg = err.Error()
			return
		}

	case "set":
		if !strings.HasPrefix(matchPattern, "*") {
			matchPattern = "*" + matchPattern
		}
		if !strings.HasSuffix(matchPattern, "*") {
			matchPattern = matchPattern + "*"
		}
		loadSetHandle := func() ([]types.SetEntryItem, bool, bool, error) {
			var items []types.SetEntryItem
			var cursor uint64
			var reset bool
			var subErr error
			var loadedKey []string
			scanSize := int64(Preferences().GetScanSize())
			if param.Full || matchPattern != "*" {
				// load all
				cursor, reset = 0, true
				items = []types.SetEntryItem{}
				for {
					loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
					if subErr != nil {
						return items, reset, false, subErr
					}
					for _, val := range loadedKey {
						items = append(items, types.SetEntryItem{
							Value: strutil.EncodeRedisKey(val),
						})
						if doConvert {
							if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
								items[len(items)-1].DisplayValue = dv
							}
						}
					}
					if cursor == 0 {
						break
					}
				}
			} else {
				if param.Reset {
					cursor, reset = 0, true
				} else {
					cursor, _, reset = getEntryCursor()
				}
				loadedKey, cursor, subErr = client.SScan(ctx, key, cursor, matchPattern, scanSize).Result()
				items = make([]types.SetEntryItem, len(loadedKey))
				for i, val := range loadedKey {
					items[i].Value = strutil.EncodeRedisKey(val)
					if doConvert {
						if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
							items[i].DisplayValue = dv
						}
					}
				}
			}
			setEntryCursor(cursor)
			return items, reset, cursor == 0, nil
		}

		data.Value, data.Reset, data.End, err = loadSetHandle()
		data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format
		if err != nil {
			resp.Msg = err.Error()
			return
		}

	case "zset":
		if !strings.HasPrefix(matchPattern, "*") {
			matchPattern = "*" + matchPattern
		}
		if !strings.HasSuffix(matchPattern, "*") {
			matchPattern = matchPattern + "*"
		}
		loadZSetHandle := func() ([]types.ZSetEntryItem, bool, bool, error) {
			var items []types.ZSetEntryItem
			var reset bool
			var cursor uint64
			scanSize := int64(Preferences().GetScanSize())
			doFilter := matchPattern != "*"
			if param.Full || doFilter {
				// load all
				var loadedVal []string
				cursor, reset = 0, true
				items = []types.ZSetEntryItem{}
				for {
					loadedVal, cursor, err = client.ZScan(ctx, key, cursor, matchPattern, scanSize).Result()
					if err != nil {
						return items, reset, false, err
					}
					var score float64
					for i := 0; i < len(loadedVal); i += 2 {
						if score, err = strconv.ParseFloat(loadedVal[i+1], 64); err == nil {
							items = append(items, types.ZSetEntryItem{
								Value: strutil.EncodeRedisKey(loadedVal[i]),
								Score: score,
							})
							if doConvert {
								if dv, _, _ := convutil.ConvertTo(loadedVal[i], param.Decode, param.Format, decoder); dv != loadedVal[i] {
									items[len(items)-1].DisplayValue = dv
								}
							}
						}
					}
					if cursor == 0 {
						break
					}
				}
			} else {
				if param.Reset {
					cursor, reset = 0, true
				} else {
					cursor, _, reset = getEntryCursor()
				}
				var loadedVal []redis.Z
				loadedVal, err = client.ZRangeWithScores(ctx, key, int64(cursor), int64(cursor)+scanSize-1).Result()
				cursor = cursor + uint64(scanSize)
				if len(loadedVal) < int(scanSize) {
					cursor = 0
				}

				items = make([]types.ZSetEntryItem, 0, len(loadedVal))
				for _, z := range loadedVal {
					val := strutil.AnyToString(z.Member, "", 0)
					if doFilter && !strings.Contains(val, param.MatchPattern) {
						continue
					}
					entry := types.ZSetEntryItem{
						Value: strutil.EncodeRedisKey(val),
					}
					if math.IsInf(z.Score, 1) {
						entry.ScoreStr = "+inf"
					} else if math.IsInf(z.Score, -1) {
						entry.ScoreStr = "-inf"
					} else {
						entry.Score = z.Score
					}
					items = append(items, entry)
					if doConvert {
						if dv, _, _ := convutil.ConvertTo(val, param.Decode, param.Format, decoder); dv != val {
							items[len(items)-1].DisplayValue = dv
						}
					}
				}
			}
			setEntryCursor(cursor)
			return items, reset, cursor == 0, nil
		}

		data.Value, data.Reset, data.End, err = loadZSetHandle()
		data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format
		if err != nil {
			resp.Msg = err.Error()
			return
		}

	case "stream":
		loadStreamHandle := func() ([]types.StreamEntryItem, bool, bool, error) {
			var msgs []redis.XMessage
			var last string
			var reset bool
			doFilter := matchPattern != "*"
			if param.Full || doFilter {
				// load all
				last, reset = "", true
				msgs, err = client.XRevRange(ctx, key, "+", "-").Result()
			} else {
				scanSize := int64(Preferences().GetScanSize())
				if param.Reset {
					last = ""
				} else {
					_, last, reset = getEntryCursor()
				}
				if len(last) <= 0 {
					last = "+"
				}
				if last != "+" {
					// add 1 more item when continue scan
					msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize+1).Result()
					msgs = msgs[1:]
				} else {
					msgs, err = client.XRevRangeN(ctx, key, last, "-", scanSize).Result()
				}
				scanCount := len(msgs)
				if scanCount <= 0 || scanCount < int(scanSize) {
					last = ""
				} else if scanCount > 0 {
					last = msgs[scanCount-1].ID
				}
			}
			setEntryXLast(last)
			items := make([]types.StreamEntryItem, 0, len(msgs))
			for _, msg := range msgs {
				it := types.StreamEntryItem{
					ID:    msg.ID,
					Value: msg.Values,
				}
				var displayValue strings.Builder
				for k, v := range msg.Values {
					if displayValue.Len() > 0 {
						displayValue.WriteString(", ")
					}
					if str, ok := v.(string); ok {
						displayValue.WriteByte('"')
						displayValue.WriteString(k)
						displayValue.WriteByte('"')
						displayValue.WriteByte(':')
						displayValue.WriteString(str)
					}
				}
				it.DisplayValue = displayValue.String()
				if doFilter && !strings.Contains(it.DisplayValue, param.MatchPattern) {
					continue
				}
				items = append(items, it)
			}
			if err != nil {
				return items, reset, false, err
			}
			return items, reset, last == "", nil
		}

		data.Value, data.Reset, data.End, err = loadStreamHandle()
		data.Match, data.Decode, data.Format = param.MatchPattern, param.Decode, param.Format
		if err != nil {
			resp.Msg = err.Error()
			return
		}

	case "rejson-rl":
		var jsonStr string
		data.KeyType = "JSON"
		jsonStr, err = client.JSONGet(ctx, key).Result()
		data.Value, data.Decode, data.Format = convutil.ConvertTo(jsonStr, types.DECODE_NONE, types.FORMAT_JSON, nil)
	}
	if err != nil {
		resp.Msg = err.Error()
		return
	}
	resp.Success = true
	resp.Data = data
	return
}

// ConvertValue convert value with decode method and format
// blank decode indicate auto decode
// blank format indicate auto format
func (b *browserService) ConvertValue(value any, decode, format string) (resp types.JSResp) {
	str := strutil.DecodeRedisKey(value)
	value, decode, format = convutil.ConvertTo(str, decode, format, Preferences().GetDecoder())
	resp.Success = true
	resp.Data = map[string]any{
		"value":  value,
		"decode": decode,
		"format": format,
	}
	return
}

// SetKeyValue set value by key
// @param ttl <= 0 means keep current ttl
func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp types.JSResp) {
	item, err := b.getRedisClient(param.Server, param.DB)
	if err != nil {
		resp.Msg = err.Error()
		return
	}

	client, ctx := item.client, item.ctx
	key := strutil.DecodeRedisKey(param.Key)
	var expiration time.Duration
	if param.TTL < 0 {
		if expiration, err = client.PTTL(ctx, key).Result(); err != nil {
			expiration = redis.KeepTTL
		}
	} else {
		expiration = time.Duration(param.TTL) * time.Second
	}
	// use default decode type and format
	if len(param.Decode) <= 0 {
		param.Decode = types.DECODE_NONE
	}
	if len(param.Format) <= 0 {
		param.Format = types.FORMAT_RAW
	}
	var savedValue any
	switch strings.ToLower(param.KeyType) {
	case "string":
		if str, ok := param.Value.(string); !ok {
			resp.Msg = "invalid string value"
			return
		} else {
			if savedValue, err = convutil.SaveAs(str, param.Format, param.Decode, Preferences().GetDecoder()); err != nil {
				resp.Msg = fmt.Sprintf(`save to type "%s" fail: %s`, param.Format, err.Error())
				return
			}
			_, err = client.Set(ctx, key, savedValue, 0).Result()
			// set expiration lonely, not "keepttl"
			if err == nil && expiration > 0 {
				client.Expire(ctx, key, expiration)
			}
		}
	case "list":
		if strs, ok := param.Value.([]any); !ok {
			resp.Msg = "invalid list value"
			return
		} else {
			err = client.LPush(ctx, key, strs...).Err()
			if err == nil && 
Download .txt
gitextract_sgnqk2bp/

├── .github/
│   ├── CONTRIBUTING.md
│   ├── CONTRIBUTING_zh.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── docker-publish.yml
│       ├── release-linux-webkit2-41.yaml
│       ├── release-linux.yaml
│       ├── release-macos.yaml
│       └── release-windows.yaml
├── .gitignore
├── .prettierignore
├── Dockerfile
├── LICENSE
├── README.md
├── README_es.md
├── README_fr.md
├── README_ja.md
├── README_ko.md
├── README_pt.md
├── README_ru.md
├── README_tr.md
├── README_tw.md
├── README_zh.md
├── backend/
│   ├── api/
│   │   ├── auth.go
│   │   ├── browser_api.go
│   │   ├── cli_api.go
│   │   ├── connection_api.go
│   │   ├── monitor_api.go
│   │   ├── preferences_api.go
│   │   ├── pubsub_api.go
│   │   ├── router.go
│   │   ├── system_api.go
│   │   └── websocket_hub.go
│   ├── consts/
│   │   ├── app_name_desktop.go
│   │   ├── app_name_web.go
│   │   └── default_config.go
│   ├── services/
│   │   ├── browser_service.go
│   │   ├── cli_service.go
│   │   ├── connection_service.go
│   │   ├── connection_service_web.go
│   │   ├── ga_service.go
│   │   ├── monitor_service.go
│   │   ├── platform_desktop.go
│   │   ├── platform_web.go
│   │   ├── preferences_service.go
│   │   ├── pubsub_service.go
│   │   └── system_service.go
│   ├── storage/
│   │   ├── connections.go
│   │   ├── local_storage.go
│   │   └── preferences.go
│   ├── types/
│   │   ├── connection.go
│   │   ├── js_resp.go
│   │   ├── preferences.go
│   │   ├── redis_wrapper.go
│   │   └── view_type.go
│   └── utils/
│       ├── coll/
│       │   └── set.go
│       ├── constraints.go
│       ├── convert/
│       │   ├── base64_convert.go
│       │   ├── binary_convert.go
│       │   ├── bitset_convert.go
│       │   ├── brotli_convert.go
│       │   ├── cmd_convert.go
│       │   ├── common.go
│       │   ├── common_nonwindows.go
│       │   ├── common_windows.go
│       │   ├── convert.go
│       │   ├── deflate_convert.go
│       │   ├── gzip_convert.go
│       │   ├── hex_convert.go
│       │   ├── json_convert.go
│       │   ├── lz4_convert.go
│       │   ├── msgpack_convert.go
│       │   ├── php_convert.go
│       │   ├── pickle_convert.go
│       │   ├── unicode_json_convert.go
│       │   ├── xml_convert.go
│       │   ├── yaml_convert.go
│       │   └── zstd_convert.go
│       ├── map/
│       │   └── map_util.go
│       ├── math/
│       │   └── math_util.go
│       ├── proxy/
│       │   └── http.go
│       ├── redis/
│       │   └── log_hook.go
│       ├── slice/
│       │   └── slice_util.go
│       └── string/
│           ├── any_convert.go
│           ├── common.go
│           ├── json_formatter.go
│           └── key_convert.go
├── build/
│   ├── README.md
│   ├── darwin/
│   │   ├── Info.dev.plist
│   │   └── Info.plist
│   ├── dmg/
│   │   ├── background.tiff
│   │   ├── fix-app
│   │   └── fix-app_zh
│   ├── linux/
│   │   └── tiny-rdm_0.0.0_amd64/
│   │       ├── DEBIAN/
│   │       │   └── control
│   │       └── usr/
│   │           ├── local/
│   │           │   └── bin/
│   │           │       └── .gitkeep
│   │           └── share/
│   │               └── applications/
│   │                   └── tiny-rdm.desktop
│   └── windows/
│       ├── info.json
│       ├── installer/
│       │   ├── project.nsi
│       │   └── wails_tools.nsh
│       └── wails.exe.manifest
├── docker/
│   ├── entrypoint.sh
│   └── nginx.conf
├── docker-compose.yml
├── docs/
│   └── index.html
├── frontend/
│   ├── .prettierrc
│   ├── README.md
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── AppContent.vue
│   │   ├── assets/
│   │   │   └── fonts/
│   │   │       └── OFL.txt
│   │   ├── components/
│   │   │   ├── LoginPage.vue
│   │   │   ├── common/
│   │   │   │   ├── AutoRefreshForm.vue
│   │   │   │   ├── DropdownSelector.vue
│   │   │   │   ├── EditableTableColumn.vue
│   │   │   │   ├── EditableTableRow.vue
│   │   │   │   ├── FileOpenInput.vue
│   │   │   │   ├── FileSaveInput.vue
│   │   │   │   ├── IconButton.vue
│   │   │   │   ├── RedisTypeSelector.vue
│   │   │   │   ├── RedisTypeTag.vue
│   │   │   │   ├── ResizeableWrapper.vue
│   │   │   │   ├── SwitchButton.vue
│   │   │   │   ├── ToolbarControlWidget.vue
│   │   │   │   └── TtlInput.vue
│   │   │   ├── content/
│   │   │   │   ├── ContentLogPane.vue
│   │   │   │   ├── ContentPane.vue
│   │   │   │   ├── ContentServerPane.vue
│   │   │   │   └── ContentValueTab.vue
│   │   │   ├── content_value/
│   │   │   │   ├── ContentCli.vue
│   │   │   │   ├── ContentEditor.vue
│   │   │   │   ├── ContentEntryEditor.vue
│   │   │   │   ├── ContentMonitor.vue
│   │   │   │   ├── ContentPubsub.vue
│   │   │   │   ├── ContentSearchInput.vue
│   │   │   │   ├── ContentServerStatus.vue
│   │   │   │   ├── ContentSlog.vue
│   │   │   │   ├── ContentToolbar.vue
│   │   │   │   ├── ContentValueHash.vue
│   │   │   │   ├── ContentValueJson.vue
│   │   │   │   ├── ContentValueList.vue
│   │   │   │   ├── ContentValueSet.vue
│   │   │   │   ├── ContentValueStream.vue
│   │   │   │   ├── ContentValueString.vue
│   │   │   │   ├── ContentValueWrapper.vue
│   │   │   │   ├── ContentValueZSet.vue
│   │   │   │   └── FormatSelector.vue
│   │   │   ├── dialogs/
│   │   │   │   ├── AboutDialog.vue
│   │   │   │   ├── AddFieldsDialog.vue
│   │   │   │   ├── ConnectionDialog.vue
│   │   │   │   ├── DecoderDialog.vue
│   │   │   │   ├── DeleteKeyDialog.vue
│   │   │   │   ├── ExportKeyDialog.vue
│   │   │   │   ├── FlushDbDialog.vue
│   │   │   │   ├── GroupDialog.vue
│   │   │   │   ├── ImportKeyDialog.vue
│   │   │   │   ├── KeyFilterDialog.vue
│   │   │   │   ├── NewKeyDialog.vue
│   │   │   │   ├── PreferencesDialog.vue
│   │   │   │   ├── RenameKeyDialog.vue
│   │   │   │   └── SetTtlDialog.vue
│   │   │   ├── icons/
│   │   │   │   ├── Add.vue
│   │   │   │   ├── AddGroup.vue
│   │   │   │   ├── AddLink.vue
│   │   │   │   ├── AlignCenter.vue
│   │   │   │   ├── AlignLeft.vue
│   │   │   │   ├── Binary.vue
│   │   │   │   ├── Bottom.vue
│   │   │   │   ├── Checkbox.vue
│   │   │   │   ├── Checked.vue
│   │   │   │   ├── Clear.vue
│   │   │   │   ├── Close.vue
│   │   │   │   ├── Cluster.vue
│   │   │   │   ├── Code.vue
│   │   │   │   ├── Config.vue
│   │   │   │   ├── Connect.vue
│   │   │   │   ├── Conversion.vue
│   │   │   │   ├── Copy.vue
│   │   │   │   ├── CopyLink.vue
│   │   │   │   ├── Database.vue
│   │   │   │   ├── Delete.vue
│   │   │   │   ├── Detail.vue
│   │   │   │   ├── Down.vue
│   │   │   │   ├── Edit.vue
│   │   │   │   ├── EditFile.vue
│   │   │   │   ├── Export.vue
│   │   │   │   ├── Filter.vue
│   │   │   │   ├── Folder.vue
│   │   │   │   ├── FullScreen.vue
│   │   │   │   ├── Github.vue
│   │   │   │   ├── Help.vue
│   │   │   │   ├── Import.vue
│   │   │   │   ├── Key.vue
│   │   │   │   ├── Lang.vue
│   │   │   │   ├── Layer.vue
│   │   │   │   ├── ListView.vue
│   │   │   │   ├── LoadAll.vue
│   │   │   │   ├── LoadList.vue
│   │   │   │   ├── Loading.vue
│   │   │   │   ├── Log.vue
│   │   │   │   ├── Logout.vue
│   │   │   │   ├── Monitor.vue
│   │   │   │   ├── Moon.vue
│   │   │   │   ├── More.vue
│   │   │   │   ├── OffScreen.vue
│   │   │   │   ├── Pause.vue
│   │   │   │   ├── Pin.vue
│   │   │   │   ├── Play.vue
│   │   │   │   ├── Plus.vue
│   │   │   │   ├── Publish.vue
│   │   │   │   ├── QRCode.vue
│   │   │   │   ├── Record.vue
│   │   │   │   ├── Refresh.vue
│   │   │   │   ├── Save.vue
│   │   │   │   ├── Search.vue
│   │   │   │   ├── Server.vue
│   │   │   │   ├── Sort.vue
│   │   │   │   ├── SpellCheck.vue
│   │   │   │   ├── Status.vue
│   │   │   │   ├── Structure.vue
│   │   │   │   ├── Subscribe.vue
│   │   │   │   ├── Sun.vue
│   │   │   │   ├── Terminal.vue
│   │   │   │   ├── ThemeAuto.vue
│   │   │   │   ├── Timer.vue
│   │   │   │   ├── TreeView.vue
│   │   │   │   ├── Twitter.vue
│   │   │   │   ├── Unlink.vue
│   │   │   │   ├── Update.vue
│   │   │   │   ├── WindowClose.vue
│   │   │   │   ├── WindowMax.vue
│   │   │   │   ├── WindowMin.vue
│   │   │   │   └── WindowRestore.vue
│   │   │   ├── new_value/
│   │   │   │   ├── AddHashValue.vue
│   │   │   │   ├── AddListValue.vue
│   │   │   │   ├── AddZSetValue.vue
│   │   │   │   ├── NewHashValue.vue
│   │   │   │   ├── NewJsonValue.vue
│   │   │   │   ├── NewListValue.vue
│   │   │   │   ├── NewSetValue.vue
│   │   │   │   ├── NewStreamValue.vue
│   │   │   │   ├── NewStringValue.vue
│   │   │   │   └── NewZSetValue.vue
│   │   │   └── sidebar/
│   │   │       ├── BrowserPane.vue
│   │   │       ├── BrowserTree.vue
│   │   │       ├── ConnectionPane.vue
│   │   │       ├── ConnectionTree.vue
│   │   │       ├── ConnectionTreeItem.vue
│   │   │       └── Ribbon.vue
│   │   ├── consts/
│   │   │   ├── browser_tab_type.js
│   │   │   ├── connection_type.js
│   │   │   ├── key_view_type.js
│   │   │   ├── localstorage_key.js
│   │   │   ├── support_redis_type.js
│   │   │   ├── text_align_type.js
│   │   │   ├── tree_context_menu.js
│   │   │   └── value_view_type.js
│   │   ├── langs/
│   │   │   ├── en-us.json
│   │   │   ├── es-es.json
│   │   │   ├── fr-fr.json
│   │   │   ├── index.js
│   │   │   ├── ja-jp.json
│   │   │   ├── ko-kr.json
│   │   │   ├── pt-br.json
│   │   │   ├── ru-ru.json
│   │   │   ├── tr-tr.json
│   │   │   ├── zh-cn.json
│   │   │   └── zh-tw.json
│   │   ├── main.js
│   │   ├── objects/
│   │   │   ├── redisDatabaseItem.js
│   │   │   ├── redisNodeItem.js
│   │   │   ├── redisServerState.js
│   │   │   └── tabItem.js
│   │   ├── stores/
│   │   │   ├── browser.js
│   │   │   ├── connections.js
│   │   │   ├── dialog.js
│   │   │   ├── preferences.js
│   │   │   └── tab.js
│   │   ├── styles/
│   │   │   ├── content.scss
│   │   │   └── style.scss
│   │   └── utils/
│   │       ├── analytics.js
│   │       ├── api.js
│   │       ├── byte_convert.js
│   │       ├── chart.js
│   │       ├── date.js
│   │       ├── decoder_cmd.js
│   │       ├── discrete.js
│   │       ├── extra_theme.js
│   │       ├── glob_pattern.js
│   │       ├── i18n.js
│   │       ├── key_convert.js
│   │       ├── monaco.js
│   │       ├── platform.js
│   │       ├── promise.js
│   │       ├── render.js
│   │       ├── rgb.js
│   │       ├── theme.js
│   │       ├── version.js
│   │       ├── wails_runtime.js
│   │       └── websocket.js
│   └── vite.config.js
├── go.mod
├── go.sum
├── main.go
├── main_web.go
└── wails.json
Download .txt
SYMBOL INDEX (840 symbols across 87 files)

FILE: backend/api/auth.go
  type rateLimiter (line 31) | type rateLimiter struct
    method allow (line 48) | func (rl *rateLimiter) allow(ip string) bool {
  function InitAuth (line 100) | func InitAuth() {
  function IsAuthEnabled (line 124) | func IsAuthEnabled() bool {
  type tokenPayload (line 129) | type tokenPayload struct
  function generateToken (line 135) | func generateToken(username, ip string) (string, time.Time) {
  function validateToken (line 148) | func validateToken(token, ip string) bool {
  function getClientIP (line 185) | func getClientIP(c *gin.Context) string {
  function AuthMiddleware (line 200) | func AuthMiddleware() gin.HandlerFunc {
  function SecurityHeaders (line 222) | func SecurityHeaders() gin.HandlerFunc {
  function registerAuthRoutes (line 234) | func registerAuthRoutes(r *gin.Engine) {
  function handleLogin (line 240) | func handleLogin(c *gin.Context) {
  function handleLogout (line 287) | func handleLogout(c *gin.Context) {
  function handleAuthStatus (line 293) | func handleAuthStatus(c *gin.Context) {

FILE: backend/api/browser_api.go
  function registerBrowserRoutes (line 13) | func registerBrowserRoutes(rg *gin.RouterGroup) {

FILE: backend/api/cli_api.go
  function registerCLIRoutes (line 13) | func registerCLIRoutes(rg *gin.RouterGroup) {

FILE: backend/api/connection_api.go
  function registerConnectionRoutes (line 15) | func registerConnectionRoutes(rg *gin.RouterGroup) {

FILE: backend/api/monitor_api.go
  function registerMonitorRoutes (line 13) | func registerMonitorRoutes(rg *gin.RouterGroup) {

FILE: backend/api/preferences_api.go
  function registerPreferencesRoutes (line 13) | func registerPreferencesRoutes(rg *gin.RouterGroup) {

FILE: backend/api/pubsub_api.go
  function registerPubsubRoutes (line 13) | func registerPubsubRoutes(rg *gin.RouterGroup) {

FILE: backend/api/router.go
  constant maxRequestBodySize (line 15) | maxRequestBodySize = 10 << 20
  function SetupRouter (line 18) | func SetupRouter() *gin.Engine {
  function getRequestHost (line 80) | func getRequestHost(c *gin.Context) string {
  function stripPort (line 88) | func stripPort(host string) string {
  function extractOriginHost (line 99) | func extractOriginHost(origin string) string {
  function isSameOrigin (line 111) | func isSameOrigin(c *gin.Context, origin string) bool {
  function csrfProtection (line 118) | func csrfProtection() gin.HandlerFunc {
  function wsAuthCheck (line 158) | func wsAuthCheck() gin.HandlerFunc {

FILE: backend/api/system_api.go
  function safeTempPath (line 20) | func safeTempPath(reqPath string) (string, error) {
  function sanitizeFilename (line 35) | func sanitizeFilename(name string) string {
  function registerSystemRoutes (line 48) | func registerSystemRoutes(rg *gin.RouterGroup) {

FILE: backend/api/websocket_hub.go
  constant wsMaxMessageSize (line 18) | wsMaxMessageSize = 1 << 20
  constant wsWriteWait (line 20) | wsWriteWait = 10 * time.Second
  constant wsMaxClients (line 22) | wsMaxClients = 50
  type WSMessage (line 26) | type WSMessage struct
  type WSHub (line 32) | type WSHub struct
    method Emit (line 60) | func (h *WSHub) Emit(event string, data any) {
    method HandleWebSocket (line 78) | func (h *WSHub) HandleWebSocket(c *gin.Context) {
    method handleIncoming (line 123) | func (h *WSHub) handleIncoming(msg WSMessage) {
  function Hub (line 40) | func Hub() *WSHub {
  function RegisterHandler (line 133) | func RegisterHandler(event string, handler func(data any)) {

FILE: backend/consts/app_name_desktop.go
  constant APP_DATA_FOLDER (line 5) | APP_DATA_FOLDER = "TinyRDM"

FILE: backend/consts/app_name_web.go
  constant APP_DATA_FOLDER (line 5) | APP_DATA_FOLDER = "tinyrdm"

FILE: backend/consts/default_config.go
  constant DEFAULT_FONT_SIZE (line 3) | DEFAULT_FONT_SIZE = 14
  constant DEFAULT_ASIDE_WIDTH (line 4) | DEFAULT_ASIDE_WIDTH = 300
  constant DEFAULT_WINDOW_WIDTH (line 5) | DEFAULT_WINDOW_WIDTH = 1024
  constant DEFAULT_WINDOW_HEIGHT (line 6) | DEFAULT_WINDOW_HEIGHT = 768
  constant MIN_WINDOW_WIDTH (line 7) | MIN_WINDOW_WIDTH = 960
  constant MIN_WINDOW_HEIGHT (line 8) | MIN_WINDOW_HEIGHT = 640
  constant DEFAULT_LOAD_SIZE (line 9) | DEFAULT_LOAD_SIZE = 10000
  constant DEFAULT_SCAN_SIZE (line 10) | DEFAULT_SCAN_SIZE = 3000

FILE: backend/services/browser_service.go
  type slowLogItem (line 32) | type slowLogItem struct
  type entryCursor (line 40) | type entryCursor struct
  type connectionItem (line 49) | type connectionItem struct
  type browserService (line 59) | type browserService struct
    method Start (line 80) | func (b *browserService) Start(ctx context.Context) {
    method Stop (line 84) | func (b *browserService) Stop() {
    method OpenConnection (line 97) | func (b *browserService) OpenConnection(name string) (resp types.JSRes...
    method CloseConnection (line 246) | func (b *browserService) CloseConnection(name string) (resp types.JSRe...
    method createRedisClient (line 260) | func (b *browserService) createRedisClient(ctx context.Context, selCon...
    method getRedisClient (line 305) | func (b *browserService) getRedisClient(server string, db int) (item *...
    method getRedisClient2 (line 362) | func (b *browserService) getRedisClient2(server string, db int) (item ...
    method loadDBSize (line 378) | func (b *browserService) loadDBSize(ctx context.Context, client redis....
    method setClientCursor (line 384) | func (b *browserService) setClientCursor(server string, db int, cursor...
    method parseInfo (line 396) | func (b *browserService) parseInfo(info string) map[string]map[string]...
    method parseDBItemInfo (line 419) | func (b *browserService) parseDBItemInfo(info string) map[string]int {
    method ServerInfo (line 432) | func (b *browserService) ServerInfo(name string) (resp types.JSResp) {
    method OpenDatabase (line 454) | func (b *browserService) OpenDatabase(server string, db int) (resp typ...
    method scanKeys (line 476) | func (b *browserService) scanKeys(ctx context.Context, client redis.Un...
    method existsKey (line 537) | func (b *browserService) existsKey(ctx context.Context, client redis.U...
    method LoadNextKeys (line 560) | func (b *browserService) LoadNextKeys(server string, db int, match, ke...
    method LoadNextAllKeys (line 605) | func (b *browserService) LoadNextAllKeys(server string, db int, match,...
    method LoadAllKeys (line 645) | func (b *browserService) LoadAllKeys(server string, db int, match, key...
    method GetKeyType (line 674) | func (b *browserService) GetKeyType(param types.KeySummaryParam) (resp...
    method GetKeySummary (line 709) | func (b *browserService) GetKeySummary(param types.KeySummaryParam) (r...
    method GetKeyDetail (line 783) | func (b *browserService) GetKeyDetail(param types.KeyDetailParam) (res...
    method ConvertValue (line 1233) | func (b *browserService) ConvertValue(value any, decode, format string...
    method SetKeyValue (line 1247) | func (b *browserService) SetKeyValue(param types.SetKeyParam) (resp ty...
    method GetHashValue (line 1389) | func (b *browserService) GetHashValue(param types.GetHashParam) (resp ...
    method SetHashValue (line 1428) | func (b *browserService) SetHashValue(param types.SetHashParam) (resp ...
    method AddHashField (line 1522) | func (b *browserService) AddHashField(server string, db int, k any, ac...
    method AddListItem (line 1585) | func (b *browserService) AddListItem(server string, db int, k any, act...
    method SetListItem (line 1633) | func (b *browserService) SetListItem(param types.SetListParam) (resp t...
    method SetSetItem (line 1697) | func (b *browserService) SetSetItem(server string, db int, k any, remo...
    method UpdateSetItem (line 1745) | func (b *browserService) UpdateSetItem(param types.SetSetParam) (resp ...
    method UpdateZSetValue (line 1800) | func (b *browserService) UpdateZSetValue(param types.SetZSetParam) (re...
    method AddZSetValue (line 1905) | func (b *browserService) AddZSetValue(server string, db int, k any, ac...
    method AddStreamValue (line 1963) | func (b *browserService) AddStreamValue(server string, db int, k any, ...
    method RemoveStreamValues (line 2006) | func (b *browserService) RemoveStreamValues(server string, db int, k a...
    method SetKeyTTL (line 2033) | func (b *browserService) SetKeyTTL(server string, db int, k any, ttl i...
    method BatchSetTTL (line 2060) | func (b *browserService) BatchSetTTL(server string, db int, ks []any, ...
    method DeleteKey (line 2141) | func (b *browserService) DeleteKey(server string, db int, k any, async...
    method DeleteOneKey (line 2232) | func (b *browserService) DeleteOneKey(server string, db int, k any) (r...
    method DeleteKeys (line 2260) | func (b *browserService) DeleteKeys(server string, db int, ks []any, s...
    method DeleteKeysByPattern (line 2328) | func (b *browserService) DeleteKeysByPattern(server string, db int, pa...
    method ExportKey (line 2398) | func (b *browserService) ExportKey(server string, db int, ks []any, pa...
    method ImportCSV (line 2473) | func (b *browserService) ImportCSV(server string, db int, path string,...
    method FlushDB (line 2583) | func (b *browserService) FlushDB(server string, db int, async bool) (r...
    method RenameKey (line 2631) | func (b *browserService) RenameKey(server string, db int, key, newKey ...
    method GetCmdHistory (line 2654) | func (b *browserService) GetCmdHistory(pageNo, pageSize int) (resp typ...
    method CleanCmdHistory (line 2677) | func (b *browserService) CleanCmdHistory() (resp types.JSResp) {
    method GetSlowLogs (line 2684) | func (b *browserService) GetSlowLogs(server string, num int64) (resp t...
    method GetClientList (line 2743) | func (b *browserService) GetClientList(server string) (resp types.JSRe...
  function Browser (line 69) | func Browser() *browserService {

FILE: backend/services/cli_service.go
  type cliService (line 16) | type cliService struct
    method runCommand (line 45) | func (c *cliService) runCommand(server, data string) {
    method echo (line 70) | func (c *cliService) echo(server string, data any, newLineReady bool) {
    method echoReady (line 82) | func (c *cliService) echoReady(server string) {
    method echoError (line 86) | func (c *cliService) echoError(server, data string) {
    method getRedisClient (line 90) | func (c *cliService) getRedisClient(server string) (redis.UniversalCli...
    method Start (line 109) | func (c *cliService) Start(ctx context.Context) {
    method StartCli (line 114) | func (c *cliService) StartCli(server string, db int) (resp types.JSRes...
    method CloseCli (line 141) | func (c *cliService) CloseCli(server string) (resp types.JSResp) {
    method CloseAll (line 156) | func (c *cliService) CloseAll() {
  type cliOutput (line 24) | type cliOutput struct
  function Cli (line 33) | func Cli() *cliService {

FILE: backend/services/connection_service.go
  type cmdHistoryItem (line 30) | type cmdHistoryItem struct
  type connectionService (line 37) | type connectionService struct
    method Start (line 56) | func (c *connectionService) Start(ctx context.Context) {
    method buildOption (line 60) | func (c *connectionService) buildOption(config types.ConnectionConfig)...
    method createRedisClient (line 241) | func (c *connectionService) createRedisClient(config types.ConnectionC...
    method ListSentinelMasters (line 329) | func (c *connectionService) ListSentinelMasters(config types.Connectio...
    method TestConnection (line 362) | func (c *connectionService) TestConnection(config types.ConnectionConf...
    method ListConnection (line 379) | func (c *connectionService) ListConnection() (resp types.JSResp) {
    method getConnection (line 385) | func (c *connectionService) getConnection(name string) *types.Connecti...
    method GetConnection (line 390) | func (c *connectionService) GetConnection(name string) (resp types.JSR...
    method SaveConnection (line 398) | func (c *connectionService) SaveConnection(name string, param types.Co...
    method DeleteConnection (line 419) | func (c *connectionService) DeleteConnection(name string) (resp types....
    method SaveSortedConnection (line 430) | func (c *connectionService) SaveSortedConnection(sortedConns types.Con...
    method CreateGroup (line 441) | func (c *connectionService) CreateGroup(name string) (resp types.JSRes...
    method RenameGroup (line 452) | func (c *connectionService) RenameGroup(name, newName string) (resp ty...
    method DeleteGroup (line 463) | func (c *connectionService) DeleteGroup(name string, includeConn bool)...
    method SaveLastDB (line 474) | func (c *connectionService) SaveLastDB(name string, db int) (resp type...
    method SaveRefreshInterval (line 493) | func (c *connectionService) SaveRefreshInterval(name string, interval ...
    method ExportConnections (line 511) | func (c *connectionService) ExportConnections() (resp types.JSResp) {
    method ImportConnections (line 570) | func (c *connectionService) ImportConnections() (resp types.JSResp) {
    method ParseConnectURL (line 623) | func (c *connectionService) ParseConnectURL(url string) (resp types.JS...
  function Connection (line 45) | func Connection() *connectionService {

FILE: backend/services/connection_service_web.go
  method ExportConnectionsToBytes (line 19) | func (c *connectionService) ExportConnectionsToBytes() ([]byte, string, ...
  method ImportConnectionsFromBytes (line 52) | func (c *connectionService) ImportConnectionsFromBytes(data []byte) (res...

FILE: backend/services/ga_service.go
  type gaService (line 16) | type gaService struct
    method SetSecretKey (line 54) | func (a *gaService) SetSecretKey(measurementID, secretKey string) {
    method isValid (line 59) | func (a *gaService) isValid() bool {
    method sendEvent (line 63) | func (a *gaService) sendEvent(events ...GaEventItem) error {
    method Startup (line 98) | func (a *gaService) Startup(version string) {
  type GaDataItem (line 22) | type GaDataItem struct
  type GaEventItem (line 27) | type GaEventItem struct
  function GA (line 35) | func GA() *gaService {

FILE: backend/services/monitor_service.go
  type monitorItem (line 17) | type monitorItem struct
  type monitorService (line 26) | type monitorService struct
    method getItem (line 47) | func (c *monitorService) getItem(server string) (*monitorItem, error) {
    method Start (line 74) | func (c *monitorService) Start(ctx context.Context) {
    method StartMonitor (line 79) | func (c *monitorService) StartMonitor(server string) (resp types.JSRes...
    method processMonitor (line 102) | func (c *monitorService) processMonitor(mutex *sync.Mutex, ch <-chan s...
    method StopMonitor (line 141) | func (c *monitorService) StopMonitor(server string) (resp types.JSResp) {
    method StopAll (line 160) | func (c *monitorService) StopAll() {
    method ExportLog (line 170) | func (c *monitorService) ExportLog(logs []string) (resp types.JSResp) {
  function Monitor (line 36) | func Monitor() *monitorService {

FILE: backend/services/platform_desktop.go
  function EventsEmit (line 18) | func EventsEmit(ctx context.Context, event string, data ...any) {
  function EventsOnce (line 23) | func EventsOnce(ctx context.Context, event string, callback func(data .....
  function EventsOn (line 28) | func EventsOn(ctx context.Context, event string, callback func(data ...a...
  function EventsOff (line 33) | func EventsOff(ctx context.Context, event string) {
  function OpenFileDialog (line 38) | func OpenFileDialog(ctx context.Context, opts OpenDialogOptions) (string...
  function SaveFileDialog (line 43) | func SaveFileDialog(ctx context.Context, opts SaveDialogOptions) (string...
  function ScreenGetAll (line 48) | func ScreenGetAll(ctx context.Context) ([]Screen, error) {
  function WindowMaximise (line 53) | func WindowMaximise(ctx context.Context) {
  function WindowIsFullscreen (line 58) | func WindowIsFullscreen(ctx context.Context) bool {
  function WindowGetSize (line 63) | func WindowGetSize(ctx context.Context) (int, int) {
  function WindowIsMaximised (line 68) | func WindowIsMaximised(ctx context.Context) bool {
  function WindowIsMinimised (line 73) | func WindowIsMinimised(ctx context.Context) bool {
  function WindowIsNormal (line 78) | func WindowIsNormal(ctx context.Context) bool {
  function IsWeb (line 83) | func IsWeb() bool { return false }
  function IsDesktop (line 86) | func IsDesktop() bool { return true }

FILE: backend/services/platform_web.go
  type OpenDialogOptions (line 15) | type OpenDialogOptions struct
  type SaveDialogOptions (line 21) | type SaveDialogOptions struct
  type FileFilter (line 28) | type FileFilter struct
  type Screen (line 32) | type Screen struct
  type ScreenSize (line 37) | type ScreenSize struct
  function EventsEmit (line 43) | func EventsEmit(ctx context.Context, event string, data ...any) {
  function EventsOnce (line 55) | func EventsOnce(ctx context.Context, event string, callback func(data .....
  function EventsOn (line 69) | func EventsOn(ctx context.Context, event string, callback func(data ...a...
  function EventsOff (line 79) | func EventsOff(ctx context.Context, event string) {
  function OpenFileDialog (line 87) | func OpenFileDialog(ctx context.Context, opts OpenDialogOptions) (string...
  function SaveFileDialog (line 92) | func SaveFileDialog(ctx context.Context, opts SaveDialogOptions) (string...
  function ScreenGetAll (line 97) | func ScreenGetAll(ctx context.Context) ([]Screen, error) {
  function WindowMaximise (line 102) | func WindowMaximise(ctx context.Context) {}
  function WindowIsFullscreen (line 105) | func WindowIsFullscreen(ctx context.Context) bool { return false }
  function WindowGetSize (line 108) | func WindowGetSize(ctx context.Context) (int, int) { return 1024, 768 }
  function WindowIsMaximised (line 111) | func WindowIsMaximised(ctx context.Context) bool { return false }
  function WindowIsMinimised (line 114) | func WindowIsMinimised(ctx context.Context) bool { return false }
  function WindowIsNormal (line 117) | func WindowIsNormal(ctx context.Context) bool { return true }
  function IsWeb (line 120) | func IsWeb() bool { return true }
  function IsDesktop (line 123) | func IsDesktop() bool { return false }

FILE: backend/services/preferences_service.go
  type preferencesService (line 21) | type preferencesService struct
    method GetPreferences (line 41) | func (p *preferencesService) GetPreferences() (resp types.JSResp) {
    method SetPreferences (line 47) | func (p *preferencesService) SetPreferences(pf types.Preferences) (res...
    method UpdatePreferences (line 59) | func (p *preferencesService) UpdatePreferences(value map[string]any) (...
    method RestorePreferences (line 69) | func (p *preferencesService) RestorePreferences() (resp types.JSResp) {
    method GetFontList (line 83) | func (p *preferencesService) GetFontList() (resp types.JSResp) {
    method GetBuildInDecoder (line 105) | func (p *preferencesService) GetBuildInDecoder() (resp types.JSResp) {
    method GetLanguage (line 119) | func (p *preferencesService) GetLanguage() string {
    method SetAppVersion (line 124) | func (p *preferencesService) SetAppVersion(ver string) {
    method GetAppVersion (line 132) | func (p *preferencesService) GetAppVersion() (resp types.JSResp) {
    method SaveWindowSize (line 140) | func (p *preferencesService) SaveWindowSize(width, height int, maximis...
    method GetWindowSize (line 155) | func (p *preferencesService) GetWindowSize() (width, height int, maxim...
    method GetWindowPosition (line 167) | func (p *preferencesService) GetWindowPosition(ctx context.Context) (x...
    method SaveWindowPosition (line 190) | func (p *preferencesService) SaveWindowPosition(x, y int) {
    method GetScanSize (line 199) | func (p *preferencesService) GetScanSize() int {
    method GetDecoder (line 208) | func (p *preferencesService) GetDecoder() []convutil.CmdConvert {
    method CheckForUpdate (line 240) | func (p *preferencesService) CheckForUpdate() (resp types.JSResp) {
    method UpdateEnv (line 269) | func (p *preferencesService) UpdateEnv() {
  function Preferences (line 29) | func Preferences() *preferencesService {
  type FontItem (line 78) | type FontItem struct
  type sponsorItem (line 225) | type sponsorItem struct
  type upgradeInfo (line 231) | type upgradeInfo struct

FILE: backend/services/pubsub_service.go
  type pubsubItem (line 14) | type pubsubItem struct
  type subMessage (line 22) | type subMessage struct
  type pubsubService (line 28) | type pubsubService struct
    method getItem (line 49) | func (p *pubsubService) getItem(server string) (*pubsubItem, error) {
    method Start (line 72) | func (p *pubsubService) Start(ctx context.Context) {
    method Publish (line 77) | func (p *pubsubService) Publish(server, channel, payload string) (resp...
    method StartSubscribe (line 101) | func (p *pubsubService) StartSubscribe(server string) (resp types.JSRe...
    method processSubscribe (line 122) | func (p *pubsubService) processSubscribe(mutex *sync.Mutex, ch <-chan ...
    method StopSubscribe (line 163) | func (p *pubsubService) StopSubscribe(server string) (resp types.JSRes...
    method StopAll (line 182) | func (p *pubsubService) StopAll() {
  function Pubsub (line 38) | func Pubsub() *pubsubService {

FILE: backend/services/system_service.go
  type systemService (line 13) | type systemService struct
    method Start (line 35) | func (s *systemService) Start(ctx context.Context, version string) {
    method Info (line 56) | func (s *systemService) Info() (resp types.JSResp) {
    method SelectFile (line 71) | func (s *systemService) SelectFile(title string, extensions []string) ...
    method SaveFile (line 98) | func (s *systemService) SaveFile(title string, defaultName string, ext...
    method loopWindowEvent (line 125) | func (s *systemService) loopWindowEvent() {
  function System (line 21) | func System() *systemService {

FILE: backend/storage/connections.go
  type ConnectionsStorage (line 13) | type ConnectionsStorage struct
    method defaultConnections (line 24) | func (c *ConnectionsStorage) defaultConnections() types.Connections {
    method defaultConnectionItem (line 28) | func (c *ConnectionsStorage) defaultConnectionItem() types.ConnectionC...
    method getConnections (line 51) | func (c *ConnectionsStorage) getConnections() (ret types.Connections) {
    method GetConnections (line 74) | func (c *ConnectionsStorage) GetConnections() (ret types.Connections) {
    method GetConnectionsFlat (line 79) | func (c *ConnectionsStorage) GetConnectionsFlat() (ret types.Connectio...
    method GetConnection (line 92) | func (c *ConnectionsStorage) GetConnection(name string) *types.Connect...
    method GetGroup (line 116) | func (c *ConnectionsStorage) GetGroup(name string) *types.Connection {
    method saveConnections (line 127) | func (c *ConnectionsStorage) saveConnections(conns types.Connections) ...
    method CreateConnection (line 139) | func (c *ConnectionsStorage) CreateConnection(param types.ConnectionCo...
    method UpdateConnection (line 184) | func (c *ConnectionsStorage) UpdateConnection(name string, param types...
    method DeleteConnection (line 223) | func (c *ConnectionsStorage) DeleteConnection(name string) error {
    method SaveSortedConnection (line 254) | func (c *ConnectionsStorage) SaveSortedConnection(sortedConns types.Co...
    method CreateGroup (line 295) | func (c *ConnectionsStorage) CreateGroup(name string) error {
    method RenameGroup (line 316) | func (c *ConnectionsStorage) RenameGroup(name, newName string) error {
    method DeleteGroup (line 341) | func (c *ConnectionsStorage) DeleteGroup(group string, includeConnecti...
  function NewConnections (line 18) | func NewConnections() *ConnectionsStorage {

FILE: backend/storage/local_storage.go
  type localStorage (line 14) | type localStorage struct
    method Load (line 27) | func (l *localStorage) Load() ([]byte, error) {
    method Store (line 37) | func (l *localStorage) Store(data []byte) error {
  function NewLocalStore (line 19) | func NewLocalStore(filename string) *localStorage {
  function ensureDirExists (line 50) | func ensureDirExists(path string) error {

FILE: backend/storage/preferences.go
  type PreferencesStorage (line 15) | type PreferencesStorage struct
    method DefaultPreferences (line 28) | func (p *PreferencesStorage) DefaultPreferences() types.Preferences {
    method getPreferences (line 32) | func (p *PreferencesStorage) getPreferences() (ret types.Preferences) {
    method GetPreferences (line 47) | func (p *PreferencesStorage) GetPreferences() (ret types.Preferences) {
    method setPreferences (line 61) | func (p *PreferencesStorage) setPreferences(pf *types.Preferences, key...
    method savePreferences (line 87) | func (p *PreferencesStorage) savePreferences(pf *types.Preferences) er...
    method SetPreferences (line 100) | func (p *PreferencesStorage) SetPreferences(pf *types.Preferences) err...
    method UpdatePreferences (line 108) | func (p *PreferencesStorage) UpdatePreferences(values map[string]any) ...
    method RestoreDefault (line 123) | func (p *PreferencesStorage) RestoreDefault() types.Preferences {
  function NewPreferences (line 20) | func NewPreferences() *PreferencesStorage {

FILE: backend/types/connection.go
  type ConnectionCategory (line 3) | type ConnectionCategory
  type ConnectionConfig (line 5) | type ConnectionConfig struct
  type Connection (line 33) | type Connection struct
  type Connections (line 39) | type Connections
  type ConnectionDB (line 41) | type ConnectionDB struct
  type ConnectionSSL (line 50) | type ConnectionSSL struct
  type ConnectionSSH (line 59) | type ConnectionSSH struct
  type ConnectionSentinel (line 70) | type ConnectionSentinel struct
  type ConnectionCluster (line 77) | type ConnectionCluster struct
  type ConnectionProxy (line 81) | type ConnectionProxy struct

FILE: backend/types/js_resp.go
  type JSResp (line 3) | type JSResp struct
  type KeySummaryParam (line 9) | type KeySummaryParam struct
  type KeySummary (line 15) | type KeySummary struct
  type KeyDetailParam (line 22) | type KeyDetailParam struct
  type KeyDetail (line 33) | type KeyDetail struct
  type SetKeyParam (line 44) | type SetKeyParam struct
  type SetListParam (line 55) | type SetListParam struct
  type SetHashParam (line 67) | type SetHashParam struct
  type SetSetParam (line 80) | type SetSetParam struct
  type SetZSetParam (line 92) | type SetZSetParam struct
  type GetHashParam (line 105) | type GetHashParam struct

FILE: backend/types/preferences.go
  type Preferences (line 5) | type Preferences struct
  function NewPreferences (line 13) | func NewPreferences() Preferences {
  type PreferencesBehavior (line 45) | type PreferencesBehavior struct
  type PreferencesGeneral (line 55) | type PreferencesGeneral struct
  type PreferencesEditor (line 70) | type PreferencesEditor struct
  type PreferencesCli (line 81) | type PreferencesCli struct
  type PreferencesDecoder (line 87) | type PreferencesDecoder struct

FILE: backend/types/redis_wrapper.go
  type ListEntryItem (line 3) | type ListEntryItem struct
  type ListReplaceItem (line 9) | type ListReplaceItem struct
  type HashEntryItem (line 15) | type HashEntryItem struct
  type HashReplaceItem (line 21) | type HashReplaceItem struct
  type SetEntryItem (line 28) | type SetEntryItem struct
  type ZSetEntryItem (line 33) | type ZSetEntryItem struct
  type ZSetReplaceItem (line 40) | type ZSetReplaceItem struct
  type StreamEntryItem (line 47) | type StreamEntryItem struct

FILE: backend/types/view_type.go
  constant FORMAT_RAW (line 3) | FORMAT_RAW = "Raw"
  constant FORMAT_JSON (line 4) | FORMAT_JSON = "JSON"
  constant FORMAT_UNICODE_JSON (line 5) | FORMAT_UNICODE_JSON = "Unicode JSON"
  constant FORMAT_YAML (line 6) | FORMAT_YAML = "YAML"
  constant FORMAT_XML (line 7) | FORMAT_XML = "XML"
  constant FORMAT_HEX (line 8) | FORMAT_HEX = "Hex"
  constant FORMAT_BINARY (line 9) | FORMAT_BINARY = "Binary"
  constant FORMAT_BITSET (line 10) | FORMAT_BITSET = "BitSet"
  constant DECODE_NONE (line 12) | DECODE_NONE = "None"
  constant DECODE_BASE64 (line 13) | DECODE_BASE64 = "Base64"
  constant DECODE_GZIP (line 14) | DECODE_GZIP = "GZip"
  constant DECODE_DEFLATE (line 15) | DECODE_DEFLATE = "Deflate"
  constant DECODE_ZSTD (line 16) | DECODE_ZSTD = "ZStd"
  constant DECODE_LZ4 (line 17) | DECODE_LZ4 = "LZ4"
  constant DECODE_BROTLI (line 18) | DECODE_BROTLI = "Brotli"
  constant DECODE_MSGPACK (line 19) | DECODE_MSGPACK = "Msgpack"
  constant DECODE_PHP (line 20) | DECODE_PHP = "PHP"
  constant DECODE_PICKLE (line 21) | DECODE_PICKLE = "Pickle"

FILE: backend/utils/coll/set.go
  type Void (line 10) | type Void struct
  type Set (line 13) | type Set
  function NewSet (line 19) | func NewSet[T Hashable](elems ...T) Set[T] {
  method Add (line 32) | func (s Set[T]) Add(elem T) bool {
  method AddN (line 44) | func (s Set[T]) AddN(elems ...T) int {
  method Merge (line 60) | func (s Set[T]) Merge(other Set[T]) int {
  method Contains (line 65) | func (s Set[T]) Contains(elem T) bool {
  method ContainAny (line 74) | func (s Set[T]) ContainAny(elems ...T) bool {
  method Equals (line 88) | func (s Set[T]) Equals(other Set[T]) bool {
  method ContainAll (line 101) | func (s Set[T]) ContainAll(elems ...T) bool {
  method Remove (line 115) | func (s Set[T]) Remove(elem T) bool {
  method RemoveN (line 127) | func (s Set[T]) RemoveN(elems ...T) int {
  method RemoveSub (line 143) | func (s Set[T]) RemoveSub(subSet Set[T]) int {
  method Filter (line 159) | func (s Set[T]) Filter(filterFunc func(i T) bool) []T {
  method Size (line 170) | func (s Set[T]) Size() int {
  method IsEmpty (line 175) | func (s Set[T]) IsEmpty() bool {
  method Clear (line 180) | func (s Set[T]) Clear() {
  method ToSlice (line 187) | func (s Set[T]) ToSlice() []T {
  method ToSortedSlice (line 201) | func (s Set[T]) ToSortedSlice(sortFunc func(v1, v2 T) bool) []T {
  method Each (line 210) | func (s Set[T]) Each(eachFunc func(T)) {
  method Clone (line 220) | func (s Set[T]) Clone() Set[T] {
  method String (line 232) | func (s Set[T]) String() string {
  method MarshalJSON (line 238) | func (s Set[T]) MarshalJSON() ([]byte, error) {
  method UnmarshalJSON (line 247) | func (s *Set[T]) UnmarshalJSON(b []byte) error {
  method GormDataType (line 259) | func (s Set[T]) GormDataType() string {

FILE: backend/utils/constraints.go
  type Hashable (line 3) | type Hashable interface
  type SignedNumber (line 7) | type SignedNumber interface
  type UnsignedNumber (line 11) | type UnsignedNumber interface

FILE: backend/utils/convert/base64_convert.go
  type Base64Convert (line 8) | type Base64Convert struct
    method Enable (line 10) | func (Base64Convert) Enable() bool {
    method Encode (line 14) | func (Base64Convert) Encode(str string) (string, bool) {
    method Decode (line 18) | func (Base64Convert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/binary_convert.go
  type BinaryConvert (line 9) | type BinaryConvert struct
    method Enable (line 11) | func (BinaryConvert) Enable() bool {
    method Encode (line 15) | func (BinaryConvert) Encode(str string) (string, bool) {
    method Decode (line 31) | func (BinaryConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/bitset_convert.go
  type BitSetConvert (line 10) | type BitSetConvert struct
    method Enable (line 12) | func (BitSetConvert) Enable() bool {
    method Encode (line 16) | func (BitSetConvert) Encode(str string) (string, bool) {
    method Decode (line 29) | func (BitSetConvert) Decode(str string) (string, bool) {
  function encodeToRedisBitset (line 48) | func encodeToRedisBitset(numbers []string) []byte {
  function getBitSet (line 94) | func getBitSet(redisResponse []byte) []bool {

FILE: backend/utils/convert/brotli_convert.go
  type BrotliConvert (line 11) | type BrotliConvert struct
    method Enable (line 13) | func (BrotliConvert) Enable() bool {
    method Encode (line 17) | func (BrotliConvert) Encode(str string) (string, bool) {
    method Decode (line 34) | func (BrotliConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/cmd_convert.go
  type CmdConvert (line 9) | type CmdConvert struct
    method Enable (line 20) | func (c CmdConvert) Enable() bool {
    method Encode (line 24) | func (c CmdConvert) Encode(str string) (string, bool) {
    method Decode (line 51) | func (c CmdConvert) Decode(str string) (string, bool) {
  constant replaceholder (line 18) | replaceholder = "{VALUE}"

FILE: backend/utils/convert/common.go
  function writeExecuteFile (line 11) | func writeExecuteFile(content []byte, filename string) (string, error) {

FILE: backend/utils/convert/common_nonwindows.go
  function runCommand (line 9) | func runCommand(name string, arg ...string) ([]byte, error) {

FILE: backend/utils/convert/common_windows.go
  function runCommand (line 10) | func runCommand(name string, arg ...string) ([]byte, error) {

FILE: backend/utils/convert/convert.go
  type DataConvert (line 10) | type DataConvert interface
  function ConvertTo (line 61) | func ConvertTo(str, decodeType, formatType string, customDecoder []CmdCo...
  function decodeWith (line 88) | func decodeWith(str, decodeType string, customDecoder []CmdConvert) (val...
  function autoDecode (line 117) | func autoDecode(str string, customDecoder []CmdConvert) (value, resultDe...
  function viewAs (line 188) | func viewAs(str, formatType string) (value, resultFormat string) {
  function autoViewAs (line 204) | func autoViewAs(str string) (value, resultFormat string) {
  function SaveAs (line 235) | func SaveAs(str, format, decode string, customDecoder []CmdConvert) (val...

FILE: backend/utils/convert/deflate_convert.go
  type DeflateConvert (line 11) | type DeflateConvert struct
    method Enable (line 13) | func (d DeflateConvert) Enable() bool {
    method Encode (line 17) | func (d DeflateConvert) Encode(str string) (string, bool) {
    method Decode (line 37) | func (d DeflateConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/gzip_convert.go
  type GZipConvert (line 11) | type GZipConvert struct
    method Enable (line 13) | func (GZipConvert) Enable() bool {
    method Encode (line 17) | func (GZipConvert) Encode(str string) (string, bool) {
    method Decode (line 35) | func (GZipConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/hex_convert.go
  type HexConvert (line 8) | type HexConvert struct
    method Enable (line 10) | func (HexConvert) Enable() bool {
    method Encode (line 14) | func (HexConvert) Encode(str string) (string, bool) {
    method Decode (line 24) | func (HexConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/json_convert.go
  type JsonConvert (line 8) | type JsonConvert struct
    method Enable (line 10) | func (JsonConvert) Enable() bool {
    method Decode (line 14) | func (JsonConvert) Decode(str string) (string, bool) {
    method Encode (line 23) | func (JsonConvert) Encode(str string) (string, bool) {

FILE: backend/utils/convert/lz4_convert.go
  type LZ4Convert (line 10) | type LZ4Convert struct
    method Enable (line 12) | func (LZ4Convert) Enable() bool {
    method Encode (line 16) | func (LZ4Convert) Encode(str string) (string, bool) {
    method Decode (line 34) | func (LZ4Convert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/msgpack_convert.go
  type MsgpackConvert (line 9) | type MsgpackConvert struct
    method Enable (line 11) | func (MsgpackConvert) Enable() bool {
    method Encode (line 15) | func (c MsgpackConvert) Encode(str string) (string, bool) {
    method Decode (line 33) | func (MsgpackConvert) Decode(str string) (string, bool) {
    method TryFloatToInt (line 60) | func (c MsgpackConvert) TryFloatToInt(input any) any {

FILE: backend/utils/convert/php_convert.go
  type PhpConvert (line 7) | type PhpConvert struct
    method Enable (line 70) | func (p *PhpConvert) Enable() bool {
    method Encode (line 77) | func (p *PhpConvert) Encode(str string) (string, bool) {
    method Decode (line 84) | func (p *PhpConvert) Decode(str string) (string, bool) {
  constant phpDecodeCode (line 11) | phpDecodeCode = `
  function NewPhpConvert (line 45) | func NewPhpConvert() *PhpConvert {

FILE: backend/utils/convert/pickle_convert.go
  type PickleConvert (line 8) | type PickleConvert struct
    method Enable (line 93) | func (p *PickleConvert) Enable() bool {
    method Encode (line 100) | func (p *PickleConvert) Encode(str string) (string, bool) {
    method Decode (line 107) | func (p *PickleConvert) Decode(str string) (string, bool) {
  constant pickleDecodeCode (line 12) | pickleDecodeCode = `
  function NewPickleConvert (line 56) | func NewPickleConvert() *PickleConvert {

FILE: backend/utils/convert/unicode_json_convert.go
  type UnicodeJsonConvert (line 13) | type UnicodeJsonConvert struct
    method Enable (line 15) | func (UnicodeJsonConvert) Enable() bool {
    method Decode (line 19) | func (UnicodeJsonConvert) Decode(str string) (string, bool) {
    method Encode (line 31) | func (UnicodeJsonConvert) Encode(str string) (string, bool) {
  function UnquoteUnicodeJson (line 35) | func UnquoteUnicodeJson(s []byte) ([]byte, bool) {
  function getu4 (line 67) | func getu4(s []byte) rune {
  function unquoteBytes (line 88) | func unquoteBytes(s []byte) (t []byte, ok bool) {

FILE: backend/utils/convert/xml_convert.go
  type XmlConvert (line 8) | type XmlConvert struct
    method Enable (line 10) | func (XmlConvert) Enable() bool {
    method Encode (line 14) | func (XmlConvert) Encode(str string) (string, bool) {
    method Decode (line 18) | func (XmlConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/yaml_convert.go
  type YamlConvert (line 7) | type YamlConvert struct
    method Enable (line 9) | func (YamlConvert) Enable() bool {
    method Encode (line 13) | func (YamlConvert) Encode(str string) (string, bool) {
    method Decode (line 17) | func (YamlConvert) Decode(str string) (string, bool) {

FILE: backend/utils/convert/zstd_convert.go
  type ZStdConvert (line 11) | type ZStdConvert struct
    method Enable (line 13) | func (ZStdConvert) Enable() bool {
    method Encode (line 17) | func (ZStdConvert) Encode(str string) (string, bool) {
    method Decode (line 37) | func (ZStdConvert) Decode(str string) (string, bool) {

FILE: backend/utils/map/map_util.go
  function Get (line 9) | func Get[M ~map[K]V, K Hashable, V any](m M, key K, defaultVal V) V {
  function ContainsKey (line 19) | func ContainsKey[M ~map[K]V, K Hashable, V any](m M, key K) bool {
  function MustGet (line 28) | func MustGet[M ~map[K]V, K Hashable, V any](m M, key K, getFunc func(K) ...
  function Keys (line 40) | func Keys[M ~map[K]V, K Hashable, V any](m M) []K {
  function KeySet (line 54) | func KeySet[M ~map[K]V, K Hashable, V any](m M) coll.Set[K] {
  function Values (line 66) | func Values[M ~map[K]V, K Hashable, V any](m M) []V {
  function ValueSet (line 80) | func ValueSet[M ~map[K]V, K Hashable, V Hashable](m M) coll.Set[V] {
  function Fill (line 92) | func Fill[M ~map[K]V, K Hashable, V any](dest M, src M) M {
  function Merge (line 100) | func Merge[M ~map[K]V, K Hashable, V any](mapArr ...M) M {
  function Omit (line 111) | func Omit[M ~map[K]V, K Hashable, V any](m M, omitFunc func(k K, v V) bo...
  function OmitKeys (line 125) | func OmitKeys[M ~map[K]V, K Hashable, V any](m M, keys ...K) M {
  function ContainsAnyKey (line 142) | func ContainsAnyKey[M ~map[K]V, K Hashable, V any](m M, keys ...K) bool {
  function ContainsAllKey (line 153) | func ContainsAllKey[M ~map[K]V, K Hashable, V any](m M, keys ...K) bool {
  function AnyMatch (line 164) | func AnyMatch[M ~map[K]V, K Hashable, V any](m M, matchFunc func(k K, v ...
  function AllMatch (line 174) | func AllMatch[M ~map[K]V, K Hashable, V any](m M, matchFunc func(k K, v ...
  function Reduce (line 184) | func Reduce[M ~map[K]V, K Hashable, V any, R any](m M, init R, reduceFun...
  function ToSlice (line 193) | func ToSlice[M ~map[K]V, K Hashable, V any, R any](m M, mapFunc func(k K...
  function Filter (line 202) | func Filter[M ~map[K]V, K Hashable, V any](m M, filterFunc func(k K) boo...
  function FilterToSlice (line 213) | func FilterToSlice[M ~map[K]V, K Hashable, V any, R any](m M, mapFunc fu...
  function FilterKey (line 224) | func FilterKey[M ~map[K]V, K Hashable, V any](m M, filterFunc func(k K) ...
  function Clone (line 235) | func Clone[M ~map[K]V, K Hashable, V any](src M) M {
  function Reverse (line 244) | func Reverse[M ~map[K]V, K Hashable, V Hashable](src M) map[V]K {
  function ReverseAll (line 253) | func ReverseAll[M ~map[K]V, K Hashable, V Hashable](src M) map[V][]K {
  function RemoveIf (line 262) | func RemoveIf[M ~map[K]V, K Hashable, V any](src M, cond func(key K) boo...

FILE: backend/utils/math/math_util.go
  function MaxWithIndex (line 9) | func MaxWithIndex[T Hashable](items ...T) (T, int) {
  function MinWithIndex (line 24) | func MinWithIndex[T Hashable](items ...T) (T, int) {
  function Clamp (line 39) | func Clamp[T Hashable](value T, minVal T, maxVal T) T {
  function Abs (line 52) | func Abs[T SignedNumber](val T) T {
  function Floor (line 57) | func Floor[T SignedNumber | UnsignedNumber](val T) T {
  function Ceil (line 62) | func Ceil[T SignedNumber | UnsignedNumber](val T) T {
  function Round (line 67) | func Round[T SignedNumber | UnsignedNumber](val T) T {
  function Sum (line 72) | func Sum[T SignedNumber | UnsignedNumber](items ...T) T {
  function Average (line 81) | func Average[T SignedNumber | UnsignedNumber](items ...T) T {

FILE: backend/utils/proxy/http.go
  type HttpProxy (line 14) | type HttpProxy struct
    method Dial (line 21) | func (p *HttpProxy) Dial(network, addr string) (net.Conn, error) {
  function NewHttpProxyDialer (line 74) | func NewHttpProxyDialer(u *url.URL, forward proxy.Dialer) (proxy.Dialer,...
  function init (line 93) | func init() {

FILE: backend/utils/redis/log_hook.go
  type execCallback (line 14) | type execCallback
  type LogHook (line 16) | type LogHook struct
    method DialHook (line 72) | func (l *LogHook) DialHook(next redis.DialHook) redis.DialHook {
    method ProcessHook (line 78) | func (l *LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
    method ProcessPipelineHook (line 97) | func (l *LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) ...
  function NewHook (line 21) | func NewHook(name string, cmdExec execCallback) *LogHook {
  function appendArg (line 28) | func appendArg(b []byte, v interface{}) []byte {

FILE: backend/utils/slice/slice_util.go
  function Map (line 9) | func Map[S ~[]T, T any, R any](arr S, mappingFunc func(int) R) []R {
  function FilterMap (line 19) | func FilterMap[S ~[]T, T any, R any](arr S, mappingFunc func(int) (R, bo...
  function Join (line 33) | func Join[S ~[]T, T any](arr S, sep string, toStringFunc func(int) strin...
  function JoinString (line 53) | func JoinString(arr []string, sep string) string {
  function Unique (line 60) | func Unique[S ~[]T, T Hashable](arr S) S {

FILE: backend/utils/string/any_convert.go
  function AnyToString (line 10) | func AnyToString(value interface{}, prefix string, layer int) (s string) {
  function SplitCmd (line 134) | func SplitCmd(cmd string) []string {

FILE: backend/utils/string/common.go
  function ContainsBinary (line 7) | func ContainsBinary(str string) bool {
  function IsSameChar (line 28) | func IsSameChar(str string) bool {

FILE: backend/utils/string/json_formatter.go
  type ArrayIterator (line 11) | type ArrayIterator struct
  function NewArrayIterator (line 17) | func NewArrayIterator[T any](array []T) *ArrayIterator[T] {
  method HasNext (line 25) | func (it *ArrayIterator[T]) HasNext() bool {
  method PeekNext (line 30) | func (it *ArrayIterator[T]) PeekNext() *T {
  method Next (line 38) | func (it *ArrayIterator[T]) Next() *T {
  function JSONBeautify (line 46) | func JSONBeautify(value string, indent string) string {
  function JSONMinify (line 54) | func JSONMinify(value string) string {
  function format (line 59) | func format(value string, indent string, newLine string, separator strin...
  function consumeWhitespaces (line 110) | func consumeWhitespaces(iter *ArrayIterator[rune]) {
  function consumeString (line 121) | func consumeString(iter *ArrayIterator[rune]) string {
  function convertUnicodeString (line 150) | func convertUnicodeString(str string) string {

FILE: backend/utils/string/key_convert.go
  function EncodeRedisKey (line 10) | func EncodeRedisKey(key string) any {
  function DecodeRedisKey (line 23) | func DecodeRedisKey(key any) string {
  function AnyToInt (line 50) | func AnyToInt(val any) (int, bool) {

FILE: frontend/src/consts/localstorage_key.js
  constant STORAGE_THEME_KEY (line 1) | const STORAGE_THEME_KEY = 'rdm_theme'
  constant STORAGE_LANG_KEY (line 2) | const STORAGE_LANG_KEY = 'rdm_lang'

FILE: frontend/src/main.js
  function setupApp (line 19) | async function setupApp() {

FILE: frontend/src/objects/redisDatabaseItem.js
  class RedisDatabaseItem (line 4) | class RedisDatabaseItem {
    method constructor (line 5) | constructor({ db = 0, alias = '', keyCount = 0, maxKeys = 0 }) {

FILE: frontend/src/objects/redisNodeItem.js
  class RedisNodeItem (line 7) | class RedisNodeItem {
    method constructor (line 25) | constructor({
    method _sortNodes (line 62) | _sortNodes(nodeList) {
    method _sortingCompare (line 77) | _sortingCompare(a, b) {
    method _sortedIndex (line 99) | _sortedIndex(arr, item) {
    method tidy (line 116) | tidy(skipSort) {
    method sortChildren (line 140) | sortChildren() {
    method addChild (line 149) | addChild(child, sorted) {
    method removeChild (line 163) | removeChild(predicate) {
    method getChildren (line 171) | getChildren() {
    method reCalcKeyCount (line 175) | reCalcKeyCount() {

FILE: frontend/src/objects/redisServerState.js
  class RedisServerState (line 12) | class RedisServerState {
    method constructor (line 33) | constructor({
    method dispose (line 67) | dispose() {
    method closeDatabase (line 75) | closeDatabase() {
    method setDatabaseKeyCount (line 81) | setDatabaseKeyCount(db, maxKeys) {
    method updateDBKeyCount (line 96) | updateDBKeyCount(db, updateVal) {
    method setDBKeyCount (line 108) | setDBKeyCount(db, count) {
    method getRoot (line 119) | getRoot() {
    method getDatabase (line 139) | getDatabase() {
    method addNode (line 149) | addNode(type, keyPath, node) {
    method addKeyNodes (line 159) | addKeyNodes(keys, sortInsert) {
    method renameKey (line 261) | renameKey(key, newKey) {
    method removeKeyNode (line 296) | removeKeyNode(key, isLayer) {
    method tidyNode (line 366) | tidyNode(key, skipResort) {
    method getNode (line 419) | getNode(type, keyPath) {
    method deleteChildrenKeyNodes (line 428) | deleteChildrenKeyNodes(key) {
    method getFilter (line 451) | getFilter() {
    method setFilter (line 470) | setFilter({ pattern, type, exact = false }) {
    method addDecodeHistory (line 483) | addDecodeHistory(key, db, format = '', decode = '') {
    method getDecodeHistory (line 504) | getDecodeHistory(key, db) {

FILE: frontend/src/objects/tabItem.js
  class TabItem (line 4) | class TabItem {
    method constructor (line 38) | constructor({

FILE: frontend/src/stores/browser.js
  method anyConnectionOpened (line 84) | anyConnectionOpened() {
  method isConnected (line 94) | isConnected(name) {
  method closeAllConnection (line 102) | async closeAllConnection() {
  method getDBList (line 117) | getDBList(server) {
  method getServerVersion (line 129) | getServerVersion(server) {
  method getDatabase (line 143) | getDatabase(server, db) {
  method getSelectedDB (line 157) | getSelectedDB(server) {
  method getKeyStruct (line 172) | getKeyStruct(server, includeRoot) {
  method getReloadKey (line 185) | getReloadKey(server) {
  method reloadServer (line 191) | reloadServer(server) {
  method openConnection (line 236) | async openConnection(name, reload) {
  method closeConnection (line 291) | async closeConnection(name) {
  method openDatabase (line 310) | async openDatabase(server, db) {
  method closeDatabase (line 340) | closeDatabase(server, db) {
  method getServerInfo (line 365) | async getServerInfo(server, mute) {
  method loadKeySummary (line 392) | async loadKeySummary({ server, db, key, clearValue, redirect = true }) {
  method loadKeyType (line 454) | async loadKeyType({ server, db, key }) {
  method reloadKey (line 490) | async reloadKey({ server, db, key, decode, format, matchPattern, showLoa...
  method loadKeyDetail (line 527) | async loadKeyDetail({ server, db, key, format, decode, matchPattern, res...
  method convertValue (line 587) | async convertValue({ value, decode, format }) {
  method scanKeys (line 608) | async scanKeys({ server, db, match = '*', exact = false, matchType = '',...
  method _loadKeys (line 641) | async _loadKeys({ server, db, match, exact, matchType, all }) {
  method loadMoreKeys (line 663) | async loadMoreKeys(server, db) {
  method loadAllKeys (line 690) | async loadAllKeys(server, db) {
  method reloadLayer (line 709) | async reloadLayer(server, db, prefix) {
  method getSeparator (line 753) | getSeparator(server) {
  method getNode (line 767) | getNode(key) {
  method getParentNode (line 807) | getParentNode(key) {
  method setKey (line 845) | async setKey({ server, db, key, keyType, value, ttl, format = formatType...
  method setHash (line 910) | async setHash({
  method addHashField (line 988) | async addHashField({ server, db, key, action, fieldItems, reload }) {
  method getHashField (line 1025) | async getHashField({ server, db, key, field, decode = decodeTypes.NONE, ...
  method removeHashField (line 1049) | async removeHashField({ server, db, key, field, reload }) {
  method prependListItem (line 1082) | async prependListItem({ server, db, key, values, reload }) {
  method appendListItem (line 1122) | async appendListItem({ server, db, key, values, reload }) {
  method updateListItem (line 1168) | async updateListItem({
  method removeListItem (line 1239) | async removeListItem({ server, db, key, index, reload }) {
  method addSetItem (line 1279) | async addSetItem({ server, db, key, value, reload }) {
  method updateSetItem (line 1320) | async updateSetItem({
  method removeSetItem (line 1378) | async removeSetItem({ server, db, key, value, reload }) {
  method addZSetItem (line 1413) | async addZSetItem({ server, db, key, action, vs, reload }) {
  method updateZSetItem (line 1456) | async updateZSetItem({
  method removeZSetItem (line 1523) | async removeZSetItem({ server, db, key, value, reload }) {
  method addStreamValue (line 1558) | async addStreamValue({ server, db, key, id, values, reload }) {
  method removeStreamValues (line 1597) | async removeStreamValues({ server, db, key, ids, reload }) {
  method setTTL (line 1629) | async setTTL(server, db, key, ttl) {
  method setTTLs (line 1647) | async setTTLs(server, db, keys, ttl) {
  method deleteKey (line 1708) | async deleteKey(server, db, key, soft) {
  method deleteKeys (line 1744) | async deleteKeys(server, db, keys) {
  method deleteByPattern (line 1808) | async deleteByPattern(server, db, pattern) {
  method exportKeys (line 1870) | async exportKeys(server, db, keys, path, expire) {
  method importKeysFromCSVFile (line 1930) | async importKeysFromCSVFile(server, db, path, conflict, ttl, reload) {
  method flushDatabase (line 1977) | async flushDatabase(server, db, async) {
  method renameKey (line 2009) | async renameKey(server, db, key, newKey) {
  method getCmdHistory (line 2030) | async getCmdHistory(pageNo, pageSize) {
  method cleanCmdHistory (line 2048) | async cleanCmdHistory() {
  method getClientList (line 2062) | async getClientList(server) {
  method getSlowLog (line 2083) | async getSlowLog(server, num) {
  method getKeyFilter (line 2098) | getKeyFilter(server) {
  method setKeyFilter (line 2116) | setKeyFilter(server, { pattern, type, exact = false }) {
  method setSelectedFormat (line 2131) | setSelectedFormat(server, key, db, format, decode) {

FILE: frontend/src/stores/connections.js
  method initConnections (line 66) | async initConnections(force) {
  method getConnectionProfile (line 131) | async getConnectionProfile(name) {
  method newDefaultConnection (line 152) | newDefaultConnection(name) {
  method mergeConnectionProfile (line 211) | mergeConnectionProfile(dest, src) {
  method getConnection (line 236) | getConnection(name) {
  method saveConnection (line 259) | async saveConnection(name, param) {
  method saveConnectionSorted (line 274) | async saveConnectionSorted() {
  method deleteConnection (line 302) | async deleteConnection(name) {
  method createGroup (line 319) | async createGroup(name) {
  method renameGroup (line 334) | async renameGroup(name, newName) {
  method deleteGroup (line 352) | async deleteGroup(name, includeConn) {
  method saveLastDB (line 367) | async saveLastDB(name, db) {
  method getDefaultKeyFilter (line 380) | getDefaultKeyFilter(name) {
  method getDefaultSeparator (line 390) | getDefaultSeparator(name) {
  method getRefreshInterval (line 400) | getRefreshInterval(name) {
  method saveRefreshInterval (line 411) | async saveRefreshInterval(name, interval) {
  method exportConnections (line 425) | async exportConnections() {
  method importConnections (line 445) | async importConnections() {
  method parseUrlFromClipboard (line 461) | async parseUrlFromClipboard() {

FILE: frontend/src/stores/dialog.js
  method openNewDialog (line 110) | openNewDialog() {
  method closeConnDialog (line 115) | closeConnDialog() {
  method openEditDialog (line 119) | async openEditDialog(name) {
  method openDuplicateDialog (line 127) | async openDuplicateDialog(name) {
  method openNewGroupDialog (line 156) | openNewGroupDialog() {
  method closeNewGroupDialog (line 160) | closeNewGroupDialog() {
  method openKeyFilterDialog (line 171) | openKeyFilterDialog(server, db, pattern, type) {
  method closeKeyFilterDialog (line 178) | closeKeyFilterDialog() {
  method openRenameGroupDialog (line 186) | openRenameGroupDialog(name) {
  method closeRenameGroupDialog (line 190) | closeRenameGroupDialog() {
  method openRenameKeyDialog (line 200) | openRenameKeyDialog(server, db, key) {
  method closeRenameKeyDialog (line 206) | closeRenameKeyDialog() {
  method openDeleteKeyDialog (line 216) | openDeleteKeyDialog(server, db, key = '*') {
  method closeDeleteKeyDialog (line 222) | closeDeleteKeyDialog() {
  method openExportKeyDialog (line 232) | openExportKeyDialog(server, db, keys) {
  method closeExportKeyDialog (line 238) | closeExportKeyDialog() {
  method openImportKeyDialog (line 247) | openImportKeyDialog(server, db) {
  method closeImportKeyDialog (line 252) | closeImportKeyDialog() {
  method openFlushDBDialog (line 256) | openFlushDBDialog(server, db) {
  method closeFlushDBDialog (line 261) | closeFlushDBDialog() {
  method openNewKeyDialog (line 271) | openNewKeyDialog(prefix, server, db) {
  method closeNewKeyDialog (line 277) | closeNewKeyDialog() {
  method openAddFieldsDialog (line 289) | openAddFieldsDialog(server, db, key, keyCode, type) {
  method closeAddFieldsDialog (line 297) | closeAddFieldsDialog() {
  method openTTLDialog (line 309) | openTTLDialog({ server, db, key, keys, ttl = -1 }) {
  method closeTTLDialog (line 317) | closeTTLDialog() {
  method openDecoderDialog (line 330) | openDecoderDialog({
  method closeDecoderDialog (line 347) | closeDecoderDialog() {
  method openPreferencesDialog (line 351) | openPreferencesDialog(tag = '') {
  method closePreferencesDialog (line 355) | closePreferencesDialog() {
  method openAboutDialog (line 360) | openAboutDialog() {
  method closeAboutDialog (line 363) | closeAboutDialog() {

FILE: frontend/src/stores/preferences.js
  method getSeparator (line 82) | getSeparator() {
  method themeOption (line 86) | themeOption() {
  method allThemes (line 107) | allThemes() {
  method langOption (line 115) | langOption() {
  method allLangs (line 131) | allLangs() {
  method fontOption (line 139) | fontOption() {
  method generalFont (line 151) | generalFont() {
  method editorFont (line 177) | editorFont() {
  method cliFont (line 206) | cliFont() {
  method cliCursorStyleOption (line 222) | cliCursorStyleOption() {
  method currentLanguage (line 243) | currentLanguage() {
  method isDark (line 252) | isDark() {
  method themeLocale (line 261) | themeLocale() {
  method autoCheckUpdate (line 271) | autoCheckUpdate() {
  method showLineNum (line 275) | showLineNum() {
  method showFolding (line 279) | showFolding() {
  method dropText (line 283) | dropText() {
  method editorLinks (line 287) | editorLinks() {
  method keyIconType (line 291) | keyIconType() {
  method entryTextAlign (line 295) | entryTextAlign() {
  method _applyPreferences (line 300) | _applyPreferences(data) {
  method loadPreferences (line 310) | async loadPreferences() {
  method loadFontList (line 341) | async loadFontList() {
  method loadBuildInDecoder (line 356) | async loadBuildInDecoder() {
  method loadAppVersion (line 370) | async loadAppVersion() {
  method savePreferences (line 381) | async savePreferences() {
  method resetToLastPreferences (line 391) | async resetToLastPreferences() {
  method restorePreferences (line 401) | async restorePreferences() {
  method addCustomDecoder (line 421) | addCustomDecoder({ name, enable = true, auto = true, encodePath, encodeA...
  method updateCustomDecoder (line 441) | updateCustomDecoder({
  method removeCustomDecoder (line 477) | removeCustomDecoder(name) {
  method setAsWelcomed (line 486) | setAsWelcomed(acceptTrack) {
  method checkForUpdate (line 492) | async checkForUpdate(manual = false) {

FILE: frontend/src/stores/tab.js
  method tabs (line 88) | tabs() {
  method currentTab (line 99) | currentTab() {
  method currentTabName (line 103) | currentTabName() {
  method currentCheckedKeys (line 107) | currentCheckedKeys() {
  method _setActivatedIndex (line 120) | _setActivatedIndex(idx, switchNav, subTab) {
  method openBlank (line 134) | openBlank(server) {
  method closeTab (line 142) | closeTab(tabName) {
  method upsertTab (line 167) | upsertTab({
  method updateValue (line 246) | updateValue({ server, db, key, value, format, decode, matchPattern, rese...
  method insertValueEntries (line 289) | insertValueEntries({ server, db, key, type, entries, prepend }) {
  method updateValueEntries (line 346) | updateValueEntries({ server, db, key, type, entries }) {
  method replaceValueEntries (line 404) | replaceValueEntries({ server, db, key, type, entries, index }) {
  method removeValueEntries (line 516) | removeValueEntries({ server, db, key, type, entries }) {
  method updateLoading (line 588) | updateLoading({ server, db, loading }) {
  method updateTTL (line 604) | updateTTL({ server, db, key, ttl }) {
  method emptyTab (line 616) | emptyTab(name) {
  method switchTab (line 623) | switchTab(tabIndex) {
  method switchSubTab (line 636) | switchSubTab(name) {
  method removeTab (line 649) | removeTab(tabIndex) {
  method removeTabByName (line 680) | removeTabByName(tabName) {
  method removeAllTab (line 690) | removeAllTab() {
  method setExpandedKeys (line 700) | setExpandedKeys(server, keys = []) {
  method addExpandedKey (line 720) | addExpandedKey(server, keys) {
  method toggleExpandKey (line 740) | toggleExpandKey(server, key) {
  method removeExpandedKey (line 758) | removeExpandedKey(server, key) {
  method setSelectedKeys (line 771) | setSelectedKeys(server, keys = null) {
  method getCheckedKeys (line 792) | getCheckedKeys(server) {
  method setCheckedKeys (line 806) | setCheckedKeys(server, keys = null) {
  method getActivatedKey (line 824) | getActivatedKey(server) {
  method setActivatedKey (line 835) | setActivatedKey(server, key) {

FILE: frontend/src/utils/api.js
  constant API_BASE (line 6) | const API_BASE = '/api'
  function post (line 8) | async function post(path, body = {}) {
  function get (line 22) | async function get(path, params = {}) {
  function del (line 33) | async function del(path, params = {}) {
  function ListConnection (line 46) | function ListConnection() {
  function GetConnection (line 50) | function GetConnection(name) {
  function SaveConnection (line 54) | function SaveConnection(name, param) {
  function SaveSortedConnection (line 58) | function SaveSortedConnection(conns) {
  function TestConnection (line 62) | function TestConnection(param) {
  function DeleteConnection (line 66) | function DeleteConnection(name) {
  function CreateGroup (line 70) | function CreateGroup(name) {
  function RenameGroup (line 74) | function RenameGroup(name, newName) {
  function DeleteGroup (line 78) | function DeleteGroup(name, includeConn) {
  function SaveLastDB (line 82) | function SaveLastDB(name, db) {
  function SaveRefreshInterval (line 86) | function SaveRefreshInterval(name, interval) {
  function ExportConnections (line 90) | async function ExportConnections() {
  function ImportConnections (line 119) | async function ImportConnections() {
  function ParseConnectURL (line 154) | function ParseConnectURL(url) {
  function ListSentinelMasters (line 158) | function ListSentinelMasters(param) {
  function OpenConnection (line 164) | function OpenConnection(name) {
  function CloseConnection (line 168) | function CloseConnection(name) {
  function OpenDatabase (line 172) | function OpenDatabase(server, db) {
  function ServerInfo (line 176) | function ServerInfo(name) {
  function LoadNextKeys (line 180) | function LoadNextKeys(server, db, match, keyType, exactMatch) {
  function LoadNextAllKeys (line 184) | function LoadNextAllKeys(server, db, match, keyType, exactMatch) {
  function LoadAllKeys (line 188) | function LoadAllKeys(server, db, match, keyType, exactMatch) {
  function GetKeyType (line 192) | function GetKeyType(param) {
  function GetKeySummary (line 196) | function GetKeySummary(param) {
  function GetKeyDetail (line 200) | function GetKeyDetail(param) {
  function ConvertValue (line 204) | function ConvertValue(value, decode, format) {
  function SetKeyValue (line 208) | function SetKeyValue(param) {
  function GetHashValue (line 212) | function GetHashValue(param) {
  function SetHashValue (line 216) | function SetHashValue(param) {
  function AddHashField (line 220) | function AddHashField(server, db, key, action, fieldItems) {
  function AddListItem (line 224) | function AddListItem(server, db, key, action, items) {
  function SetListItem (line 228) | function SetListItem(param) {
  function SetSetItem (line 232) | function SetSetItem(server, db, key, remove, members) {
  function UpdateSetItem (line 236) | function UpdateSetItem(param) {
  function UpdateZSetValue (line 240) | function UpdateZSetValue(param) {
  function AddZSetValue (line 244) | function AddZSetValue(server, db, key, action, valueScore) {
  function AddStreamValue (line 248) | function AddStreamValue(server, db, key, id, fieldItems) {
  function RemoveStreamValues (line 252) | function RemoveStreamValues(server, db, key, ids) {
  function SetKeyTTL (line 256) | function SetKeyTTL(server, db, key, ttl) {
  function BatchSetTTL (line 260) | function BatchSetTTL(server, db, keys, ttl, serialNo) {
  function DeleteKey (line 264) | function DeleteKey(server, db, key, async) {
  function DeleteKeys (line 268) | function DeleteKeys(server, db, keys, serialNo) {
  function DeleteKeysByPattern (line 272) | function DeleteKeysByPattern(server, db, pattern) {
  function RenameKey (line 276) | function RenameKey(server, db, key, newKey) {
  function ExportKey (line 280) | function ExportKey(server, db, keys, path, includeExpire) {
  function ImportCSV (line 284) | function ImportCSV(server, db, path, conflict, ttl) {
  function FlushDB (line 288) | function FlushDB(server, db, async) {
  function GetSlowLogs (line 292) | function GetSlowLogs(server, db, num) {
  function GetClientList (line 296) | function GetClientList(server, db) {
  function GetCmdHistory (line 300) | function GetCmdHistory() {
  function CleanCmdHistory (line 304) | function CleanCmdHistory() {
  function StartCli (line 310) | function StartCli(server, db) {
  function CloseCli (line 314) | function CloseCli(server) {
  function StartMonitor (line 320) | function StartMonitor(server) {
  function StopMonitor (line 324) | function StopMonitor(server) {
  function ExportLog (line 328) | function ExportLog(logs) {
  function Publish (line 334) | function Publish(server, channel, payload) {
  function StartSubscribe (line 338) | function StartSubscribe(server) {
  function StopSubscribe (line 342) | function StopSubscribe(server) {
  function GetPreferences (line 348) | function GetPreferences() {
  function SetPreferences (line 352) | function SetPreferences(pf) {
  function UpdatePreferences (line 356) | function UpdatePreferences(value) {
  function RestorePreferences (line 360) | function RestorePreferences() {
  constant CANDIDATE_FONTS (line 365) | const CANDIDATE_FONTS = [
  function queryBrowserFonts (line 383) | async function queryBrowserFonts() {
  function GetFontList (line 388) | async function GetFontList() {
  function GetBuildInDecoder (line 397) | function GetBuildInDecoder() {
  function GetAppVersion (line 401) | function GetAppVersion() {
  function CheckForUpdate (line 405) | function CheckForUpdate() {
  function Info (line 412) | function Info() {
  function SelectFile (line 417) | async function SelectFile(title, ext) {
  function SaveFile (line 452) | async function SaveFile(title, defaultName, ext) {
  function Login (line 460) | async function Login(username, password) {
  function Logout (line 464) | async function Logout() {

FILE: frontend/src/utils/discrete.js
  function setupMessage (line 7) | function setupMessage(message) {
  function setupNotification (line 29) | function setupNotification(notification) {
  function setupDialog (line 65) | function setupDialog(dialog) {
  function setupDiscreteApi (line 98) | async function setupDiscreteApi() {

FILE: frontend/src/utils/glob_pattern.js
  constant REDIS_GLOB_CHAR (line 3) | const REDIS_GLOB_CHAR = ['?', '*', '[', ']', '{', '}']

FILE: frontend/src/utils/key_convert.js
  function decodeRedisKey (line 8) | function decodeRedisKey(key) {
  function nativeRedisKey (line 31) | function nativeRedisKey(key, truncate) {

FILE: frontend/src/utils/monaco.js
  method open (line 47) | open(resource) {

FILE: frontend/src/utils/platform.js
  function loadEnvironment (line 5) | async function loadEnvironment() {
  function isMacOS (line 10) | function isMacOS() {
  function isWindows (line 14) | function isWindows() {
  function isWeb (line 18) | function isWeb() {

FILE: frontend/src/utils/render.js
  function useRender (line 4) | function useRender() {

FILE: frontend/src/utils/rgb.js
  function parseHexColor (line 16) | function parseHexColor(hex) {
  function hexGammaCorrection (line 36) | function hexGammaCorrection(rgb, gamma) {
  function mixColors (line 54) | function mixColors(rgba1, rgba2, weight = 0.5) {
  function toHexColor (line 74) | function toHexColor(rgb) {

FILE: frontend/src/utils/wails_runtime.js
  function EventsOn (line 13) | function EventsOn(event, callback) {
  function EventsOnce (line 17) | function EventsOnce(event, callback) {
  function EventsEmit (line 25) | function EventsEmit(event, ...data) {
  function EventsOff (line 29) | function EventsOff(event) {
  function ClipboardGetText (line 35) | async function ClipboardGetText() {
  function ClipboardSetText (line 45) | async function ClipboardSetText(text) {
  function BrowserOpenURL (line 63) | function BrowserOpenURL(url) {
  function WindowMinimise (line 69) | function WindowMinimise() {}
  function WindowMaximise (line 70) | function WindowMaximise() {}
  function WindowToggleMaximise (line 71) | function WindowToggleMaximise() {}
  function WindowIsMaximised (line 72) | function WindowIsMaximised() { return false }
  function WindowIsFullscreen (line 73) | function WindowIsFullscreen() { return false }
  function WindowSetDarkTheme (line 74) | function WindowSetDarkTheme() {}
  function WindowSetLightTheme (line 75) | function WindowSetLightTheme() {}
  function Quit (line 76) | function Quit() {}
  function Environment (line 80) | async function Environment() {

FILE: frontend/src/utils/websocket.js
  function resetReadyPromise (line 12) | function resetReadyPromise() {
  function getWsUrl (line 19) | function getWsUrl() {
  function connectWebSocket (line 24) | function connectWebSocket() {
  function waitForWebSocket (line 65) | function waitForWebSocket() {
  function reconnectWebSocket (line 73) | function reconnectWebSocket() {
  function scheduleReconnect (line 88) | function scheduleReconnect() {
  function dispatch (line 98) | function dispatch(event, data) {
  function onWsEvent (line 111) | function onWsEvent(event, callback) {
  function offWsEvent (line 118) | function offWsEvent(event, callback) {
  function sendWsMessage (line 129) | function sendWsMessage(msg) {

FILE: main.go
  constant appName (line 33) | appName = "Tiny RDM"
  function main (line 35) | func main() {

FILE: main_web.go
  function main (line 19) | func main() {
Condensed preview — 306 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,474K chars).
[
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 1925,
    "preview": "## Tiny RDM Contribute Guide\n\n### Multi-language Contributions\n\n#### Adding New Language\n\n1. New file: Add a new JSON fi"
  },
  {
    "path": ".github/CONTRIBUTING_zh.md",
    "chars": 1104,
    "preview": "## Tiny RDM 代码贡献指南\n\n### 多国语言贡献\n\n#### 增加新的语言\n1. 创建文件:在[frontend/src/langs](../frontend/src/langs/)目录下新增语言配置JSON文件,文件名格式为“"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 397,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: '[BUG]'\nlabels: ''\nassignees: ''\n---\n\n**Tiny RDM V"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 89,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: '[FEATURE]'\n---\n"
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "chars": 2061,
    "preview": "name: Release Docker Image\nrun-name: ${{ github.event.release.tag_name || github.event.inputs.tag || 'manual' }}\n\non:\n  "
  },
  {
    "path": ".github/workflows/release-linux-webkit2-41.yaml",
    "chars": 5470,
    "preview": "name: Release Linux App\nrun-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}\n\non:\n  release:\n    ty"
  },
  {
    "path": ".github/workflows/release-linux.yaml",
    "chars": 7704,
    "preview": "name: Release Linux App\nrun-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}\n\non:\n  release:\n    ty"
  },
  {
    "path": ".github/workflows/release-macos.yaml",
    "chars": 4877,
    "preview": "name: Release macOS App\nrun-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}\n\non:\n  release:\n    ty"
  },
  {
    "path": ".github/workflows/release-windows.yaml",
    "chars": 4722,
    "preview": "name: Release Windows App\nrun-name: ${{ github.event.release.tag_name || github.event.inputs.tag }}\n\non:\n  release:\n    "
  },
  {
    "path": ".gitignore",
    "chars": 107,
    "preview": "build/bin\nnode_modules\nfrontend/dist\nfrontend/wailsjs\nfrontend/package.json.md5\ndesign/\n.vscode\n.idea\ntest\n"
  },
  {
    "path": ".prettierignore",
    "chars": 21,
    "preview": "/frontend/wailsjs/**\n"
  },
  {
    "path": "Dockerfile",
    "chars": 1670,
    "preview": "# ============================================================\n# Stage 1: Build frontend\n# ============================="
  },
  {
    "path": "LICENSE",
    "chars": 35149,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "README.md",
    "chars": 6020,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_es.md",
    "chars": 5780,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_fr.md",
    "chars": 5716,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_ja.md",
    "chars": 5080,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_ko.md",
    "chars": 4671,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_pt.md",
    "chars": 5672,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_ru.md",
    "chars": 5632,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_tr.md",
    "chars": 5632,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_tw.md",
    "chars": 4413,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "README_zh.md",
    "chars": 4699,
    "preview": "<div align=\"center\">\n<a href=\"https://github.com/tiny-craft/tiny-rdm/\"><img src=\"build/appicon.png\" width=\"120\"/></a>\n</"
  },
  {
    "path": "backend/api/auth.go",
    "chars": 7414,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\""
  },
  {
    "path": "backend/api/browser_api.go",
    "chars": 14220,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/gin"
  },
  {
    "path": "backend/api/cli_api.go",
    "chars": 963,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/gin"
  },
  {
    "path": "backend/api/connection_api.go",
    "chars": 5223,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"g"
  },
  {
    "path": "backend/api/monitor_api.go",
    "chars": 1125,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/gin"
  },
  {
    "path": "backend/api/preferences_api.go",
    "chars": 1352,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/gin"
  },
  {
    "path": "backend/api/pubsub_api.go",
    "chars": 1231,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"net/http\"\n\t\"tinyrdm/backend/services\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/gin"
  },
  {
    "path": "backend/api/router.go",
    "chars": 4956,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"tinyrdm/backend/services\"\n\n\t\"github.com/gin-gonic/"
  },
  {
    "path": "backend/api/system_api.go",
    "chars": 3000,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"tinyrdm/backend/servi"
  },
  {
    "path": "backend/api/websocket_hub.go",
    "chars": 2930,
    "preview": "//go:build web\n\npackage api\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t"
  },
  {
    "path": "backend/consts/app_name_desktop.go",
    "chars": 67,
    "preview": "//go:build !web\n\npackage consts\n\nconst APP_DATA_FOLDER = \"TinyRDM\"\n"
  },
  {
    "path": "backend/consts/app_name_web.go",
    "chars": 66,
    "preview": "//go:build web\n\npackage consts\n\nconst APP_DATA_FOLDER = \"tinyrdm\"\n"
  },
  {
    "path": "backend/consts/default_config.go",
    "chars": 267,
    "preview": "package consts\n\nconst DEFAULT_FONT_SIZE = 14\nconst DEFAULT_ASIDE_WIDTH = 300\nconst DEFAULT_WINDOW_WIDTH = 1024\nconst DEF"
  },
  {
    "path": "backend/services/browser_service.go",
    "chars": 71787,
    "preview": "package services\n\nimport (\n\t\"context\"\n\t\"encoding/csv\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"net/ur"
  },
  {
    "path": "backend/services/cli_service.go",
    "chars": 3721,
    "preview": "package services\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\t\"tinyrdm/backend/types\"\n\tsliceutil \"tinyrdm/b"
  },
  {
    "path": "backend/services/connection_service.go",
    "chars": 16809,
    "preview": "package services\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"str"
  },
  {
    "path": "backend/services/connection_service_web.go",
    "chars": 2100,
    "preview": "//go:build web\n\npackage services\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\t\"tinyrdm/backend/consts\"\n\t\"tinyrdm/back"
  },
  {
    "path": "backend/services/ga_service.go",
    "chars": 2164,
    "preview": "package services\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"tinyrdm/backend/storage"
  },
  {
    "path": "backend/services/monitor_service.go",
    "chars": 4064,
    "preview": "package services\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\t\"tinyrdm/backend/types"
  },
  {
    "path": "backend/services/platform_desktop.go",
    "chars": 2589,
    "preview": "//go:build !web\n\npackage services\n\nimport (\n\t\"context\"\n\n\t\"github.com/wailsapp/wails/v2/pkg/runtime\"\n)\n\n// Type aliases f"
  },
  {
    "path": "backend/services/platform_web.go",
    "chars": 3034,
    "preview": "//go:build web\n\npackage services\n\nimport (\n\t\"context\"\n)\n\n// Callback functions - set by api package at startup to avoid "
  },
  {
    "path": "backend/services/preferences_service.go",
    "chars": 6963,
    "preview": "package services\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"tinyrdm/backend/co"
  },
  {
    "path": "backend/services/pubsub_service.go",
    "chars": 3953,
    "preview": "package services\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\t\"tinyrdm/backend/types\"\n\n\t\"github.com/redis/go-"
  },
  {
    "path": "backend/services/system_service.go",
    "chars": 3659,
    "preview": "package services\n\nimport (\n\t\"context\"\n\truntime2 \"runtime\"\n\t\"sync\"\n\t\"time\"\n\t\"tinyrdm/backend/consts\"\n\t\"tinyrdm/backend/ty"
  },
  {
    "path": "backend/storage/connections.go",
    "chars": 8475,
    "preview": "package storage\n\nimport (\n\t\"errors\"\n\t\"slices\"\n\t\"sync\"\n\t\"tinyrdm/backend/consts\"\n\t\"tinyrdm/backend/types\"\n\n\t\"gopkg.in/yam"
  },
  {
    "path": "backend/storage/local_storage.go",
    "chars": 1315,
    "preview": "package storage\n\nimport (\n\t\"os\"\n\t\"path\"\n\n\t\"tinyrdm/backend/consts\"\n\n\t\"github.com/vrischmann/userdir\"\n)\n\n// localStorage "
  },
  {
    "path": "backend/storage/preferences.go",
    "chars": 2985,
    "preview": "package storage\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"tinyrdm/backend/consts\"\n\t\"tinyrdm/backend/types\""
  },
  {
    "path": "backend/types/connection.go",
    "chars": 4683,
    "preview": "package types\n\ntype ConnectionCategory int\n\ntype ConnectionConfig struct {\n\tName            string             `json:\"na"
  },
  {
    "path": "backend/types/js_resp.go",
    "chars": 3236,
    "preview": "package types\n\ntype JSResp struct {\n\tSuccess bool   `json:\"success\"`\n\tMsg     string `json:\"msg\"`\n\tData    any    `json:"
  },
  {
    "path": "backend/types/preferences.go",
    "chars": 3754,
    "preview": "package types\n\nimport \"tinyrdm/backend/consts\"\n\ntype Preferences struct {\n\tBehavior PreferencesBehavior  `json:\"behavior"
  },
  {
    "path": "backend/types/redis_wrapper.go",
    "chars": 1253,
    "preview": "package types\n\ntype ListEntryItem struct {\n\tIndex        int    `json:\"index\"`\n\tValue        any    `json:\"v\"`\n\tDisplayV"
  },
  {
    "path": "backend/types/view_type.go",
    "chars": 540,
    "preview": "package types\n\nconst FORMAT_RAW = \"Raw\"\nconst FORMAT_JSON = \"JSON\"\nconst FORMAT_UNICODE_JSON = \"Unicode JSON\"\nconst FORM"
  },
  {
    "path": "backend/utils/coll/set.go",
    "chars": 4094,
    "preview": "package coll\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t. \"tinyrdm/backend/utils\"\n)\n\ntype Void struct{}\n\n// Set 集合, 存放不重"
  },
  {
    "path": "backend/utils/constraints.go",
    "chars": 351,
    "preview": "package utils\n\ntype Hashable interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 |"
  },
  {
    "path": "backend/utils/convert/base64_convert.go",
    "chars": 528,
    "preview": "package convutil\n\nimport (\n\t\"encoding/base64\"\n\tstrutil \"tinyrdm/backend/utils/string\"\n)\n\ntype Base64Convert struct{}\n\nfu"
  },
  {
    "path": "backend/utils/convert/binary_convert.go",
    "chars": 684,
    "preview": "package convutil\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype BinaryConvert struct{}\n\nfunc (BinaryConvert) Enable() bo"
  },
  {
    "path": "backend/utils/convert/bitset_convert.go",
    "chars": 2363,
    "preview": "package convutil\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype BitSetConvert struct{}\n\nfunc (BitSetConvert) Ena"
  },
  {
    "path": "backend/utils/convert/brotli_convert.go",
    "chars": 820,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/andybalholm/brotli\"\n)\n\ntype BrotliConvert struct{}\n\nf"
  },
  {
    "path": "backend/utils/convert/cmd_convert.go",
    "chars": 2013,
    "preview": "package convutil\n\nimport (\n\t\"encoding/base64\"\n\t\"strings\"\n\tsliceutil \"tinyrdm/backend/utils/slice\"\n)\n\ntype CmdConvert str"
  },
  {
    "path": "backend/utils/convert/common.go",
    "chars": 417,
    "preview": "package convutil\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"tinyrdm/backend/consts\"\n\n\t\"github.com/vrischmann/userdir\"\n)\n\nfunc writeExecut"
  },
  {
    "path": "backend/utils/convert/common_nonwindows.go",
    "chars": 182,
    "preview": "//go:build !windows\n\npackage convutil\n\nimport (\n\t\"os/exec\"\n)\n\nfunc runCommand(name string, arg ...string) ([]byte, error"
  },
  {
    "path": "backend/utils/convert/common_windows.go",
    "chars": 250,
    "preview": "//go:build windows\n\npackage convutil\n\nimport (\n\t\"os/exec\"\n\t\"syscall\"\n)\n\nfunc runCommand(name string, arg ...string) ([]b"
  },
  {
    "path": "backend/utils/convert/convert.go",
    "chars": 6496,
    "preview": "package convutil\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"tinyrdm/backend/types\"\n\tstrutil \"tinyrdm/backend/utils/string\"\n)\n\ntype "
  },
  {
    "path": "backend/utils/convert/deflate_convert.go",
    "chars": 928,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/klauspost/compress/flate\"\n)\n\ntype DeflateConvert stru"
  },
  {
    "path": "backend/utils/convert/gzip_convert.go",
    "chars": 886,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/klauspost/compress/gzip\"\n)\n\ntype GZipConvert struct{}"
  },
  {
    "path": "backend/utils/convert/hex_convert.go",
    "chars": 712,
    "preview": "package convutil\n\nimport (\n\t\"encoding/hex\"\n\t\"strings\"\n)\n\ntype HexConvert struct{}\n\nfunc (HexConvert) Enable() bool {\n\tre"
  },
  {
    "path": "backend/utils/convert/json_convert.go",
    "chars": 581,
    "preview": "package convutil\n\nimport (\n\t\"strings\"\n\tstrutil \"tinyrdm/backend/utils/string\"\n)\n\ntype JsonConvert struct{}\n\nfunc (JsonCo"
  },
  {
    "path": "backend/utils/convert/lz4_convert.go",
    "chars": 790,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\n\t\"github.com/pierrec/lz4/v4\"\n)\n\ntype LZ4Convert struct{}\n\nfunc (LZ4Convert) E"
  },
  {
    "path": "backend/utils/convert/msgpack_convert.go",
    "chars": 1517,
    "preview": "package convutil\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/vmihailenco/msgpack/v5\"\n)\n\ntype MsgpackConvert struct{}\n\nfunc "
  },
  {
    "path": "backend/utils/convert/php_convert.go",
    "chars": 1814,
    "preview": "package convutil\n\nimport (\n\t\"os/exec\"\n)\n\ntype PhpConvert struct {\n\tCmdConvert\n}\n\nconst phpDecodeCode = `\n<?php\n\n$action "
  },
  {
    "path": "backend/utils/convert/pickle_convert.go",
    "chars": 2647,
    "preview": "package convutil\n\nimport (\n\t\"os/exec\"\n\t\"runtime\"\n)\n\ntype PickleConvert struct {\n\tCmdConvert\n}\n\nconst pickleDecodeCode = "
  },
  {
    "path": "backend/utils/convert/unicode_json_convert.go",
    "chars": 3833,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\t\"strings\"\n\tstrutil \"tinyrdm/backend/utils/string\"\n\t\"unicode\"\n\t\"unicode/u"
  },
  {
    "path": "backend/utils/convert/xml_convert.go",
    "chars": 487,
    "preview": "package convutil\n\nimport (\n\t\"encoding/xml\"\n\t\"strings\"\n)\n\ntype XmlConvert struct{}\n\nfunc (XmlConvert) Enable() bool {\n\tre"
  },
  {
    "path": "backend/utils/convert/yaml_convert.go",
    "chars": 351,
    "preview": "package convutil\n\nimport (\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype YamlConvert struct{}\n\nfunc (YamlConvert) Enable() bool {\n\treturn "
  },
  {
    "path": "backend/utils/convert/zstd_convert.go",
    "chars": 904,
    "preview": "package convutil\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/klauspost/compress/zstd\"\n)\n\ntype ZStdConvert struct{}"
  },
  {
    "path": "backend/utils/map/map_util.go",
    "chars": 5188,
    "preview": "package maputil\n\nimport (\n\t. \"tinyrdm/backend/utils\"\n\t\"tinyrdm/backend/utils/coll\"\n)\n\n// Get 获取键值对指定键的值, 如果不存在则返回自定默认值\nf"
  },
  {
    "path": "backend/utils/math/math_util.go",
    "chars": 1530,
    "preview": "package mathutil\n\nimport (\n\t\"math\"\n\t. \"tinyrdm/backend/utils\"\n)\n\n// MaxWithIndex 查找所有元素中的最大值\nfunc MaxWithIndex[T Hashabl"
  },
  {
    "path": "backend/utils/proxy/http.go",
    "chars": 1805,
    "preview": "package proxy\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"golang.org/x/net/proxy\"\n)\n\ntype HttpPro"
  },
  {
    "path": "backend/utils/redis/log_hook.go",
    "chars": 2665,
    "preview": "package redis\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/redis/go-redis/v9\"\n)\n\ntype exec"
  },
  {
    "path": "backend/utils/slice/slice_util.go",
    "chars": 1524,
    "preview": "package sliceutil\n\nimport (\n\t\"strings\"\n\t. \"tinyrdm/backend/utils\"\n)\n\n// Map map items to new array\nfunc Map[S ~[]T, T an"
  },
  {
    "path": "backend/utils/string/any_convert.go",
    "chars": 3795,
    "preview": "package strutil\n\nimport (\n\t\"encoding/json\"\n\t\"strconv\"\n\t\"strings\"\n\tsliceutil \"tinyrdm/backend/utils/slice\"\n)\n\nfunc AnyToS"
  },
  {
    "path": "backend/utils/string/common.go",
    "chars": 656,
    "preview": "package strutil\n\nimport (\n\t\"unicode\"\n)\n\nfunc ContainsBinary(str string) bool {\n\t//buf := []byte(str)\n\t//size := 0\n\t//for"
  },
  {
    "path": "backend/utils/string/json_formatter.go",
    "chars": 3701,
    "preview": "package strutil\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n)\n\n// Convert from https://github.com/ObuchiYuki/SwiftJSONFormatter\n\n// "
  },
  {
    "path": "backend/utils/string/key_convert.go",
    "chars": 1441,
    "preview": "package strutil\n\nimport (\n\t\"strconv\"\n\tsliceutil \"tinyrdm/backend/utils/slice\"\n)\n\n// EncodeRedisKey encode the redis key "
  },
  {
    "path": "build/README.md",
    "chars": 1591,
    "preview": "# Build Directory\n\nThe build directory is used to house all the build files and assets for your application.\n\nThe struct"
  },
  {
    "path": "build/darwin/Info.dev.plist",
    "chars": 1189,
    "preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
  },
  {
    "path": "build/darwin/Info.plist",
    "chars": 1049,
    "preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
  },
  {
    "path": "build/dmg/fix-app",
    "chars": 1330,
    "preview": "#!/bin/bash\nclear\nBLACK=\"\\033[0;30m\"\nDARK_GRAY=\"\\033[1;30m\"\nBLUE=\"\\033[0;34m\"\nLIGHT_BLUE=\"\\033[1;34m\"\nGREEN=\"\\033[0;32m\""
  },
  {
    "path": "build/dmg/fix-app_zh",
    "chars": 1152,
    "preview": "#!/bin/bash\nclear\nBLACK=\"\\033[0;30m\"\nDARK_GRAY=\"\\033[1;30m\"\nBLUE=\"\\033[0;34m\"\nLIGHT_BLUE=\"\\033[1;34m\"\nGREEN=\"\\033[0;32m\""
  },
  {
    "path": "build/linux/tiny-rdm_0.0.0_amd64/DEBIAN/control",
    "chars": 258,
    "preview": "Package: {{.Name}}\nVersion: {{.Info.ProductVersion}}\nSection: base\nPriority: optional\nArchitecture: amd64\nDepends: {{.li"
  },
  {
    "path": "build/linux/tiny-rdm_0.0.0_amd64/usr/local/bin/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "build/linux/tiny-rdm_0.0.0_amd64/usr/share/applications/tiny-rdm.desktop",
    "chars": 225,
    "preview": "[Desktop Entry]\nName={{.Info.ProductName}}\nExec=/usr/local/bin/tiny-rdm %U\nTerminal=false\nType=Application\nIcon=tiny-rdm"
  },
  {
    "path": "build/windows/info.json",
    "chars": 385,
    "preview": "{\n  \"fixed\": {\n    \"file_version\": \"{{.Info.ProductVersion}}\"\n  },\n  \"info\": {\n    \"0000\": {\n      \"ProductVersion\": \"{{"
  },
  {
    "path": "build/windows/installer/project.nsi",
    "chars": 4591,
    "preview": "Unicode true\n\n####\n## Please note: Template replacements don't work in this file. They are provided with default defines"
  },
  {
    "path": "build/windows/installer/wails_tools.nsh",
    "chars": 5324,
    "preview": "# DO NOT EDIT - Generated automatically by `wails build`\n\n!include \"x64.nsh\"\n!include \"WinVer.nsh\"\n!include \"FileFunc.ns"
  },
  {
    "path": "build/windows/wails.exe.manifest",
    "chars": 1036,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com"
  },
  {
    "path": "docker/entrypoint.sh",
    "chars": 142,
    "preview": "#!/bin/sh\nset -e\n\n# Start nginx in background (serves frontend + reverse proxy)\nnginx\n\n# Start Go backend in foreground\n"
  },
  {
    "path": "docker/nginx.conf",
    "chars": 964,
    "preview": "server {\n    listen 8086;\n    server_name _;\n\n    root /usr/share/nginx/html;\n    index index.html;\n\n    # SPA fallback\n"
  },
  {
    "path": "docker-compose.yml",
    "chars": 341,
    "preview": "services:\n  tinyrdm:\n    image: ghcr.io/tiny-craft/tiny-rdm:latest\n    container_name: tinyrdm\n    restart: unless-stopp"
  },
  {
    "path": "docs/index.html",
    "chars": 187,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta http-equiv=\"refresh\" content=\"0;url=https://github.com/tiny-craft/tiny"
  },
  {
    "path": "frontend/.prettierrc",
    "chars": 172,
    "preview": "{\n  \"printWidth\": 120,\n  \"tabWidth\": 4,\n  \"singleQuote\": true,\n  \"semi\": false,\n  \"bracketSameLine\": true,\n  \"endOfLine\""
  },
  {
    "path": "frontend/README.md",
    "chars": 40,
    "preview": "# Frontend of Tiny RDM\n\nUse Vue3 + Vite\n"
  },
  {
    "path": "frontend/index.html",
    "chars": 429,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\" />\n    <meta content=\"width=device-width, initial-scal"
  },
  {
    "path": "frontend/package.json",
    "chars": 880,
    "preview": "{\n    \"name\": \"frontend\",\n    \"private\": true,\n    \"version\": \"0.0.0\",\n    \"type\": \"module\",\n    \"scripts\": {\n        \"d"
  },
  {
    "path": "frontend/src/App.vue",
    "chars": 10121,
    "preview": "<script setup>\nimport ConnectionDialog from './components/dialogs/ConnectionDialog.vue'\nimport NewKeyDialog from './comp"
  },
  {
    "path": "frontend/src/AppContent.vue",
    "chars": 9832,
    "preview": "<script setup>\nimport ContentPane from './components/content/ContentPane.vue'\nimport BrowserPane from './components/side"
  },
  {
    "path": "frontend/src/assets/fonts/OFL.txt",
    "chars": 4371,
    "preview": "Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),\n\nThis Font Software is licensed under the SIL Open F"
  },
  {
    "path": "frontend/src/components/LoginPage.vue",
    "chars": 9972,
    "preview": "<script setup>\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useThemeVars } from 'naive-ui'\nimport icon"
  },
  {
    "path": "frontend/src/components/common/AutoRefreshForm.vue",
    "chars": 1854,
    "preview": "<script setup>\nimport { isNumber } from 'lodash'\n\nconst props = defineProps({\n    loading: {\n        type: Boolean,\n    "
  },
  {
    "path": "frontend/src/components/common/DropdownSelector.vue",
    "chars": 3682,
    "preview": "<script setup>\nimport { computed, h, ref } from 'vue'\nimport { get, isEmpty, some } from 'lodash'\nimport { NIcon, NText "
  },
  {
    "path": "frontend/src/components/common/EditableTableColumn.vue",
    "chars": 1739,
    "preview": "<script setup>\nimport IconButton from './IconButton.vue'\nimport Delete from '@/components/icons/Delete.vue'\nimport Edit "
  },
  {
    "path": "frontend/src/components/common/EditableTableRow.vue",
    "chars": 577,
    "preview": "<script setup>\nimport { NInput } from 'naive-ui'\n\nconst props = defineProps({\n    isEdit: Boolean,\n    value: [String, N"
  },
  {
    "path": "frontend/src/components/common/FileOpenInput.vue",
    "chars": 1141,
    "preview": "<script setup>\nimport { SelectFile } from 'wailsjs/go/services/systemService.js'\nimport { get, isEmpty } from 'lodash'\n\n"
  },
  {
    "path": "frontend/src/components/common/FileSaveInput.vue",
    "chars": 1085,
    "preview": "<script setup>\nimport { SaveFile } from 'wailsjs/go/services/systemService.js'\nimport { get } from 'lodash'\n\nconst props"
  },
  {
    "path": "frontend/src/components/common/IconButton.vue",
    "chars": 2808,
    "preview": "<script setup>\nimport { computed, useSlots } from 'vue'\nimport { NIcon } from 'naive-ui'\n\nconst props = defineProps({\n  "
  },
  {
    "path": "frontend/src/components/common/RedisTypeSelector.vue",
    "chars": 3523,
    "preview": "<script setup>\nimport { computed, h } from 'vue'\nimport { NSpace, useThemeVars } from 'naive-ui'\nimport { types, typesBg"
  },
  {
    "path": "frontend/src/components/common/RedisTypeTag.vue",
    "chars": 3095,
    "preview": "<script setup>\nimport { computed } from 'vue'\nimport { typesBgColor, typesColor, typesShortName } from '@/consts/support"
  },
  {
    "path": "frontend/src/components/common/ResizeableWrapper.vue",
    "chars": 2658,
    "preview": "<script setup>\nimport { useThemeVars } from 'naive-ui'\nimport { ref } from 'vue'\n\n/**\n * Resizeable component wrapper\n *"
  },
  {
    "path": "frontend/src/components/common/SwitchButton.vue",
    "chars": 1787,
    "preview": "<script setup>\nimport { NIcon } from 'naive-ui'\n\nconst props = defineProps({\n    value: {\n        type: Number,\n        "
  },
  {
    "path": "frontend/src/components/common/ToolbarControlWidget.vue",
    "chars": 2887,
    "preview": "<script setup>\nimport WindowMin from '@/components/icons/WindowMin.vue'\nimport WindowMax from '@/components/icons/Window"
  },
  {
    "path": "frontend/src/components/common/TtlInput.vue",
    "chars": 1371,
    "preview": "<script setup>\nimport { computed } from 'vue'\n\nconst props = defineProps({\n    value: {\n        type: Number,\n        de"
  },
  {
    "path": "frontend/src/components/content/ContentLogPane.vue",
    "chars": 5148,
    "preview": "<script setup>\nimport { computed, h, nextTick, reactive, ref } from 'vue'\nimport IconButton from '@/components/common/Ic"
  },
  {
    "path": "frontend/src/components/content/ContentPane.vue",
    "chars": 9254,
    "preview": "<script setup>\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { find, map, toUpper } from 'lodash'\nimport u"
  },
  {
    "path": "frontend/src/components/content/ContentServerPane.vue",
    "chars": 3803,
    "preview": "<script setup>\nimport { computed } from 'vue'\nimport AddLink from '@/components/icons/AddLink.vue'\nimport useDialogStore"
  },
  {
    "path": "frontend/src/components/content/ContentValueTab.vue",
    "chars": 3324,
    "preview": "<script setup>\nimport Server from '@/components/icons/Server.vue'\nimport useTabStore from 'stores/tab.js'\nimport { compu"
  },
  {
    "path": "frontend/src/components/content_value/ContentCli.vue",
    "chars": 17156,
    "preview": "<script setup>\nimport { Terminal } from 'xterm'\nimport { FitAddon } from 'xterm-addon-fit'\nimport { computed, onMounted,"
  },
  {
    "path": "frontend/src/components/content_value/ContentEditor.vue",
    "chars": 6468,
    "preview": "<script setup>\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'\nimport * as monaco from 'mon"
  },
  {
    "path": "frontend/src/components/content_value/ContentEntryEditor.vue",
    "chars": 8238,
    "preview": "<script setup>\nimport { computed, nextTick, reactive, ref, watchEffect } from 'vue'\nimport { useThemeVars } from 'naive-"
  },
  {
    "path": "frontend/src/components/content_value/ContentMonitor.vue",
    "chars": 6234,
    "preview": "<script setup>\nimport { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'\nimport { debounce, filter"
  },
  {
    "path": "frontend/src/components/content_value/ContentPubsub.vue",
    "chars": 9884,
    "preview": "<script setup>\nimport { computed, h, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'\nimport { debounce, get"
  },
  {
    "path": "frontend/src/components/content_value/ContentSearchInput.vue",
    "chars": 5702,
    "preview": "<script setup>\nimport { computed, nextTick, reactive } from 'vue'\nimport { debounce, isEmpty, trim } from 'lodash'\nimpor"
  },
  {
    "path": "frontend/src/components/content_value/ContentServerStatus.vue",
    "chars": 26315,
    "preview": "<script setup>\nimport {\n    cloneDeep,\n    flatMap,\n    get,\n    isEmpty,\n    map,\n    mapValues,\n    pickBy,\n    random"
  },
  {
    "path": "frontend/src/components/content_value/ContentSlog.vue",
    "chars": 6752,
    "preview": "<script setup>\nimport { computed, h, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'\nimport Refresh from '@"
  },
  {
    "path": "frontend/src/components/content_value/ContentToolbar.vue",
    "chars": 6638,
    "preview": "<script setup>\nimport { validType } from '@/consts/support_redis_type.js'\nimport useDialog from 'stores/dialog.js'\nimpor"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueHash.vue",
    "chars": 15467,
    "preview": "<script setup>\nimport { computed, h, reactive, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport AddLink from '"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueJson.vue",
    "chars": 4705,
    "preview": "<script setup>\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport Copy from '@/components/ico"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueList.vue",
    "chars": 13543,
    "preview": "<script setup>\nimport { computed, h, reactive, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport AddLink from '"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueSet.vue",
    "chars": 13485,
    "preview": "<script setup>\nimport { computed, h, reactive, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport AddLink from '"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueStream.vue",
    "chars": 7791,
    "preview": "<script setup>\nimport { computed, h, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport AddLink from '@/componen"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueString.vue",
    "chars": 6784,
    "preview": "<script setup>\nimport { computed, reactive, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport Copy from "
  },
  {
    "path": "frontend/src/components/content_value/ContentValueWrapper.vue",
    "chars": 7449,
    "preview": "<script setup>\nimport { types, types as redisTypes } from '@/consts/support_redis_type.js'\nimport ContentValueString fro"
  },
  {
    "path": "frontend/src/components/content_value/ContentValueZSet.vue",
    "chars": 14721,
    "preview": "<script setup>\nimport { computed, h, reactive, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport AddLink from '"
  },
  {
    "path": "frontend/src/components/content_value/FormatSelector.vue",
    "chars": 3409,
    "preview": "<script setup>\nimport { decodeTypes, formatTypes } from '@/consts/value_view_type.js'\nimport Code from '@/components/ico"
  },
  {
    "path": "frontend/src/components/dialogs/AboutDialog.vue",
    "chars": 1807,
    "preview": "<script setup>\nimport iconUrl from '@/assets/images/icon.png'\nimport useDialog from 'stores/dialog.js'\nimport usePrefere"
  },
  {
    "path": "frontend/src/components/dialogs/AddFieldsDialog.vue",
    "chars": 7420,
    "preview": "<script setup>\nimport { computed, reactive, watchEffect } from 'vue'\nimport { types } from '@/consts/support_redis_type."
  },
  {
    "path": "frontend/src/components/dialogs/ConnectionDialog.vue",
    "chars": 40700,
    "preview": "<script setup>\nimport { every, get, includes, isEmpty, map, reject, sortBy, toNumber, trim } from 'lodash'\nimport { comp"
  },
  {
    "path": "frontend/src/components/dialogs/DecoderDialog.vue",
    "chars": 9565,
    "preview": "<script setup>\nimport useDialog from 'stores/dialog.js'\nimport { computed, reactive, ref, toRaw, watch } from 'vue'\nimpo"
  },
  {
    "path": "frontend/src/components/dialogs/DeleteKeyDialog.vue",
    "chars": 6604,
    "preview": "<script setup>\nimport { computed, nextTick, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog"
  },
  {
    "path": "frontend/src/components/dialogs/ExportKeyDialog.vue",
    "chars": 4642,
    "preview": "<script setup>\nimport { computed, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport u"
  },
  {
    "path": "frontend/src/components/dialogs/FlushDbDialog.vue",
    "chars": 3155,
    "preview": "<script setup>\nimport { reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport { useI18n }"
  },
  {
    "path": "frontend/src/components/dialogs/GroupDialog.vue",
    "chars": 3465,
    "preview": "<script setup>\nimport { computed, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport {"
  },
  {
    "path": "frontend/src/components/dialogs/ImportKeyDialog.vue",
    "chars": 5902,
    "preview": "<script setup>\nimport { computed, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport {"
  },
  {
    "path": "frontend/src/components/dialogs/KeyFilterDialog.vue",
    "chars": 3450,
    "preview": "<script setup>\nimport { computed, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport {"
  },
  {
    "path": "frontend/src/components/dialogs/NewKeyDialog.vue",
    "chars": 8990,
    "preview": "<script setup>\nimport { computed, h, nextTick, reactive, ref, watchEffect } from 'vue'\nimport { types, typesColor } from"
  },
  {
    "path": "frontend/src/components/dialogs/PreferencesDialog.vue",
    "chars": 19753,
    "preview": "<script setup>\nimport { computed, h, ref, watchEffect } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport useDialog f"
  },
  {
    "path": "frontend/src/components/dialogs/RenameKeyDialog.vue",
    "chars": 2357,
    "preview": "<script setup>\nimport { reactive, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport { useI18n } from"
  },
  {
    "path": "frontend/src/components/dialogs/SetTtlDialog.vue",
    "chars": 4046,
    "preview": "<script setup>\nimport { computed, reactive, ref, watchEffect } from 'vue'\nimport useDialog from 'stores/dialog'\nimport u"
  },
  {
    "path": "frontend/src/components/icons/Add.vue",
    "chars": 960,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/AddGroup.vue",
    "chars": 809,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/AddLink.vue",
    "chars": 674,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/AlignCenter.vue",
    "chars": 1055,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/AlignLeft.vue",
    "chars": 1053,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Binary.vue",
    "chars": 590,
    "preview": "<script setup></script>\n\n<template>\n    <svg\n        fill=\"none\"\n        height=\"24\"\n        stroke=\"currentColor\"\n     "
  },
  {
    "path": "frontend/src/components/icons/Bottom.vue",
    "chars": 882,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 4,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Checkbox.vue",
    "chars": 1049,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Checked.vue",
    "chars": 711,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 4,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Clear.vue",
    "chars": 1583,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Close.vue",
    "chars": 1568,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Cluster.vue",
    "chars": 2209,
    "preview": "<script setup>\nconst props = defineProps({\n    inverse: {\n        type: Boolean,\n        default: false,\n    },\n    stro"
  },
  {
    "path": "frontend/src/components/icons/Code.vue",
    "chars": 1143,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Config.vue",
    "chars": 1758,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Connect.vue",
    "chars": 1664,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Conversion.vue",
    "chars": 1519,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Copy.vue",
    "chars": 1127,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/CopyLink.vue",
    "chars": 1994,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Database.vue",
    "chars": 1467,
    "preview": "<script setup>\nconst props = defineProps({\n    inverse: {\n        type: Boolean,\n        default: false,\n    },\n    stro"
  },
  {
    "path": "frontend/src/components/icons/Delete.vue",
    "chars": 1083,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Detail.vue",
    "chars": 1978,
    "preview": "<script setup>\nconst props = defineProps({\n    inverse: {\n        type: Boolean,\n        default: false,\n    },\n    stro"
  },
  {
    "path": "frontend/src/components/icons/Down.vue",
    "chars": 485,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Edit.vue",
    "chars": 704,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/EditFile.vue",
    "chars": 977,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Export.vue",
    "chars": 887,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Filter.vue",
    "chars": 469,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Folder.vue",
    "chars": 1518,
    "preview": "<script setup>\nconst props = defineProps({\n    open: {\n        type: Boolean,\n        default: false,\n    },\n    strokeW"
  },
  {
    "path": "frontend/src/components/icons/FullScreen.vue",
    "chars": 1066,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Github.vue",
    "chars": 2033,
    "preview": "<script setup>\n// const props = defineProps({\n//     strokeWidth: {\n//         type: [Number, String],\n//         defaul"
  },
  {
    "path": "frontend/src/components/icons/Help.vue",
    "chars": 1373,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Import.vue",
    "chars": 876,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Key.vue",
    "chars": 573,
    "preview": "<script setup>\nconst props = defineProps({\n    fillColor: {\n        type: String,\n        default: '#f2c55c',\n    },\n})\n"
  },
  {
    "path": "frontend/src/components/icons/Lang.vue",
    "chars": 1471,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Layer.vue",
    "chars": 787,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/ListView.vue",
    "chars": 1650,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/LoadAll.vue",
    "chars": 749,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/LoadList.vue",
    "chars": 1451,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Loading.vue",
    "chars": 1553,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  },
  {
    "path": "frontend/src/components/icons/Log.vue",
    "chars": 1376,
    "preview": "<script setup>\nconst props = defineProps({\n    inverse: {\n        type: Boolean,\n        default: false,\n    },\n    stro"
  },
  {
    "path": "frontend/src/components/icons/Logout.vue",
    "chars": 887,
    "preview": "<script setup>\nconst props = defineProps({\n    strokeWidth: {\n        type: [Number, String],\n        default: 3,\n    },"
  }
]

// ... and 106 more files (download for full content)

About this extraction

This page contains the full source code of the tiny-craft/tiny-rdm GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 306 files (1.3 MB), approximately 359.8k tokens, and a symbol index with 840 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!