From e9cce4429abfd5ec65694ed9345d8a30d3c58b09 Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Mon, 26 Jan 2026 18:20:46 -0700 Subject: [PATCH 1/2] chore: tons of new stuff, bug fixes and more --- .github/AUTOMATIC_RELEASES.md | 218 +++ .github/CODE_OF_CONDUCT.md | 52 + CONTRIBUTING.md => .github/CONTRIBUTING.md | 14 +- .github/SECURITY.md | 69 + .github/VERSIONING.md | 154 ++ .github/scripts/analyze-commits.js | 262 +++ .github/scripts/get-version.js | 155 ++ .github/scripts/update-trusted-hosts.js | 198 ++ .github/scripts/validate-trusted-hosts.js | 86 + .github/scripts/validate-tsconfig.js | 102 + .github/workflows/auto-release.yml | 182 ++ .github/workflows/format-ci.yml | 24 + .github/workflows/knip-ci.yml | 38 + .github/workflows/lint-ci.yml | 24 + .github/workflows/semver-ci.yml | 115 -- .github/workflows/tsconfig-validation.yml | 60 + .github/workflows/update-trusted-hosts.yml | 94 + .github/workflows/validate-providers.yml | 166 ++ .husky/commit-msg | 4 + CHANGELOG.md | 182 ++ README.md | 202 +- app/(blog)/blog/(root)/page.tsx | 45 +- app/(blog)/blog/[slug]/page.tsx | 51 +- app/(blog)/blog/atom.xml/route.ts | 47 + app/(blog)/blog/feed.json/route.ts | 40 + app/(blog)/blog/feed.xml/route.ts | 44 + app/(blog)/opengraph-image.tsx | 105 +- app/(blog)/twitter-image.tsx | 78 +- app/(docs)/docs/[...slug]/page.tsx | 12 +- app/(docs)/docs/layout.tsx | 18 + app/(docs)/opengraph-image.tsx | 128 +- app/(docs)/twitter-image.tsx | 78 +- app/(landing)/opengraph-image.tsx | 153 +- app/(landing)/page.tsx | 15 + app/(landing)/twitter-image.tsx | 83 +- app/apple-icon.tsx | 91 + app/artifacts/layout.tsx | 14 +- app/artifacts/opengraph-image.tsx | 154 +- app/artifacts/twitter-image.tsx | 80 +- app/banner-wide/route.tsx | 114 ++ app/banner/route.tsx | 114 ++ app/brand/page.tsx | 127 ++ app/chat/layout.tsx | 15 +- app/chat/opengraph-image.tsx | 124 +- app/chat/twitter-image.tsx | 80 +- app/components/file-source.tsx | 16 +- app/components/image-modal.tsx | 19 +- app/docs-og/[...slug]/route.tsx | 2 + app/favicon.ico | Bin 196637 -> 37294 bytes app/hosting/layout.tsx | 22 + app/hosting/opengraph-image.tsx | 169 ++ app/hosting/page.tsx | 278 +++ app/hosting/provider-card.tsx | 148 ++ app/hosting/twitter-image.tsx | 169 ++ app/icon.tsx | 90 + app/layout.config.tsx | 10 +- app/layout.tsx | 210 ++- app/manifest.ts | 37 +- app/natives/layout.tsx | 14 +- app/natives/opengraph-image.tsx | 131 +- app/natives/page.tsx | 2 +- app/natives/twitter-image.tsx | 80 +- app/og/[...slug]/route.tsx | 2 + app/opengraph-image.tsx | 179 ++ app/robots.ts | 47 +- app/sitemap.ts | 103 +- app/twitter-image.tsx | 101 + commitlint.config.js | 25 + commitlintrc.json | 5 + content/blog/first-fivem-server-guide.mdx | 129 +- content/blog/fivem-evolution.mdx | 133 +- content/blog/txadmin-ultimate-guide.mdx | 144 +- content/blog/welcome.mdx | 159 +- content/docs/cfx/best-practices/index.mdx | 9 + .../performance-optimization.mdx | 9 + .../best-practices/resource-development.mdx | 9 + content/docs/cfx/best-practices/security.mdx | 10 + .../cfx/best-practices/server-management.mdx | 10 + .../cfx/common-errors/database-connection.mdx | 40 + .../docs/cfx/common-errors/event-handling.mdx | 21 + .../cfx/common-errors/framework-errors.mdx | 46 + content/docs/cfx/common-errors/index.mdx | 11 + .../cfx/common-errors/manifest-errors.mdx | 35 +- .../docs/cfx/common-errors/network-issues.mdx | 7 + .../cfx/common-errors/resource-loading.mdx | 14 + .../server-thread-hitch-warning.mdx | 26 +- .../cfx/common-errors/state-bag-issues.mdx | 16 + .../cfx/common-errors/thread-stalling.mdx | 14 +- content/docs/cfx/common-tools/debug-tools.mdx | 27 + content/docs/cfx/common-tools/index.mdx | 27 + .../docs/cfx/common-tools/network-tools.mdx | 24 + content/docs/cfx/common-tools/profiler.mdx | 14 + .../cfx/common-tools/resource-monitor.mdx | 32 + content/docs/cfx/faq.mdx | 2 + content/docs/cfx/index.mdx | 74 +- .../cfx/performance/client-optimization.mdx | 10 + .../cfx/performance/database-optimization.mdx | 11 + content/docs/cfx/performance/index.mdx | 12 +- .../cfx/performance/resource-optimization.mdx | 11 + .../cfx/performance/server-optimization.mdx | 11 + .../cfx/resource-development/client-side.mdx | 28 + .../cfx/resource-development/debugging.mdx | 16 + .../docs/cfx/resource-development/index.mdx | 32 + .../resource-development/manifest-files.mdx | 33 + .../resource-development/nui-development.mdx | 16 + .../cfx/resource-development/server-side.mdx | 33 + content/docs/cfx/support.mdx | 2 + content/docs/core/api/artifacts.mdx | 1 + content/docs/core/api/chat.mdx | 1 + content/docs/core/api/contributors.mdx | 1 + content/docs/core/api/natives.mdx | 1 + content/docs/core/api/search.mdx | 1 + content/docs/core/disclaimer.mdx | 4 + content/docs/core/faq.mdx | 54 +- content/docs/core/glossary.mdx | 73 +- content/docs/core/index.mdx | 45 +- content/docs/frameworks/esx/development.mdx | 2 + content/docs/frameworks/esx/index.mdx | 87 +- content/docs/frameworks/esx/setup.mdx | 17 + .../docs/frameworks/esx/troubleshooting.mdx | 4 + content/docs/frameworks/index.mdx | 70 +- .../docs/frameworks/qbcore/development.mdx | 2 + content/docs/frameworks/qbcore/index.mdx | 119 +- content/docs/frameworks/qbcore/setup.mdx | 21 + .../frameworks/qbcore/troubleshooting.mdx | 4 + content/docs/guides/backup-recovery.mdx | 18 + content/docs/guides/common-threats.mdx | 511 +++++ content/docs/guides/database-setup.mdx | 37 + content/docs/guides/database-tools.mdx | 6 + content/docs/guides/index.mdx | 172 +- content/docs/guides/meta.json | 9 +- content/docs/guides/resource-installation.mdx | 25 + content/docs/guides/security-permissions.mdx | 32 + content/docs/guides/server-artifacts.mdx | 18 + content/docs/guides/server-configuration.mdx | 12 + content/docs/txadmin/advanced.mdx | 16 + content/docs/txadmin/backup-system.mdx | 17 + content/docs/txadmin/configuration.mdx | 91 +- content/docs/txadmin/discord-bot.mdx | 391 ++++ content/docs/txadmin/index.mdx | 106 +- content/docs/txadmin/linux/install.mdx | 198 -- content/docs/txadmin/meta.json | 9 +- content/docs/txadmin/permissions.mdx | 153 +- content/docs/txadmin/server-management.mdx | 165 +- content/docs/txadmin/troubleshooting.mdx | 36 +- content/docs/txadmin/web-panel.mdx | 9 + content/docs/txadmin/windows/index.mdx | 56 +- content/docs/txadmin/windows/install.mdx | 566 ++++-- content/docs/vmenu/configuration.mdx | 193 +- content/docs/vmenu/features.mdx | 317 ++-- content/docs/vmenu/index.mdx | 174 +- content/docs/vmenu/permissions.mdx | 245 +-- content/docs/vmenu/setup.mdx | 58 +- content/docs/vmenu/troubleshooting.mdx | 31 +- knip.json | 79 + lib/examples/artifacts.go | 584 ------ lib/providers.ts | 113 ++ lib/trusted-hosts.ts | 84 + next.config.mjs | 14 + package.json | 40 +- packages/core/src/useFetch/index.ts | 24 +- packages/providers/GUIDELINES.md | 120 ++ packages/providers/README.md | 234 +++ packages/providers/TRUSTED_HOSTS_README.md | 148 ++ packages/providers/schema.json | 95 + packages/providers/trusted-hosts-schema.json | 68 + packages/providers/trusted-hosts.json | 30 + packages/providers/zap-hosting/provider.json | 41 + {types => packages/types}/artifacts.ts | 0 packages/ui/src/components/accordion.tsx | 56 + packages/ui/src/components/chat-sidebar.tsx | 229 ++- packages/ui/src/components/index.ts | 2 + packages/ui/src/components/mdx-components.tsx | 1659 +++++++++++++++++ .../ui/src/components/mobile-chat-drawer.tsx | 167 +- .../ui/src/components/mobile-chat-header.tsx | 31 +- packages/ui/src/components/source-code.tsx | 16 +- packages/ui/src/components/type-table.tsx | 212 +++ .../src/core/artifacts/artifacts-content.tsx | 774 +++++--- .../src/core/artifacts/artifacts-drawer.tsx | 166 +- .../src/core/artifacts/artifacts-sidebar.tsx | 38 +- .../artifacts/mobile-artifacts-header.tsx | 29 +- packages/ui/src/core/chat/ChatInterface.tsx | 208 ++- packages/ui/src/core/layout/hero.tsx | 6 +- .../core/natives/mobile-natives-header.tsx | 54 +- .../ui/src/core/natives/natives-content.tsx | 356 +++- .../src/core/natives/natives-filter-sheet.tsx | 265 +-- .../ui/src/core/natives/natives-sidebar.tsx | 393 ++-- packages/ui/src/styles/globals.css | 600 ++++++ packages/ui/src/styles/index.ts | 1 + .../ui/src/styles}/sheet-handle.css | 0 packages/utils/src/constants/keywords.ts | 187 +- public/.well-known/gpc.json | 4 + public/.well-known/security.txt | 13 + public/ai.txt | 48 + public/humans.txt | 30 + public/llms-full.txt | 186 ++ public/llms.txt | 63 + public/logo.png | Bin 374282 -> 37294 bytes public/og.png | Bin 6037604 -> 0 bytes public/opensearch.xml | 17 + .../{txadmin => citizenfx}/cfx-auth.png | Bin public/screenshots/citizenfx/cfx-portal.png | Bin 0 -> 90461 bytes .../{txadmin => citizenfx}/create-a-key.png | Bin .../server-license-name.png | Bin .../these-buttons-suck.png | Bin .../{txadmin => citizenfx}/view-your-key.png | Bin .../txadmin/discord-bot/settings.png | Bin 0 -> 290367 bytes public/screenshots/txadmin/players-page.png | Bin 0 -> 184247 bytes public/screenshots/txadmin/resources-tab.png | Bin 0 -> 160378 bytes public/zap.png | Bin 0 -> 89692 bytes tsconfig.json | 37 +- 211 files changed, 15299 insertions(+), 3727 deletions(-) create mode 100644 .github/AUTOMATIC_RELEASES.md create mode 100644 .github/CODE_OF_CONDUCT.md rename CONTRIBUTING.md => .github/CONTRIBUTING.md (93%) create mode 100644 .github/SECURITY.md create mode 100644 .github/VERSIONING.md create mode 100644 .github/scripts/analyze-commits.js create mode 100644 .github/scripts/get-version.js create mode 100644 .github/scripts/update-trusted-hosts.js create mode 100644 .github/scripts/validate-trusted-hosts.js create mode 100644 .github/scripts/validate-tsconfig.js create mode 100644 .github/workflows/auto-release.yml create mode 100644 .github/workflows/format-ci.yml create mode 100644 .github/workflows/knip-ci.yml create mode 100644 .github/workflows/lint-ci.yml delete mode 100644 .github/workflows/semver-ci.yml create mode 100644 .github/workflows/tsconfig-validation.yml create mode 100644 .github/workflows/update-trusted-hosts.yml create mode 100644 .github/workflows/validate-providers.yml create mode 100644 .husky/commit-msg create mode 100644 app/(blog)/blog/atom.xml/route.ts create mode 100644 app/(blog)/blog/feed.json/route.ts create mode 100644 app/(blog)/blog/feed.xml/route.ts create mode 100644 app/apple-icon.tsx create mode 100644 app/banner-wide/route.tsx create mode 100644 app/banner/route.tsx create mode 100644 app/brand/page.tsx create mode 100644 app/hosting/layout.tsx create mode 100644 app/hosting/opengraph-image.tsx create mode 100644 app/hosting/page.tsx create mode 100644 app/hosting/provider-card.tsx create mode 100644 app/hosting/twitter-image.tsx create mode 100644 app/icon.tsx create mode 100644 app/opengraph-image.tsx create mode 100644 app/twitter-image.tsx create mode 100644 commitlint.config.js create mode 100644 commitlintrc.json create mode 100644 content/docs/guides/common-threats.mdx create mode 100644 content/docs/txadmin/discord-bot.mdx delete mode 100644 content/docs/txadmin/linux/install.mdx create mode 100644 knip.json delete mode 100644 lib/examples/artifacts.go create mode 100644 lib/providers.ts create mode 100644 lib/trusted-hosts.ts create mode 100644 packages/providers/GUIDELINES.md create mode 100644 packages/providers/README.md create mode 100644 packages/providers/TRUSTED_HOSTS_README.md create mode 100644 packages/providers/schema.json create mode 100644 packages/providers/trusted-hosts-schema.json create mode 100644 packages/providers/trusted-hosts.json create mode 100644 packages/providers/zap-hosting/provider.json rename {types => packages/types}/artifacts.ts (100%) create mode 100644 packages/ui/src/components/accordion.tsx create mode 100644 packages/ui/src/components/mdx-components.tsx create mode 100644 packages/ui/src/components/type-table.tsx rename {styles => packages/ui/src/styles}/sheet-handle.css (100%) create mode 100644 public/.well-known/gpc.json create mode 100644 public/.well-known/security.txt create mode 100644 public/ai.txt create mode 100644 public/humans.txt create mode 100644 public/llms-full.txt create mode 100644 public/llms.txt delete mode 100644 public/og.png create mode 100644 public/opensearch.xml rename public/screenshots/{txadmin => citizenfx}/cfx-auth.png (100%) create mode 100644 public/screenshots/citizenfx/cfx-portal.png rename public/screenshots/{txadmin => citizenfx}/create-a-key.png (100%) rename public/screenshots/{txadmin => citizenfx}/server-license-name.png (100%) rename public/screenshots/{txadmin => citizenfx}/these-buttons-suck.png (100%) rename public/screenshots/{txadmin => citizenfx}/view-your-key.png (100%) create mode 100644 public/screenshots/txadmin/discord-bot/settings.png create mode 100644 public/screenshots/txadmin/players-page.png create mode 100644 public/screenshots/txadmin/resources-tab.png create mode 100644 public/zap.png diff --git a/.github/AUTOMATIC_RELEASES.md b/.github/AUTOMATIC_RELEASES.md new file mode 100644 index 0000000..c846b79 --- /dev/null +++ b/.github/AUTOMATIC_RELEASES.md @@ -0,0 +1,218 @@ +# Automatic Release System + +FixFX uses automated versioning and changelog generation based on commit messages. When you push to `develop` or `master`, the system automatically: + +1. **Analyzes commits** since the last release +2. **Determines the next version** (semantic versioning) +3. **Updates CHANGELOG.md** +4. **Creates a GitHub release** with auto-generated notes + +No manual intervention needed! + +## Commit Message Format (Conventional Commits) + +The automation works by analyzing commit messages in this format: + +``` +type: description + +type(scope): description + +type!: description (with breaking change) +``` + +### Types + +- **`feat`** - New feature → Minor version bump (1.0.0 → 1.1.0) +- **`fix`** - Bug fix → Patch version bump (1.0.0 → 1.0.1) +- **`breaking`** or **`feat!`** - Breaking change → Major version bump (1.0.0 → 2.0.0) +- **`chore`** - Build, deps, config changes (NOT included in release) +- **`docs`** - Documentation only (NOT included in release) +- **`test`** - Test changes (NOT included in release) + +### Examples + +```bash +# Feature +git commit -m "feat: add hosting provider directory structure" + +# Fix +git commit -m "fix: correct provider validation schema reference" + +# With scope +git commit -m "feat(providers): add guidelines documentation" + +# Breaking change (using !) +git commit -m "feat!: reorganize provider file structure" + +# Breaking change (using breaking keyword) +git commit -m "breaking: remove deprecated API endpoints" + +# Chore (won't trigger release) +git commit -m "chore: update dependencies" +git commit -m "docs: improve README" +``` + +## Workflow Triggers + +### ✅ Runs Automatically On + +- **Direct pushes to `develop` branch** - Analyzes commits, updates changelog, creates release +- **Direct pushes to `master` branch** - Same as develop +- **Merges to `develop`/`master`** - Same as direct pushes +- **Changes to frontend files** - Only triggers when frontend code changes + +### ❌ Does NOT Run On + +- **Pull requests** - Prevents duplicate automation when merging +- **Non-frontend changes** - Ignores changes to backend, docs, etc. +- **Manual `chore:`, `docs:`, `test:` commits** - No version bump needed + +## What Gets Released + +The system looks at commits since the **last GitHub release tag** and: + +- Counts **breaking changes** → Major version bump +- Counts **features** → Minor version bump +- Counts **fixes** → Patch version bump +- Ignores **chore/docs/test** commits + +### Examples + +``` +Last Release: v1.0.0 + +Commits since: + ✅ feat: add new feature + ✅ fix: fix a bug + +Next Release: v1.1.0 (minor bump for feature) +``` + +``` +Last Release: v1.0.0 + +Commits since: + ✅ breaking: remove old API + ✅ feat: add new feature + ✅ fix: fix a bug + +Next Release: v2.0.0 (major bump for breaking change) +``` + +## Automatic Changelog Generation + +The changelog is automatically generated from your commit messages: + +```markdown +## [1.1.0] - 2026-01-26 + +### Breaking Changes +- Remove deprecated authentication method + +### Added +- Add hosting provider directory structure +- Add provider guidelines documentation + +### Fixed +- Correct schema validation reference +``` + +This appears in: +1. **CHANGELOG.md** - Updated automatically +2. **GitHub Release Notes** - Added automatically + +## Disabling Auto-Release + +If you need to prevent a release for a particular push: + +```bash +# Use [skip-release] in commit message +git commit -m "feat: add feature [skip-release]" + +# Or use non-conventional commit format (will be listed as "Other Changes") +git commit -m "Update something random" +``` + +Note: Non-feature/fix/breaking commits are grouped as "Other Changes" and don't trigger version bumps. + +## Manual Releases + +For complete control, you can still create releases manually: + +1. Push your commits with conventional messages +2. Manually create a release on GitHub with tag `v1.2.3` +3. The system will recognize it as the latest version +4. Next auto-release will calculate from this version + +## Troubleshooting + +### "No pending changes that require a release" + +Your commits don't include `feat:`, `fix:`, or `breaking:` prefixes. + +**Solution:** Use proper conventional commit format + +```bash +# Wrong +git commit -m "added new provider support" + +# Right +git commit -m "feat: add new provider support" +``` + +### Release created but CHANGELOG not updated + +The CHANGELOG update happens in the workflow. Check: +1. Workflow logs in GitHub Actions +2. That commits use conventional format +3. That at least one `feat:`, `fix:`, or `breaking:` commit exists + +### Version not incrementing correctly + +Check the last release tag: + +```bash +git tag # List all tags +git describe --tags --abbrev=0 # Show latest tag +``` + +Make sure the tag follows `vMAJOR.MINOR.PATCH` format. + +## Commit Message Guidelines + +For the best auto-generated changelogs: + +### Good commit messages +``` +feat: add provider guidelines documentation +feat(providers): reorganize directory structure +fix: resolve schema validation issue +breaking: remove deprecated API endpoints +``` + +### Bad commit messages +``` +update stuff +fixed things +WIP: feature +various improvements +``` + +The commit message after the type/scope is included in the changelog, so keep them clear and descriptive! + +## GitHub Actions Secrets + +No additional secrets needed! The workflow uses the default `GITHUB_TOKEN` which has permission to: +- Read commits and tags +- Create releases +- Push changes back to the repository + +## Next Steps + +1. **Start using conventional commits** in your workflow +2. **Push to develop/master** when ready to release +3. **Let the automation handle** changelog and release creation +4. **Monitor GitHub Actions** to verify successful releases + +That's it! No more manual version tracking. diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b42a5b9 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,52 @@ +# Code of Conduct + +## Our Commitment + +We are committed to providing a welcoming, inclusive, and harassment-free environment for everyone who participates in the FixFX project. This applies to all project spaces including GitHub, Discord, and any other communication channels. + +## Expected Behavior + +All participants are expected to: + +- Be respectful and considerate in all interactions +- Provide constructive feedback and accept it gracefully +- Focus on what is best for the community and project +- Show empathy towards other community members +- Use inclusive language + +## Unacceptable Behavior + +The following behaviors are not tolerated: + +- Harassment, intimidation, or discrimination of any kind +- Personal attacks, insults, or derogatory comments +- Trolling or deliberately inflammatory remarks +- Publishing others' private information without consent +- Spam, excessive self-promotion, or off-topic content +- Any conduct that would be considered inappropriate in a professional setting + +## Scope + +This Code of Conduct applies to all project spaces and to individuals representing the project in public spaces. This includes the GitHub repository, Discord server, social media, and any other official channels. + +## Enforcement + +Instances of unacceptable behavior may be reported to the project maintainers: + +- **Email**: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) +- **Discord**: [discord.gg/Vv2bdC44Ge](https://discord.gg/Vv2bdC44Ge) + +All reports will be reviewed and investigated promptly and fairly. Maintainers are obligated to respect the privacy and security of the reporter. + +### Consequences + +Project maintainers will determine appropriate action for violations, which may include: + +1. A private warning with clarity on the violation +2. A public warning +3. Temporary ban from project spaces +4. Permanent ban from project spaces + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1. diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 93% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index 224622b..ab5055f 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,10 +1,10 @@ # Contributing to FixFX -Thank you for your interest in contributing to FixFX! We welcome contributions from the community and appreciate your help in making FixFX better. +Thank you for your interest in contributing to FixFX. We welcome contributions from the community. ## Code of Conduct -Please be respectful and constructive in all interactions. We are committed to providing a welcoming and inclusive environment for all contributors. +Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. We expect all contributors to be respectful and constructive. ## How to Contribute @@ -283,14 +283,10 @@ When adding new features: ## Questions? -- Check existing issues and discussions -- Ask in our [Discord community](https://discord.gg/ErBmGbZfwT) -- Create a discussion on GitHub +- Check existing [GitHub Issues](https://github.com/CodeMeAPixel/FixFX/issues) +- Join our [Discord](https://discord.gg/Vv2bdC44Ge) +- Email: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) ## License By contributing to FixFX, you agree that your contributions will be licensed under the AGPL 3.0 License. - ---- - -Thank you for contributing to FixFX! Your efforts help make FiveM development more accessible to everyone. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..ff8c239 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,69 @@ +# Security Policy + +## Supported Versions + +We actively maintain security updates for the following versions: + +| Version | Supported | +|---------|-----------| +| Latest | Yes | +| < 1.0 | No | + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly. + +### How to Report + +**Do not open a public issue for security vulnerabilities.** + +Instead, please use one of the following methods: + +1. **Email**: Send details to [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) +2. **GitHub Security Advisories**: Use the [Security tab](https://github.com/CodeMeAPixel/FixFX/security/advisories/new) to privately report the issue + +### What to Include + +When reporting a vulnerability, please provide: + +- A clear description of the vulnerability +- Steps to reproduce the issue +- Potential impact of the vulnerability +- Any suggested fixes or mitigations (if applicable) + +### Response Timeline + +- **Initial Response**: Within 48 hours +- **Status Update**: Within 7 days +- **Resolution Target**: Within 30 days for critical issues + +### After Reporting + +1. We will acknowledge receipt of your report +2. We will investigate and validate the issue +3. We will work on a fix and coordinate disclosure timing with you +4. We will credit you in the security advisory (unless you prefer anonymity) + +## Security Best Practices + +When contributing to FixFX: + +- Keep dependencies updated +- Never commit secrets, API keys, or credentials +- Follow secure coding practices +- Validate and sanitize all user inputs +- Use environment variables for sensitive configuration + +## Scope + +This security policy applies to: + +- The FixFX frontend application +- The FixFX backend API +- Official deployment infrastructure + +Third-party integrations and dependencies are outside our direct control but we will work with upstream maintainers when issues are discovered. + +## Recognition + +We appreciate security researchers who help keep FixFX safe. Contributors who responsibly disclose vulnerabilities will be acknowledged in our security advisories and README. diff --git a/.github/VERSIONING.md b/.github/VERSIONING.md new file mode 100644 index 0000000..7a02fae --- /dev/null +++ b/.github/VERSIONING.md @@ -0,0 +1,154 @@ +# Versioning Guide + +FixFX uses GitHub releases as the single source of truth for version numbers. This approach eliminates version duplication between `package.json` and `CHANGELOG.md`. + +## How It Works + +1. **GitHub Releases** are authoritative - version lives in release tags (e.g., `v1.0.0`) +2. **CHANGELOG.md** documents changes for each version +3. **Script** (`get-version.js`) fetches the latest release from GitHub API +4. **package.json** does NOT have a hardcoded version field + +## Getting the Current Version + +### From Command Line + +```bash +# Get version as plain text +npm run version + +# Get version as JSON +npm run version:json + +# Direct script usage +node .github/scripts/get-version.js +``` + +### In JavaScript/TypeScript Code + +```typescript +// Async import and use +import { getVersion } from '../.github/scripts/get-version.js'; + +const version = await getVersion(); +console.log(`FixFX v${version}`); +``` + +### In Build Process + +Add to your build script: + +```bash +node .github/scripts/get-version.js --file public/version.txt +``` + +## Creating a New Release + +When releasing a new version: + +1. **Update CHANGELOG.md** with the version changes +2. **Create a GitHub Release** with tag `v1.0.0` (format: `vMAJOR.MINOR.PATCH`) +3. **Script automatically discovers** the version +4. **No need to update package.json** + +### Example Release Process + +```bash +# 1. Update CHANGELOG.md sections +# Change "## [1.1.0] - Unreleased" to "## [1.1.0] - 2026-01-26" + +# 2. Commit changes +git add CHANGELOG.md +git commit -m "chore: release v1.1.0" +git push + +# 3. Create release on GitHub +# Go to https://github.com/CodeMeAPixel/FixFX/releases/new +# Tag: v1.1.0 +# Title: Release v1.1.0 +# Copy content from CHANGELOG for this version +# Click "Publish release" + +# 4. The version is now discoverable by the script! +npm run version # outputs: 1.1.0 +``` + +## Tag Naming Convention + +- **Must start with `v`**: `v1.0.0` ✅ (not `1.0.0`) +- **Must follow Semantic Versioning**: `vMAJOR.MINOR.PATCH` +- **Optional prerelease**: `v1.0.0-beta.0`, `v1.0.0-rc.1` +- **Invalid**: `v1.0`, `1.0.0`, `version-1.0.0` + +## Fallback Behavior + +If no releases exist yet: +- Script returns `0.0.0-unknown` +- Useful during initial development +- Once you create first release, it auto-discovers + +### For Strict Mode (CI/CD) + +```bash +node .github/scripts/get-version.js --strict +# Exits with code 1 if fetch fails +``` + +## GitHub API Rate Limits + +The script respects GitHub's API rate limits: + +- **Unauthenticated**: 60 requests/hour +- **Authenticated**: 5,000 requests/hour (if `GITHUB_TOKEN` env var set) + +For CI/CD, set the token: + +```bash +GITHUB_TOKEN=your_token npm run version +``` + +## When to Use This Approach + +✅ **Good for:** +- Open source projects with public releases +- Teams that release regularly +- Reducing merge conflicts on version changes +- Keeping version in one place (GitHub) + +❌ **Not ideal for:** +- Private packages without public releases +- High-frequency build systems (performance sensitive) +- Offline-first development (no API access) + +## Troubleshooting + +### "No releases found" + +You haven't created any releases yet. Create the first one: + +```bash +# On GitHub: Releases → Draft a new release +# Tag: v1.0.0 +# Publish +``` + +### "GitHub API request timed out" + +Network issue or GitHub API is slow. Try again or set `GITHUB_TOKEN` for priority. + +### "Invalid version format" + +Release tag doesn't match expected format. Use `vMAJOR.MINOR.PATCH` format. + +## Alternative: Reading from File + +If you prefer storing version in a file instead: + +```json +// version.json +{ + "version": "1.0.0" +} +``` + +But GitHub releases approach is cleaner since releases are already required for distribution. diff --git a/.github/scripts/analyze-commits.js b/.github/scripts/analyze-commits.js new file mode 100644 index 0000000..222ecff --- /dev/null +++ b/.github/scripts/analyze-commits.js @@ -0,0 +1,262 @@ +#!/usr/bin/env node + +/** + * Analyze commits since the last release + * + * Determines the next version based on commit messages using + * conventional commits format (feat:, fix:, breaking:) + * + * Usage: + * node analyze-commits.js + * node analyze-commits.js --json + */ + +const { execSync } = require('child_process'); + +const REPO_OWNER = 'CodeMeAPixel'; +const REPO_NAME = 'FixFX'; + +function exec(command) { + try { + return execSync(command, { encoding: 'utf-8' }).trim(); + } catch (error) { + throw new Error(`Command failed: ${command}\n${error.message}`); + } +} + +function getLastTag() { + try { + return exec('git describe --tags --abbrev=0 2>/dev/null || echo ""'); + } catch { + return ''; + } +} + +function getCommitsSinceTag(tag) { + try { + if (!tag) { + // No tags yet, get all commits + return exec('git log --oneline --all'); + } + return exec(`git log ${tag}..HEAD --oneline`); + } catch (error) { + return ''; + } +} + +function parseVersion(versionString) { + // Remove 'v' prefix if present + const version = versionString.replace(/^v/, ''); + const parts = version.split('.'); + + return { + major: parseInt(parts[0]) || 0, + minor: parseInt(parts[1]) || 0, + patch: parseInt(parts[2]) || 0, + prerelease: parts[3] ? parts.slice(3).join('.') : null, + }; +} + +function parseCommits(commitLog) { + const commits = commitLog.split('\n').filter(Boolean); + + const analysis = { + features: [], + fixes: [], + breaking: [], + other: [], + }; + + for (const commit of commits) { + const match = commit.match(/^([a-f0-9]+)\s+(.+?):\s*(.+?)(?:\s*\((.+?)\))?$/); + + if (!match) { + analysis.other.push(commit); + continue; + } + + const [, hash, type, message, scope] = match; + const commitData = { + hash: hash.substring(0, 7), + type, + message: message.trim(), + scope: scope || null, + full: commit, + }; + + if (type === 'feat') { + analysis.features.push(commitData); + } else if (type === 'fix') { + analysis.fixes.push(commitData); + } else if (type === 'breaking' || message.includes('BREAKING CHANGE')) { + analysis.breaking.push(commitData); + } else { + analysis.other.push(commitData); + } + } + + return analysis; +} + +function calculateNextVersion(currentVersion, analysis) { + const current = parseVersion(currentVersion || '0.0.0'); + + // Breaking changes = major version bump + if (analysis.breaking.length > 0) { + return { + major: current.major + 1, + minor: 0, + patch: 0, + }; + } + + // Features = minor version bump + if (analysis.features.length > 0) { + return { + major: current.major, + minor: current.minor + 1, + patch: 0, + }; + } + + // Fixes only = patch version bump + if (analysis.fixes.length > 0) { + return { + major: current.major, + minor: current.minor, + patch: current.patch + 1, + }; + } + + // No relevant commits + return null; +} + +function formatVersion(versionObj) { + if (!versionObj) return null; + return `${versionObj.major}.${versionObj.minor}.${versionObj.patch}`; +} + +function generateChangelogEntry(version, analysis) { + const date = new Date().toISOString().split('T')[0]; + let entry = `## [${version}] - ${date}\n\n`; + + if (analysis.breaking.length > 0) { + entry += `### Breaking Changes\n`; + for (const commit of analysis.breaking) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.features.length > 0) { + entry += `### Added\n`; + for (const commit of analysis.features) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.fixes.length > 0) { + entry += `### Fixed\n`; + for (const commit of analysis.fixes) { + entry += `- ${commit.message}${commit.scope ? ` (${commit.scope})` : ''} (${commit.hash})\n`; + } + entry += '\n'; + } + + if (analysis.other.length > 0) { + entry += `### Other Changes\n`; + for (const commit of analysis.other) { + entry += `- ${commit}\n`; + } + entry += '\n'; + } + + return entry; +} + +async function analyzeCommits() { + try { + // Get the last tag + const lastTag = getLastTag(); + const currentVersion = lastTag ? lastTag.replace(/^v/, '') : '0.0.0'; + + // Get commits since last tag + const commitLog = getCommitsSinceTag(lastTag); + + if (!commitLog) { + return { + hasPendingChanges: false, + currentVersion, + nextVersion: null, + analysis: null, + changelog: null, + }; + } + + // Parse commits + const analysis = parseCommits(commitLog); + + // Calculate next version + const nextVersion = calculateNextVersion(currentVersion, analysis); + + if (!nextVersion) { + return { + hasPendingChanges: false, + currentVersion, + nextVersion: null, + analysis, + changelog: null, + }; + } + + const nextVersionString = formatVersion(nextVersion); + const changelog = generateChangelogEntry(nextVersionString, analysis); + + return { + hasPendingChanges: true, + currentVersion, + nextVersion: nextVersionString, + analysis, + changelog, + tag: `v${nextVersionString}`, + }; + } catch (error) { + console.error(`Error analyzing commits: ${error.message}`); + process.exit(1); + } +} + +async function main() { + const args = process.argv.slice(2); + const useJson = args.includes('--json'); + + const result = await analyzeCommits(); + + if (useJson) { + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(`Current Version: ${result.currentVersion}`); + if (result.nextVersion) { + console.log(`Next Version: ${result.nextVersion}`); + console.log(`Tag: ${result.tag}`); + console.log(`\nPending Changes:`); + console.log(` - Features: ${result.analysis.features.length}`); + console.log(` - Fixes: ${result.analysis.fixes.length}`); + console.log(` - Breaking: ${result.analysis.breaking.length}`); + console.log(` - Other: ${result.analysis.other.length}`); + } else { + console.log('No pending changes that require a release'); + } + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} + +module.exports = { analyzeCommits, parseCommits, calculateNextVersion, generateChangelogEntry }; diff --git a/.github/scripts/get-version.js b/.github/scripts/get-version.js new file mode 100644 index 0000000..7866b5c --- /dev/null +++ b/.github/scripts/get-version.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node + +/** + * Get the current version from GitHub releases + * + * This script fetches the latest release from the FixFX repository + * and extracts the version from the tag name. + * + * Usage: + * node get-version.js # outputs to stdout + * node get-version.js --file # writes to file + * node get-version.js --json # outputs as JSON object + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const REPO_OWNER = 'CodeMeAPixel'; +const REPO_NAME = 'FixFX'; +const DEFAULT_VERSION = '0.0.0-unknown'; + +async function fetchLatestRelease() { + return new Promise((resolve, reject) => { + const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`; + + const options = { + hostname: 'api.github.com', + path: `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`, + method: 'GET', + headers: { + 'User-Agent': 'FixFX-Version-Script', + 'Accept': 'application/vnd.github.v3+json', + }, + timeout: 5000, + }; + + // Use GitHub token if available for higher rate limits + if (process.env.GITHUB_TOKEN) { + options.headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`; + } + + const req = https.request(options, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + if (res.statusCode === 404) { + // No releases found + resolve(null); + return; + } + + if (res.statusCode !== 200) { + reject(new Error(`GitHub API returned ${res.statusCode}: ${data}`)); + return; + } + + const release = JSON.parse(data); + resolve(release); + } catch (error) { + reject(new Error(`Failed to parse GitHub API response: ${error.message}`)); + } + }); + }); + + req.on('timeout', () => { + req.destroy(); + reject(new Error('GitHub API request timed out')); + }); + + req.on('error', reject); + req.end(); + }); +} + +function extractVersion(tagName) { + // Remove leading 'v' if present (e.g., 'v1.0.0' → '1.0.0') + let version = tagName.replace(/^v/, ''); + + // Validate semver format (basic check) + if (!/^\d+\.\d+\.\d+/.test(version)) { + throw new Error(`Invalid version format: ${tagName}`); + } + + return version; +} + +async function getVersion(options = {}) { + try { + const release = await fetchLatestRelease(); + + if (!release) { + console.warn(`No releases found for ${REPO_OWNER}/${REPO_NAME}, using default version`); + return DEFAULT_VERSION; + } + + const version = extractVersion(release.tag_name); + return version; + } catch (error) { + if (options.strict) { + console.error(`Error fetching version: ${error.message}`); + process.exit(1); + } else { + console.warn(`Error fetching version: ${error.message}, using default version`); + return DEFAULT_VERSION; + } + } +} + +async function main() { + const args = process.argv.slice(2); + const options = {}; + + // Parse arguments + for (let i = 0; i < args.length; i++) { + if (args[i] === '--file') { + options.file = args[i + 1]; + i++; + } else if (args[i] === '--json') { + options.json = true; + } else if (args[i] === '--strict') { + options.strict = true; + } + } + + const version = await getVersion(options); + + if (options.json) { + console.log(JSON.stringify({ version, repo: `${REPO_OWNER}/${REPO_NAME}` }, null, 2)); + } else if (options.file) { + try { + fs.writeFileSync(options.file, version, 'utf-8'); + console.log(`Version written to ${options.file}: ${version}`); + } catch (error) { + console.error(`Failed to write version to file: ${error.message}`); + process.exit(1); + } + } else { + console.log(version); + } +} + +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error:', error.message); + process.exit(1); + }); +} + +module.exports = { fetchLatestRelease, extractVersion, getVersion }; diff --git a/.github/scripts/update-trusted-hosts.js b/.github/scripts/update-trusted-hosts.js new file mode 100644 index 0000000..1cc9c9c --- /dev/null +++ b/.github/scripts/update-trusted-hosts.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node + +/** + * Scraper for FiveM trusted hosting providers + * Automatically fetches and validates hosting providers from FiveM's official registry + * Used by GitHub Actions to keep the trusted-hosts.json file up to date + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const FIVEM_HOSTING_URL = 'https://fivem.net/server-hosting'; +const TRUSTED_HOSTS_FILE = path.join(__dirname, 'trusted-hosts.json'); +const SCHEMA_FILE = path.join(__dirname, 'trusted-hosts-schema.json'); + +/** + * Fetch the FiveM hosting page and extract provider information + */ +async function fetchFiveMListing() { + return new Promise((resolve, reject) => { + https.get(FIVEM_HOSTING_URL, { headers: { 'User-Agent': 'FixFX-TrustedHostsScraper/1.0' } }, (res) => { + let data = ''; + + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + try { + resolve(data); + } catch (error) { + reject(error); + } + }); + }).on('error', reject); + }); +} + +/** + * Parse hosting provider information from HTML + * This extracts provider cards that follow a predictable structure + */ +function parseHostingProviders(html) { + const providers = []; + + // Look for hosting provider cards - they typically have specific patterns + // Pattern 1: Look for links with typical hosting provider domain patterns + const linkRegex = /]*href=["']([^"']*(?:zap-hosting|gtxgaming|nitrado|g-portal|firestorm|nitrado|gameservers|lgsm)[^"']*)["'][^>]*>([^<]+)<\/a>/gi; + let match; + + const seen = new Set(); + + while ((match = linkRegex.exec(html)) !== null) { + const url = match[1]; + const name = match[2].trim(); + + // Skip duplicates and invalid entries + if (!url || !name || seen.has(url.toLowerCase())) continue; + + seen.add(url.toLowerCase()); + + // Normalize URL + const normalizedUrl = new URL(url.includes('://') ? url : `https://${url}`).href; + + providers.push({ + id: name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''), + name: name, + url: normalizedUrl, + description: `Trusted FiveM/RedM hosting provider`, + verified: true, + lastVerified: new Date().toISOString() + }); + } + + // Known hosting providers fallback (if scraping doesn't find them) + const knownProviders = [ + { id: 'zap-hosting', name: 'ZAP-Hosting', url: 'https://zap-hosting.com' }, + { id: 'gtxgaming', name: 'GTXGaming', url: 'https://gtxgaming.co.uk' }, + { id: 'nitrado', name: 'Nitrado', url: 'https://nitrado.net' }, + { id: 'g-portal', name: 'G-Portal', url: 'https://www.g-portal.com' }, + { id: 'firestorm-servers', name: 'Firestorm Servers', url: 'https://firestormservers.com' }, + { id: 'gameservers', name: 'GameServers', url: 'https://www.gameservers.com' } + ]; + + // Add known providers if not already found + for (const known of knownProviders) { + if (!providers.find(p => p.id === known.id)) { + providers.push({ + ...known, + description: `Trusted FiveM/RedM hosting provider`, + verified: false, + lastVerified: new Date().toISOString() + }); + } + } + + return providers; +} + +/** + * Validate provider against schema + */ +function validateAgainstSchema(provider, schema) { + const errors = []; + + // Check required fields + if (!provider.id) errors.push('Missing required field: id'); + if (!provider.name) errors.push('Missing required field: name'); + if (!provider.url) errors.push('Missing required field: url'); + + // Validate ID format + if (provider.id && !/^[a-z0-9-]+$/.test(provider.id)) { + errors.push(`Invalid id format: ${provider.id}`); + } + + // Validate URL format + if (provider.url) { + try { + new URL(provider.url); + } catch { + errors.push(`Invalid URL format: ${provider.url}`); + } + } + + // Validate string lengths + if (provider.name && provider.name.length > 255) { + errors.push(`Name exceeds maximum length: ${provider.name.length} > 255`); + } + + if (provider.description && provider.description.length > 500) { + errors.push(`Description exceeds maximum length`); + } + + return errors; +} + +/** + * Main execution + */ +async function main() { + try { + console.log('🌐 Fetching FiveM trusted hosting providers...'); + const html = await fetchFiveMListing(); + + console.log('📊 Parsing provider information...'); + const hosts = parseHostingProviders(html); + + if (hosts.length === 0) { + console.warn('⚠️ No providers found. Using defaults.'); + } else { + console.log(`✅ Found ${hosts.length} hosting providers`); + } + + // Load schema for validation + const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); + + // Validate each provider + const validationErrors = []; + for (const host of hosts) { + const errors = validateAgainstSchema(host, schema); + if (errors.length > 0) { + console.warn(`⚠️ Validation issues for ${host.name}:`, errors); + validationErrors.push({ provider: host.name, errors }); + } + } + + // Create output object + const output = { + lastUpdated: new Date().toISOString(), + source: 'https://fivem.net/server-hosting', + hosts: hosts.sort((a, b) => a.name.localeCompare(b.name)) + }; + + // Write to file + fs.writeFileSync(TRUSTED_HOSTS_FILE, JSON.stringify(output, null, 2)); + console.log(`📝 Updated ${TRUSTED_HOSTS_FILE}`); + + // Print summary + console.log('\n📋 Summary:'); + console.log(` Total providers: ${hosts.length}`); + console.log(` Verified: ${hosts.filter(h => h.verified).length}`); + console.log(` Unverified: ${hosts.filter(h => !h.verified).length}`); + + if (validationErrors.length > 0) { + console.warn(`\n⚠️ Found ${validationErrors.length} validation warnings`); + process.exit(0); // Don't fail on warnings + } + + console.log('\n✅ Successfully updated trusted hosts list'); + process.exit(0); + } catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); + } +} + +main(); diff --git a/.github/scripts/validate-trusted-hosts.js b/.github/scripts/validate-trusted-hosts.js new file mode 100644 index 0000000..7ccedab --- /dev/null +++ b/.github/scripts/validate-trusted-hosts.js @@ -0,0 +1,86 @@ +#!/usr/bin/env node + +/** + * Validator for trusted-hosts.json + * Ensures the file conforms to the schema and contains valid data + */ + +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); + +const TRUSTED_HOSTS_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts.json'); +const SCHEMA_FILE = path.join(__dirname, '..', '..', 'packages', 'providers', 'trusted-hosts-schema.json'); + +try { + // Load files + const trustedHosts = JSON.parse(fs.readFileSync(TRUSTED_HOSTS_FILE, 'utf8')); + const schema = JSON.parse(fs.readFileSync(SCHEMA_FILE, 'utf8')); + + // Initialize AJV validator + const ajv = new Ajv(); + const validate = ajv.compile(schema); + + // Validate against schema + const isValid = validate(trustedHosts); + + if (!isValid) { + console.error('❌ Schema validation failed:\n'); + validate.errors.forEach(error => { + console.error(` ${error.instancePath || 'root'}: ${error.message}`); + }); + process.exit(1); + } + + // Additional validations + const errors = []; + const warnings = []; + + // Check for duplicate IDs + const ids = new Set(); + for (const host of trustedHosts.hosts) { + if (ids.has(host.id)) { + errors.push(`Duplicate provider ID: ${host.id}`); + } + ids.add(host.id); + } + + // Check for duplicate URLs + const urls = new Set(); + for (const host of trustedHosts.hosts) { + const normalizedUrl = new URL(host.url).href; + if (urls.has(normalizedUrl)) { + warnings.push(`Duplicate URL detected: ${host.url}`); + } + urls.add(normalizedUrl); + } + + // Check lastUpdated is recent (within 30 days) + const lastUpdated = new Date(trustedHosts.lastUpdated); + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + if (lastUpdated < thirtyDaysAgo) { + warnings.push('List was last updated more than 30 days ago. Consider running the scraper.'); + } + + // Report results + if (errors.length > 0) { + console.error('❌ Validation errors:\n'); + errors.forEach(error => console.error(` • ${error}`)); + process.exit(1); + } + + if (warnings.length > 0) { + console.warn('⚠️ Validation warnings:\n'); + warnings.forEach(warning => console.warn(` • ${warning}`)); + } + + console.log('✅ trusted-hosts.json validation passed'); + console.log(` Total providers: ${trustedHosts.hosts.length}`); + console.log(` Last updated: ${new Date(trustedHosts.lastUpdated).toLocaleString()}`); + console.log(` Source: ${trustedHosts.source}`); + + process.exit(0); +} catch (error) { + console.error('❌ Error:', error.message); + process.exit(1); +} diff --git a/.github/scripts/validate-tsconfig.js b/.github/scripts/validate-tsconfig.js new file mode 100644 index 0000000..ef113f3 --- /dev/null +++ b/.github/scripts/validate-tsconfig.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Load both versions +const currentPath = 'tsconfig.json'; +const basePath = 'base-branch/tsconfig.json'; + +let currentConfig, baseConfig; + +try { + currentConfig = JSON.parse(fs.readFileSync(currentPath, 'utf8')); + baseConfig = JSON.parse(fs.readFileSync(basePath, 'utf8')); +} catch (error) { + console.error('Error parsing JSON files:', error.message); + process.exit(1); +} + +let hasErrors = false; +const errors = []; + +// Check for removed keys in compilerOptions +if (currentConfig.compilerOptions && baseConfig.compilerOptions) { + for (const key in baseConfig.compilerOptions) { + if (!(key in currentConfig.compilerOptions)) { + errors.push(`❌ Removed compiler option: "${key}"`); + hasErrors = true; + } + } +} + +// Check for modified values in compilerOptions +if (currentConfig.compilerOptions && baseConfig.compilerOptions) { + for (const key in baseConfig.compilerOptions) { + const baseValue = JSON.stringify(baseConfig.compilerOptions[key]); + const currentValue = JSON.stringify(currentConfig.compilerOptions[key]); + + if (baseValue !== currentValue && key in currentConfig.compilerOptions) { + errors.push(`❌ Modified compiler option "${key}": "${baseValue}" → "${currentValue}"`); + hasErrors = true; + } + } +} + +// Check for removed keys in paths +if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { + for (const key in baseConfig.compilerOptions.paths) { + if (!(key in currentConfig.compilerOptions.paths)) { + errors.push(`❌ Removed path alias: "${key}"`); + hasErrors = true; + } + } +} + +// Check for modified paths values +if (currentConfig.compilerOptions?.paths && baseConfig.compilerOptions?.paths) { + for (const key in baseConfig.compilerOptions.paths) { + const baseValue = JSON.stringify(baseConfig.compilerOptions.paths[key]); + const currentValue = JSON.stringify(currentConfig.compilerOptions.paths[key]); + + if (baseValue !== currentValue && key in currentConfig.compilerOptions.paths) { + errors.push(`❌ Modified path alias "${key}": ${baseValue} → ${currentValue}`); + hasErrors = true; + } + } +} + +// Check for removed keys in include +if (currentConfig.include && baseConfig.include) { + const baseIncludes = new Set(baseConfig.include); + for (const item of baseIncludes) { + if (!currentConfig.include.includes(item)) { + errors.push(`❌ Removed include pattern: "${item}"`); + hasErrors = true; + } + } +} + +// Check for removed keys in exclude +if (currentConfig.exclude && baseConfig.exclude) { + const baseExcludes = new Set(baseConfig.exclude); + for (const item of baseExcludes) { + if (!currentConfig.exclude.includes(item)) { + errors.push(`❌ Removed exclude pattern: "${item}"`); + hasErrors = true; + } + } +} + +if (hasErrors) { + console.log('\n❌ tsconfig.json validation FAILED\n'); + console.log('Critical Configuration Protection:\n'); + errors.forEach(error => console.log(error)); + console.log('\n⚠️ Only ADDITIONS to tsconfig.json are allowed.'); + console.log('Removing or modifying existing configurations will break the site.\n'); + process.exit(1); +} else { + console.log('✅ tsconfig.json validation PASSED'); + console.log('Only additions detected (or no changes to existing configuration).\n'); + process.exit(0); +} diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..bded2e4 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,182 @@ +name: Auto Release & Changelog + +on: + push: + branches: + - develop + - master + paths: + - 'frontend/**' + - '.github/workflows/auto-release.yml' + # Explicitly exclude pull requests + # This workflow only runs on direct pushes/merges to branches + +jobs: + analyze: + name: Analyze Commits & Check for Release + runs-on: ubuntu-latest + outputs: + has-changes: ${{ steps.analyze.outputs.has-changes }} + next-version: ${{ steps.analyze.outputs.next-version }} + tag: ${{ steps.analyze.outputs.tag }} + changelog: ${{ steps.analyze.outputs.changelog }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history to analyze commits + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Analyze commits since last release + id: analyze + working-directory: frontend + run: | + # Run the analysis script and capture JSON output + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + + echo "Analysis Result:" + echo "$ANALYSIS" | jq '.' + + # Extract values + HAS_CHANGES=$(echo "$ANALYSIS" | jq -r '.hasPendingChanges') + NEXT_VERSION=$(echo "$ANALYSIS" | jq -r '.nextVersion // empty') + TAG=$(echo "$ANALYSIS" | jq -r '.tag // empty') + + # Read changelog (save to temp file for multiline output) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog // empty') + + echo "has-changes=$HAS_CHANGES" >> $GITHUB_OUTPUT + echo "next-version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + + # Store changelog in a file for the next step + if [ -n "$CHANGELOG" ]; then + echo "$CHANGELOG" > /tmp/changelog-entry.md + echo "changelog-file=/tmp/changelog-entry.md" >> $GITHUB_OUTPUT + fi + + update-changelog: + name: Update CHANGELOG.md + needs: analyze + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get changelog entry + id: get-changelog + working-directory: frontend + run: | + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') + + # Use a delimiter for multiline output + echo "entry<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "CHANGELOG_DELIMITER" >> $GITHUB_OUTPUT + + - name: Update CHANGELOG.md + working-directory: frontend + run: | + # Read the current CHANGELOG + CURRENT=$(cat CHANGELOG.md) + + # Get the changelog entry + ENTRY="${{ steps.get-changelog.outputs.entry }}" + + # Find the line with "## [1.1.0] - Unreleased" and replace with new version + # This handles the unreleased section by replacing it with the new version + + # Create new changelog with the entry inserted after the header + { + head -n 8 CHANGELOG.md # Keep header and intro + echo "" + echo "$ENTRY" + tail -n +9 CHANGELOG.md | sed 's/## \[1\.1\.0\] - Unreleased/## [Unreleased] - TBD/' || tail -n +9 CHANGELOG.md + } > CHANGELOG.md.tmp + + mv CHANGELOG.md.tmp CHANGELOG.md + + - name: Commit and push changelog + run: | + cd frontend + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add CHANGELOG.md + git commit -m "chore: update changelog for version ${{ needs.analyze.outputs.next-version }}" + git push + + create-release: + name: Create GitHub Release + needs: [analyze, update-changelog] + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Get release notes from changelog + id: release-notes + working-directory: frontend + run: | + ANALYSIS=$(node .github/scripts/analyze-commits.js --json) + CHANGELOG=$(echo "$ANALYSIS" | jq -r '.changelog') + + echo "notes<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "RELEASE_NOTES_DELIMITER" >> $GITHUB_OUTPUT + + - name: Create Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ needs.analyze.outputs.tag }} + release_name: Release ${{ needs.analyze.outputs.next-version }} + body: ${{ steps.release-notes.outputs.notes }} + draft: false + prerelease: false + + - name: Log Release Created + run: | + echo "✅ Release created!" + echo "Tag: ${{ needs.analyze.outputs.tag }}" + echo "Version: ${{ needs.analyze.outputs.next-version }}" + + notify-no-changes: + name: Notify if No Changes + needs: analyze + runs-on: ubuntu-latest + if: needs.analyze.outputs.has-changes == 'false' + + steps: + - name: Log status + run: | + echo "ℹ️ No changes requiring a release detected" + echo "Please commit changes with conventional commit format:" + echo " feat: Add new feature" + echo " fix: Fix a bug" + echo " breaking: Breaking change" diff --git a/.github/workflows/format-ci.yml b/.github/workflows/format-ci.yml new file mode 100644 index 0000000..3b36ac6 --- /dev/null +++ b/.github/workflows/format-ci.yml @@ -0,0 +1,24 @@ +name: Format +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + prettier: + name: Prettier + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Check formatting + run: bun run format:check diff --git a/.github/workflows/knip-ci.yml b/.github/workflows/knip-ci.yml new file mode 100644 index 0000000..fe9eefa --- /dev/null +++ b/.github/workflows/knip-ci.yml @@ -0,0 +1,38 @@ +name: Knip Validation +on: + push: + branches: [master, develop] + +jobs: + build_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Build application + run: bun run build + + - name: Validate Export Usage + run: bun run knip:exports + + - name: Validate Dependency Usage + run: bun run knip:deps + + - name: Validate File Usage + run: bun run knip:files + + - name: Validate Production Usage + run: bun run knip:prod + + - name: Notify success + run: echo "Build completed successfully!" \ No newline at end of file diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml new file mode 100644 index 0000000..80abdbc --- /dev/null +++ b/.github/workflows/lint-ci.yml @@ -0,0 +1,24 @@ +name: Lint +on: + push: + branches: [master, develop] + pull_request: + branches: [master, develop] + +jobs: + eslint: + name: ESLint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run ESLint + run: bun run lint diff --git a/.github/workflows/semver-ci.yml b/.github/workflows/semver-ci.yml deleted file mode 100644 index 52898c1..0000000 --- a/.github/workflows/semver-ci.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Release CI - -on: - workflow_dispatch: - inputs: - semver: - description: 'Select version bump type: major, minor, patch' - required: false - default: 'patch' - type: choice - options: - - major - - minor - - patch - prerelease: - description: 'Pre-release identifier (e.g., alpha, beta, rc.1). Leave empty for stable releases.' - required: false - default: '' - custom_version: - description: 'Specify a custom version. This will override the semver and prerelease inputs.' - required: false - default: '' - release_notes: - description: 'Release notes for this version' - required: false - default: '' - create_release: - description: 'Create a GitHub release' - type: boolean - required: false - default: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build application - run: npm run build - - - name: Set Git identity - run: | - git config --local user.email "toxic.dev09@gmail.com" - git config --local user.name "Toxic Dev" - - - name: Get current version - id: current_version - run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT - - - name: Version bump - id: version_bump - run: | - if [ -n "${{ github.event.inputs.custom_version }}" ]; then - NEW_VERSION="${{ github.event.inputs.custom_version }}" - npm version $NEW_VERSION -m "Bump version to %s [skip ci]" - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - elif [ -n "${{ github.event.inputs.semver }}" ]; then - if [ -z "${{ github.event.inputs.prerelease }}" ]; then - NEW_VERSION=$(npm version ${{ github.event.inputs.semver }} -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - else - NEW_VERSION=$(npm version ${{ github.event.inputs.semver }} --preid=${{ github.event.inputs.prerelease }} -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - fi - else - NEW_VERSION=$(npm version patch -m "Bump version to %s [skip ci]") - echo "new_version=${NEW_VERSION:1}" >> $GITHUB_OUTPUT - fi - - - name: Generate changelog - id: changelog - if: github.event.inputs.create_release == 'true' - run: | - if [ -n "${{ github.event.inputs.release_notes }}" ]; then - echo "${{ github.event.inputs.release_notes }}" > CHANGELOG.md - else - echo "## What's Changed" > CHANGELOG.md - git log --pretty=format:"* %s" ${{ steps.current_version.outputs.version }}..HEAD >> CHANGELOG.md - fi - echo "changelog<> $GITHUB_OUTPUT - cat CHANGELOG.md >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Push changes - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GIT_TOKEN }} - branch: ${{ github.ref }} - tags: true - - - name: Create GitHub Release - if: github.event.inputs.create_release == 'true' - uses: softprops/action-gh-release@v1 - with: - tag_name: v${{ steps.version_bump.outputs.new_version }} - name: Release v${{ steps.version_bump.outputs.new_version }} - body: ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: ${{ github.event.inputs.prerelease != '' }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/tsconfig-validation.yml b/.github/workflows/tsconfig-validation.yml new file mode 100644 index 0000000..62b0807 --- /dev/null +++ b/.github/workflows/tsconfig-validation.yml @@ -0,0 +1,60 @@ +name: Validate tsconfig.json Changes + +on: + pull_request: + paths: + - 'tsconfig.json' + +jobs: + validate-tsconfig: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + path: base-branch + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Validate tsconfig.json changes + run: node .github/scripts/validate-tsconfig.js + + - name: Comment on PR if validation fails + if: failure() + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `## ⚠️ tsconfig.json Validation Failed + +This pull request attempts to **remove or modify** critical tsconfig.json configurations. This is not allowed as it will break the site. + +### ✅ What's Allowed: +- **Adding** new compiler options +- **Adding** new path aliases +- **Adding** new include/exclude patterns + +### ❌ What's Not Allowed: +- Removing existing compiler options +- Modifying existing compiler option values +- Removing path aliases +- Modifying path alias mappings +- Removing include/exclude patterns + +Please revert your changes to the existing configuration and only add new options if needed.` + }) diff --git a/.github/workflows/update-trusted-hosts.yml b/.github/workflows/update-trusted-hosts.yml new file mode 100644 index 0000000..de8baa4 --- /dev/null +++ b/.github/workflows/update-trusted-hosts.yml @@ -0,0 +1,94 @@ +name: Update Trusted Hosting Providers + +on: + schedule: + # Run every Monday at 00:00 UTC to check for updates + - cron: '0 0 * * 1' + + # Allow manual triggering + workflow_dispatch: + +jobs: + update-trusted-hosts: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: develop + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install ajv + + - name: Update trusted hosts list + run: node .github/scripts/update-trusted-hosts.js + + - name: Validate trusted hosts list + run: node .github/scripts/validate-trusted-hosts.js + + - name: Check for changes + id: changes + run: | + if git diff --quiet packages/providers/trusted-hosts.json; then + echo "changed=false" >> $GITHUB_OUTPUT + else + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + chore: update trusted hosting providers list + + Automatically updated from https://fivem.net/server-hosting + branch: chore/update-trusted-hosts + delete-branch: true + title: 'chore: update trusted hosting providers list' + body: | + ## 🤖 Automated Update + + This PR automatically updates the trusted hosting providers list from the official FiveM registry. + + **Changes:** + - Updated `packages/providers/trusted-hosts.json` + - Validated against schema + - Last updated: ${{ github.event.head_commit.timestamp || 'Manual trigger' }} + + The list is scraped from: https://fivem.net/server-hosting + + ### Verification Checklist + - [x] Schema validation passed + - [x] No duplicate provider IDs + - [x] All provider URLs are valid + + ✅ Ready to merge + labels: | + automated + dependencies + reviewers: | + ${{ github.repository_owner }} + + - name: Log summary + if: always() + run: | + echo "## Trusted Hosts Update Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.changes.outputs.changed }}" == "true" ]; then + echo "✅ Updates found and PR created" >> $GITHUB_STEP_SUMMARY + else + echo "✅ List is up to date" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Source:** https://fivem.net/server-hosting" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/validate-providers.yml b/.github/workflows/validate-providers.yml new file mode 100644 index 0000000..e8c167d --- /dev/null +++ b/.github/workflows/validate-providers.yml @@ -0,0 +1,166 @@ +name: Validate Provider Files + +on: + pull_request: + paths: + - 'frontend/packages/providers/**/*.json' + +jobs: + validate: + name: Validate Provider JSON + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install ajv-cli + run: npm install -g ajv-cli ajv-formats + + - name: Validate provider files + run: | + SCHEMA_FILE="packages/providers/schema.json" + ERRORS=0 + + # Find all provider.json files in subdirectories + for file in packages/providers/*/provider.json; do + # Skip if file doesn't exist (no providers) + if [ ! -f "$file" ]; then + continue + fi + + echo "Validating: $file" + + # Validate against schema + if ! ajv validate -s "$SCHEMA_FILE" -d "$file" --spec=draft7 -c ajv-formats; then + echo "❌ Validation failed for $file" + ERRORS=$((ERRORS + 1)) + else + echo "✅ Valid: $file" + fi + + # Additional checks + ID=$(jq -r '.id' "$file") + FILENAME=$(basename "$file" .json) + + if [ "$ID" != "$FILENAME" ]; then + echo "❌ Error: id '$ID' does not match filename '$FILENAME'" + ERRORS=$((ERRORS + 1)) + fi + done + + if [ $ERRORS -gt 0 ]; then + echo "" + echo "❌ $ERRORS error(s) found in provider files" + exit 1 + fi + + echo "" + echo "✅ All provider files are valid!" + + - name: Check for duplicate IDs + run: | + ERRORS=0 + IDS_FILE=$(mktemp) + + # Extract all IDs from provider.json files + for file in packages/providers/*/provider.json; do + if [ -f "$file" ]; then + ID=$(jq -r '.id' "$file" 2>/dev/null) + if [ -n "$ID" ] && [ "$ID" != "null" ]; then + echo "$ID" >> "$IDS_FILE" + fi + fi + done + + # Check for duplicates + if [ -f "$IDS_FILE" ]; then + DUPLICATES=$(sort "$IDS_FILE" | uniq -d) + + if [ -n "$DUPLICATES" ]; then + echo "❌ Duplicate provider IDs found:" + echo "$DUPLICATES" + ERRORS=1 + else + echo "✅ No duplicate provider IDs found" + fi + + rm "$IDS_FILE" + fi + + if [ $ERRORS -gt 0 ]; then + exit 1 + fi + + - name: Validate URLs + run: | + ERRORS=0 + + # Check all provider.json files + for file in packages/providers/*/provider.json; do + if [ ! -f "$file" ]; then + continue + fi + + # Check all URLs in the file + URLS=$(jq -r '.. | strings | select(test("^https?://"))' "$file" 2>/dev/null) + + while IFS= read -r url; do + if [ -z "$url" ]; then + continue + fi + + # Basic URL format check + if ! echo "$url" | grep -qE '^https?://[a-zA-Z0-9]'; then + echo "❌ Invalid URL format in $file: $url" + ERRORS=$((ERRORS + 1)) + fi + done <<< "$URLS" + done + + if [ $ERRORS -gt 0 ]; then + exit 1 + fi + + echo "✅ All URLs are properly formatted" + + - name: Verify directory structure + run: | + ERRORS=0 + + # Check that each provider directory has a provider.json + for dir in packages/providers/*/; do + dirname=$(basename "$dir") + + # Skip schema files and system directories + if [[ "$dirname" == "."* ]] || [ "$dirname" = "schema.json" ] || [ "$dirname" = "trusted-hosts.json" ] || [ "$dirname" = "trusted-hosts-schema.json" ]; then + continue + fi + + if [ ! -f "$dir/provider.json" ]; then + echo "⚠️ Warning: Provider directory '$dirname' missing provider.json" + ERRORS=$((ERRORS + 1)) + else + # Verify directory name matches provider ID + ID=$(jq -r '.id' "$dir/provider.json" 2>/dev/null) + if [ "$ID" != "$dirname" ]; then + echo "⚠️ Warning: Directory name '$dirname' doesn't match provider ID '$ID'" + echo " Consider renaming directory to match the provider ID" + fi + fi + done + + if [ $ERRORS -gt 0 ]; then + echo "" + echo "⚠️ $ERRORS warning(s) found (non-fatal)" + fi + + echo "✅ Directory structure verified" diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..62aa592 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a47ea..80a325d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,188 @@ All notable changes to FixFX will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - Unreleased + +### Added + +#### Hosting Providers & Partnerships System +- **Directory-Based Provider Structure** - Reorganized provider files into subdirectories + - Moved from flat `provider-name.json` to `provider-name/provider.json` structure + - Allows schemas and documentation to coexist with provider data + - Improved file organization and maintainability +- **Provider Guidelines & Code of Conduct** - Comprehensive standards documentation (`packages/providers/GUIDELINES.md`) + - Service quality requirements (99.5%+ uptime SLA, ≤4h support response) + - Customer support standards (24/7 availability, documentation, responsiveness) + - Fair pricing expectations and discount legitimacy validation + - Technical standards for FiveM/RedM compatibility + - Ethical business practices and code of conduct + - Partnership application and approval workflow + - Performance monitoring and termination clauses +- **Provider JSON Schema Validation** - Enhanced schema enforcement + - Added `$schema` reference to all provider files pointing to `../schema.json` + - Enables IDE schema validation for provider.json files + - GitHub Actions validates schema compliance on pull requests + - Ensures consistent data quality and structure +- **Trusted Hosts Documentation** - Complete reference for automated provider system + - Usage examples for TypeScript utility functions + - Manual provider addition process + - Troubleshooting guide for scraper and validation + - Fallback mechanisms and validation strategies + +#### StepList Component Enhancements +- **Image Support** - Steps can now include images with positioning + - Added `image`, `imageAlt`, and `imagePosition` props + - Images are zoomable using fumadocs ImageZoom component + - Supports `top`, `bottom`, `left`, `right` positioning +- **Markdown Link Support** - Descriptions now render clickable links + - Added `parseMarkdownLinks` helper function + - Supports standard markdown `[text](url)` syntax +- **Inline Alert Support** - Steps can include contextual alerts + - Added `alert` prop with `type` and `message` fields + - Supports `info`, `warning`, `success`, `error`, and `tip` types + - Styled consistently with InfoBanner component + +#### SEO and Modern Web Standards +- **LLMs.txt** - AI crawler documentation files + - `/llms.txt` - Summary for AI models + - `/llms-full.txt` - Comprehensive documentation for LLMs +- **AI.txt** - AI crawler guidelines and permissions +- **Humans.txt** - Site credits and team information +- **Security.txt** - Security policy in `.well-known/security.txt` +- **GPC.json** - Global Privacy Control signal in `.well-known/gpc.json` +- **OpenSearch** - Browser search integration via `/opensearch.xml` +- **Blog Feeds** - RSS, Atom, and JSON Feed support + - `/blog/feed.xml` - RSS 2.0 feed + - `/blog/atom.xml` - Atom 1.0 feed + - `/blog/feed.json` - JSON Feed 1.1 +- **JSON-LD Schemas** - Structured data for search engines + - WebSite schema with SearchAction + - Organization schema + - SoftwareApplication schema +- **AI Crawler Rules** - Added rules for GPTBot, Claude-Web, ChatGPT-User, Anthropic-AI, PerplexityBot, Cohere-AI in robots.txt + +#### Dynamic Icons +- **App Icon** - Dynamic 512x512 icon with gradient background (`app/icon.tsx`) +- **Apple Touch Icon** - Dynamic 180x180 icon for iOS (`app/apple-icon.tsx`) +- **Brand Page** - `/brand` page displaying icon with download buttons + +#### Documentation +- **Discord Bot Guide** - Comprehensive txAdmin Discord bot setup documentation + - Bot creation and permissions + - Configuration options + - Command reference + - Troubleshooting section + +#### GitHub Community Files +- **SECURITY.md** - Vulnerability reporting process and response timeline +- **CODE_OF_CONDUCT.md** - Community guidelines based on Contributor Covenant + +#### Developer Tooling +- **Husky** - Git hooks for automated checks + - Pre-commit hook running lint-staged + - Commit-msg hook running commitlint +- **Lint-staged** - Run linters on staged files only + - ESLint for JS/TS files + - Prettier for formatting all file types +- **Commitlint** - Enforce conventional commit messages + - Uses `@commitlint/config-conventional` preset +- **Knip** - Dead code detection + - Configured for monorepo structure with packages + - Custom entry points and path aliases + +#### Artifacts Page Enhancements +- **Hosting Panel Version Strings** - Added Pterodactyl/Pelican version support + - Accordion section in featured cards showing full version string (e.g., `24769-315823736cfbc085104ca0d32779311cd2f1a5a8`) + - Quick copy button with Terminal icon on artifact list items + - Compatible with Pterodactyl, Pelican, and similar hosting panel egg configurations + +- **Artifact Stats from API** - Stats cards now show full totals from backend + - Total, Recommended, Latest, Active, EOL counts reflect all filtered results + - Previously only showed counts for current page + +- **EOL/Deprecated Artifact Warnings** - Safety improvements for unsupported versions + - Warning banner on deprecated/EOL artifact cards explaining download restriction + - Link to CFX EOL documentation (https://aka.cfx.re/eol) + - Download buttons disabled with tooltip explanation + - Visual distinction with amber (deprecated) and red (EOL) styling + +- **Accordion Component** - New Radix-based accordion component + - Smooth expand/collapse animations + - Accessible keyboard navigation + - Used for hosting panel version sections + +### Fixed +- **EOL filter parameter** - Fixed `includeEol` not being sent when set to "No" + - Now always sends `includeEol` parameter explicitly to backend + - Previously only sent when true, causing backend default (true) to override UI setting + +- **InfoBanner Alignment** - Fixed icon and title not being perfectly inline + - Wrapped icon in flex container with consistent height + - Applied matching `leading-6` to title text + +- **StepList Alert Alignment** - Fixed icon and title alignment in step alerts + - Same fix as InfoBanner using flex containers + +- **ImageZoom Empty Src** - Fixed error when ImageZoom received empty src string + - Added guard to check `step.image.trim() !== ""` before rendering + - Added guard in docs page for markdown images with `props.src` check + +- **Hero Button Consistency** - Fixed Troubleshoot Issues button arrow visibility + - Arrow now always visible instead of appearing on hover + - Matches Get Started button behavior + +#### Styling & CSS Enhancements +- **Comprehensive CSS System** - Major expansion of `globals.css` with reusable utilities + - **Custom Scrollbar** - Sleek, minimal scrollbar styling with `.custom-scrollbar` + - **Gradient Text** - Utilities: `text-gradient-blue`, `text-gradient-purple`, `text-gradient-green`, `text-gradient-orange` + - **Glow Effects** - `glow-blue`, `glow-purple`, `glow-green`, `glow-sm` for neon-style effects + - **Glassmorphism** - `glass`, `glass-card`, `glass-dark` for frosted glass effects + - **Gradient Backgrounds** - `bg-mesh` (multi-color mesh), `bg-dots`, `bg-grid` patterns + - **Card Effects** - `card-hover` with lift animation, `card-glow` with mouse-tracking radial gradient + - **Status Badges** - `badge-recommended`, `badge-latest`, `badge-active`, `badge-deprecated`, `badge-eol` + - **Code Block Styles** - `code-block`, `code-block-header`, `inline-code` + - **Loading States** - `skeleton`, `skeleton-text`, `skeleton-title`, `skeleton-avatar`, `skeleton-card` + - **Chat Interface** - `chat-bubble`, `chat-bubble-user`, `chat-bubble-assistant`, `chat-input` + - **Contributor Styles** - `contributor-avatar` with hover ring effect, `contributor-badge` + - **Table Styles** - Complete table wrapper with hover states + - **Component Classes** - `artifact-card`, `native-card`, `feature-card`, `hero-badge`, `hero-title`, `hero-glow` + - **Documentation** - `docs-callout-info`, `docs-callout-warning`, `docs-callout-danger`, `docs-callout-tip` + - **TOC Styles** - `toc-link`, `toc-progress` for table of contents + - **Sidebar** - `sidebar-link` with active state + - **Utilities** - `transition-base`, `transition-slow`, `focus-ring`, `search-highlight` + - **Z-Index Scale** - Structured z-index system from `z-behind` to `z-tooltip` + +#### Animations +- **New Animation Utilities** - Smooth, performant CSS animations + - `animate-fade-in` - Fade in effect + - `animate-slide-up` / `animate-slide-down` - Slide animations + - `animate-scale-in` - Scale entrance + - `animate-float` - Floating effect (6s infinite) + - `animate-shimmer` - Shimmer loading effect + - `animate-gradient` - Animated gradient backgrounds + - `animate-glow-pulse` - Pulsing glow effect + - `animate-border-flow` - Flowing border gradient +- **Button Shine Effect** - `btn-glow` with shine animation on hover +- **Link Underline** - `link-underline` with animated underline on hover + +### Changed +- Updated `txAdmin Windows Install` guide with StepList images and alerts +- Updated to `NextJS v15.5.9` as it is the latest stable `15.x` version not requiring significant changes +- Enhanced sitemap with blog posts and improved priority structure +- Updated README.md with accurate project information +- Updated CONTRIBUTING.md with correct Discord and email contacts +- Added `knip.json` configuration for dead code detection + +### Fixed +- **Next.js 15 Dynamic Params** - Fixed `params` must be awaited error in `/docs/[...slug]/page.tsx` + - Changed `params` type from `{ slug?: string[] }` to `Promise<{ slug?: string[] }>` + - Added `const { slug } = await params;` before accessing properties + - Aligns with Next.js 15+ requirements for dynamic route params + +- **Hydration Mismatch** - Fixed React hydration warning in root layout + - Added `suppressHydrationWarning` to `` element + - Prevents warnings from theme provider dynamically adding `dark` class and `color-scheme` style + ## [1.0.0] - 2026-01-25 ### Added diff --git a/README.md b/README.md index e517fc1..0f8449a 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,188 @@ -# FixFX Wiki +# FixFX [![Build](https://github.com/CodeMeAPixel/FixFX/actions/workflows/build-ci.yml/badge.svg)](https://github.com/CodeMeAPixel/FixFX/actions/workflows/build-ci.yml) +[![License: AGPL 3.0](https://img.shields.io/badge/License-AGPL%203.0-blue.svg)](LICENSE) -A comprehensive documentation platform for FiveM development, providing detailed guides, tutorials, and best practices for the FiveM community. +A documentation platform for FiveM, RedM, and the CitizenFX ecosystem. Guides, tutorials, native references, and tools for server developers. + +> [!INFO] +>FixFX is an independent community project. It is not affiliated with or endorsed by Cfx.re, Rockstar Games, txAdmin, Take-Two Interactive or any other entities referenced throughout the documentation. ## Features -- 📚 Extensive documentation covering various aspects of FiveM development -- 🎨 Modern, responsive UI with dark mode support -- 🔍 Advanced search functionality -- 📝 Interactive code examples with syntax highlighting -- 🌐 Multi-language support -- 📱 Mobile-friendly design -- 🚀 Fast and optimized performance +- Documentation for FiveM, RedM, txAdmin, vMenu, and popular frameworks +- Native function reference with search and filtering +- Artifacts browser for server builds +- AI-powered chat assistant for troubleshooting +- Full-text search across all documentation +- Dark mode interface +- Mobile responsive design ## Tech Stack -- **Framework**: Next.js -- **UI**: React, Tailwind CSS -- **Documentation**: MDX -- **Code Highlighting**: Shiki -- **Icons**: Lucide +- **Framework**: Next.js 15 +- **Language**: TypeScript +- **Styling**: Tailwind CSS +- **Documentation**: MDX with Fumadocs +- **Backend**: Go (separate repository) - **Deployment**: Vercel ## Getting Started -1. Clone the repository: - ```bash - git clone https://github.com/CodeMeAPixel/FixFX.git - ``` - -2. Install dependencies: - ```bash - cd FixFX - pnpm install - ``` +### Prerequisites -3. Start the development server: - ```bash - pnpm dev - ``` +- Node.js 18 or later +- Bun (recommended) or npm/pnpm -4. Open [http://localhost:3000](http://localhost:3000) in your browser. +### Installation -## Documentation Structure +```bash +git clone https://github.com/CodeMeAPixel/FixFX.git +cd FixFX/frontend +bun install +``` -The documentation is organized into several main sections: +### Development -- **Core**: Fundamental concepts and principles -- **CFX**: FiveM-specific tools and features -- **Common Tools**: Essential utilities and best practices -- **Guides**: Step-by-step tutorials and walkthroughs +```bash +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +### Production Build + +```bash +bun run build +bun start +``` + +## Project Structure + +``` +frontend/ +├── app/ # Next.js app router +├── content/ # MDX documentation files +├── lib/ # Utility functions +├── packages/ +│ ├── core/ # React hooks +│ ├── ui/ # UI components +│ └── utils/ # Shared utilities +├── public/ # Static assets +├── styles/ # Global styles +└── types/ # TypeScript definitions +``` + +## Documentation + +Documentation content is located in `content/docs/` and organized by topic: + +- `core/` - Core concepts and fundamentals +- `cfx/` - CitizenFX platform documentation +- `txadmin/` - txAdmin server management +- `vmenu/` - vMenu configuration +- `frameworks/` - ESX, QBCore, and other frameworks +- `guides/` - Tutorials and how-to guides + +## Hosting Providers & Partnerships + +This project includes a partnership program for hosting providers offering exclusive benefits to the FiveM and RedM communities. + +### For Server Owners + +Visit the [Hosting Partners page](/hosting) to browse verified hosting providers with exclusive FixFX discounts: + +- **Affiliate Partners**: Providers with exclusive discount codes (e.g., ZAP-Hosting with 20% off) +- **Trusted Hosts**: Automatically curated list from [fivem.net/server-hosting](https://fivem.net/server-hosting) + +### For Hosting Providers + +We're always looking for quality hosting providers interested in partnerships. Here's what we offer: + +✅ **Reach**: Exposure to thousands of FiveM/RedM server owners +✅ **Trust**: Featured on our dedicated hosting page +✅ **Tracking**: Affiliate links for conversion attribution +✅ **Support**: Community visibility and marketing assistance + +#### Partnership Requirements & Code of Conduct + +Your hosting service should meet these criteria: + +- Must host **FiveM** and/or **RedM** servers +- **99.6%+ uptime** with responsive 24/7 support +- Provide an **exclusive discount code** or special offer +- Supply **trackable affiliate/referral links** +- Maintain **quality standards** and community respect +- Adhere to our [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) + +#### How to Apply + +1. **Review** the [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) +2. **Read** the [Partnership Requirements & Process](./packages/providers/README.md) +3. **Create** your provider directory with `provider.json` +4. **Submit** a Pull Request to `frontend/packages/providers/` +5. **Review**: Our team responds within 3-5 business days + +**Example JSON file** (in `your-hosting/provider.json`): + +```json +{ + "$schema": "../schema.json", + "id": "your-hosting", + "name": "Your Hosting Company", + "website": "https://your-hosting.com", + "description": "Premium FiveM and RedM hosting with DDoS protection and 24/7 support.", + "discount": { + "percentage": 20, + "code": "FIXFX20", + "duration": "Lifetime" + }, + "links": [ + { + "label": "FiveM Servers", + "url": "https://your-hosting.com/affiliate?campaign=fixfx", + "description": "High-performance FiveM servers" + } + ], + "features": [ + "99.9% Uptime SLA", + "DDoS Protection", + "Auto-backup & Restore", + "24/7 Support", + "1-Click Install" + ], + "priority": 10 +} +``` + +#### System Features + +- **Automated Validation**: GitHub Actions validates all provider JSON files +- **Schema Enforcement**: JSON Schema ensures consistent data quality +- **Trusted Hosts Scraper**: Weekly automation to maintain current FiveM provider list +- **CI/CD Integration**: Automatic PR creation for provider updates + +For detailed information, see: +- [Provider Guidelines & Code of Conduct](./packages/providers/GUIDELINES.md) - Standards and expectations +- [Partnership Requirements & Process](./packages/providers/README.md) - Detailed how-to guide +- [Trusted Hosts Documentation](./packages/providers/TRUSTED_HOSTS_README.md) - Automation details ## Contributing -We welcome contributions from the community! Here's how you can help: +Contributions are welcome. Please read our [Contributing Guide](.github/CONTRIBUTING.md) before submitting a pull request. 1. Fork the repository -2. Create a new branch for your feature -3. Make your changes -4. Submit a pull request +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Commit your changes (`git commit -m 'feat: add your feature'`) +4. Push to the branch (`git push origin feature/your-feature`) +5. Open a pull request + +## Community -Please ensure your contributions follow our [contribution guidelines](CONTRIBUTING.md). +- **GitHub Issues**: [Report bugs or request features](https://github.com/CodeMeAPixel/FixFX/issues) +- **Discord**: [discord.gg/Vv2bdC44Ge](https://discord.gg/Vv2bdC44Ge) +- **Email**: [hey@codemeapixel.dev](mailto:hey@codemeapixel.dev) ## License -This project is licensed under the AGPL 3.0 License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +This project is licensed under the AGPL 3.0 License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/app/(blog)/blog/(root)/page.tsx b/app/(blog)/blog/(root)/page.tsx index 1832a2d..c5350a9 100644 --- a/app/(blog)/blog/(root)/page.tsx +++ b/app/(blog)/blog/(root)/page.tsx @@ -1,42 +1,45 @@ import { FixFXIcon } from "@ui/icons"; import { blog } from "@/lib/docs/source"; import Link from "next/link"; -import { Calendar, Clock, ArrowRight } from "lucide-react"; +import { Calendar, ArrowRight } from "lucide-react"; export default function BlogPage() { const posts = blog.getPages(); return ( -
+
{/* Header */} -
-
- -

Blog

+
+
+ + Latest Articles
-

- News, guides, and insights from the FixFX community +

+ FixFX Blog +

+

+ News, guides, and insights from the FixFX community. Learn best practices, get updates, and discover new features.

{/* Posts grid */} -
+
{posts.map((post, index) => ( {/* Gradient accent on hover */} -
+
{/* Meta info */} {post.data.date && ( -
- - +
+ + {new Date(post.data.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', @@ -47,19 +50,19 @@ export default function BlogPage() { )} {/* Title */} -

+

{post.data.title}

{/* Description */} -

+

{post.data.description}

{/* Read more link */} -
+
Read article - +
@@ -68,8 +71,8 @@ export default function BlogPage() { {/* Empty state */} {posts.length === 0 && ( -
-

No posts yet. Check back soon!

+
+

No posts yet. Check back soon for new content!

)}
diff --git a/app/(blog)/blog/[slug]/page.tsx b/app/(blog)/blog/[slug]/page.tsx index cd151ab..f51d207 100644 --- a/app/(blog)/blog/[slug]/page.tsx +++ b/app/(blog)/blog/[slug]/page.tsx @@ -1,5 +1,6 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import { CornerDownLeft } from "@ui/icons"; +import { Calendar } from "lucide-react"; import { notFound } from "next/navigation"; import { blog } from "@/lib/docs/source"; import Link from "next/link"; @@ -7,45 +8,63 @@ import Link from "next/link"; export default async function BlogPost(props: { params: Promise<{ slug: string }>; }) { - const params = await props.params; - const page = blog.getPage([params.slug]); + const { slug } = await props.params; + const page = blog.getPage([slug]); if (!page) notFound(); const Mdx = page.data.body; return ( <> -
-

+
+

{page.data.title}

-

+

{page.data.description}

-
+
+
+ + {new Date(page.data.date).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + })} + {page.data.author && ( + <> + + {page.data.author} + + )} +
- Back + Back to Blog
-
-
+
+

-
+
-

Written by

-

{page.data.author}

+

Written by

+

{page.data.author}

-

At

-

- {new Date(page.data.date).toDateString()} +

Published

+

+ {new Date(page.data.date).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric' + })}

diff --git a/app/(blog)/blog/atom.xml/route.ts b/app/(blog)/blog/atom.xml/route.ts new file mode 100644 index 0000000..8812575 --- /dev/null +++ b/app/(blog)/blog/atom.xml/route.ts @@ -0,0 +1,47 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = ` + + FixFX Blog + News, tutorials, and updates for the FiveM, RedM, and CitizenFX community + + + ${DOCS_URL}/ + ${new Date().toISOString()} + + FixFX Team + https://github.com/CodeMeAPixel + + ${DOCS_URL}/favicon-32x32.png + ${DOCS_URL}/logo.png + © ${new Date().getFullYear()} FixFX. All rights reserved. + ${posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map( + (post) => ` + + <![CDATA[${post.data.title}]]> + + ${DOCS_URL}/blog/${post.slugs.join("/")} + ${new Date(post.data.date).toISOString()} + ${new Date(post.data.date).toISOString()} + + ${post.data.author} + + + ` + ) + .join("")} +`; + + return new Response(feed, { + headers: { + "Content-Type": "application/atom+xml; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/blog/feed.json/route.ts b/app/(blog)/blog/feed.json/route.ts new file mode 100644 index 0000000..9b1b97c --- /dev/null +++ b/app/(blog)/blog/feed.json/route.ts @@ -0,0 +1,40 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = { + version: "https://jsonfeed.org/version/1.1", + title: "FixFX Blog", + home_page_url: DOCS_URL, + feed_url: `${DOCS_URL}/blog/feed.json`, + description: "News, tutorials, and updates for the FiveM, RedM, and CitizenFX community", + icon: `${DOCS_URL}/android-chrome-512x512.png`, + favicon: `${DOCS_URL}/favicon-32x32.png`, + authors: [ + { + name: "FixFX Team", + url: "https://github.com/CodeMeAPixel", + }, + ], + language: "en-US", + items: posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map((post) => ({ + id: `${DOCS_URL}/blog/${post.slugs.join("/")}`, + url: `${DOCS_URL}/blog/${post.slugs.join("/")}`, + title: post.data.title, + summary: post.data.description || "", + date_published: new Date(post.data.date).toISOString(), + authors: [{ name: post.data.author }], + })), + }; + + return new Response(JSON.stringify(feed, null, 2), { + headers: { + "Content-Type": "application/feed+json; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/blog/feed.xml/route.ts b/app/(blog)/blog/feed.xml/route.ts new file mode 100644 index 0000000..ec1c9b6 --- /dev/null +++ b/app/(blog)/blog/feed.xml/route.ts @@ -0,0 +1,44 @@ +import { blogPosts } from "@/../source.config"; +import { DOCS_URL } from "@utils/index"; + +export async function GET() { + const posts = blogPosts.getPages(); + + const feed = ` + + + FixFX Blog + ${DOCS_URL} + News, tutorials, and updates for the FiveM, RedM, and CitizenFX community + en-US + ${new Date().toUTCString()} + + + ${DOCS_URL}/logo.png + FixFX Blog + ${DOCS_URL} + + ${posts + .sort((a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()) + .map( + (post) => ` + + <![CDATA[${post.data.title}]]> + ${DOCS_URL}/blog/${post.slugs.join("/")} + ${DOCS_URL}/blog/${post.slugs.join("/")} + + ${post.data.author} + ${new Date(post.data.date).toUTCString()} + ` + ) + .join("")} + +`; + + return new Response(feed, { + headers: { + "Content-Type": "application/xml; charset=utf-8", + "Cache-Control": "public, max-age=3600, s-maxage=3600", + }, + }); +} diff --git a/app/(blog)/opengraph-image.tsx b/app/(blog)/opengraph-image.tsx index 6915267..b387202 100644 --- a/app/(blog)/opengraph-image.tsx +++ b/app/(blog)/opengraph-image.tsx @@ -14,42 +14,117 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Blog + ✍️
+ + {/* Main title */}
- Latest updates and insights from the FiveM community + + Fix + + + FX + + + Blog +
+ + {/* Subtitle */} +

+ Latest updates, tutorials, and insights from the FiveM community +

), { ...size, - }, + } ); } diff --git a/app/(blog)/twitter-image.tsx b/app/(blog)/twitter-image.tsx index 7cc0c38..29b0384 100644 --- a/app/(blog)/twitter-image.tsx +++ b/app/(blog)/twitter-image.tsx @@ -3,8 +3,8 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; export const alt = "FixFX Blog"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + ✍️
+ + {/* Main title */}
- Blog + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Blog +

), { ...size, - }, + } ); } diff --git a/app/(docs)/docs/[...slug]/page.tsx b/app/(docs)/docs/[...slug]/page.tsx index 3655e13..42fffff 100644 --- a/app/(docs)/docs/[...slug]/page.tsx +++ b/app/(docs)/docs/[...slug]/page.tsx @@ -4,6 +4,7 @@ import { DocsTitle, DocsDescription, } from "fumadocs-ui/page"; +import { ImageZoom } from "fumadocs-ui/components/image-zoom"; import { source } from "@/lib/docs/source"; import { metadataImage } from "@/lib/docs/metadata"; import defaultMdxComponents from "fumadocs-ui/mdx"; @@ -14,17 +15,19 @@ import { Editor } from "@ui/core/docs/editor"; export default async function Page({ params, }: { - params: { slug?: string[] }; + params: Promise<{ slug?: string[] }>; }) { + const { slug } = await params; + // Redirect to overview if no slug is provided (root /docs path) - if (!params.slug || params.slug.length === 0) { + if (!slug || slug.length === 0) { return source.getPage(['overview']); } - const page = source.getPage(params.slug); + const page = source.getPage(slug); if (!page) notFound(); - const MDX = page.data.body + const MDX = page.data.body; return ( props.src ? : null, Editor: Editor }} /> diff --git a/app/(docs)/docs/layout.tsx b/app/(docs)/docs/layout.tsx index 598fa49..73dcac9 100644 --- a/app/(docs)/docs/layout.tsx +++ b/app/(docs)/docs/layout.tsx @@ -4,6 +4,24 @@ import { baseOptions } from "@/app/layout.config"; import { FixFXIcon } from "@ui/icons"; import { source } from "@/lib/docs/source"; import type { ReactNode } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: { + default: "Documentation", + template: "%s | FixFX Docs", + }, + description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Guides, tutorials, and API references.", + alternates: { + canonical: "https://fixfx.wiki/docs", + }, + openGraph: { + title: "FixFX Documentation", + description: "Comprehensive documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem.", + url: "https://fixfx.wiki/docs", + type: "website", + }, +}; const docsOptions: DocsLayoutProps = { ...baseOptions, diff --git a/app/(docs)/opengraph-image.tsx b/app/(docs)/opengraph-image.tsx index 71c2ee0..7d5909c 100644 --- a/app/(docs)/opengraph-image.tsx +++ b/app/(docs)/opengraph-image.tsx @@ -14,42 +14,142 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Docs + 📚
+ + {/* Main title */}
+ + Fix + + + FX + + + Docs + +
+ + {/* Subtitle */} +

+ Comprehensive guides and tutorials for the CitizenFX ecosystem +

+ + {/* Tags */} +
- Comprehensive guides and information for the CitizenFX ecosystem + {["FiveM", "RedM", "txAdmin", "vMenu"].map((tag) => ( +
+ {tag} +
+ ))}
), { ...size, - }, + } ); } diff --git a/app/(docs)/twitter-image.tsx b/app/(docs)/twitter-image.tsx index 38e3735..0dce458 100644 --- a/app/(docs)/twitter-image.tsx +++ b/app/(docs)/twitter-image.tsx @@ -3,8 +3,8 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; export const alt = "FixFX Documentation"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 📚
+ + {/* Main title */}
- Docs + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Documentation +

), { ...size, - }, + } ); } diff --git a/app/(landing)/opengraph-image.tsx b/app/(landing)/opengraph-image.tsx index a8737b6..7160feb 100644 --- a/app/(landing)/opengraph-image.tsx +++ b/app/(landing)/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX"; +export const alt = "FixFX - Your FiveM & RedM Resource Hub"; export const size = { width: 1200, height: 630, @@ -14,51 +14,166 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+
+ + {/* Badge */} +
- FixFX +
+ + Open Source Documentation +
+ + {/* Main title */}
+ + Fix + + + FX + +
+ + {/* Subtitle */} +

- Comprehensive guides for the CitizenFX ecosystem -

+ Your comprehensive resource for FiveM,{" "} + RedM, and the{" "} + CitizenFX ecosystem. +

+ + {/* Bottom indicators */}
- Powered by the Community +
+
+ Free & Open Source +
+
+
+ Community Driven +
+
+
+ Always Updated +
), { ...size, - }, + } ); } diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index 64e400d..2b93334 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -3,6 +3,21 @@ import { Features } from "@ui/core/landing/features"; import { DocsPreview } from "@ui/core/landing/docs-preview"; import { Hero } from "@ui/core/layout/hero"; import { Contributors } from "@ui/core/landing/contributors"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, vMenu, and the CitizenFX ecosystem. Your one-stop resource for server development.", + alternates: { + canonical: "https://fixfx.wiki", + }, + openGraph: { + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + url: "https://fixfx.wiki", + type: "website", + }, +}; export default function HomePage() { return ( diff --git a/app/(landing)/twitter-image.tsx b/app/(landing)/twitter-image.tsx index c78fe8c..3ce03d4 100644 --- a/app/(landing)/twitter-image.tsx +++ b/app/(landing)/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX"; +export const alt = "FixFX - Your FiveM & RedM Resource Hub"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,88 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Main title */} +
- FixFX + + Fix + + + FX +
-
- FiveM Guides -
+ FiveM & RedM Resource Hub +

), { ...size, - }, + } ); } diff --git a/app/apple-icon.tsx b/app/apple-icon.tsx new file mode 100644 index 0000000..502967c --- /dev/null +++ b/app/apple-icon.tsx @@ -0,0 +1,91 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const size = { + width: 180, + height: 180, +}; + +export const contentType = "image/png"; + +export default function AppleIcon() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs - scaled down */} +
+
+
+ + {/* FixFX Icon */} + + {/* First path */} + + + {/* Second path */} + + +
+ ), + { + ...size, + } + ); +} diff --git a/app/artifacts/layout.tsx b/app/artifacts/layout.tsx index 07f66cf..8b2498d 100644 --- a/app/artifacts/layout.tsx +++ b/app/artifacts/layout.tsx @@ -1,8 +1,18 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'Artifact Explorer', - description: 'Explore the CitizenFX artifacts for both GTA V AND RDR2', + title: 'FiveM & RedM Artifact Explorer', + description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds for Windows and Linux.', + keywords: ['FiveM artifacts', 'RedM artifacts', 'FXServer download', 'CitizenFX artifacts', 'FiveM server files', 'RedM server files'], + alternates: { + canonical: 'https://fixfx.wiki/artifacts', + }, + openGraph: { + title: 'FiveM & RedM Artifact Explorer | FixFX', + description: 'Browse and download FiveM and RedM server artifacts. Find recommended, latest, and stable builds.', + url: 'https://fixfx.wiki/artifacts', + type: 'website', + }, }; export default function ArtifactsLayout({ diff --git a/app/artifacts/opengraph-image.tsx b/app/artifacts/opengraph-image.tsx index fe62f1b..f2e1ebe 100644 --- a/app/artifacts/opengraph-image.tsx +++ b/app/artifacts/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Artifacts"; +export const alt = "FixFX Artifacts - FiveM & RedM Server Builds"; export const size = { width: 1200, height: 630, @@ -14,42 +14,166 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Artifacts + 📦
+ + {/* Main title */}
+ + Fix + + + FX + + + Artifacts + +
+ + {/* Subtitle */} +

+ Download FiveM and RedM server builds with version tracking +

+ + {/* Status badges */} +
- Access FiveM and RedM server artifacts +
+ ✓ Recommended +
+
+ Latest +
+
+ Active +
), { ...size, - }, + } ); } diff --git a/app/artifacts/twitter-image.tsx b/app/artifacts/twitter-image.tsx index 7176bc4..a9e1af4 100644 --- a/app/artifacts/twitter-image.tsx +++ b/app/artifacts/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Artifacts"; +export const alt = "FixFX Artifacts - FiveM & RedM Server Builds"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 📦
+ + {/* Main title */}
- Artifacts + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Artifacts +

), { ...size, - }, + } ); } diff --git a/app/banner-wide/route.tsx b/app/banner-wide/route.tsx new file mode 100644 index 0000000..64d54c9 --- /dev/null +++ b/app/banner-wide/route.tsx @@ -0,0 +1,114 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; + +export async function GET() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs - spread horizontally for wide banner */} +
+
+
+
+
+ + {/* Subtle grid pattern overlay */} +
+ + {/* Subtle gradient overlays */} +
+
+
+ ), + { + width: 2560, + height: 720, + } + ); +} diff --git a/app/banner/route.tsx b/app/banner/route.tsx new file mode 100644 index 0000000..10a8627 --- /dev/null +++ b/app/banner/route.tsx @@ -0,0 +1,114 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; + +export async function GET() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+
+
+ + {/* Subtle grid pattern overlay */} +
+ + {/* Subtle noise texture simulation with dots */} +
+
+
+ ), + { + width: 1920, + height: 1080, + } + ); +} diff --git a/app/brand/page.tsx b/app/brand/page.tsx new file mode 100644 index 0000000..c0cd275 --- /dev/null +++ b/app/brand/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { FixFXIcon } from "@ui/icons"; +import { Download } from "lucide-react"; + +export default function BrandPage() { + return ( +
+ {/* Background gradient orbs - adjusted for mobile */} +
+
+
+ + {/* Content */} +
+ {/* Icon */} +
+ +
+ + {/* Title */} +
+ + Fix + + + FX + +
+ + {/* Subtitle */} +

+ Your comprehensive resource for FiveM, RedM, and the CitizenFX ecosystem. +

+ + {/* Brand Guidelines */} +
+

Brand Guidelines

+ + {/* Color Palette */} +
+

Primary Colors

+
+
+
+ #2563EB +
+
+
+ #3B82F6 +
+
+
+ #06B6D4 +
+
+
+ #0A0A0F +
+
+
+ + {/* Typography */} +
+

Typography

+
+

+ Font Family: Inter, system-ui, sans-serif +

+

+ Logo Text: "Fix" in gradient, "FX" in foreground +

+
+
+ + {/* Usage Notes */} +
+

Usage Notes

+
    +
  • + + Use the icon on dark backgrounds for best visibility +
  • +
  • + + Maintain minimum padding around the logo +
  • +
  • + + Do not distort or rotate the logo +
  • +
  • + + Do not change the gradient colors +
  • +
+
+
+ + {/* Download buttons */} + + + {/* SVG Download */} +

+ Need a different format? Contact us for SVG or other formats. +

+
+
+ ); +} diff --git a/app/chat/layout.tsx b/app/chat/layout.tsx index 8a0933d..a1afd33 100644 --- a/app/chat/layout.tsx +++ b/app/chat/layout.tsx @@ -1,9 +1,18 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: 'Fixie - AI Assistant', - description: 'Your intelligent AI assistant for the CitizenFX ecosystem. Get help with FiveM, RedM, txAdmin, server configuration, Lua scripting, and more.', - keywords: ['FiveM', 'RedM', 'txAdmin', 'CitizenFX', 'AI Assistant', 'Lua', 'Server Development'], + title: 'Fixie AI - FiveM & RedM Assistant', + description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, server configuration, and troubleshooting.', + keywords: ['FiveM AI', 'RedM AI', 'txAdmin help', 'CitizenFX assistant', 'Lua scripting help', 'FiveM support', 'server development help'], + alternates: { + canonical: 'https://fixfx.wiki/chat', + }, + openGraph: { + title: 'Fixie AI - FiveM & RedM Assistant | FixFX', + description: 'AI-powered assistant for FiveM and RedM development. Get instant help with Lua scripting, txAdmin setup, and more.', + url: 'https://fixfx.wiki/chat', + type: 'website', + }, }; export default function AskLayout({ diff --git a/app/chat/opengraph-image.tsx b/app/chat/opengraph-image.tsx index 67997ab..3cb4538 100644 --- a/app/chat/opengraph-image.tsx +++ b/app/chat/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Chat"; +export const alt = "FixFX Chat - AI-Powered FiveM Assistant"; export const size = { width: 1200, height: 630, @@ -14,42 +14,136 @@ export default function Image() { (
+ {/* Background gradient orbs */}
+
+ + {/* Icon */} +
- FixFX Chat + 🤖
+ + {/* Main title */}
+ + Fix + + + FX + + + Chat + +
+ + {/* Subtitle */} +

+ AI-powered assistant for FiveM and RedM development +

+ + {/* Badge */} +
- AI-powered assistance for FiveM development +
+ + Powered by AI +
), { ...size, - }, + } ); } diff --git a/app/chat/twitter-image.tsx b/app/chat/twitter-image.tsx index 7113770..c7be150 100644 --- a/app/chat/twitter-image.tsx +++ b/app/chat/twitter-image.tsx @@ -1,10 +1,10 @@ import { ImageResponse } from "next/og"; export const runtime = "nodejs"; -export const alt = "FixFX Chat"; +export const alt = "FixFX Chat - AI-Powered FiveM Assistant"; export const size = { - width: 506, - height: 506, + width: 1200, + height: 630, }; export const contentType = "image/png"; @@ -14,41 +14,87 @@ export default function Image() { (
+ {/* Background gradient orb */}
+ + {/* Icon */} +
- FixFX + 🤖
+ + {/* Main title */}
- Chat + + Fix + + + FX +
+ + {/* Subtitle */} +

+ Chat +

), { ...size, - }, + } ); } diff --git a/app/components/file-source.tsx b/app/components/file-source.tsx index 2b2bff7..06cb9fe 100644 --- a/app/components/file-source.tsx +++ b/app/components/file-source.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';import { API_URL } from "@/packages/ interface FileSourceProps { filePath: string; - title?: string; + title?: string | { text: string; href: string }; } const languageMap: Record = { @@ -24,6 +24,20 @@ const languageMap: Record = { '.bash': 'bash', '.sql': 'sql', '.lua': 'lua', + '.go': 'go', + '.mod': 'go', + '.sum': 'text', + '.py': 'python', + '.rb': 'ruby', + '.java': 'java', + '.cs': 'csharp', + '.cpp': 'cpp', + '.c': 'c', + '.h': 'c', + '.hpp': 'cpp', + '.xml': 'xml', + '.toml': 'toml', + '.env': 'bash', }; function getLanguage(filePath: string): string { diff --git a/app/components/image-modal.tsx b/app/components/image-modal.tsx index 41bd532..c6ea3fd 100644 --- a/app/components/image-modal.tsx +++ b/app/components/image-modal.tsx @@ -10,9 +10,11 @@ interface ImageModalProps { title?: string; width?: number; height?: number; + className?: string; + caption?: string; } -export function ImageModal({ src, alt, title, width, height }: ImageModalProps) { +export function ImageModal({ src, alt, title, width, height, className, caption }: ImageModalProps) { const [isOpen, setIsOpen] = useState(false); const [mounted, setMounted] = useState(false); @@ -122,11 +124,11 @@ export function ImageModal({ src, alt, title, width, height }: ImageModalProps) ); return ( - <> +
{/* Thumbnail */} + + {/* Caption */} + {caption && ( +
+ {caption} +
+ )} {/* Portal to body */} {mounted && isOpen && createPortal(modal, document.body)} - +
); } diff --git a/app/docs-og/[...slug]/route.tsx b/app/docs-og/[...slug]/route.tsx index 8702edb..0bec07b 100644 --- a/app/docs-og/[...slug]/route.tsx +++ b/app/docs-og/[...slug]/route.tsx @@ -8,6 +8,8 @@ export const GET = metadataImage.createAPI((page) => { title: page.data.title, description: page.data.description, site: "FixFX", + primaryColor: "#3b82f6", + primaryTextColor: "#f8fafc", }); }); diff --git a/app/favicon.ico b/app/favicon.ico index 92389deed5b7bad578ed1bdaecff950f20882efc..fbfb4fa5854a6fa69a1b17c75d84b4ae70477ce5 100644 GIT binary patch literal 37294 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 bSKR&wXFhwOWg3Rc00000NkvXXu0mjf44`Gb literal 196637 zcmeEv1$-0f)_(8z|K#5NBvGYysZNlUeJrlTf#GEj&78R?fInQ3T=Ec9EMEHtlrcG}#s zB<*Qen!-AlrBmI?(7BemD5-T$y4T*Hl>UB5@k}(eYEC+{aRI%$pG3Aa_ifxud_U{|#PEQ>RWTO2`W)S{}`&jn>dojmoN0S1y zw_Y|n5#%TP_qB4Qr4|J|satU$S~jpVy?OCWxNUlM_XdRx2*%%g%l>`2Y-y-rfG2HV zJewkRY@opvf24JTJJH)0PmS;K=H5*@HM9vG4`lbB-E{w1|4Ef|W}=PrXVM>EzjD7H zvv)I%tD2j(4C_qqMDOwb(H*)lx`o-_7vcUsGi_ZqpQDrTfpPmnXhyBPv~y5vy0mQ> zCGA*7xAv^0YfDB@T${Z3{2WI9v-@vUARFyny_o*|;}792=;DD8T2wa=Ev=k|Hr4dD zPQUDFsdW(_I=FU;*?S~}hS0i3`MAH|*T7eneuDfq{xKc>T*8-*ZCGmd9@h_r*yNue z|Lybf@)NOXnb~_JAKqe9euVwQ9xlF zW8A-`+5XKzn_DUGUqKJpdu4hCs?G!IjMG2d`#|SO^-STcX{e-k8mj1sSwDfK|cxa z%dU@49i+&PB`7M`S2KNjGo4$>pH6LALCLpn(bK0U-NWAdA>KQzeQ^qd{)ujqUDNw_ zEs%i*mCs4hTUT*<_4DV?^yJAC`ts!qduzHMeTWXUEl&IEXQN}#uUg)}E%s~Qa{d&t zb&VNK7=8YLF1izSkao5zM%!z9(*fuYZSS9++T=|~qZ*c_$H~{ZdlmnWeMa2s>9ny{ zHri1)o2K_~;Gd4#6!xa>rE*f2{OM_4x5o7RUb6V@xvxLJe@n3oN70V@et3T$Rrg;5 zy0l3FZ`!?l9>pHnOM|NyqFx2l(UKmG>Barq7N7A2pK)Q)C<+bCPRAPgTBUylpET4U zz>BsooohUAE*uY~Q8f$EfFkK>Rj(%WLgqQXe0WP&mW-lf!8sK7Zwaq7R4rF#+Pr9v z@x6tcy>ud!Ce$fJBZ_69^}UyJ+#==$=pbgFqS$fxE0W$$18$E>t#?K0_~ za^GLUXH2hOn0_mnk+$@0O83r1(9^^?dWQ8PDUMRE#nX%U2)aDF8J%n9C#xSAeHs-6 zuQskSlSknL6HkWH+=fMHM!C$iv`#Ks*Dx<_4GN$=O#=wsueTyXX>+rpw4sIIeS(V)|`x1?bKl@}F@pDwGbiDQ1^`6V6AbpGQ~DMQ3-e z1AQ!M^#Gq?uYMNJUv|Dsto9?t?OreY8IPkwalRC{>wFQ;cXmF`teKlG?Ac)Cv-pc= zF^B9v9|ik?o$m|k=B48^2H||4XgJ>|QsOF{?`s@8-%a*OR+?Wyy?w&jMNa%5v%~zb zDq%4{kbA)al-gkhv;32aK@XP6mX<2P7OMeUte$^*3i>fUHP4Ivk}o55E|`gW6wX9_ zVUrDlO*W!r78+MND@`uP+hy}AdD7y_p3p5`v>tnN3(nSEIR8R%J{^U95FX@9F-^16 zdDv)IV541claubW^QVU$bCIK;KcjqBT390ot%L5_-0UaX-l8b&ZB>*Gwk<}-V4p-l zK4UtUq%$pZ>;CzZnB54uy0~i%y}Eac^Xp#UyFqWbb(7xSze(>OCey>vH5A*v5I#S< zZqGlqRA!o0D-T`Vw^6)ZEWUnuF`CZyszj%o`04ii!{N(Msh*qS_geq#=NBR=`sWH1 z4j)@D{tqmgi6&IfL*ekpZYL+x2T?pg{P52%MA6A^73gTgY!uNryDp#KrwDXzmE07& zXA@^j-nw;@5)%@L*{|Z)Q+)Q(E*0ni?ANeHeiRYx=kVuuFPMRbR{D`*c8h3^GxGWK zXY}UH8{@}6z7S2Joh#6;I^Go8&{vnw?^GZo4Xl`lPKiI;_=!Yc@chj7eH0%}dpcI2 zEw#L8cRgR-KEHFpjI?**G|?woe)%48jeUI@(z>2Bgh%MUjEgvB|oAKXk^8yDd8pQF#;C|5da_mdCxDxa4+A%^vHf%G)3Wi`5Q zzCp_zjxsbG`n?mdXS_{2X+rE zHgcdvLA&I?x?frfX1>bGh1g?!{3_{e6pd+6irD85_$edJZ(D;N+PH_iN4C(>Rt359 zNA3Qv2wS~=KG?>~7g+ck*KiLL0!wk9KMeYBal7ichYRd+6<_y{ZlRNH3aLB)5DTdR zUt|m7Zp=@WdtHx>qNzcpiGBWBy?^U0zBhONaOWRlBx>!SI*1*&E#Xfmc3QuO+qj1nEy~bb*quwNdef@v@a+(L z&_n+P=1NPQOXZ^QU7Jl0E%Vh~+{4;dWnuq;4?2E+(2vl+@CUn>`H`Y_Tfc{UagnsC zO<9iqnx4OdZTquLUigdUf231;LS&xa^rHv3hi&c3a{SkW{$l&P6XHPwE9Rz{y<1El zDD%}r+{5k;<JHMD}XK?+-7=c znXjHBHWmGId1d?Cj^`KrKjweTshf|k9NI4P$)+E@ycngs{~`FF%>P9|~VeZ(vpA!fmHZY>bEXoI)~%bj&W?4mnj7c2+X zuV`i(Sd7nQk3uYAC&9$@9 zHpClu!KYw3$Ahq|kHW8Gx!*|mmZx#HvmA1KiyU;hC7(;a(KaXDMvUV=a#4>F=a9eu zbavc-7A_aBc>k5WXc1yD%c^?eJoch>c!o{2ykQe$qn(J!?5mHQsv$0O9I@XB*Aft(>$bxG-&KT$naDElk^*6``FiiqPJcMJTlOPjtA=PjswpQ3`Kgl%hHm zLu{uwooSVq%V8_g|Beo<{Daxb9`?ENU2(?q6Ac?2&&<`I%fMGc61(L|2dRH7RZtU%k6~jV=rirc;P5 zxlaDg>HiyifVm9{QPQzJD(=|yD~$eeLmN>m|C|G zT{ygxE?v4rmoHzYw{PFd=I6xs|K{#>iW(eDp@{3Hmi}k)kBPMl(&eyy+^xNP_l}|> zBPk{(hF+t-N$K?(_Z~5buQ^h6eu?BqS^l-jPhZ&Y<7yS8i(%$^<|AUb2?^bBA_YyIqTa`|$D^g-`BnvY&0Bh}#JE|IaLT zQ7u2k9oF^?A74GE$SJ)I^v`BP{`H|ht6A(+A}0;3TZ&liVHe~+nE%4`A+Z;xoR z71%SJn%Y~%{&oER15v}&wkYaQHm#)lHHo1c3($q5+Mgk2 zT5q2I%zu&fzZ{dFddPXS{K=0(H>@z$c|J_efr12R( zfe*3Md-ML6?ETwM|5~W;YFgNr_OC~MsU&ys7|$@XQ$y+r`QiQFEX3@eDRr0)p^Xph z8RGB^9Q_0N+FM8cUlwlo zkT9z^MItvY>%XYD|N5w_JGoyQ?btIc!843N3{{B#OlEP!hVXwiKEs!{DRlMsU+HwS zoR;wi74{F4pXQ|kDDseXyG)?JaD9qrSk^6&Mk5C}p$s3t;^GgDU?+tvp_}3R>DI}8 zlpMZ~?qJ=G*iZMc?nmyYhggrI_S0jmr>8`fTm-6F&Y{GkY3pS2iD=I=o*Xv8wAcJj0sqfiwy66Z80? z7au=FKFkY&$>C%P^WVFDdvdQXShb|djxWM%+!v5HbjkS z*9!UR%+Uj?=r8=zbHv*=^$euh=}0T4y0wsr(yD5x zM-cKCTJPTh`4Q(|nEgMrMj^U*;-HpC;9}Mta zZV^fd*Y*r7|8l&4qg0I_3i(H7|4**>6D39**2IU`c!ux+jkumsdH>hB@*~{;z+%jQ z%t+H3{6tqHwLe4Dz((rg=T49xasHRt|FZ&%(zVDVn)vYMb`qT))F@T+7lQxD?Ekrq ziqZ8b?ay#_a3kyaBL~l4wfT4EKQ0I^MmM65YU0CN@F8x9mHeHPh6@ywz8 zwX*%DR({0w8(jTB?Oa^_zW4wPS z`Cljx`u<8q3a~n@N?2pCjCET0fwZ5D)j1Uch6KX_;cB%|S6&wNS`|=RUD+=kRYScN z>-VpdGdk#}OY-_OV$^g{llH+s*Q{u+S#E!NjJ zT!^8fhHE@xU=s~>T+B<2EA+N(7VRsOP5gJ#c%+w;(TE>UdgEOx(nQs2dJeJ z>U6}k`En0 zUV!yPpA7OvY}%KrZIbezJ--(Im&NnT$$tZh{I^8iqMZEqMD9r{|F-!5dqq!LQrn+a zH!4VLg9_4y;DWTdaY5SBq#$i?T8MTvFGPD=6ruwy3)8_?h3QD^!gQ=n5jxqn2t@!> zC%XMl6w|RNooSQLLjJku2j0(3Gx;~wIhxBq_P@gOJ8S>XL>zE+i_&!c_#S$GGm)O( zxI!=3x_O0O8deIfY`wb0uh+K{h^;qlB`46kJ4y61?l@f>SclHQKC{^Wcz*N!uk`$` z-T%{=9oMWB-Hth?$)1+}^5>MNlr*C!oobpxmjBlGe;T9y=k%Wr``<$TW%fU_!I{#g`|K;o-LH~0xfZ&pJr6yyXu7V zU&;QF%KxCE8EJZ>l63v_3FD*OxpSNL?Ac9`sD*umI!so}XzRtE{p_?~VE_A=o?nIi z%h~^_wEtQBcuFARG^dYSJl>;65ApmFl$e-E?``adk15aT3~H))|A)uYQS1M>e*Yu( z5Bs0hk4FF}ba z_%1xVaQ*uAi?J?^J$uBoe!9dzxczUT|HSbRiT!Wy`HyEG-Jmojo<1o&lIAO`1?Os! zr}j0(f4mL+w?F>D_;20*F~z^E`9ISA&+@in|GzVG=c`8a0=~*8&Cf0VgO|xkw7p{` zT2(cxiT{%LkE{28U77fw<^In|y-T9!a?v#U^2sq8Jcx^+9qr50TGSKB#s7@O;0$7rZr#mbM3?E=uhw>bLo+I&3r|o~#?YGJU+#5;!w`ZY@)W1|VI=IAf8a#p= z?Cn^dHrGT?i%k4i&H3+){oe$2|6R-Erg5$7Qip<>siUEHnTvn3_#dl%F6~2ymd{_pJlFBku1@lRIs*bZ1PVO!T3?bSCgU(oX5 zy{Id4A7;oZZzc#~% z2hPypkhMm>3(qcG84cL^-wU-*y`cll@*l-B)7T2YAOH;xNY*PR10$msl)`JEEP=7WUvTv6E1Frp)N`4f+d>Vaq zP!1RkPIa$9CxKb1l>fD7|1%uudIfOK*zxRQ8f+ZZlgob#@n07I=JMa@VVPb9G5ZzM z9j3tx(BMpu3KR)!1ikVf;5wF`X!4Y$u8u z*NIM#??h*Q>rAl|I#V20{KU?5aZ(q$Jh=-cOzBE-=#7%g|2yLUS4Yod=c>ghKH6~_ zY#-Cpoc}`o3-bT-Dxj|onD0v}FUv8|05Is;Ot+u@lTw8>h`~K{Zl>kY;~_*oGzYnoCdo<1J*}3y}Wt- z6RZD`eSSylpRAt$68qOm{I3CgfL=99&?OycuxD%!{`s*7zNPx7M(7#qQ>z4BJ{_T2 z?&W@w?S*}y!7S|k-+{@_f9DoL)paH9YT!h*;ci#Wf`cD@B8w~7=#50j{ zBCh&p3=jO!_#WKzF9jB!Mg6CQ|3dwzTKRY6{I^yA+7`Ww!|IiyBpqmQ1T^5DpTjd0 z)xXL-e`@Z3R{txG|98ljo<<<|a}~JTj=Y#0g#Fe7^`EH!MGdgA{#Ww+LjL23Gxooo z^^Yw7GrD1EV%X!3yci9Rg9iNb8}eVepa0IOf9wX_`*DF~h+&^Q@?tbNF`)NX{{{XF^-rw+WoqMcbTiIz8bnX( z&ey-&+y7u;|2t>@OY@@%1&huZo|Hb(q*8jwMnCG>qKzHIDr$OA5?yBowh4`nd`X2=SCw_h*{>|#27q+ZG zcO9m|V~Ph2`2L@7p#E9N{}}4um$a%#cQ3>^xECJNg{j?b*1v1Df8_H2Qvcu9`7ho7 z0{?~hFYA9;*196y(}4z;Km)G-%fOLl4|*^ue+9#;_Q^!pn2Y{cl?0A8PBLmHZ#s z{zsYqFE0Kq%6}}W^CS8n3!wk80N4MxN!)e;zTsE#EHl+!iiw>hf4w{g3TZnYUQd(up-z@Vm2e3%|&Yrya+bSo70y8z>5H8 z1hbdeoG>=Kn$7C!g_sem8T(a)8Ntp>Ham*VY-ZT6zX3O5Qd!iEu)HaEZt!|@9`Ys& zkvCy8^_C%Syi$rCf!Gn7@y2G5vpJ)CQ9sJ!2gdjjVu&ZuKg#A#Mgu>B&AvSge9|~z zNL&zONI-|MS;)8A`g5~7?g2;Qp$v|MZTj#tn;CuB3?zN&la;0#>BHk{*rE@^tCyt@ zuobxYf{{Lc9DPo6^vU58`dG-12yet(e)yg<+wx<;-_;;Ln~{4^qz{Wf8tKE!4^JOw z%1?T3=C|VhGSuUn)5rS$V)M}y_ZRZh^HGQlC3X2wp` zr|dy}%Kmy@s82Q3ryNIpN;v9MqJlBM$BzBwuKH&VYIfGNEJMeq52Wx}LnvbQ5Q>^J zgresRrPIF;r88J(=MJTF^M+E~ykQhSe;8d{FpMr?U0FDc60njM4X3M%htu_?BZ1-2 zgPVPN7Bo_)fA~1AHu{+KkH!9SSU;Qg*GynrtZ7}2*vwHqUSHn4r0eqsQVj5;Q%yhX zq<@&-yRwzOw7E8a5cG2n4xjxj=G&>0AF&_I*8yoczbxcO2l}wM$nuuu=*~IE>M8`8 z`|>7*l0X~od}s5w?9e}AoGjb)(O*9c_79t(yrg+qx*e+@Z4&1XM*VnpZtgv^f9#=; zHvMCTK1_bF?6D6SHvJ;dCRr!ie8#+hg!w~|U(U|WZb%LL&|-htsh`>GiUpW|oqX1@ z+C^bcvAN+_77V4Mpbf)c;qB*d(>@#pT#`A#B`GP&v08UAZ7zW}hZo7jb;Ft5jtn>WqJ z$1vN=Mtsbk^GjF#EMp(Cd1(_+Z>F{8?h9mx>7+j-bJ$of*4c%lX%prdDaOYH{cO}f z_Qwyk(MOyA{xfh{#y8SV8$nq905;dxFJFXjxjwZs3&+s<+L-r*`b;7XjIx(s}ZLZ}l z+Fv61aU=V%DSFL*E|-_4c5O)qLI^P3<)zqO>E;_zrlhKd-@a`>boHCrc7RgFXKU?JAk%J#n z(v$W~A5I_PBk1vZ_xLVFjc!M~;DZ_ccZ>byTK~O1dYRjn%tgnxZzK@M>@h#!xi^7E z-Enp?{md|ZO!Wy_Xb>=shL`rDy)*Qr(LFjfrX6O60Y8q_Cn(T|-(Swm@1&=?z&mPP zJQsz5HVl(d?)3pWW;1Bi3uhP8&nEitGe3p`_hnQW zT;sp9`Q0sw`O}eY8x(0H(2LP11T+BCH$E~S4N|=pwU6Z z5Cwc}`|U$MKd9DzW>^``iu%*xt+r_+JS3yh)~Um(AI>hOpT(F_eEm-r8pmifZLqF1 zIybHz@S}Vz>NlJfKV(=^jep8XhqkV_DK`Rbge#-bHk|A1d>;V)EYuX6>Z7vIgmSgC*Wg5Aa`oY59+Ib7}icuk(_jJi+0+GX|x?QV&^;4&vNx+ zlgoS4p&9zo=mTgJKfWEEKn^SdHDo6GsIA|yoquOoR1JW`73u(O1R62Ba3^e^p*Y`} zem2PuU;o4E(x+AMrXw?oa1qqBw*i}&d9`tbM;UH+{d>Z5!$(npZR%88OM52g=b~5 zu^TiR1sjOYrABGG zbjGnhO)-u3P9H{NU;~W><{e*OC#ugx4dUD?=wlq;lj1`bV=bZ!SeLdeqRU$r)0M4@ zDFG{S+hV$kb#41%x{h^Y#}c}UmArEa-Nw4JYYE-Oy1!>R-CQ<`&bG}@>g&gB>mSw& zFVy$4^SxWm(scQ(el*$#+h;t^cgy-iFPeke!?~ETz5w_*i!tMSnOSdy7a!kX{yY3r z=6`Yi1+$-FA9D5&&i63b(I=pLBCxmk`d;VAk1#8*7O=8<)G9+)bfgip4=0rK=5Vu2 z^}XuqpB3Zt7WoVN&o8z1p@5}Q2UuFYYL}seSjT!{#d|Gu#!zDC`;>C#xMH^IpUvZ| zqWF&meO#O0Vg70Fx@9R*M;aXjjoA6l^s_K`(H4Ca<%jvFi2a!ApT+W{uYD-!A6|Y$ z@eSZH_p1jQozqW_4ueMQe4hpV%;r+srjOeCa4~)Kvk%4kSxO%kUkw6YS^xUwDCwMI zeWv2Qb_6t37B(ZgDcX___M-x zap=lu#QJwu)$oyE&)LYoOY&dZ;)m+|cP0DBVt+~MGrItj?zhGj>DC4PXcPq+t*PlF zs&5g~M`S-MV=*nAX?w(OCZ@whvunKU>I; zq&`Z>pR)S8Da|TT^2Jz(_L%U;Y_FXLjW*WywO&7FR=?_+{0_sqQKNsP{gtdfb6WGt zbXzwXrBKYA!I=H$`|Z?cGWn6K-!Sxdv-;i{Eh^KUOM1ys3Y`Uww$=4@%l>YQ{Uy=Q z!u}HXzq9)ASuLx8M(2d>;?VUI`rl%IA51&y`C8V8r?UTUK_3PEW1fG{Osv1)_f}Qt z?qz*w^cggY1C4g0hF-Zo!JYMwxITf;53>H0d9AC`y~}Y9$&c{Ij7H~wA4K~aNcvMG z_K!XOyEE)VseYE$C$Rh=>u*}vMqe6Tm^+9LpofQ>PxX!V?}+outUqpX+iGc3){t^E8Ap`zr9HtMCzrpY&)`3Qk6XPAyL&6_3?2XjGmr&!s z^YyEi_K#fs&m_^@DTYt7u}e)|X>@%7@JX7x4xhwAepK4e;`3e5&q95I7+-4+W^-)- zHsy&KgLr%!#1L8hCmI+xr?FxLd>dfg!~x?be*RFpfW_h4ED-Q*hSAkU!|3{w5e9sl zd`_!R5c13R`0sM|p|n1V)wf9V@2o!IJ3f*X{HSjjpUq-_>7buY^7F5U&nCqLx7=SQ z{3w1dmV4yi|AiqUzd!nS3OJnt42QHm)^RK&4k)qT@y+zvDBzVcoO7d32b*&;Fgd4kb+N$vMqSbDiYDDJ%>5JU zAK2`xRlqP^Q-h!XwE;cko6rL+)TwO;jwzdiBlMcFSsKhWu5-~oMBz*4<1<3pRd)iN`LUc zoc=k;oc=j@`hy3`^v}i9A3QM8Un>71`eS~Hy!`7ye>U4*hW?193jUTb=Rg(xQD3J@ z{yF;db#_Mi2TlpYD$z#&>~s#YZ<2q#=x@S9cCP%V68$;-Bc(r|>$gsSTl&Y9^yl-s z4(lI1^}ithl|5+*H~W*v{$I)A|9jI~V2G^eX5ay51$FLQz=v(9bLX*)&`Zl^L7~YOt=QK8 z{*;Wf*W&zT^mn@b<0|@l5{oTu=~17OqmR(zggAP9^QIOBSSuU}n_qt|R*J5R3?V(8kOKEMs;`#xmpZ)X2kp}(U2<1GCzV}Ht> zzh?HQZTn{yX3lKxUXPw!yCj5oopVL+_{04x=}dd<1I#92xXrTk|7Pr;Ob**WGg#dD z=eqRd>P6?!-0-e{e*HrC_b#W?&^^(OeI3yMYV1!n@~_AF>wx_OA8Z?p)giwEs$s z_9t5vK+TAc%J`3g{w}e9?9u-+_K!{b-(>%=nv<2F{X=KV{U2ZH&i>V~{|j(<8OHdV zMf}Ge{i8 zqerxP(g5N=k|a=C<(rwyqrQLpRU?(0&iFKCH%{)!LtG z?H{B4Db0U5OaH5}e^BpVEE6s2WJ!C0mM>nspd*J5)4|YCI(P0I-Me>>J^~+J>xVN;FMLd5QBNk~(y7or^uYt*j%*IVZBoGK>G(aXfM$7HSl52V}{M8OPA=$lP9Xr zWG44F4{oB}p#3&r4RQH*>rhX%+)Ii3;r%l{!aCd*S;}A3xB`7ta;v z*D$`^0PVLmEkqk@dZGSAl7B}(r%nCiH2SBG{loguXEEAeyCRU$p(~sJ@)0`7Vh=Fd zAKpS+nii(@m@{C1{goc}k6itg>+65I11Di-d;MrHFpkZa`2)Es;l;`Q;jI(`+OMq+ zj6YWY_AS+4sn-AM>)*Pe4sk~N`nt)zKx^U3Y)i%mcD;Uh8*OS*m{y|(WgTX;nCcHD z`bU-h`R&_3T`)gsVsrF%UQZOr<+SVFm@wK1+OMqYXYwH7f4V{UPi|QQJkY1-(mD^4=)kYR#u-TJ^*`$;m&Bj-roS=%qmlk%HSsq+{4X`}9~OW%fw>j-x#e+l&0}r;K20c~dKZ^E`4gF)q{%MT)Vcjd{ zr&FQ3kdyjKk-I~vcZIyv0r-*(Lr97KtpAI}pXY)H_g%6N66yHBU_Sng80c<`_|Laz z|1fOoZWZ!VG-xlpnExNSdo%T^n3p;NztS5085Yj~>BjaPvO{lEj}fBg)cOk@9}Mg0Sxe_YU^ z4n1%Y4^9pWM*h*8!)nv!|GL2bQPF?b0JQH^E+0kix1_zm1jd6X?1TQ5@=`bOK)^YX z?Y~3)?NF8j?&u;O+)AVf@W5vN-NODv{Yfh4zv^MGLWi>XC}RH(fz*muo(G#~0C>RS zpA3gkmi~tNH&%bLq*EQ_f}Pw~fjfQ;T@*Pei1widLb3iNSSJ2v*8eM4e~7oMH*;1qZ;tV&+Y`psyC{(S!h!z5hR zp$0Gs^}%fv?nCbSIyr%2h6K@JY5lEQ`8SLISjqnyeUNtW<|BKn5miQ0m_2(-()^rIE-e4b`0S}IX2g>#5uC;%vVCH4>l6mR) zo^1jN9li=Y7+Ey{{ukys!}b{>Q-3bt53v4=mEgfc7wm&Kx2{kucyJ6eGdcW?RJ4EC z%)h23^3t*0jP~*~eC=fh(?v1Z2cxUy1r~EAtMq3xC0YN`>Q3N+PS~H;?wjp{b3=nL zlhcQr$?2NgXm9Oq?f*BHtBu*Uk;Z}ttp3x4zaiIu=gIZot?gV( zH{48Xcm5Xp;5>K`9^|7#{ipW&^9tGiN$snar0Z9l8xvqWIJ0{LjjtYnd8(PXd7;+u ze^~$Nx-PEcLHw{FJNi$poWC0EpUQ|s^le&)ZY3pH6BUR5jhzRK2fx(_;PAYy(SJHH zm{|YyhOV`B!~L}O0B@78&_(dTX8*Or_K&&#;qiYkw-e{@ui!zlvw47haCX-Qng||@ zERoqJ{uRR?*woEMJh*k4E)5R?{y_Fr!5?7u=YVE)Dfz0C_W|Pp_QB+u0i6FO*gx$2 zwZtFe@i#VhtV$;^qc{Bb5fu6RaEh8cf}*ib%^N|dv0|{!%pXCq^GDLT1tTdA>-@rz z6pwXb(MY<4b$Rhfx`LIkWE3T0C1G7%I*P7g-M~EBn=8i9m9Z^>zme04^p}(Wbi`(| zGW@q1n8`Y@d0o1F)wwl1Oc%xOUQbiN1J?h@Fm>hW&*Q%_?AUn~v(Q4U#g($q(#lzB zd6lfF|H(?Lu+~=dr1jN3X=4pf+Fa8U`4=zRhP9)%7wxL!MSBnf-B-_>4qzRu?@fmr zU@mpTY;+9kL|`@wZ#v+ue=DWGcKgQy|BcIkrK7sw!O)iV=*~4K?*r%}#)BEP184$f zUt8)QEBJ@NKD59;w7332ZT!JZ{`Ikcgqdwp{7Dvn9@etHOL!0m9?Sv{CY8w|!JjnD zw$&v6{G2ya{#Ql*T`m2cV*i-*zZCzop?^9W(W*Y(b-_M}+r5rv*9qYKFJ^zb6#r8$ z{@|4OzbO8uBL5YbD?J6~N=KtwH=w)MldPH04*whT(awVhzt;`m@PEwcuSxzT_0Jah zcg(|<)*o8X-&yfD>-LXS{sY-uXl^!iIvUfa0o`)}58}atdEmhe%pN!5->RkmUl@O~ z32J}FwrxoFZa9A*Fdi(Z7eKSX18%mtTKdb{KPvRUh5WlG{y;PEV0^oVbl(L$xBwn3 zs-KU;KV-AhE%0yu;`WaTf7QJHzeS$(G@(7?!BvNKzO^@C=fTC@YiV))0Al`^1^wml zx1DW&sQPHZ1Y51h_}uN)7Sf(P>}dD0?V{+HeO=dP;% z)M)>3=dXnRhWh_D;KAe$;K5C2?}M*&3HxAqgM74*;SX2!=H|;Q`@d5AUxoi`q5s{< z{*j|U-~YnuZ>M%_L=SIWv!+TM{x_zJE`tXv8|I_M;K4HJ9$EV9Y5$~h{gp}oE7PCV zpLfWIdCHw!!Go{h!5Uf(9xScmDNp}YwSUyqUy1aOV*RHSf6x5-A}RXv+b|DzrHM{WE;ZT!brel4LxHF8Bk$JOK|LCp&o`pbtI~`+(v9f5#s%?|)(SxAVF+rYFwk0d!H) z?$s0m9+>bqB=NVuSpOYAe@edp+Sq?lnDswqr1?L)fCtzIj0amV1B35>l+gcQ1b@Jy z|A^r)q4slO_a^k@_6=)Cs>A>GCwOobJYe%jEc+i-_+Rc~|5)k2(>(vnQ2*Z#wV#W7 zG@+-?<^gokwLPn7M`O%_F~c8|(*N7*ziXU31DVrb+x)x!sQp~h(*-=hK43i9)g&M0 z9~toX)aW0z_K#WoO(FlTw*O8|{!20bV?+N#_elGXSpDs?UQOxg9p~$!>)-*Kf5hQW zx>f#5jsL3${(wyX7kU6#{p||y;Mtv<4(ohtZ@_%C8{ok{@L*S6%>RN7WWE1UrvK|3 ziT}v=zst~{)&H~p$Kt5-|D~Dpc>rB>W6w%D&Z3`gzf>fX)h`CTXP1K`Eo1P?-+V-}k$@wXM@KQ{D_6YL+Q{=1Rry<6L-Ie2i( znksSl-{hexmDxALx{deP0`?-@rRUBiRB;6b>v=C2v`zx4bS=^wfLqZ<212m6yhe=X@hxg6#XVr?DJic;<;J8b({ zdjmFilg)mNY@OfE`GZ!@U#HtY()fd#`6ImlWvl;mDx1HFy2I@QUB!dtn7^4{UH_xQ z_KzC>%O3kjhW-}#OH%rq@CW4jk68Wf4j1vja{jEd>VHJ~$Eo&@g8gZB{#vI$t3RKG z+1I-UwWe1OZd+3&j{h6_^dIhDWthKg=zshx#(y-@e|AOmzhLbi+?wvjhI6yWA76>Z zHI|+j)>B-cCa`6gL5}NlwnX#C*&OnhY;iNl*&On7^vbYav=xjXLzTz(8LQ z?AwjN{bc#aEvQ4>hS^g)5qsXv@-JBX5&H>kfcOu5-y^`l3Jdh5(ABH|H#Du<@&$=h3p?Q{o`!Iz{M z&dSG1i{h*(jqs+Dn|v9m9hNZ5p%Z!u#QFU0s3q-*#o4roDfNL} z+YgxbLSA_wt7{QqD-JK2g+^j={#~i8G`4hB8V7tE;f$Gt_?{T&V+M*`o_Z_S^n?06|w$$-~@8QGG9{~RSF8CgML^D_qH1MT^ z@IMa0|6qCOqmBHKhw$TbK|y{7U&Le&pN2nj#^{q|r#Qq#;#=gP3s{$0=A_G53GfRO z4Yu*MHaY1A*3Gv5bgP{|-Nw4x-k%#EDG%UjzUB zWcY8WozQLnLkF1ef8M9C)c@-FKLNa-R2CSchW$T{;VfF+|KLA*zNPwKng2e#{)gTd z@Q3*QFXF!y{eQy9e_sEa^Pi8&TJT>{|6B6^KBxcX_@Bn|{Fl@J!v1%J|8nO)`e?=H zKd1jy^WVz(Z_xka4d=fi|E-<>a{QN^|GfV9;btG3o&R$C-wOX(Edsy)Mf}GZU^@Th z^uN;i5B|dj5TF0|q5F;cU%dab%GiG{;lGvhUl0Bp>_373e2s&g{#QEx752Y@|7&6U zG5e3>zajQxlmDvpzmokY$A2aL&&Q|5{O9dIne$&W|7GkyL+ym)`acuN?|(k0X{rCM z?0-4_L-z~%-@5(h3A_o$e=lRsVYwmaz;OS>d4~a&A3A^>gph|g3|Tk|KOhWw2$qXD!Q~=+4fRNVz{B*T)5iU;aQ-Xme+B+y-&@%K ziu_lw|K#j{tNa)Fe>UxZ1OHXq|4QdSUth!QKlA;shW|M8>-*b-9_4CjwtThet z(AtK1XkEj+v;k{lU|!nPC@+OzZ3)Ut+px9=2hh&O0kjKicas3xi?y$5K01IE+AJR( z!aCeMA02IxkHSz3dkl4eC$LVU7A+i$)#5~9ooZ8nP9qn{;%Jr}JP_?lZ6qv)k!rQpiet4q;zcgq|)+a9^$ zrhbN;q&ENG(*JWe|6iW}F8BWp{AXBm`$l%6R}b$vUen^>JG@ScrHg|bi1fc!`(G~p zBeMT&?f<{3|IgJ}n#X^HT+`kW;QfQt=l!dM7>XZQm&3!hdH!qi|3&;4+y8R$Uu*n# z#Q(E-{sT*f#s5^>|4RDbGXBH+eJy)M)`2Ydnebqsn7|p63)Z_9oPWzAI#!2 z3?hfzf$~1r2mEb4>|qM z<^R%a%>T>tU*7(=KmIF@|7;!7-Ys~af}V?*fepDisK)vaN&ZKj{#WL|eEo+^{8w)O zTg(4I4!)`UKlA{r|JgFwU3h<N$ zpIZJq>i?q#bWCZ?BOKb9&PSb~#Kc5;_39P<0WAB}c%@vsKv4r5aTr93^?z3PzvKSD zS^P(=|NmnCKf_lTjeQ?7ybHa0e4k#weocFJ@1|9&R#A9(INiN-m)^gBPi#K8D_$wr zFH+>7APxh}YW=T-|LW{NrTCA6{jbUYSJMA7`(Mug!~8V9{@aEAKYFZ3!tdWatP7<) zx@TnX>({R!1G{Mc{Q0zL(_ z|3>>?RR6EL|F1{<&t(4%1+L$Sl38ifQ1JetiT46e-@JK4hYp5vdSLtZ?R4zeF^Z3m zr+fGA(c8Cgbw3+YZd{`9LBZ%jW*C_0K}PMLY5(&$bk^)Yb^Bl0|I?QLcai^ZcK++= z{{vTZ7HRZ-- zj`s}z)U5wQ_xe9&{>%ISO8p-a{u}qdDE=ete>e002cZXOXo;+}VTg6!3ru~7y&fGI zNqhJ1rHF_Kx^(H1Q3vQ@6ENPN9MqV@AGWCfw#I*B|BnX$U$Or~&i_-$|9lhvzg+w; z3vvBlIP=$YyuT~3*yi>9yLS|O_6&uEh0(cl=ji6moAlzv3;GJYP)A-VH?Po%K~1>& z&z)TVmoN6ey83@p{kOCD&*Hy^_}^c^|L>1JK!&TiP9yIH{(gWDaPdMsoj!Gn^A8_A zdPE;TeiZ&$^Of=bIC#(1|AO~BHP-)2{eScLkCOkVr2n!1)#m>d_rC`F-;wx_T>Xa{ z{~x?xt5WAHG4tbx50sRc06DlockbLFW+U2j2Bh3dpksrZa@aGL{ePnUZ%zM8{6CKW zcGrKH^uLh*vF`t?=>HVXf4TZEIsXs6zZU+Vnf+&<{ofZeHToCJLTiR}v1;on@m%`z zPuTOxH*e658`tUi^XHoVLB{(q@SejSLfyaP{eMpE|M=$p|4h^myk9+7AKnYH@cGk6 zdH_H0;ll^?0XjkLE9F)q9Ua_^@BaY)h*|%S-1)C!|Lg7l3;SO&|L^qppB?=l7WJQ) zDFs}szD2XpD)9b=6(3J6@1(!?L=O-|G3V(*L(x z|0~4*1p7}M|CKxcIk>`2WCw=)>{8t5bL{$ikN|pXtr36#8r<7QlFa z1iWXsa~mZ6|3>}K?SF>;J(l$6f~B zzjOib1)=&AGmO4|{%qk3q};wnhlaGIHAdXYY|8te)Bh6xU&Z+^U;nF+|JPLiq1FG_ zQ2(LU|Fc>DY0CezdA2=@WT6#9x{)h+Z<2vO=ntF+f;JUo{|)EAod3^w{}bk%5B(Xue;}~L;_3A1{W~d3T>kyrClBbv`wzNP(m8TgR;GVt!% zBRUBgSPdDFKmXfKXU%R-uAyJ|I-6LVE;1N>A;d1ZXyE@Ap^hAx>~-5xdY$v zA8T>@Uxoc=Q~yi+f5d(m{%cRjzyQcVs2j;Z1Y}@CZC}KH&G~PP|H;LFzx)3p{9ify z&*cA0@gILFoRJ2V%}xiG&ZN)oa27m$NRg0%jgWy&V*EGgeYe1Ww8;M}#eY%<|FIQj z8Meunf!fLX|MK{sy^Ca|!R4~kAvcnNs42hD<~qK#SyTQ;2LIWW`G0Z!kD~prRR5tv z{}1v&9g28UTkyWE1piyU|I^g})fX}_q+AX^~j)|K;+3n)$EQ|1+=uZ36!-rd{eOk{zheKNd;0%Pe@sXHYL}$A6Nfoh?*5G= zn%bo?W&l7RKo@i>VDkUX&wtDM&;F2s;T3YwQMZwS2Xq=Tu&tgiU;inW|1tXi%KG0d z{-ZsVKXX|}^|CE=39gu+?e^vkAj{Hw>uC&y& zV^Fb7G`eC=I_5?)5Cc81v%a5U{(~t0W9|I68UK+x|CQsvmi-?()&I-If2{cbjlla~ z!28(ahoo#b`~Lp*B$@#k=!_Zw7XP=U|JnW@h+054|79>_U`(Z)bbO`z=z%j+d(m#l z05^jYH9%bdmo@$y?SBpR|C;juBL0i*f5rT-Qv6@N{ol|(E%mHX+$Qe@LAZZCk!E%Y z=J?+YI>0pl#bp1R&;Jqi{}s3=0DlyKb!4741Iv(EBn)lRdeWz zo7e-fkb!-W0S^CN)&3Xxe|Fn{8u~vp`+v6gfBl?kse85JbmrJWfvuXZOa^9m4F>;F z2ZH#oGXE{{A4i}M__r$lbaJ)($iTU&J!yYKKMwytweTNYRsUT#M_TGutr(pNvybdXrJ+t$ zit5gLu?)=Z7EFCu4-j;Ks`;PbKR5rcbQYRYB?p~YI@N9TK>V~GbObWM=Kr`3|54=s z*^2*h^}h=GU*i8)2k$#nE=s3#-uD9Ug)7qo^ST9r|CvPmSJM9i{vSX8bv$HXYSkQc zav76>)UPFDXTb%?Kv+uFyGJY{5R)6!~dL6CJRlknv=qpO>+;M@X39;2pKpA8DR6jO!yDH|7YI+ zuQmT;>i?FF|EuD^_55!Y@V;%OpIpX!L9iZNOP~dif&Pg7E6;zH;eXkzG!Zf|qgqai za3dMG1Q|FUoE`Ju%;!IH=fBM~1{nU+5QhI{rvEMZFU)^n^WUbF%R;lO`%~14>27fr ze0*|`u1xPi;mG|*zz<~eKP>HkSNs3+`oE&jKdEoCdi407db}ki9yF- zz!v}c`LCk+KhOog*D%O{8}t`28AyN(@bw=$%>R+Z{}tnZiuoVLe@pzwO0fT{=gdeG z`gEbE51rqSZA(U&3@rJ%5e|CRH<&c%O(4CF{p6FC`3z5Yx=HXdHPOiLjH z!x8&8^4~E3MLhq3&Hs?j|C|XKm=75^1d;zby0rqWWJ|{=dBbuWa!D ztD+CIX3h*Wxo=nK0r!xBWsm`u{}~1BciH_9oiFA8tO{NPyL|7_cTCjU?5|EuD^Lj1?9|2Hkw^3On1AOp`*u}@p92bOnlM57S< zA7jA(7xus6{71}zn~fQeb07nYYX3;brVpez^!vtboKNT3!q@RwtOxkwW`145^>PTm zuHc%$)|Lg7h?T^xg>-f6Lb_&H*KxhEZ6Vz>tXta`QnF#)#`Vq)wieM{tb17ZcP*it zOGjY-Zy}0-4|ukj691L-zsdeL@n5w6b@2bI0sFr?Rt>B=kb$ZF+(!mhKn7U;M|}SC z{5R=; zN2>ps|2G4?SLVM&{~PyzHuV2zlfnOy^#9BCf22bFzp(!m{eMUJk3BD)|5(pI9R=pj zNV5h&1|GYI46Nx9$mM?~AqQY-|MT{rtp1nT|EpjFsN%m||F4n%qVvCzaQ^EY|2M>c zZ1P|3{C5=p5i-yyPez(Opu5}1z*@)v%l}M<4>*;J{TTMY;`y(z|Hbwn&VSzi(~AG8 zT>m5A|HbS-Y5j+L<3BdU{GZ@FnP|?y?)1V<^uRjE0L%ZF@n7-$m*@X#V87YD|E<;k zn%RHZdHc^8|5Nh+Y}7SpLW4{~`VZ z|6i^CUoF9ZQ^bFg>;JRHf2a3eQ=d+a|c|4sT|&i_}B|Eq8Rhlc#W-1)CN{$qM-7LbYNyO9iR>>0@Ae`Z?K z|A+w@<3C3KPf7oO$A7HrpPuStHGtj^G~&PU{r^pJr>7SAGSPw|J?W*J=z&d;0ha%n z4I5Ck{x{fv-=zQVO8f`NfW`ceJn5-rzRa}HjbvamWMCFz|8pP%a}fhJ#sB2(KdJwx zc>c@z|0>RZv-*Fd{iikl>z?=z&4B;a9IJKy%(Q4|FM9dZ9qfS%6w<39m;W*6KjOa* z`~UwU_>Y|Zr-c71gZ~KppSA@u)8b*hDCOy6!8A*?>*J&gv;{Kodqva&7~}s6`(Lg9 zZ;bzo`u~mb9~uAO=>LiOKh5^P8~guO?Em20=J-D?!26cK{$luF?H~h7+(ZVRKnAu# z2Dtnmm;aIHzYzbo#DC}GzsCHJG5)8={0AldZ`=Pj#s6II|688_8O-tjAOjr=W}#)n ze{l;vz+_+>WMBbuz+C=MM*l1Of0FpWk^iFouj2f7UjIkx`2R}#zjXkQ|H$yaIu^=8 z%SX763~YxCEUfHl$p0Gh|APM~iT`{%{=X9cwc1qYVq|Z6 z^(^)6fhS4vwBwhCv=}m=&i@zHe+c=1x&3eL{5Qt`MEyTD^Z&N%e+2z+R{t-q|IwWP zvyJ~C=l?f_-QUcN|K|9w9r94Y$&F-SCuCqr6;E1*b6|zA|G)eHR0TCaLj9LC z|Em=L;rOot|G6t5I z`aiVh|5e~WTJC>q^B>s!r>wMgbYFVy26}+Wz@FX>paZ-t=D(QiKZ*VSSM&dU(^5sO z$_n+Lbsz)Q;{TTXH$VS70{2fm|BvB+^@I$p8v_}5p899O)1>pX7c#I4GGJ}~i{t*k z$o?~n|7g?ymi-?(`Tvjs`SV|2{&V<`!2kNCs3)y=BN^BS8CYH2i_G#rR_A}Zi~ld; zzu5j)%>U|>|F`A;o1g!d_`eMQt9LO^+Ay{s^uSZWG)uKB(*ye<18X4z?mPd{vj0OF z|52;|FSGyc^8XF`-{$-W;6DQYtuJI?<2d(_foF;5DYQ=mT35r%F#pvJ{eO%39}4^5 zP5gi9`EQQ@o0-G^>Q~&8HjVq0-nfAtc$OH)$$)A8yNdb`RqSN{^8w0S&a;6>`61xyAGK@V)i9QY9F{1-d(|JwY2CH^b;|C;(g zl+J&d_^(y}Pd@*r)i z|6zCh*TjEQ{jb9Qm$U!m{C{isk5>6FlmAEFuQgU1#D1jqU+03Efd4I>|77AnhyMr} z7*x`Wwz!cD9O+x1LLdX?{8wN9DaHS?#DCG2|JR29AnE@z@?X^dY198#s{c^h|ML9j z_CIWX^YdTA|Du^W{I9`~fo&7|)7ux%Qq3O_KKMD#f}@aut+l-+^WRkF|K;L;|LXof z?0#GGKZcg_qU{q0xQPs$qcF(8wmROlQ!f5z&Hgji|NM34KiI7QkkSA0`(GLVpW%NE zEA34?m<+sh2R(3(j`gcgJ0JtQkOSU>*&lm_`5#*IzsCKq)c>QD|IyTf_f1;J*UU26olW2L6lmzZL#V`#&7c|2WP6m$CmHiT{}8 z|K#WYs+s@5@V`bv26jz$4;groc#cj$26jURjQii3{+FEpdgDJnK<)oSSJwYa`acx+ zzZw5+`2QC8|C0G1z8N^M+5g~eEDp&Z|Y!rmy-H=;AKKA zML`Ba8~D&6U_U9%f6*5IRp!5C{$E4<&szVlEA#&<{C_t8(Gvfg$A4t_UlSk$`=<@2 z_iiEs(f#YuA;i?_r|CIDU`2R2F|6Aa{8}Pr! z1N%{!|1t$KaBv2bffuRf4+tNel5mz{AOm56J_h_xbNnxL{I{+D1^>^i|HC5xFUkK{ zJOAbOzt#MoDF0(-|LI}>E8Bl#4EWyy{{{TdX_a7q%^2!7GH_-+?V5{ePwSkB0OA8}k2+_P>Dt4W66fzXJPd4rJii?BQ;q2bc_;A6S>7 zf_?uI_>cPK|1Il(`1qd+|KDK$3-+HG|AGBB6WC94tNPM$P6l2I=6R}JnGD232BI7L zisrv4I zQ(p#NC!M2) z9a!8m2h-iNC+Xg~2yWfSm8}PC#qsMQu53LLt;gphh^;5*BaQ1RzJ3}XNzX*13TT_2~QH`+vMq|BwD}^nh$d55RV;9jN=+g&NS^r~zd2U-n@gK;DPT z{~_*s7_~rb{(Bf^z_9txCt&x7BL~FK|HS+k_<^T&sQHJs8e`EcR9r=F? z{jb&jGtGZB^Z&}4^uKlf8|MGW;D1T+KNb7`Is6}`{tuP;e^>C|>HfbG|DEjr8}`2$ z|3&`4k^hGC-xB{<9{*3ke^J8!GxmST)qks~|548WOZ|U0n*X3S|EB`~N51}BY5&{S z|5E--{Xg^b-!}g5cl<{g|Bs9RtFZrc=fC6rKR5r=f%y;C^}nS4Use6DhW!7x=KrhZ zzux(OYyQ7h{Qqwj|4~%`ZC3xG1O9{B{cq;~8P0#3{U5-8DB`&PuQ>nJ+WD_~{)3+Q z4|4loss39j|6^JIVOIYI{(F3XeW$>83Vf%)cM5!`z;_D#%TZuTV1t_f`-A5Xc<}$N zU8`DHnXzv&>jht$wi&4{EG0@XO%GCHl75|Ka@~XJ5|h-}S-f{uT1n7#nw_?crN-rMo>? z+-=3s&u14`{ZRgG+JAUd`uBhPo8Oo*m*~eTgLiy=`ee|%&pp#j9#^J(xj&{?sx_up zbn@l*Az^R+=YJCBWhwZdZ6n85s`cBSeU4^+*Dm?tupip?uKLxd$+GnSky7;ke2!)t zmS&iDYtM+#r(u)hruAqTe|>S0!EY93C|~6Ge-AtoQsL>e$>;yyv&UIdrY~CgV(2eB zz6`qh^PhSCc(7|`mbM`~E+yak$AjP7HS_t$kgY{-t$Wq2+~$6>zy5vt?7A_dgW7HW zvM^oq+WXoyoxHExm?y=@S6+8DZ_1i25ifEdn9yV8giDj6uRSdCARxkPa_?HlXC>cX z`DxIanG4pfPC5AWOTJQ};d4vQeX*rUtxan~D^J~?er@@CFUvI;pS}2yvEj9D~{=FWo6zq4xFx4~9*te75jr&wz{b{{3U)x1-no zGk#v$|K~HjNr7scUau_ovE|TyM^0_Iw`%V1J}v)a!}&V#MOV)a8CQGVr-(ZpU+pWL z%g5g*W77?x9oFY8oyEV!oL5yE{pY$T|K&fh%gA)AFa40e->_VhW~R;4r(pb|GoM2P z3h(&eA}x}CD6wy#m+${u`*`x$=f3MNj9r{#L!+l}BKppVEgMn5j#*JkAIY}h|iArO4aH&7&?4(#PKq7+ufZwZ0=*97v+BIz3x!G zI<<0)d!I1jh+eMVF83K0YoCYQ#E_PV+=MAl$y1TUiI69|t5 z(EAEBoFn%P*wX}P|5mXA0V%v#6$Fwv*1Z@dH8|V{dpD}_CD?HzAMQ}QNCL#WAAWE? zlybEIKg%pRyHH>SVSj={$}v^JzCF{eBT~9M(bwuC_;)ziHS&7GMPlTYd*+N9}f#IVca(rU38T)6=k?Ci?XF~T57J^9wnh_!fC>B;X ziv9L0%w(=#&pyc_Wd&&Dnz%^V6DBmgUSAqV(T!{t#AC%T26uOk^!W@AlyL60aN zGtn!>KX+&oR5>?tGvA@?hLc?vX2^9h0G`bVBLJKY-!Iw-pslH^n9Ga8MTfNYtBk!a zOOMcqBcw4t-?7Sx|5I?2&9lH`*>WH#7m|mI7Mv*)Cfygp$SuT8D;6+|6BR+DA@N#c z@{wfkZPiQC&*y@b@w}kw3Euo?IUbEolGuAoQ|&#t<1B$YCKu4pEI%Y=4JTf0Zrmnr zap6~gR_s)6i0TShcVqK1uYWQci2Et4`T1K$4e{I`H=W5B8J?1E`sEko9Yt;Y_8y_X zGV?QNH-!#1g9o#?nF2PuHv~AN)#ac4vh=LWP6?wvc>-w==BiJeK%dQrN+TKbXy?0>>nxQ80Dj)y5~h0EnmS;7FszLsVVtLBjFd zGIvSQ$l7+rEt-;>FrV7M3qeP*ja+wkEu~i|dReBCZzH%1O6f+Y7yWS|LjwyabNWo4 zeAbuoeG@kn)H&arVKk1@X`Yh_M800|VX8km+kn(vG^N8q%kw$ok0tKc%odO&u+dS5`$$moy-fR6N+VS%#QNW9~aC^ z*nN`s&B*-O7Uu9w*pqF4sByGAT6?yJuy%9=#)nV(5xg?wf4OnkleM?6aCS3h%EyIH z;IjL*`U=>PP#cdAr5Hn0fzUX4h8yh*jE($p)m|}{-;j>e7Qp#q%6oxFNQt(Q*8gf4 zjJv=lKWt`tdPGY*w?)9&X?~c%jclqQa%w!v!dfM?D=1FOTy@j?iqwduVTffILm@Mt zfDb1ySG;cLj?3og#`F-%!p^z3`rftD-Js?KpXCm1Si$_dk~z;&bId3f_*r z3MjQ+UToe>G6S0~d-rh9p$qh}H|7e(N4OQ16zRho^Z7wR@pB$7SK$iy^sZ&tGjtg3 z(H^0CQ**CsY3z!UIp1Fy&6)qOe9Ns_6eZ}V+9PM6t`6lwRmBV6%QLx~9`O;y3%L>w zd-2kj{>o1nJvt-X*@JllJm{MwhI$SE249I8#F^6X&_!ZPn0*Py+LRDOTq##GLLZ;d zgDn-Nlp`=uzb+FLeH&0#LnpRi4`q2S+o|Ue9t!Uu0(T9-PM2r54z9d%8qqFiqEjEI zh|e_Bbf!LuK&Nd&&0r^rzKTjD51dyLl>KRl8hq&g64QNUjw~OnVwJ&y+Jdg}v^s}X z@TF}~1!pC&=J3b&AcgfuP=k?P=w|Z{oH*Lx9Pxte&eZ)pC$Yf4eyE31D#9@~Yxt$VRSk_aUoD=5R%B2$v$?mSPG{fkKD5*8FgrSZ}0o_Noic^Ms@zzo!2a z4CZ*QL_m-43kPr@QYi=7t#ZJ+~kh`$doE$H34?;&2E;{+mS?t4-Phh8XPU9E3X$dL#nQ)%`S`WLRwB z$c|N*Lwgok^xAihHca4kd>PVOS~o**lmCtazc4NBfKqQwt}pY}Rf3w+G3Bd??pKiC zHk`ZpMVD3`jy-F56I76<>cZVzEJa3rz`>ox z<;$d!CyGK%SAZFg_yI*7a8Lb7ZnfDJ{j%c<;c3E8_jlLpeh0$WK-=oI)(rm7lVOND ze_EkLFerMCi<0AS0OsXXBjcaK-xE3s20Y{%?3Iu#Dx7VwL}@#N9Cx(UMcOP9#l4uT z`RE~>`T4c2CB4QtR^RN=Gkct9H0BGc3M*t4b$g>h@h1XBQ{``K|3-GA*`9~Wt{yGc zWB9?(a3Z}%?|VdNo4h3N&+-Y`5lGs-3*vQYh$YtS^ z4(4UcYlg?>Pvh6=*nYzK&=&l^8wQi9YM&M0l0odDgbKTvJka?|OTC?FrB^$x^r>+q zrS}-S?mJ%b$tZ-%UjZgEpQ*rW)+4*oY@XQYUEAtUP(Pk~ZVD8i6rxYgU$!i=636+e zZL}o*^}d}0qq6)+)98>!)APKORnmEioC_sP3Gm<>peYif6fpdK0@Q3KcQNsnoFBf) z%T6v!BOP;A&qNGk$!hm0DjFzG3rZrTv>_<|8Ts1*e}@{Y?rMw))#J7HL4;gx5Q1Vb zxhNYL--kT+ecZll-Ep8z@RqJoD)PauMzp@htO?Kx&o&g@*+4oOKygFDQN;bn0*Ml@ z%3nQWGymSBj&eu}AW_y>UMl9Q?qX(3`gZ6N)IC3Z`Ge{2Yl8dpF%jEBdw7lv%;@O) zEPuQM;k}lUz$eQdJ4SGu-8z5hE$fr8@LMvudw)%$g}>Wd-p#ZM?ut@GHXQ6)mt(Ox zr#frl6Z>knB&!VVHUQ@(DQ+1&y1MM>zx`kA`mmG7&Wt{151>h&p-ZTpAW zyQXiYrSw>A3VzUB$uOt!?AwDs*7aQ0?_6c*lhsjDi6reEOjRHgbB?jDaO%UNga`sWio=<%Qp3WIY3o8(3GIu0f%!xTqd@bQyLzK1pbontV0eze6KF3i5v=5it3zx-eY_ZG~YrzR9L|4T>b8y^&a$)+H4_A6@4Jvr|5 zB?CEl7m*v*L&AmpbkDc6cVUQZg~5pNU; zGK0WlV4qX{qU=l1s5ukeW~~)g|CiiG*rQG`7YPrZFQE4!`_*YHczfs>awUGYD)7^y zR+;-*$;H}p(BR(T-8{zssI;@c$1k18WwtLS#(y+95+ToX8jp#t@9X8xNa3@lJ+(y( z=%|9(CMzoGMSG_@RcJw(PsK+f2^BSabJ4!B{;?}^O`6UMT*Gdy=n{JvdCV40D=KpP z9}!H7BPNeM7{x^m;3I>lF2$#Y_32GF4pQ@|bk-2^+c{0^-~Bxb)HD=pf`MiIicoCWv9)z}cYZK|&m(zca1d5ec9dn2neUYq10Ryj{}5I7O99Lo&+{NZ#pavt zA^?~cbZ6MQyP;PK?qOT^xEOe4YlX{w2_;`h6G`h3q)m4mm`*Mm2ANF0c{$XXfY%fA zODI7lqUPTrS5XpLk_n*iF0D%PI{xjzF}TD*vt(PkbuZXIj3q#1V*wq<7F$~7?it`7 ziYc6ys4aX;(y$3e8nQ^-=KJNYnh_Z4;p}|V z7o=i?5$#!tbpFWxrw!Nr>xAa5%2%xA=2klWtij|1c5Db&T7BH_$)PZ4E&{`fP-!64 zsDbn6OUgL#bT8|8xmHQx4qVvs!`BLWF2u;!wz!FiYBB~KT%=A(dQXJFr*I8ktg`Cx zF2D!!DIkR!AToL`vWWlL!*-O?uj^OFbtRW~6cP8%IHU5Di_b6=#RAW+=1C!f`wigf zO*2w)7f0pZ`aSt7%6f~=I5sxrbqkhYko=aGtANEz0TI%kHBjXDKZlt*zO+q1eGapfpuDkx!A{Avg#WCv|`dFWqYAhi5~7p_$LMg~j~mWQq&=z2aV{^QO#{hf+3 z?~4UULn9Y{UZw3FJ$=$RT}g;gYb3R|-a{fc;~zLhM{Yzy%la=LnWj?HNPktMs`;A0 z0&6C(KL{4 z=xVMg;}o(xU@LB<%6!7gO0pHPUJVwU3jJ<^uxDYQ_kw|BZ7|_8z{F^*<8%B37QQ3&7J2GnT;?rxxCE!U7EQc-oAVB z+JtVX}9Yfy<~N9Z0qSn{AOL8i8kVO+yb|(1N-{;(}{Nc6eks?pm)P_eLVGfc#}-d z!|^L?cw_OvH$3Dz|Ih`X#;28nXfYaN_&E&dWkty}r{k?+yQPO7aA6VOqlVG|mKq3Q z1_ct#3&Jsaa=^b$H2*T;;{0P1ba-Xy@dU4PxdKE#Tj|JBUe0dF%&9`dI5#=V!poM& zD$2hFj1o3A`GvbL6~_y^Yw8$|C}4=DXBIKU&X46zMzFnpt;t;Ef-%NnKO;xO=U&{6 z0!rBY_<1tMYS&94_$DLg{K0(;HG0>jQrNOkfm^>Hwea<0i}B+Y$6bBpNNj!MMlJ61 zv;495?|9r?CLX5Y>~kb`2T{K-*6H${Y?jF~(V`fCMKAzRIM8$&3R~x*dw2n6Hqt-N zHaE)q3%D6apZG9mi16=o%~%P6MU=QPgGM#njuD^DN0*mW;w#voNL{<2J1kA0eV_4D zcQ&-%IAef2>Q4cRPa;52^wZ#xW8a0)31C{C#qBP`t_%^ES8R#8(E~cXI3f18z8Odg z!_Zm7&D7AG%>0W*$CEq4t4jwF}|uVQTaKdFIh3?HySUho917{LI4(hdO-QxBrP9C<(l=rFF-AglGIxz&8--wNVg z`opdojr0R~At1K@YJ-5sU(j#-4gXlb_aA4w0${H)D%keSFM+2f=)Yz*nq6P#)+DQr zou1K}{AEcZqQ10jgeQeLTg+>aIadv*qHZ8+)dVzQdxiMF(MsT#yO?{r*NX+qd_!tQ zrfa*q8Od7+oR#qXFw$69|T?#eiX$;Enxin{vOMycvqX6sY}_8 z-O2deR*VP>=rJNg?3E*_9`RpRrC}cDfWL8f&1SxgP6-jP00BF8g7$nZOrAbc;u<6u9AskZ^koxV^~ zKIs8(@zpkObn^Bqc%QRfW^f$#6pJT$H_B13y9a2R#W?3V7?rJYP{#itkO~+cq{0FE z6Si3)7>DeqjXkqWWiVREnLV5pS#4}!OfOE)N_+%W>M(F1#39BQXHAQeuIC_%z#myS z4dc`UV5&Lb*l&+UCrBWL;nnyxUy@yq>-fzmB3x{3q-R56F)J@?pu>0z8Yjpq3DleK zHS~R1dzRC87hx9dPsMxO0~5pv$a(M^=dN;IpG7WWlfn4xP^W z5XJRZes{aXb+*&`*>cdjncu>Y+O#zn6DQH+%c*t_Go!FbWr@nKGIJ5wRgk7&b|_(* z4on7(@&2-BNheP0SJbfZQTrf^W}2HMtc_Fq7nbP4A<}W)N_qw$=3j(ID$d<43ucN` zhBcx;3g&rTmX$CM=)HeLecMMO5EAyfM$5j6P18NXiI;QnTjHTYSvIPW_i`Pg*in-e zgZa7URH`lrd~1-If~oENo1RDu^_(}c?5(iO12r-YV`a7>ZUy`cPTNOKtcgreVckbw zP%dP{q3m{M^=PG+=Afz3sRQ6x;h%&Tc+zfro-oLHgG8(dR=V zrt8yH@bX?kZbv?%-=S1rhi?7TT?2nSs~0Djw)ZO8Ytqk9kC zZ8t}1T+wvtM3I|;`T`%IQyh?Hbjj$Cp6R$(kb8<68Q&e?ge*b_pH@u9(b zgCX#shtzK;{#uWxT{Ig*3mIzHqJB$Lxq4$?XA{J#^hnL{C&9y`t$&9Oz7h8GeZs0)ARWD zGHiM-aO*R3l=T})M&;yVx#zmWf+yf@0*gw8pzwAv4$LQ{t>%n)g4Rf~Sw*WsFF``f zDI;76q%{62*JItsuC!qkd+QfSmjJ!iP9u?!b)^^6()rRO|B+=o>JNI3+v^vW@&3l{ zSAun{9cqLnCZy51)ZD4CcrfMA*<1+taGLyianNTRZwidy!p8P;5b|s+r`x*~mSs`n z9!F{hg#5n|D7B}n;i#`WM`rje@o+J@6W7(vVyR%nTkike&le z=VYiv5{`8ymbBJyEyFK3Pf4}^o?mqyk2a&lhJB#ZfCEMUs1% z!{F!Z#Op4a$>LVw%<)0UR3~~344_v{F$Ya-5G=N{#FU8u9F5odeR`t&x=k!4- znT>=@BehU@sSOSHtBeHMkz2^1syAR!X^ge~dC5Q47aZhGHaT(Muhh$r6PLSs6r!w; z7`6DQUtWk-5M|`!_1H=f?izvpmHKi?BVJ>awIYckYLbPpdkC3Ysb_VxkZUVZ$uOj) z`>dYR6qg&XqJFt`fL@q@DqYtVy~n)~qg_6ile9e!cTRLe zt<}_xFG+iy8?R;55$dLK+S!V?+BlN;{TKcb5hCx7d?#Vz>?|LB@c#e&sI`6U-tGSZ zsvVLK!hblv>-zw#6Wm21yy8jIt z{030cM1=z%w|h;G@mQ-m2evByJY7g67e$UG7@fd7Q$;Kj0Vb9>&06s>=D5fMa(r%F zI`6q5q*6j36-X5C9mAj&`$PQ@Mp|?al*l%_ggcfjCH5T#dZs$aJayH1WDL3URd6aQ65Xy* zuTxV)NS*_Ey^RE#k`giyg|~vXZh8)TK*#^l5Ckc}uRaU%d(5f@JHwZ-F{_>eB{;gG zR>v>FW&CFO?gqf~tgBYgWlPL?is7Ylf(kKv=vT#*Y-IE$7@~B^p!(EoE(@Y`{&;01 z%qcUHz&{AdAxSK^g9ETKznd3K0X!h{VKZ*#^i%bzD>)>-w?Yfxy6w9_jPJ}FJ4@G< zQ6n~ar!no%i#1C{ox~hZ@%$sN4YR$;;b_%BK@7k+E-bWdH*(SQpdI(6A3SULO3}#9 z>WnwGtST_GL>br(rCb5fQIob^Ki-@LPMHRkGET!NC!E_`m`CfO*tB&$xkQqeB<1Qv zqW5bIy2bzFwyV<*k8NNo5pgEmdRT*CNQnSHq;IYFG>P!fSy`W}hUK6A)6GS+Kf!DM zN^G)$)S`;Sq^$TFnC%PPSrhfble|JbMaN}#qPJKF4mSqT6fJgsh$YqHgf4fim8%GG z1gUy0gS-U}np_FECR_+~T<%YC)!S}m-*o+zd^O<`nLF6c*PiOvvzJwgvwY`NJI|YX z0z&0KN+La&yvvGK_-fVGyTyntt&UCYXs|3G0hMjkM_H{X}A%A+KN-LnyOw=ljLYgp9Fa)|FRs5#`>0JecacA8?W(s)dLDJ`WQ{zTb5hI{GAG} zq%Z*Y>|)mGp(tJtOMGvT;@My0F4a23;pU)|-B`H-uB~TYQ8%Qe=YjVs4q=;oey-y! zS1%mH=fUP|rKyH&2dOtC68$wqtQGZf-9*%!xYU!$dvMXBCF9RNtE&I+W=jt*S}_)H zwTc|sI9mjoK3~!_9Clk%Ys_@`J6!c0;_I{`%y3mfKmvMuZlR3Cs05fMfJ>TRr=R_~ z#iXqOvg+DvP0up0ZupuB9F6~IO3SZ$AdZ5dP?ebs@}a-_jbb3?#nEzn-@D#+Rsnu+ zDG`>IA?Nkso?bNd{YqHYToh4E~q4Wr<_f;7blCKeenCdl=PgW+}Wc}G67!xG%oIXqaNwojby z^_%uOnbm{bSX-YYiJglC>8=8R=QvG4+(*4ChW!{?&#Jl;Dx9V)2=^~=ML1y&Ng;wC zRZMmawii+PQiMI?-p74SMh$di@n}c~HZ6-Kc{Ydsn=sxj(FB54d26qB%oxO0hn3Z} zJI8eS^fQ*E0tNFoA^kUy5WvD!wJ5Ts9KNCM^8z;!y;OJ%RXD?xj9Re_XjSp#K!HP; zc|FAP;TMVObvEa$stay@Z;NkPC>zrYtFh{bl8*Z>EO3rAas9CT#zW?{5A%PS$Bkv` z^^~d3?8xv|!u=60nZnOJI=CPUCwb6>3vvMA%I{KW61%lS*sRoc<74}~_&|d2je7qe zrdVbe!Ca-@*p)%%5@rTkbcrR>Dh~c&6;D22AgS?H0N}0|Fl0b?VBFxDK%tIw(jI9O zFG;dCb|sHgB5cCH<$I|9X%Z(@k)&0h{{7*rvuQdx`$f zOV4rA3PM2`l#$3iN+FoMv>)=`W2my}t9er-#1M}9`;hl_LrXbp zUKlI^mjj==RaA<#iX<{wBQ2SnpBQi0^Ua}NG5vN%_6uTB9ZRpILaRV5t((LR@ z{Mji*s;Y*~*Miow7ROjXe7||87xxDpjC(c|e{(D1?&WB$A=Py0c zzbhsNkf^=0^upzocGGWhBN=BRt}U9XANRW21WtVu&9M5jb79ipwgsL%e!ozSyBKjCaF-3kB zsSP&@L$f6|VZnQw-zTtar#QJJ>&RrbeNIEnG%CWD{#HG>$Ho?X+Gyv*P^9=Y{X!~u zc8p;9v3Q3J9#!{y`nkh3pRX`&9X%f9TRM>rd<}k~NmQtyjKlo&<3tLZGt}I9CPj-l zjm(2;h4-KU){P*RW_5uX1|9E65O7YK=$g4naNVBePb>53RnRy0Le{_XjLrm5U_6Lz zl0>17JPyp=ZE1Rwu`=-UvCg`RjJ*tkqw!Q1m5Ud-@>a#?^DFzhzYG-E%4+RAGvJ?b3($T|!uGxXNO@CbSE;H& zkT2Qmxn`c@eQpDxRTWZXt|xraJz*)0=u)=6209)V;dZoB1>wY@g*+uquO4&u+U|!hDytTQ~&bz zLS&|4q?=wCVvS-vvhF+h4k<&?R2~gg9?r4{=tVs6wjR?bYVD75#<4fFtz)Bu0UI4r z&5wu5T(s})4n76Ay2s?F!W6rAzCSvheXRY}L6F;N{Rn%32VA(;}`i z^x)ktMEdLDs)VqzL@Zr{us1E-K2he3M5~)q{UvIanmml)>9(*^nrFMJ@>8%XbD_LO z@-;jYsf25hsMspf&NuS0f6~0mWTG}zi@&XvSdOZ$)^lORWk6WLVw=jlv3oRTlWRoW zBFX-VO8PPX0a3gEpZ@Ug@*a8GANug({RZ1^-O%GHz0v5dY2&5@QcL@YxJ#m?GJStK zuc(mUYLC4QICmCB38iTJ$Y+4Qi@_7Ar>TiOD#Nqh!n`8-6paPkemXsoyMEdv>hb52MxpXc1 z<6I~DgCytHhUgjKil4Jk-?uScZz=f^>v^30OML;GtIGNs z_IQuJJQ=DQN0cQqqvrPXzA+{AN!^%l8*21Mdpv9CLw)rt{&^8&uf!O zg!HTYE9DU}x4mQxk(raAOj8DL$JE6&a^S~Z=sIdy5uc87v7)uCJXS_(v^n&by-d}9 zoDY5I0saT+O$hz3G^Rkha^KOL9WH}&&o*2s?MZ9$+bS1gae4NFJFzMOF0kb|`!ob( zRkwhI>DU47hSSgcWogs}po4Z#?MzpKxjqgZiOfkks~m(KIghFna+E179?4BFz?6Lm zX&RMUjYkHJZC;gL!Oazdr5waFR2g45%jcsw-zxc$%Y9r409HC2H;^DkJ{>skXAd2E zbm?)2dSd!YaW_m;Ff)Au9b|$MH8Y|qs$nE zj*HZP1-WFSX{`Th)K(e!dB#kQ{#Z|%Fs+Z8l-mhU!sP%2qniBYm@Y!5COb-icl)j1 zDFaXxHu(sT5J*4~h5+>ZoX-0#n_<9X4y4NPNds<#bmF3@71U_$8}gT<*21=O1Z9eq zU+Vj!tf<9GMxokBKRiPa%^(+nmCN5`?k>TCnzi*`NWx|Ps-u6b^Y?4L0E`6_L2G!b z)dterHz`7PWck`qvc=`4se)?AG-E#5xOC-h!WumyVv!f|YS!w}6y*eL{$D?uzc&g=Ma z&ia^A%Lc)hFY`x=AG5pcgC6J)4@6`qFNcrEyC%sSO2^k)l!jO=*)u8tm9cir{XW96 zTAu2lO4-6v>+1brswwTP;fs|b7xm6{oS36oee&?8R7Wt=`leGeaIW37p$_L0Pzr z)=zs*Rc0LJmuT|Xl}I_LQ!vK&NFUdn{6Wi2fXrC0rQ4;aD?-mdiZ50j))kO$H zU4qhVYqL;0b1f_GqXuL+wju(eL7!vI^Q7agg zx796YlLQJA_^JMq-Z<@wh+{O}@kL(`;=~MmfFQ`6{gJm>!H^o9_xFP`F#6&Q>&(mvpX8uXri^A9-n5~e?YY%^TGLLhfmv7oInc4i z*92}mTjve^h(nS~A;@-}yB-@j-K0T{KuKNebATf+no;#`cZbZKGf+NH&0X&;sxdS+ zkoXtzOn|!22-G1yr!WXwqg2Y9b}&1Ms37tss&FcX~>twUXFDG68XGXqZ)Wjof#2j1pd^R!QTSivaoZ2V3*F*=;@Jqv;QuZKs_WM z@xNVX(ecUm*>rJd^-XcA%#pwXYhq86A;(HKzUzA3O0iDHv4sG?E50$s1;67AJkZPl zy>w&K-{*2WeEV5(ii!XZ|8G2k?#*|zxCb+$jli!>8qDl#@V!+RWxUw!$vJReYs zKW6pJ;7j2TYai411IjNJdeVf2>s@*{=GG13x|RC9jDq^iu?h59oCtB-Gp;JL0Uf8| zYlTH)a`aLrwQ?5$trciQZxolLi7JGf!x04komRqECC7Ua*PhH~NL^twywOf`dZ}N< z$#VZQo#N6Oth+7RBXV6qR)0B*>ZnCsr|TJ1Ed{579c|@RUPb{#r;oCPL5YU zWOQ*(Nu~1@eiZ-@dRufhI{-0GCv3*{(*>coM9$X?2D<|KqZSiWQJxtahO4n(cV1{L$dY6U||qF$p_M2dnmQw z6>5%;F)3td+aTuA=LBMW+idU_Ddc}YlBK0X{DVQsDE|I4^-z_)Y7X@?-VpYVvMCWi zWpNKG?oMwq)ZWNvSkWLyCYexaSQu;-dOt4pWNVZ!!Bb^xgC|~`=8CYpY(gtVdFEU( zi+Mx>CBH@iE_t*uE>)4g#bts0dntPcmJp~Ew*LgvS>|aFWHT2mveIeKE6|miyi(_~ zg@Q!K{Ms@51)hyQ%07!#3kvpYHFho7%GE?OW3P%OWA+blgi@mX{8B%o&Woq)09&eB zmhY3^YNnhkMVO#eWnEC;R|;qWj9zno^BVo%O+HQg%u;QG|E?7FILNk9AkP1r(Uh=t zqh7x6_n6!+8Ju^${faj_m6?(g78Va;ZM7>$!rE*H=Of?%iyxX76d7olA~*>c7x z0RA41S`fB&w`A{fSg%`|-!>UrF5zJSv(yV3(WLY&wU0fJ8`cM+%BqZIb+)Xi zgM9KJT$NL8qLT_QmT`yf#}cm+9vZ#o<4#o-Q|~19+;vNcC+DV zk}(Lb!3jpeYt%6ERd`x1cIPd?MwsJ4abqD|CS|!P`uJSVodVG$P+S9VR+msK8)gP^ zm;s(%V!UShfD}Se+1$+*?yY z)8~)viN@3R^{n4eF^fR(I@^SDCzB^7A$~@bMtyvpd{(-!oVs+3d(^d@BL0hK$UlBQ zxL-@j!T&0CwckLYQfPDSWKkz-G^gJH4E|VH5kkx2r`AFVIPm!^3H%tM_r3nImS5-s z(z2G&F{;~MZ1%2Oao%~m%>UD3yu8DZcd!L;L<$0z*?f_73eNCEU{1-A5D{lRGNO5J z{Z9Y0VIv`?RNp0NhdY+#o0R{ADO1q7^KrD9g!kyF`|b$=WoS;8wAi0{G`s|aSZGhV zPr{iLrk0;?x+US!HGa#L$8C!A;&$&^C<=d z@vF-k?{1(U>;f^ga=kmAfWj|}zt8@}fE=z;M+mFbf@FCNm~dTe=+d$~V8O%u_fL+(2RTT!}p(rP+vR_fV?qu zXnG?v@qVYMvh6DI^0;7#DlJ&iqyq3vTj{gOobenn zv_wfPal-`Fb|q-8{f(oJJES{0b5s=((ExOa3ty=DStxVx(1?d;XTVP6HQ$vCY(}AM ziHO>%r!r|j_ohy<&sE7qJDDmy&S#PU$mvS>IOwFGb6kT{2J2h(D~RY>5JZpW9gWAS5ZT4FhEC$85gLWmSY5asgOo!v z(x8=LS=ztB%FzVeReyuNzo6ppO8Qq_t>%f#6nPDd!Xfh}*?KEZ{ZEh^OinY4M7!DO zPV!ANKFUpXE8pRrsH%6fEPBc!MShyvIi|jd^t?S3bDQM!qR~WgD_Q82LKoE8_Ec2o zc}wC*d(a(i#|a9LZXjd82Z_qEPf%{Qx^I%%ZSi~ppO0HGRn861vXD0C2jbkH$i|#v z(%vEWnx(yq!RIw4ROAskz&ui+#D8uJAO4Uk{ErsEeP@h;Ms^Tnbor&IJF8n}>iO}3 z3Pn;Ae%EOKG$akCXJLIr2oUk9O<2dowDv)+Jf)D;cM(p zOp=GqjIRTqfvXL%Bay}ZY~vo9_I;-!Kfh@D`Gq3#yS-PpbxJ!cXrg0`8#!Y9CS5vTH&XZT$%H>{+0%%>fVMjebYDML zY;Dscdp&0y#(JeJH21DYbADwU)1t@NJ>Jl!&1yW;XUe(?@@6 zdf5cQX&rV67gMyjzVhbu+6u`9*S-yz{5pJ!NGSL}Qy?F}GJj%CL-u1$ty~Kfd3*ZX zdhsf7vYeRQ>yfEH<~3rf)09{zn5*;TekfU@5v3EILY{c0$o%sTeq9Jd^sIP>gU-4_ zCAw!qEoY>cF=n1%M8xZ(9AA9wFVcx)ueEY}U>zY>B4z`J=h;_G6-HWadgcCAuMbEx zdNgO>MTubkdjUn3^1(*WFx~VND_t6$Tt9mrEQF!}LpQx| zTxqQqJk-*KvQha!byaE%OA|zbZM6ZJF(dL13A++UH0iI=IrUnX7l0I>|;`?vcG$m;e- z(qp~#31e~IUv9Vi)`jn4S8A;VyerrUf5e3r()}PMaYVOE4<~;rAr19SoR*v7BV+S? zP=E4lbG!f4`EJH*y7<_MSGpb8e?o^5+W`Ej-TC1&Kq1CtMw^;3c&9qV$Fm$w~r2PS&z$ zauzTwGo?Lx2IMQDY2KFC`FBuNCF2!|hdlTvz)g<62sNGqug8`lc8y}MJ zmX~`xkqlplANNu&H(!%J7V+DCdcsc!Q#Y}Ua7-Q7OK?DBG)QF2@>)zHRr}sRcvbbMK4EVGB zwg938gZsK;4B-4DID*EUc{oKwQOucYZG9!hkCL&w-yKtTxE;a0U9w0~BWB>g_+Fsx z)-F40T2Q-AYPJ%YWzyHNN*o-lT8~Gy{(iXI>pzy+{;BU%;c6hugZjnH>z7!A*yI*F z9JyZ%i&ql_R?B|mD@DobPYPTzOi$4#&w$Hl+-6JMeU2Y z{r?Ao>jT0ip6rM0m%BQA^g|-%abU813eSG3hEHju6yT1bLx@@;f9!|seo^e5_(-C% z^k81Mevvs4(@>^^ztjIC=_=UjYI^NCxVuAfhvHfe?ohP2JG8jFySuwXacOaPcXy|_ zyWI1B_Xq4}vNM?^leLn;%}JA03XL=8SD{yuj?f2rZCih)r0>LA$|oEd-BoeK*7d7| z)y)>zV4w8)HwXq@`g}?@m!@3v-1mN>YIKDo`brWpJdT()3a66H&e40qVzdTrHOTjm z`5ayU_m4NHTzd+FF8pen?SFD=RixIXp?rNOVPyuJ`mntEG3+@}>kRj#$v$zkH1 zQeK}V_+eY|=B2Tb$B3h+Pa{lM5q<) zSFV$(ZWP~;-4!3;s*Bd3NC)W*ch0=pkE1ZJZ??R9Rh~3QdVS9))>ss@wR=9uIT0^^ z7;5=MMZ~0&Ot8D9^PzXhRS*uNz9+~%Qr&;E3z%f1k$k@{F5dnz9Ak(#so}A`O|JJJGvtgEXAHUH2 za9EXgeP24)0JvDb5!9x9*sjp-`VW#PUT56E+KchBp z1c{kHI}*V?Nuvi#)`O>l^#n_s3Be>`4onBJ4FrV@qtQ+%q)KJqs|a1zPVC*b+sh=; zJB_e~7>`DNh>btCL&naD;RPG2db&hn&J)rCpH{hl|uHBhS#Vypnuc*7vzgw-QsTf9Ac!-BUi-+xAELN{uTWpy<`6>!xIS& zq%z}TjNfxXO7&{5n9hhd$+%by!iD^ic}ZP@MovKs)PEhMdP;N@h`QW}PU_~NdOHV? zP2ujkMAT?9tnjrFT%$_R9AfJFrXyUp$ylY_8?fT;c8wo9G?sfS#`quQTIWfUf@AN- zCFy{wqZl_%=%J5vlmyIyY^4!eP#={z1BUNn>^>t5*U&s_neUge!h~kvd}}JR%}~~; zdHvZ%G_NyeR`_G0w49GcgtsIjpxzZH`bX@RZWveOqI^bsi>rGN^re4 zQuQRrS3vLXj0PN#lNqYFf|h(usK6>fJT$%K7Q55KlN-05e3zM|$bm}5*n+3@6gh*w zG&yw@RApl(6F?Q>WjSFa0ZhXNsPw<7Ria|&yzs^BhXfR*FOiz0(j&Z8LT%nDd0oOb z?H&CoO1f&&e^R4)Bixr~EB%XS`;khZy7yH7(3Kbatbd)A_}^U$d&#<2lSD7r^40kR z>Jmv`@9B-_up-&dP(Uw@FHzvj-UOoM>oCMAX zZq7AS`z~+k_uyQx4%UJi1nM!CHrt^o5=1n=+7NY)p0g`aSsGc7rq*EJw1_c__VIH< zS|1WLPTm;)|FDQ_&&h~h(n?J9(TXE!Yi4sLb&8-@MHHj|Y;Pbp>$WbJfr{ng>u09t z&XlZ-gUAT2WT$3-r|W{Cef=ya{<>}yCb$uN5%X&FC@Ww9oQnR(!m+ejRbHCLzl46Z zP$^GYVX97tJ(#7wo5w)$6%I|f0T>mX^i;(`qK5>E-lT9#``|%&_lh7TIo0_0?I_O- zvN-HTJ`{X=qGFJ_mN`=IvZoX3Ufa5Yl=`n#b_-9cNqhb5Gjs!ZwQs+efXs*#~p-W%y6?VaeBn(-E)W);O-3bmSVZ zNAb&DBOz7CiQ5F+lbwyEvQHn;#=AH`)Cp_^TP=*OJtRfG%d9pX2DRXyEuj-$)U8D8 z*&-u`5QjGGgNm(Yq#C}*y{C_Z`_oFyKJ5rX6FU)>`iIAE9Ii# z-86)@!90=XY-EP4yC1fZRHT4mc}IyADp=4k+rL{{h(M!!yY1485afNteEL9L`Ve!x z-b>23dgZH}=&cFe93{%6BKn|%t>NE-UWEVjiNxLJ^tJnz^O{LxwAsGsuKHMAzxyJ` zbK19bmy#PLEX{9h-?KMWh+S3mq}*tado=_4t6hnKlmkl|7l9=W4)qXZ=w(>d$G@ zyeP%I;-M1rl;eH~`hfRo(^i1^_UOOz6hqB_Tjj-B-D&@$7Rpj|Im6ug^)6limI!sqcFQsiB&Kl}>%D?^-bNUxQK|otV|d$^*lYP z?^@|^6E!9J$Y>G~ZsQW^Iv$zfg}o6%0DWBRdr;Y}>DE9g%QRn+bOv+3uZZ`aFHtPO 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 Q2><{907*qoM6N<$f;k{children}; +} diff --git a/app/hosting/opengraph-image.tsx b/app/hosting/opengraph-image.tsx new file mode 100644 index 0000000..aee9d41 --- /dev/null +++ b/app/hosting/opengraph-image.tsx @@ -0,0 +1,169 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const alt = "FixFX Hosting Partners - Trusted Game Server Hosting"; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +export default function Image() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */} +
+
🤝
+ + Official Partners + +
+ + {/* Main title */} +
+ + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM servers +

+ + {/* Bottom indicators */} +
+
+
+ Exclusive Discounts +
+
+
+ High Performance +
+
+
+ 24/7 Support +
+
+
+ ), + { + ...size, + } + ); +} diff --git a/app/hosting/page.tsx b/app/hosting/page.tsx new file mode 100644 index 0000000..e392063 --- /dev/null +++ b/app/hosting/page.tsx @@ -0,0 +1,278 @@ +import Link from "next/link"; +import Image from "next/image"; +import { + Server, + Zap, + Shield, + Headphones, + ExternalLink, + Percent, + ChevronRight, + Handshake, + Cloud, +} from "lucide-react"; +import { getHostingProviders } from "@/lib/providers"; +import { ProviderCard } from "./provider-card"; + +export default async function HostingPage() { + const providers = await getHostingProviders(); + + // Calculate dynamic stats + const maxDiscount = providers.length > 0 + ? Math.max(...providers.map(p => p.discount.percentage)) + : 20; + + // Get first provider's links for CTA (fallback to ZAP-Hosting) + const firstProvider = providers[0]; + const fivemLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("fivem"))?.url + || "https://zap-hosting.com/FixFXFiveM"; + const redmLink = firstProvider?.links.find(l => l.label.toLowerCase().includes("redm"))?.url + || "https://zap-hosting.com/FixFXRedM"; + const vpsLink = "https://zap-hosting.com/a/8d785f5b626ef6617320d4bca50a4e344c464437?voucher=FixFX-a-8909"; + return ( +
+ {/* Hero Section */} +
+ {/* Background effects */} +
+
+
+
+ +
+ {/* Breadcrumb */} + + +
+ {/* Badge */} +
+ + Trusted Partners +
+ + {/* Title */} +

+ Hosting{" "} + + Partners + +

+ + {/* Description */} +

+ We've partnered with the best game server hosting providers to bring you exclusive + discounts. Get your FiveM or RedM server up and running with trusted, high-performance hosting. +

+ + {/* Stats */} +
+
+

{maxDiscount}%+

+

Exclusive Discounts

+
+
+
+

24/7

+

Support Available

+
+
+
+

{providers.length}

+

Trusted Partners

+
+
+
+
+
+ + {/* Partners Grid */} +
+ {providers.length > 0 ? ( +
+ {providers.map((provider) => ( + + ))} +
+ ) : ( +
+
+ +
+

No Partners Yet

+

+ We're actively looking for hosting partners.{" "} + + Learn how to become a partner + +

+
+ )} +
+ + {/* Why Partner Section */} +
+
+

+ Why Use Our Partner Links? +

+
+
+
+ +
+

Exclusive Discounts

+

+ Get special pricing not available anywhere else. Our partner codes provide ongoing savings + for as long as you own your server. +

+
+
+
+ +
+

Vetted Providers

+

+ We only partner with hosting providers we trust and have personally tested. Quality and + reliability are our top priorities. +

+
+
+
+ +
+

Support FixFX

+

+ Using our partner links helps support the continued development of FixFX and keeps our + documentation free for everyone. +

+
+
+
+
+ + {/* CTA Section */} +
+
+ {/* Bottom Text */} +
+

+ By using our partner links, you support FixFX while getting exclusive discounts. Thank you for your support! +

+
+
+
+ + {/* Become a Partner Section */} +
+
+
+
+ {/* Header */} +
+
+ + Partnership Opportunity +
+

+ Want to Partner with FixFX? +

+

+ We're always looking for quality hosting providers who want to offer exclusive benefits to our community. +

+
+ + {/* Benefits Grid */} +
+
+
+ +
+

Reach Active Community

+

+ Connect with thousands of FiveM and RedM server owners +

+
+
+
+ +
+

Build Trust

+

+ Showcase your reliability and commitment to quality +

+
+
+
+ +
+

Tracked Attribution

+

+ Use affiliate links to track conversions and ROI +

+
+
+ + {/* Guidelines Preview */} +
+

Partnership Requirements

+
+

+ + FiveM and/or RedM server hosting support +

+

+ + 99.6%+ uptime with responsive 24/7 support +

+

+ + Exclusive discount or special offer for FixFX users +

+

+ + Affiliate/referral links for tracking +

+
+
+ + {/* CTA Buttons */} +
+ + + View Partnership Guidelines + + + + Ask on Discord + + +
+ + {/* Footer Note */} +

+ We review all partnership applications within 3-5 business days. + FixFX reserves the right to accept or decline requests at our discretion. +

+
+
+
+
+
+ ); +} diff --git a/app/hosting/provider-card.tsx b/app/hosting/provider-card.tsx new file mode 100644 index 0000000..7e4064e --- /dev/null +++ b/app/hosting/provider-card.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { + Server, + Zap, + Shield, + Copy, + Check, + ExternalLink, + Percent, + Clock, + Globe, +} from "lucide-react"; +import { cn } from "@/app/lib/utils"; +import type { HostingProvider } from "@/lib/providers"; + +function CopyButton({ text, className }: { text: string; className?: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( + + ); +} + +export function ProviderCard({ provider }: { provider: HostingProvider }) { + return ( +
+ {/* Highlight badge */} + {provider.highlight && ( +
+ + + {provider.highlight} + +
+ )} + + {/* Header */} +
+
+
+ +
+
+

{provider.name}

+

+ {provider.description} +

+
+
+
+ + {/* Discount section */} +
+
+
+
+ +
+
+

{provider.discount.percentage}% OFF

+
+ + {provider.discount.duration} +
+
+
+
+ + {provider.discount.code} + + +
+
+
+ + {/* Links */} +
+

+ + Quick Links +

+
+ {provider.links.map((link) => ( + +
+

+ {link.label} +

+

{link.description}

+
+ + + ))} +
+
+ + {/* Features */} +
+

+ + Features Included +

+
+ {provider.features.map((feature) => ( +
+ + {feature} +
+ ))} +
+
+
+ ); +} diff --git a/app/hosting/twitter-image.tsx b/app/hosting/twitter-image.tsx new file mode 100644 index 0000000..aee9d41 --- /dev/null +++ b/app/hosting/twitter-image.tsx @@ -0,0 +1,169 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const alt = "FixFX Hosting Partners - Trusted Game Server Hosting"; +export const size = { + width: 1200, + height: 630, +}; + +export const contentType = "image/png"; + +export default function Image() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* Badge */} +
+
🤝
+ + Official Partners + +
+ + {/* Main title */} +
+ + Hosting Partners + +
+ + {/* Subtitle */} +

+ Trusted hosting providers for your{" "} + FiveM and{" "} + RedM servers +

+ + {/* Bottom indicators */} +
+
+
+ Exclusive Discounts +
+
+
+ High Performance +
+
+
+ 24/7 Support +
+
+
+ ), + { + ...size, + } + ); +} diff --git a/app/icon.tsx b/app/icon.tsx new file mode 100644 index 0000000..c76e854 --- /dev/null +++ b/app/icon.tsx @@ -0,0 +1,90 @@ +import { ImageResponse } from "next/og"; + +export const runtime = "nodejs"; +export const size = { + width: 512, + height: 512, +}; + +export const contentType = "image/png"; + +export default function Icon() { + return new ImageResponse( + ( +
+ {/* Background gradient orbs */} +
+
+
+ + {/* FixFX Icon */} + + {/* First path */} + + + {/* Second path */} + + +
+ ), + { + ...size, + } + ); +} diff --git a/app/layout.config.tsx b/app/layout.config.tsx index 5257557..acdf423 100644 --- a/app/layout.config.tsx +++ b/app/layout.config.tsx @@ -2,7 +2,7 @@ import type { HomeLayoutProps } from "fumadocs-ui/layouts/home"; import { GITHUB_LINK, DISCORD_LINK } from "@/packages/utils/src"; import { FixFXIcon } from "@ui/icons"; import { FaDiscord } from "react-icons/fa"; -import { Gamepad, Home, PlugZap, LogsIcon, Bot } from "lucide-react"; +import { Gamepad, Home, PlugZap, LogsIcon, Bot, X } from "lucide-react"; export const baseOptions: HomeLayoutProps = { disableThemeSwitch: true, @@ -18,10 +18,16 @@ export const baseOptions: HomeLayoutProps = { links: [ { type: "icon", - text: "", + text: "Discord", icon: , url: "https://discord.gg/Vv2bdC44Ge", }, + { + type: "icon", + text: "Twitter/X", + icon: , + url: "https://twitter.com/FixFXWiki", + }, { type: "main", text: "Home", diff --git a/app/layout.tsx b/app/layout.tsx index 938bb83..c48a9ac 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,16 +1,33 @@ -import { Banner } from 'fumadocs-ui/components/banner'; import { RootProvider } from "fumadocs-ui/provider"; import { Analytics } from "@vercel/analytics/react"; import Script from "next/script"; import { inter, jetbrains } from "@/lib/fonts"; import { keywords } from "@utils/index"; -import '@/styles/sheet-handle.css'; import type { ReactNode } from "react"; -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "@ui/styles"; +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + { media: "(prefers-color-scheme: dark)", color: "#0a0a0a" }, + ], + width: "device-width", + initialScale: 1, + maximumScale: 5, +}; + export const metadata: Metadata = { metadataBase: new URL("https://fixfx.wiki"), + + /** Canonical & Alternates */ + alternates: { + canonical: "https://fixfx.wiki", + languages: { + "en-US": "https://fixfx.wiki", + }, + }, + /** OpenGraph */ openGraph: { type: "website", @@ -18,20 +35,31 @@ export const metadata: Metadata = { url: "https://fixfx.wiki", locale: "en_US", creators: ["@CodeMeAPixel"], - description: "Comprehensive guides and information for the CitizenFX ecosystem.", + title: "FixFX - FiveM & RedM Documentation Hub", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + images: [ + { + url: "/opengraph-image.png", + width: 1200, + height: 630, + alt: "FixFX - Your FiveM & RedM Resource Hub", + }, + ], }, twitter: { - title: "FixFX", + title: "FixFX - FiveM & RedM Documentation Hub", card: "summary_large_image", creator: "@CodeMeAPixel", - site: "https://fixfx.wiki", - description: "Comprehensive guides and information for the CitizenFX ecosystem.", + site: "@FixFXWiki", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + images: ["/twitter-image.png"], }, /** OpenGraph */ /** PWA */ applicationName: "FixFX", appleWebApp: { + capable: true, statusBarStyle: "default", title: "FixFX", }, @@ -40,36 +68,56 @@ export const metadata: Metadata = { }, formatDetection: { telephone: false, + email: false, + address: false, }, /** PWA */ title: { - default: "FixFX", + default: "FixFX - FiveM & RedM Documentation Hub", template: "%s | FixFX", }, - description: "Comprehensive guides and information for the CitizenFX ecosystem.", - creator: "@CodeMeAPixel", - authors: { - url: "https://github.com/CodeMeAPixel", - name: "Pixelated", - }, + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem. Your one-stop resource for server development.", + creator: "CodeMeAPixel", + publisher: "FixFX", + authors: [ + { + url: "https://github.com/CodeMeAPixel", + name: "Pixelated", + }, + ], keywords: keywords, + category: "Documentation", - /** Icons */ + /** Icons */ icons: { - icon: "/favicon.ico", + icon: [ + { url: "/favicon.ico", sizes: "any" }, + { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, + { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, + ], shortcut: "/favicon.ico", - apple: "/apple-touch-icon.png", + apple: [ + { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }, + ], + other: [ + { + rel: "mask-icon", + url: "/logo.png", + }, + ], }, - /** Icons */ + /** Icons */ /** Robots */ robots: { index: true, follow: true, + nocache: false, googleBot: { index: true, follow: true, + noimageindex: false, "max-video-preview": -1, "max-image-preview": "large", "max-snippet": -1, @@ -77,15 +125,139 @@ export const metadata: Metadata = { }, verification: { google: process.env.GOOGLE_VERIFICATION_CODE ?? undefined, + yandex: process.env.YANDEX_VERIFICATION_CODE ?? undefined, + other: { + "msvalidate.01": process.env.BING_VERIFICATION_CODE ?? "", + }, }, /** Robots */ }; +// JSON-LD Structured Data for SEO +const websiteJsonLd = { + "@context": "https://schema.org", + "@type": "WebSite", + name: "FixFX", + alternateName: ["FixFX Wiki", "FixFX Documentation"], + url: "https://fixfx.wiki", + description: "Comprehensive guides, tutorials, and documentation for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + inLanguage: "en-US", + publisher: { + "@type": "Organization", + name: "FixFX", + logo: { + "@type": "ImageObject", + url: "https://fixfx.wiki/logo.png", + width: 512, + height: 512, + }, + }, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: "https://fixfx.wiki/docs?search={search_term_string}", + }, + "query-input": "required name=search_term_string", + }, + sameAs: [ + "https://github.com/CodeMeAPixel/FixFX", + "https://discord.gg/fixfx", + ], +}; + +const organizationJsonLd = { + "@context": "https://schema.org", + "@type": "Organization", + name: "FixFX", + url: "https://fixfx.wiki", + logo: "https://fixfx.wiki/logo.png", + description: "Documentation hub for FiveM, RedM, txAdmin, and the CitizenFX ecosystem.", + foundingDate: "2024", + sameAs: [ + "https://github.com/CodeMeAPixel/FixFX", + "https://discord.gg/fixfx", + ], + contactPoint: { + "@type": "ContactPoint", + contactType: "technical support", + url: "https://github.com/CodeMeAPixel/FixFX/issues", + }, +}; + +const softwareJsonLd = { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "FixFX Documentation", + applicationCategory: "DeveloperApplication", + operatingSystem: "Web", + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + aggregateRating: { + "@type": "AggregateRating", + ratingValue: "5", + ratingCount: "100", + bestRating: "5", + worstRating: "1", + }, +}; + +const jsonLd = [websiteJsonLd, organizationJsonLd, softwareJsonLd]; + export default function Layout({ children, }: Readonly<{ children: ReactNode }>) { return ( - + + + {/* JSON-LD Structured Data */} + - - - - - - -``` - -```css -/* html/style.css */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Arial', sans-serif; - background: transparent; - color: white; -} - -#container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 600px; - height: 400px; - background: rgba(0, 0, 0, 0.9); - border-radius: 10px; - border: 1px solid #333; -} - -#header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - border-bottom: 1px solid #333; -} - -#close-btn { - background: #ff4757; - color: white; - border: none; - border-radius: 50%; - width: 30px; - height: 30px; - cursor: pointer; - font-size: 16px; -} - -#content { - padding: 20px; -} -``` - -```javascript -// html/script.js -$(document).ready(function() { - // Hide UI on ESC key - document.onkeyup = function(data) { - if (data.which == 27) { - closeUI(); - } - } - - // Close button click - $('#close-btn').click(function() { - closeUI(); - }); - - // Listen for messages from Lua - window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'open': - openUI(data.data); - break; - case 'close': - closeUI(); - break; - case 'updateData': - updateUI(data.data); - break; - } - }); -}); - -function openUI(data) { - $('#container').fadeIn(300); - $.post('https://esx_myresource/uiLoaded', JSON.stringify({})); -} - -function closeUI() { - $('#container').fadeOut(300); - $.post('https://esx_myresource/closeUI', JSON.stringify({})); -} - -function updateUI(data) { - // Update UI with new data -} -``` - -### Lua NUI Integration - -```lua --- Client-side NUI management -local isUIOpen = false - --- Open UI -function OpenUI(data) - if isUIOpen then return end - - isUIOpen = true - SetNuiFocus(true, true) - SendNUIMessage({ - action = 'open', - data = data - }) -end - --- Close UI -function CloseUI() - if not isUIOpen then return end - - isUIOpen = false - SetNuiFocus(false, false) - SendNUIMessage({ - action = 'close' - }) -end - --- NUI Callbacks -RegisterNUICallback('uiLoaded', function(data, cb) - -- UI has loaded - cb('ok') -end) - -RegisterNUICallback('closeUI', function(data, cb) - CloseUI() - cb('ok') -end) - --- Export functions for other resources -exports('OpenUI', OpenUI) -exports('CloseUI', CloseUI) -``` - -## Event System - -### Custom Events - -```lua --- Server-side events -RegisterNetEvent('esx_myresource:doSomething', function(data) - local xPlayer = ESX.GetPlayerFromId(source) - - if not xPlayer then return end - - -- Validate data - if not data or not data.value then - return - end - - -- Process request - local success = processRequest(xPlayer, data) - - -- Send response - TriggerClientEvent('esx_myresource:requestResult', source, success) -end) - --- Client-side events -RegisterNetEvent('esx_myresource:requestResult', function(success) - if success then - ESX.ShowNotification('Request successful!') - else - ESX.ShowNotification('Request failed!', 'error') - end -end) -``` - -### Callbacks - -```lua --- Server-side callback -ESX.RegisterServerCallback('esx_myresource:getData', function(source, cb, playerId) - local xPlayer = ESX.GetPlayerFromId(playerId) - if xPlayer then - cb(xPlayer) - else - cb(false) - end -end) - --- Client-side callback usage -ESX.TriggerServerCallback('esx_myresource:getData', function(playerData) - if playerData then - -- Use player data - end -end, GetPlayerServerId(PlayerId())) -``` - -## Commands - -### Creating Commands - -```lua --- Server-side command with ESX integration -RegisterCommand('givemoney', function(source, args, rawCommand) - local xPlayer = ESX.GetPlayerFromId(source) - if not xPlayer then return end - - -- Check permissions - if xPlayer.getGroup() ~= 'admin' then - xPlayer.showNotification('You need admin permissions', 'error') - return - end - - local targetId = tonumber(args[1]) - local amount = tonumber(args[2]) - - if not targetId or not amount then - xPlayer.showNotification('Usage: /givemoney [id] [amount]', 'error') - return - end - - local xTarget = ESX.GetPlayerFromId(targetId) - if not xTarget then - xPlayer.showNotification('Player not found', 'error') - return - end - - xTarget.addMoney(amount) - xPlayer.showNotification('Money given successfully') - xTarget.showNotification('You received $' .. amount) -end, false) - --- Client-side command -RegisterCommand('showjob', function(source, args, rawCommand) - local playerData = ESX.GetPlayerData() - ESX.ShowNotification('Your job: ' .. playerData.job.label .. ' - Grade: ' .. playerData.job.grade_label) -end, false) -``` - -## Configuration Management - -### config.lua Template - -```lua -Config = {} -Config.Locale = 'en' - --- General settings -Config.Debug = false - --- Feature toggles -Config.EnableFeatureA = true -Config.EnableFeatureB = false - --- Timing settings -Config.Cooldowns = { - action1 = 5000, -- 5 seconds - action2 = 30000, -- 30 seconds -} - --- Job permissions -Config.AuthorizedJobs = { - 'police', - 'ambulance', - 'mechanic' -} - -Config.JobPermissions = { - mechanic = { - repair = 0, -- Grade 0+ - advanced = 2, -- Grade 2+ - boss = 3 -- Grade 3+ - }, - police = { - arrest = 0, - impound = 1, - commander = 3 - } -} - --- Locations -Config.Locations = { - mechanic_shop = { - coords = vector3(123.45, 678.90, 12.34), - heading = 90.0, - radius = 2.0, - blip = { - sprite = 446, - color = 2, - scale = 0.8, - name = "Mechanic Shop" - } - } -} - --- Items and pricing -Config.Items = { - repair_kit = { - item = 'repair_kit', - price = 500, - label = 'Repair Kit' - } -} - --- Money settings -Config.Prices = { - repair = 1000, - paint = 500 -} -``` - -## Localization - -### Locale Files - -```lua --- locales/en.lua -Locales['en'] = { - -- Notifications - ['not_enough_money'] = 'You don\'t have enough money', - ['action_completed'] = 'Action completed successfully', - ['invalid_amount'] = 'Invalid amount', - - -- Jobs - ['job_mechanic'] = 'Mechanic', - ['job_police'] = 'Police Officer', - - -- Items - ['item_repair_kit'] = 'Repair Kit', - ['item_used'] = 'You used %s', - - -- Commands - ['command_usage'] = 'Usage: %s', - ['player_not_found'] = 'Player not found', - ['no_permission'] = 'You don\'t have permission', -} -``` - -```lua --- locales/es.lua -Locales['es'] = { - -- Notifications - ['not_enough_money'] = 'No tienes suficiente dinero', - ['action_completed'] = 'Acción completada exitosamente', - ['invalid_amount'] = 'Cantidad inválida', - - -- Jobs - ['job_mechanic'] = 'Mecánico', - ['job_police'] = 'Oficial de Policía', - - -- Items - ['item_repair_kit'] = 'Kit de Reparación', - ['item_used'] = 'Usaste %s', - - -- Commands - ['command_usage'] = 'Uso: %s', - ['player_not_found'] = 'Jugador no encontrado', - ['no_permission'] = 'No tienes permisos', -} -``` - -### Using Translations - -```lua --- In your scripts -local message = _U('not_enough_money') -xPlayer.showNotification(message, 'error') - --- With parameters -local message = _U('item_used', 'Repair Kit') -ESX.ShowNotification(message) -``` - -## Testing & Debugging - -### Debug Functions - -```lua --- Debug utility -local function DebugPrint(...) - if Config.Debug then - print('^3[ESX-MYRESOURCE]^7', ...) - end -end - --- Server-side debugging -local function LogAction(xPlayer, action, data) - if Config.Debug then - print(string.format('^3[ESX-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s', - xPlayer.getName(), - xPlayer.identifier, - action, - json.encode(data) - )) - end -end -``` - -### Testing Checklist - -- [ ] Resource starts without errors -- [ ] ESX object initializes properly -- [ ] Database connections work -- [ ] Player events trigger correctly -- [ ] Jobs and permissions work -- [ ] Money transactions function -- [ ] Items can be used/given/removed -- [ ] UI opens/closes properly -- [ ] Commands execute with proper permissions -- [ ] Localization works -- [ ] No console errors or warnings - -## Best Practices - -### Code Organization - -1. **Follow ESX Conventions**: Use ESX naming conventions and patterns -2. **Use Callbacks**: For data requests between client and server -3. **Validate Everything**: Never trust client input -4. **Handle Errors**: Use pcall for database operations -5. **Performance**: Avoid unnecessary loops and timers - -### Security - -1. **Server Validation**: Always validate on the server -2. **Permission Checks**: Verify job/permissions before actions -3. **Rate Limiting**: Prevent spam/abuse -4. **Secure Events**: Use source validation - -### Performance - -1. **Efficient Queries**: Use proper database indexes -2. **Caching**: Cache frequently accessed data -3. **Resource Cleanup**: Clean up on resource stop -4. **Minimal UI**: Keep NUI lightweight - -This comprehensive development guide provides everything needed to create professional ESX resources following best practices and framework conventions. + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/index.mdx b/content/docs/frameworks/esx/index.mdx index e4fb82f..d7648da 100644 --- a/content/docs/frameworks/esx/index.mdx +++ b/content/docs/frameworks/esx/index.mdx @@ -4,201 +4,8 @@ description: The ESX framework for FiveM servers. icon: "Package" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -ESX (ES Extended) is one of the most popular frameworks for FiveM servers, providing a comprehensive system for roleplay servers. **ESX Legacy** is the modern, actively maintained version with significant improvements. - -## Overview - -ESX Legacy is a completely refactored framework offering enhanced performance, security, and modern FiveM compatibility: - -### ESX Legacy Features - - - -## Key Features - - - -## Getting Started - - - - Complete setup guide including database configuration, resource installation, and server setup. - - - - Learn how to create resources, use ESX APIs, and implement custom features with best practices. - - - - Common issues and solutions for ESX installation, configuration, and runtime problems. - - - -## Quick Start - -### Modern ESX Legacy Integration - -```lua --- Modern ESX initialization (ESX Legacy) -local ESX = exports['es_extended']:getSharedObject() - --- Using ox_lib for UI -local ox_lib = exports.ox_lib - --- Player management -local xPlayer = ESX.GetPlayerFromId(source) -- Server-side -local playerData = ESX.GetPlayerData() -- Client-side - --- Modern account operations -xPlayer.addMoney(1000) -- Add cash -xPlayer.addAccountMoney('bank', 5000) -- Add bank money -xPlayer.removeAccountMoney('bank', 500) -- Remove from bank -xPlayer.getAccount('bank').money -- Get bank balance - --- Inventory management with metadata -xPlayer.addInventoryItem('bread', 5, {quality = 100}) -local itemCount = xPlayer.getInventoryItem('water').count -xPlayer.removeInventoryItem('bread', 2) - --- Job system -xPlayer.setJob('police', 2) -- Set job with grade -if xPlayer.job.name == 'police' and xPlayer.job.grade >= 2 then - -- Lieutenant or higher -end - --- Using ox_lib for notifications -ox_lib:notify({ - title = 'ESX', - description = 'You received $1000', - type = 'success' -}) -``` - -### Modern UI with ox_lib - -```lua --- Context menu (replaces old esx_menu) -ox_lib:registerContext({ - id = 'my_menu', - title = 'Menu Title', - options = { - { - title = 'Option 1', - description = 'Description', - icon = 'hand', - onSelect = function() - -- Action - end - }, - { - title = 'Option 2', - arrow = true, - event = 'my:event' - } - } -}) - -ox_lib:showContext('my_menu') - --- Progress bar -ox_lib:progressBar({ - duration = 5000, - label = 'Crafting...', - useWhileDead = false, - canCancel = true, - disable = { - move = true, - car = true, - combat = true - } -}) - --- Text UI -ox_lib:showTextUI('[E] - Interact') -ox_lib:hideTextUI() -``` -RegisterNetEvent('esx:setJob', function(job) - ESX.PlayerData.job = job - -- Update UI/permissions -end) - --- Player loaded (server) -AddEventHandler('esx:playerLoaded', function(playerId, xPlayer) - -- Server-side player initialization -end) -``` - -## Architecture - -ESX follows a modular architecture where each system is handled by separate resources: - -- **es_extended** - Core framework -- **esx_society** - Organization management -- **esx_datastore** - Data persistence -- **esx_menu_*** - UI menu systems -- **Job Resources** - Police, ambulance, mechanic jobs -- **Economy Resources** - Shops, banks, real estate - -## Community - -- [ESX Legacy GitHub](https://github.com/esx-framework/esx-legacy) -- [Official Documentation](https://documentation.esx-framework.org/) -- [Community Discord](https://discord.gg/esx) -- [FiveM Forums](https://forum.cfx.re/c/development/esx/22) - - - ESX Legacy is the official successor to the original ESX framework, featuring improved performance, better security, and active maintenance. - - - - Always test ESX resources and modifications on a development server before deploying to production. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/setup.mdx b/content/docs/frameworks/esx/setup.mdx index 4c972fc..f3519cd 100644 --- a/content/docs/frameworks/esx/setup.mdx +++ b/content/docs/frameworks/esx/setup.mdx @@ -4,454 +4,8 @@ description: Complete guide to setting up and installing ESX framework. icon: "Download" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers the complete installation and initial setup of ESX framework for your FiveM server. - -## Prerequisites - -Before installing ESX, ensure you have: - -- **FiveM Server**: A properly configured FiveM server -- **MySQL Database**: MySQL 8.0+ or MariaDB 10.6+ -- **Git**: For cloning repositories -- **Basic Command Line Knowledge**: Comfort with terminal commands - -## Installation Methods - - - -### Method 1: ESX Legacy (Recommended) - -ESX Legacy is the modern, updated version of ESX with improved performance and security. - -#### 1. Database Setup - -Create a new MySQL database: - -```sql -CREATE DATABASE esx_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_secure_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### 2. Server Structure - -Create the proper directory structure: - -```bash -# Navigate to your server's resources folder -cd /path/to/your/server/resources - -# Create ESX folder structure -mkdir -p [esx] -mkdir -p [standalone] -``` - -#### 3. Core Framework Installation - -```bash -# Clone ESX Legacy core -cd [esx] -git clone https://github.com/esx-framework/esx_core.git -git clone https://github.com/esx-framework/es_extended.git - -# Essential resources -git clone https://github.com/esx-framework/esx_multicharacter.git -git clone https://github.com/esx-framework/esx_menu_default.git -git clone https://github.com/esx-framework/esx_menu_dialog.git -git clone https://github.com/esx-framework/esx_menu_list.git -git clone https://github.com/esx-framework/esx_notify.git -git clone https://github.com/esx-framework/esx_textui.git -git clone https://github.com/esx-framework/esx_context.git - -# Basic gameplay resources -git clone https://github.com/esx-framework/esx_basicneeds.git -git clone https://github.com/esx-framework/esx_billing.git -git clone https://github.com/esx-framework/esx_society.git -git clone https://github.com/esx-framework/esx_datastore.git -git clone https://github.com/esx-framework/esx_addonaccount.git -git clone https://github.com/esx-framework/esx_addoninventory.git - -# Jobs -git clone https://github.com/esx-framework/esx_ambulancejob.git -git clone https://github.com/esx-framework/esx_policejob.git -git clone https://github.com/esx-framework/esx_mechanicjob.git - -# Properties and vehicles -git clone https://github.com/esx-framework/esx_property.git -git clone https://github.com/esx-framework/esx_vehicleshop.git -git clone https://github.com/esx-framework/esx_lscustom.git -``` - -#### 4. Database Dependencies - -Install required database resources: - -```bash -# Navigate to standalone folder -cd ../[standalone] - -# Clone database connector (choose one) -git clone https://github.com/overextended/oxmysql.git -# OR -git clone https://github.com/GHMatti/ghmattimysql.git mysql-async -``` - -#### 5. Database Import - -Import ESX database structure: - -```bash -# Import base database -mysql -u esx_user -p esx_server < [esx]/es_extended/installation/esx_legacy.sql - -# Import additional resource databases -mysql -u esx_user -p esx_server < [esx]/esx_society/installation/esx_society.sql -mysql -u esx_user -p esx_server < [esx]/esx_datastore/installation/esx_datastore.sql -mysql -u esx_user -p esx_server < [esx]/esx_addonaccount/installation/esx_addonaccount.sql -mysql -u esx_user -p esx_server < [esx]/esx_addoninventory/installation/esx_addoninventory.sql -mysql -u esx_user -p esx_server < [esx]/esx_billing/installation/esx_billing.sql -mysql -u esx_user -p esx_server < [esx]/esx_vehicleshop/installation/esx_vehicleshop.sql -mysql -u esx_user -p esx_server < [esx]/esx_lscustom/installation/esx_lscustom.sql -# Continue for other resources with database requirements -``` - -### Method 2: ESX Template (Alternative) - -For a quicker start, use the ESX server template: - -```bash -# Clone the complete template -git clone https://github.com/esx-framework/esx-serverdumps.git - -# This includes: -# - Pre-configured server structure -# - All essential resources -# - Database files -# - Basic configuration -``` - -## Configuration - -### 1. Database Connection - - - -Configure your database connection in `es_extended/config.lua`: - -```lua -Config = {} -Config.Locale = 'en' - -Config.Accounts = { - bank = _U('account_bank'), - black_money = _U('account_black_money'), - money = _U('account_money') -} - -Config.StartingAccountMoney = { - bank = 50000, - black_money = 0, - money = 5000 -} - -Config.StartingInventoryItems = false -- Set to false to disable - -Config.DefaultSpawn = {x = -269.4, y = -955.3, z = 31.22, h = 205.8} - -Config.EnablePlayerManagement = true -Config.EnableSocietyOwnedVehicles = false -Config.EnableLicenses = false -Config.EnableJailAccount = false -Config.EnablePVP = true -Config.MaxWeight = 24 -Config.PaycheckInterval = 7 * 60000 -Config.EnableDebug = false -``` - -### 2. Server.cfg Configuration - -Add ESX resources to your `server.cfg`: - -```cfg -# Database -ensure oxmysql -# OR ensure mysql-async - -# ESX Legacy Framework -ensure es_extended - -# ESX Core Resources -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list -ensure esx_notify -ensure esx_textui -ensure esx_context - -# ESX Base Resources -ensure esx_datastore -ensure esx_addonaccount -ensure esx_addoninventory -ensure esx_society -ensure esx_billing - -# Character System -ensure esx_multicharacter - -# Basic Needs -ensure esx_basicneeds - -# Jobs -ensure esx_policejob -ensure esx_ambulancejob -ensure esx_mechanicjob - -# Vehicles & Properties -ensure esx_vehicleshop -ensure esx_lscustom -ensure esx_property - -# Database connection string -set mysql_connection_string "mysql://esx_user:your_secure_password@localhost/esx_server?charset=utf8mb4" - -# Server configuration -set sv_hostname "My ESX Server" -set sv_maxclients 32 -set server_description "An ESX FiveM Server" - -# ESX specific -set es_enableCustomData 1 -set esx_multicharacter_enabled true - -# Licensing -sv_licenseKey "your_license_key_here" -``` - -### 3. Resource Loading Order - -Proper loading order is crucial for ESX: - -```cfg -# 1. Database connector -ensure oxmysql - -# 2. Core framework -ensure es_extended - -# 3. Menu systems -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list - -# 4. UI Systems -ensure esx_notify -ensure esx_textui -ensure esx_context - -# 5. Base systems -ensure esx_datastore -ensure esx_addonaccount -ensure esx_addoninventory -ensure esx_society - -# 6. Character system -ensure esx_multicharacter - -# 7. Core gameplay -ensure esx_basicneeds -ensure esx_billing - -# 8. Jobs -ensure esx_policejob -ensure esx_ambulancejob -ensure esx_mechanicjob - -# 9. Vehicles & Properties -ensure esx_vehicleshop -ensure esx_lscustom -ensure esx_property - -# 10. Custom resources (add your custom resources last) -``` - -## Verification & Testing - -### 1. Server Startup - -Start your server and verify ESX loads properly: - -```bash -# Check the console for errors -# Look for messages like: -# [es_extended] ESX Started! -# [esx_multicharacter] Multicharacter Started! -``` - -### 2. Database Verification - -Check that database tables were created: - -```sql -USE esx_server; -SHOW TABLES; - --- You should see tables like: --- users --- vehicles --- user_accounts --- jobs --- job_grades --- etc. -``` - -### 3. In-Game Testing - -1. **Connect to Server**: Join your server -2. **Character Creation**: Test the multicharacter system -3. **Basic Functions**: Try commands like `/me`, `/job`, `/money` -4. **Job System**: Test changing jobs with `/setjob [job] [grade]` - -## Common Installation Issues - -### Database Connection Errors - -**Error**: `Failed to execute query: Access denied` - -**Solution**: -```sql --- Recreate user with proper permissions -DROP USER 'esx_user'@'localhost'; -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -### Resource Loading Errors - -**Error**: `Resource [es_extended] couldn't be started` - -**Solutions**: -1. Check resource path: Ensure resources are in `[esx]` folder -2. Verify manifest: Check `fxmanifest.lua` syntax -3. Dependencies: Ensure database connector loads before es_extended - -### Shared Object Errors - -**Error**: `ESX object is nil` - -**Solution**: -```lua --- Use proper ESX initialization -ESX = exports['es_extended']:getSharedObject() - --- Alternative (legacy method) -ESX = nil -TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) -``` - -## Post-Installation Steps - -### 1. Admin Setup - -Add yourself as admin: - -```sql --- Method 1: Direct database -INSERT INTO users (identifier, name, accounts, job, job_grade, group) -VALUES ('license:your_license_here', 'Your Name', '{"bank":25000,"black_money":0,"money":5000}', 'admin', 0, 'admin'); - --- Method 2: In-game command (if you have permissions) -/setjob admin 0 -``` - -### 2. Basic Server Configuration - -Configure basic server settings: - -```lua --- In es_extended/config.lua -Config.DefaultSpawn = {x = -269.4, y = -955.3, z = 31.22, h = 205.8} -- Customize spawn location -Config.StartingAccountMoney = {bank = 10000, black_money = 0, money = 1000} -- Starting money -Config.PaycheckInterval = 7 * 60000 -- Paycheck every 7 minutes -``` - -### 3. Job Configuration - -Add or modify jobs in the database: - -```sql --- Add custom job -INSERT INTO jobs (name, label) VALUES ('taxi', 'Taxi Driver'); - --- Add job grades -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) -VALUES ('taxi', 0, 'driver', 'Driver', 200, '{}', '{}'); - -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) -VALUES ('taxi', 1, 'experienced', 'Experienced Driver', 300, '{}', '{}'); -``` - -### 4. Society Configuration - -Set up societies for jobs: - -```sql --- Create society for job -INSERT INTO addon_account (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); - -INSERT INTO datastore (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); - -INSERT INTO addon_inventory (name, label, shared) -VALUES ('society_taxi', 'Taxi Company', 1); -``` - -## ESX vs ESX Legacy - -### ESX Legacy Advantages - -- **Better Performance**: Optimized code and reduced resource usage -- **Security Improvements**: Better protection against exploits -- **Modern Code**: Updated to use newer FiveM features -- **Active Development**: Regular updates and bug fixes -- **Compatibility**: Better compatibility with modern resources - -### Migration from Old ESX - -If migrating from old ESX to ESX Legacy: - -1. **Backup Everything**: Database and server files -2. **Update Resources**: Replace old ESX resources with Legacy versions -3. **Database Migration**: Run migration scripts if provided -4. **Test Thoroughly**: Verify all functionality works - -## Next Steps - -After successful installation: - -1. **[ESX Development](/docs/frameworks/esx/development)** - Learn about ESX development -2. **[ESX Troubleshooting](/docs/frameworks/esx/troubleshooting)** - Common issues and solutions -3. **[Resource Management](/docs/frameworks/esx/resources)** - Managing and updating ESX resources - - - Congratulations! You now have a working ESX server. Take time to familiarize yourself with the framework before adding custom resources. - - - - Always backup your database and server files before making major changes or updates. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/esx/troubleshooting.mdx b/content/docs/frameworks/esx/troubleshooting.mdx index a02acc5..5017de0 100644 --- a/content/docs/frameworks/esx/troubleshooting.mdx +++ b/content/docs/frameworks/esx/troubleshooting.mdx @@ -4,741 +4,8 @@ description: Common issues and solutions for ESX framework. icon: "Bug" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers common issues encountered when working with ESX framework and their solutions. - -## Installation Issues - - - -### Database Connection Problems - -#### Error: "Access denied for user" - -**Symptoms:** -``` -[ERROR] Access denied for user 'esx_user'@'localhost' (using password: YES) -``` - -**Solutions:** - -1. **Check Database Credentials:** -```sql --- Verify user exists -SELECT User, Host FROM mysql.user WHERE User = 'esx_user'; - --- If user doesn't exist, create it -CREATE USER 'esx_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON esx_server.* TO 'esx_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -2. **Verify Connection String:** -```cfg -# In server.cfg, ensure connection string is correct -set mysql_connection_string "mysql://esx_user:your_password@localhost/esx_server?charset=utf8mb4" -``` - -3. **Check MySQL Service:** -```bash -# Windows -net start mysql80 - -# Linux -sudo systemctl start mysql -sudo systemctl status mysql -``` - -#### Error: "Database 'esx_server' doesn't exist" - -**Solution:** -```sql -CREATE DATABASE esx_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Resource Loading Issues - -#### Error: "Resource [es_extended] couldn't be started" - -**Symptoms:** -``` -[ERROR] Could not load resource es_extended -[ERROR] Failed to start resource es_extended -``` - -**Solutions:** - -1. **Check Resource Path:** -``` -resources/ -├── [esx]/ -│ └── es_extended/ # Should be here -├── [standalone]/ -└── [voice]/ -``` - -2. **Verify fxmanifest.lua:** -```lua --- Check for syntax errors in fxmanifest.lua -fx_version 'cerulean' -game 'gta5' --- Ensure proper structure -``` - -3. **Check Dependencies:** -```cfg -# Ensure database connector loads before es_extended -ensure oxmysql -ensure es_extended -``` - -#### Error: "ESX object is nil" - -**Symptoms:** -- Scripts can't access ESX functions -- Error: "attempt to index a nil value (global 'ESX')" - -**Solutions:** - -1. **Proper ESX Initialization:** -```lua --- Modern method (recommended) -ESX = exports['es_extended']:getSharedObject() - --- Legacy method (still works) -ESX = nil -TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) - --- Using imports (ESX Legacy) --- Add @es_extended/imports.lua to shared_scripts -``` - -2. **Wait for ESX to Load:** -```lua --- If getting nil, wait for ESX -ESX = nil -Citizen.CreateThread(function() - while ESX == nil do - TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) - Citizen.Wait(0) - end -end) -``` - -### Database Schema Issues - -#### Error: "Table doesn't exist" - -**Symptoms:** -- MySQL errors about missing tables -- Resources fail to start - -**Solutions:** - -1. **Import Missing Tables:** -```bash -# Import base ESX schema -mysql -u esx_user -p esx_server < [esx]/es_extended/installation/esx_legacy.sql - -# Import resource-specific tables -mysql -u esx_user -p esx_server < [esx]/esx_society/installation/esx_society.sql -mysql -u esx_user -p esx_server < [esx]/esx_datastore/installation/esx_datastore.sql -``` - -2. **Check Required Tables:** -```sql --- Verify essential tables exist -SHOW TABLES; - --- Should include: --- users --- jobs --- job_grades --- vehicles --- owned_vehicles --- user_accounts --- user_inventory -``` - -## Player Data Issues - -### Player Data Not Loading - -#### Error: "Player data is nil" - -**Symptoms:** -- Player spawns but has no data -- Commands don't work -- Money/job shows as nil - -**Solutions:** - -1. **Check Player Loading Events:** -```lua --- Client-side -RegisterNetEvent('esx:playerLoaded', function(xPlayer) - ESX.PlayerData = xPlayer - print('Player data loaded:', json.encode(xPlayer)) -end) -``` - -2. **Verify Database Schema:** -```sql --- Check if users table exists and has data -SELECT COUNT(*) FROM users; -DESCRIBE users; -``` - -3. **Check for Database Corruption:** -```sql --- Look for corrupted player data -SELECT identifier, name, LENGTH(accounts) as accounts_length -FROM users -WHERE accounts IS NULL OR accounts = '' OR JSON_VALID(accounts) = 0; -``` - -#### Error: "Player already exists" - -**Symptoms:** -- Cannot create new character -- Gets stuck on character creation - -**Solutions:** - -1. **Check for Duplicate Identifiers:** -```sql --- Find duplicate identifiers -SELECT identifier, COUNT(*) as count -FROM users -GROUP BY identifier -HAVING count > 1; - --- Remove duplicates (keep most recent) -DELETE u1 FROM users u1 -INNER JOIN users u2 -WHERE u1.id < u2.id AND u1.identifier = u2.identifier; -``` - -2. **Clear Character Data:** -```sql --- Remove specific player data -DELETE FROM users WHERE identifier = 'license:your_license_here'; -``` - -### Money/Account Issues - -#### Error: "Money is not updating" - -**Solutions:** - -1. **Check Account Format:** -```sql --- Verify accounts are valid JSON -SELECT identifier, accounts, JSON_VALID(accounts) as is_valid -FROM users -WHERE JSON_VALID(accounts) = 0; - --- Fix invalid account data -UPDATE users -SET accounts = '{"bank":5000,"black_money":0,"money":500}' -WHERE JSON_VALID(accounts) = 0; -``` - -2. **Check Money Functions:** -```lua --- Server-side: Proper money handling -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer then - local currentMoney = xPlayer.getMoney() - xPlayer.addMoney(1000) - print('Money before:', currentMoney, 'After:', xPlayer.getMoney()) -end -``` - -#### Error: "Account type doesn't exist" - -**Solutions:** - -1. **Check Account Configuration:** -```lua --- In es_extended/config.lua -Config.Accounts = { - bank = _U('account_bank'), - black_money = _U('account_black_money'), - money = _U('account_money') -} -``` - -2. **Add Missing Account Types:** -```sql --- If using custom accounts, ensure they're configured properly -``` - -### Job Issues - -#### Error: "Job doesn't exist" - -**Solutions:** - -1. **Verify Job in Database:** -```sql --- Check if job exists -SELECT * FROM jobs WHERE name = 'your_job_name'; - --- Add missing job -INSERT INTO jobs (name, label) VALUES ('mechanic', 'Mechanic'); - --- Add job grades -INSERT INTO job_grades (job_name, grade, name, label, salary, skin_male, skin_female) VALUES -('mechanic', 0, 'trainee', 'Trainee', 200, '{}', '{}'), -('mechanic', 1, 'mechanic', 'Mechanic', 400, '{}', '{}'); -``` - -2. **Check Job Assignment:** -```lua --- Server-side: Proper job setting -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer then - xPlayer.setJob('mechanic', 1) -end -``` - -## Inventory Issues - -### Inventory Not Working - -#### Error: "Items not showing" - -**Solutions:** - -1. **Check Inventory Table:** -```sql --- Verify user_inventory table exists -DESCRIBE user_inventory; - --- Check for data -SELECT * FROM user_inventory LIMIT 5; -``` - -2. **Verify Item Registration:** -```lua --- Check if items are properly registered in database --- Items should be in the items table or configured in resources -``` - -3. **Check Weight System:** -```lua --- Ensure weight calculations work -local xPlayer = ESX.GetPlayerFromId(source) -local currentWeight = xPlayer.getWeight() -local maxWeight = xPlayer.maxWeight -print('Weight:', currentWeight, '/', maxWeight) -``` - -#### Error: "Cannot add item" - -**Solutions:** - -1. **Check Item Limits:** -```lua --- Verify item can be carried -local xPlayer = ESX.GetPlayerFromId(source) -if xPlayer.canCarryItem('bread', 5) then - xPlayer.addInventoryItem('bread', 5) -else - print('Cannot carry item - weight or space limit') -end -``` - -2. **Check Item Configuration:** -```sql --- Verify item exists in items table (if using database items) -SELECT * FROM items WHERE name = 'your_item_name'; -``` - -### Vehicle Issues - -#### Error: "Vehicles not spawning" - -**Solutions:** - -1. **Check Vehicle Database:** -```sql --- Verify owned_vehicles table -DESCRIBE owned_vehicles; - --- Check vehicle data format -SELECT owner, plate, vehicle FROM owned_vehicles LIMIT 5; -``` - -2. **Check Vehicle Model:** -```lua --- Verify vehicle model exists -local model = GetHashKey('adder') -if not IsModelInCdimage(model) then - print('Vehicle model not found') - return -end -``` - -3. **Check Vehicle Ownership:** -```sql --- Verify player owns the vehicle -SELECT * FROM owned_vehicles WHERE owner = 'license:your_license_here'; -``` - -## Society/Job Issues - -### Society Not Working - -#### Error: "Society account not found" - -**Solutions:** - -1. **Check Society Tables:** -```sql --- Verify society tables exist -SHOW TABLES LIKE 'addon_%'; - --- Should show: --- addon_account --- addon_inventory --- datastore -``` - -2. **Create Missing Society:** -```sql --- Create society account -INSERT INTO addon_account (name, label, shared) VALUES ('society_police', 'Police', 1); -INSERT INTO datastore (name, label, shared) VALUES ('society_police', 'Police', 1); -INSERT INTO addon_inventory (name, label, shared) VALUES ('society_police', 'Police', 1); -``` - -3. **Check Society Access:** -```lua --- Server-side: Access society account -TriggerEvent('esx_addonaccount:getSharedAccount', 'society_police', function(account) - if account then - print('Society balance:', account.money) - else - print('Society account not found') - end -end) -``` - -## Menu System Issues - -### Menus Not Working - -#### Error: "Menu doesn't open" - -**Solutions:** - -1. **Check Menu Resources:** -```cfg -# Ensure menu resources are loaded -ensure esx_menu_default -ensure esx_menu_dialog -ensure esx_menu_list -``` - -2. **Check Menu Dependencies:** -```lua --- Verify menu system is available -if ESX.UI.Menu then - -- Menu system loaded -else - print('Menu system not available') -end -``` - -3. **Test Basic Menu:** -```lua --- Client-side: Test menu opening -ESX.UI.Menu.Open('default', GetCurrentResourceName(), 'test_menu', { - title = 'Test Menu', - align = 'top-left', - elements = { - {label = 'Option 1', value = 'option1'}, - {label = 'Option 2', value = 'option2'} - } -}, function(data, menu) - -- Handle selection - menu.close() -end, function(data, menu) - -- Handle close - menu.close() -end) -``` - -## Notification Issues - -#### Error: "Notifications not showing" - -**Solutions:** - -1. **Check Notification Resource:** -```cfg -# Ensure notification resource is loaded -ensure esx_notify -``` - -2. **Test Notifications:** -```lua --- Client-side: Test notification -ESX.ShowNotification('Test message') - --- Server-side: Send to player -local xPlayer = ESX.GetPlayerFromId(source) -xPlayer.showNotification('Test message') -``` - -## Performance Issues - -### High Resource Usage - -#### Server Performance Problems - -**Symptoms:** -- High CPU usage -- Lag/stuttering -- Thread hitches - -**Solutions:** - -1. **Identify Resource Usage:** -``` -# In server console -resmon - -# Look for resources using high CPU/memory -``` - -2. **Optimize Database Queries:** -```lua --- Bad: Multiple queries in loop -for i = 1, #players do - MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = players[i] - }, function(result) - -- Process result - end) -end - --- Good: Single query with IN clause -local identifiers = {} -for i = 1, #players do - table.insert(identifiers, players[i]) -end - -MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier IN (@identifiers)', { - ['@identifiers'] = table.concat(identifiers, "','") -}, function(results) - -- Process all results at once -end) -``` - -3. **Reduce Timer Frequency:** -```lua --- Bad: Too frequent -Citizen.CreateThread(function() - while true do - Citizen.Wait(0) -- Runs every frame - -- Heavy operation - end -end) - --- Good: Reasonable frequency -Citizen.CreateThread(function() - while true do - Citizen.Wait(5000) -- Runs every 5 seconds - -- Heavy operation - end -end) -``` - -### Memory Leaks - -#### Increasing Memory Usage - -**Solutions:** - -1. **Check for Event Listeners:** -```lua --- Ensure proper cleanup -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - -- Cleanup code here - ESX = nil - end -end) -``` - -2. **Clear Player References:** -```lua --- Clear player data on disconnect -AddEventHandler('esx:playerDropped', function(playerId, reason) - -- Clear any stored player references - if playerCache[playerId] then - playerCache[playerId] = nil - end -end) -``` - -## Debug Techniques - -### Logging and Debugging - -#### Enable Debug Mode - -```lua --- In config.lua -Config.Debug = true - --- Debug function -local function DebugPrint(...) - if Config.Debug then - print('^3[DEBUG]^7', ...) - end -end - --- Usage -DebugPrint('Player data:', json.encode(ESX.GetPlayerData())) -``` - -#### Database Debugging - -```lua --- Check database operations -local function SafeQuery(query, parameters, callback) - MySQL.Async.fetchAll(query, parameters, function(result) - if result then - callback(result) - else - print('^1[ERROR]^7 Database query failed:', query) - end - end) -end -``` - -### Console Commands for Debugging - -```lua --- Server-side debug commands -RegisterCommand('esx-debug-player', function(source, args) - local playerId = tonumber(args[1]) or source - local xPlayer = ESX.GetPlayerFromId(playerId) - - if xPlayer then - print('=== PLAYER DEBUG ===') - print('Identifier:', xPlayer.identifier) - print('Name:', xPlayer.getName()) - print('Job:', xPlayer.job.name, 'Grade:', xPlayer.job.grade) - print('Money:', xPlayer.getMoney()) - print('Bank:', xPlayer.getAccount('bank').money) - else - print('Player not found') - end -end, true) - -RegisterCommand('esx-debug-db', function(source, args) - local playerId = tonumber(args[1]) or source - local xPlayer = ESX.GetPlayerFromId(playerId) - - if xPlayer then - MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = xPlayer.identifier - }, function(result) - if result then - print('=== DATABASE DEBUG ===') - print('Raw data:', json.encode(result)) - end - end) - end -end, true) -``` - -## Common Error Messages - -### Script Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `attempt to index a nil value (global 'ESX')` | ESX not initialized | Proper ESX initialization | -| `attempt to call a nil value (method 'getMoney')` | xPlayer is nil | Check if player exists | -| `bad argument #1 to 'pairs'` | Trying to iterate nil value | Check if table exists | -| `attempt to index a nil value (field 'job')` | Player data not loaded | Wait for esx:playerLoaded | - -### Database Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Table 'esx_server.users' doesn't exist` | Missing database tables | Import SQL files | -| `Column 'accounts' cannot be null` | Invalid data insertion | Provide default values | -| `Duplicate entry` | Trying to insert duplicate key | Use proper constraints | -| `Data too long for column` | Value exceeds column length | Increase column size | - -## Prevention Best Practices - -### Code Quality - -1. **Always Check for Nil:** -```lua -local xPlayer = ESX.GetPlayerFromId(source) -if not xPlayer then return end -``` - -2. **Use Error Handling:** -```lua -MySQL.Async.fetchSingle('SELECT * FROM users WHERE identifier = @identifier', { - ['@identifier'] = identifier -}, function(result) - if result then - -- Process result - else - print('No player found with identifier:', identifier) - end -end) -``` - -3. **Validate Inputs:** -```lua -RegisterNetEvent('myresource:server:action', function(data) - local xPlayer = ESX.GetPlayerFromId(source) - if not xPlayer then return end - - if not data or type(data) ~= 'table' then - return - end - - if not data.amount or type(data.amount) ~= 'number' or data.amount <= 0 then - return - end - - -- Process valid data -end) -``` - -### Testing - -1. **Test with Multiple Players** -2. **Test Resource Restart** -3. **Test Database Disconnection** -4. **Test Invalid Inputs** -5. **Monitor Resource Usage** - -### Monitoring - -1. **Regular Database Backups** -2. **Monitor Server Performance** -3. **Check Error Logs** -4. **Update Dependencies** - - - Regular maintenance and monitoring can prevent most issues before they become problems. - - - - Always test changes on a development server before applying to production. - + +We are working hard to get the full ESX documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/development.mdx b/content/docs/frameworks/qbcore/development.mdx index 124df2c..3f0b139 100644 --- a/content/docs/frameworks/qbcore/development.mdx +++ b/content/docs/frameworks/qbcore/development.mdx @@ -4,860 +4,8 @@ description: Complete guide to developing resources for QBCore framework. icon: "Code" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers everything you need to know about developing custom resources for QBCore framework. - -## Development Environment Setup - -### Prerequisites - -- **Code Editor**: VS Code with Lua extensions recommended -- **Git**: For version control -- **Database Tool**: HeidiSQL, phpMyAdmin, or similar -- **QBCore Server**: Running development server - -### Recommended VS Code Extensions - -```json -{ - "recommendations": [ - "sumneko.lua", - "actboy168.lua-debug", - "keyring.lua", - "koihik.vscode-lua-format", - "trixnz.vscode-lua" - ] -} -``` - -### Development Server Setup - -Create a separate development server configuration: - -```cfg -# server-dev.cfg -set sv_hostname "QBCore Development Server" -set sv_maxclients 4 -sv_licenseKey "your_license_key" - -# Developer permissions -add_ace group.admin command allow -add_ace group.admin resource allow -add_principal identifier.steam:your_steam_id group.admin - -# Fast restart for development -sv_scriptHookAllowed 1 -set developer_mode true -``` - -## Resource Structure - -### Standard QBCore Resource Structure - -``` -qb-resourcename/ -├── client/ -│ ├── main.lua -│ ├── events.lua -│ └── utils.lua -├── server/ -│ ├── main.lua -│ ├── events.lua -│ └── callbacks.lua -├── shared/ -│ ├── config.lua -│ ├── items.lua -│ └── locale.lua -├── html/ # For NUI resources -│ ├── index.html -│ ├── style.css -│ └── script.js -├── database/ # Database files -│ └── qb-resourcename.sql -├── locales/ # Translation files -│ ├── en.lua -│ ├── es.lua -│ └── fr.lua -├── fxmanifest.lua -└── README.md -``` - -### fxmanifest.lua Template - -```lua -fx_version 'cerulean' -game 'gta5' - -author 'Your Name ' -description 'QBCore Resource Description' -version '1.0.0' -repository 'https://github.com/yourusername/qb-resourcename' - -shared_scripts { - '@qb-core/shared/locale.lua', - 'locales/en.lua', - 'shared/*.lua' -} - -client_scripts { - 'client/*.lua' -} - -server_scripts { - '@oxmysql/lib/MySQL.lua', - 'server/*.lua' -} - -ui_page 'html/index.html' -- For NUI resources - -files { - 'html/index.html', - 'html/style.css', - 'html/script.js' -} - -lua54 'yes' - -dependencies { - 'qb-core', - 'oxmysql' -} - -provide 'qb-resourcename' -- Optional: for resource replacement -``` - -## Core Integration - -### Getting QBCore Object - -```lua --- Client-side -local QBCore = exports['qb-core']:GetCoreObject() - --- Server-side -local QBCore = exports['qb-core']:GetCoreObject() - --- Alternative method (deprecated but still works) -QBCore = nil -CreateThread(function() - while QBCore == nil do - TriggerEvent('QBCore:GetObject', function(obj) QBCore = obj end) - Wait(200) - end -end) -``` - -### Player Data Management - -#### Getting Player Data - -```lua --- Server-side -local Player = QBCore.Functions.GetPlayer(source) -if Player then - local playerData = Player.PlayerData - local citizenId = Player.PlayerData.citizenid - local job = Player.PlayerData.job - local money = Player.PlayerData.money -end - --- Client-side -local PlayerData = QBCore.Functions.GetPlayerData() -if PlayerData then - local job = PlayerData.job - local money = PlayerData.money -end -``` - -#### Player Events - -```lua --- Client-side: Listen for player data updates -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - PlayerData = QBCore.Functions.GetPlayerData() - -- Initialize your resource after player loads -end) - -RegisterNetEvent('QBCore:Client:OnJobUpdate', function(JobInfo) - PlayerData.job = JobInfo - -- Handle job updates -end) - -RegisterNetEvent('QBCore:Client:OnMoneyChange', function(moneyType, amount, operation) - -- Handle money changes -end) - --- Server-side: Player management -AddEventHandler('QBCore:Server:OnPlayerLoaded', function(Player) - -- Handle player loading -end) - -AddEventHandler('playerDropped', function() - local src = source - local Player = QBCore.Functions.GetPlayer(src) - if Player then - -- Handle player disconnect - end -end) -``` - -## Database Integration - -### Using oxmysql - -```lua --- SELECT query -MySQL.Async.fetchAll('SELECT * FROM players WHERE job = ?', {jobName}, function(result) - if result[1] then - -- Handle results - end -end) - --- SELECT single row -MySQL.Async.fetchSingle('SELECT * FROM players WHERE citizenid = ?', {citizenId}, function(result) - if result then - -- Handle single result - end -end) - --- INSERT query -MySQL.Async.execute('INSERT INTO my_table (citizenid, data) VALUES (?, ?)', { - citizenId, - json.encode(data) -}, function(affectedRows) - if affectedRows > 0 then - -- Success - end -end) - --- UPDATE query -MySQL.Async.execute('UPDATE players SET money = ? WHERE citizenid = ?', { - json.encode(money), - citizenId -}, function(affectedRows) - -- Handle update -end) -``` - -### Modern oxmysql (Promise-based) - -```lua --- Using promises (recommended) -local result = MySQL.query.await('SELECT * FROM players WHERE job = ?', {jobName}) -if result[1] then - -- Handle results -end - --- With error handling -local success, result = pcall(MySQL.query.await, 'SELECT * FROM players WHERE citizenid = ?', {citizenId}) -if success and result[1] then - -- Handle success -else - print('Database query failed') -end -``` - -## Item System - -### Creating Custom Items - -Add items to `qb-core/shared/items.lua`: - -```lua -QBShared.Items = { - -- Existing items... - - ['my_custom_item'] = { - name = 'my_custom_item', - label = 'My Custom Item', - weight = 500, - type = 'item', - image = 'my_custom_item.png', - unique = false, - useable = true, - shouldClose = true, - combinable = nil, - description = 'This is my custom item description' - }, - - ['weapon_custom'] = { - name = 'weapon_custom', - label = 'Custom Weapon', - weight = 2000, - type = 'weapon', - ammotype = 'AMMO_PISTOL', - image = 'weapon_custom.png', - unique = true, - useable = false, - description = 'A custom weapon' - } -} -``` - -### Item Usage Events - -```lua --- Server-side: Register useable item -QBCore.Functions.CreateUseableItem('my_custom_item', function(source, item) - local Player = QBCore.Functions.GetPlayer(source) - if Player then - -- Item usage logic - TriggerClientEvent('qb-myresource:client:useItem', source, item) - end -end) - --- Client-side: Handle item usage -RegisterNetEvent('qb-myresource:client:useItem', function(item) - -- Client-side item effects - QBCore.Functions.Notify('You used ' .. item.label, 'success') -end) -``` - -### Inventory Management - -```lua --- Server-side inventory functions -local Player = QBCore.Functions.GetPlayer(source) - --- Add item -Player.Functions.AddItem('my_item', 1, false, {quality = 100}) - --- Remove item -Player.Functions.RemoveItem('my_item', 1) - --- Get item -local item = Player.Functions.GetItemByName('my_item') -if item then - print('Player has ' .. item.amount .. ' of ' .. item.label) -end - --- Check if player has item -local hasItem = Player.Functions.GetItemByName('my_item') ~= nil -``` - -## Job System - -### Creating Custom Jobs - -Add jobs to `qb-core/shared/jobs.lua`: - -```lua -QBShared.Jobs = { - -- Existing jobs... - - ['mechanic'] = { - label = 'Mechanic', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = { - name = 'Trainee', - payment = 50 - }, - ['1'] = { - name = 'Mechanic', - payment = 75 - }, - ['2'] = { - name = 'Expert Mechanic', - payment = 100 - }, - ['3'] = { - name = 'Shop Supervisor', - payment = 125 - }, - ['4'] = { - name = 'Shop Owner', - isboss = true, - payment = 150 - }, - }, - } -} -``` - -### Job Management Functions - -```lua --- Server-side: Job management -local Player = QBCore.Functions.GetPlayer(source) - --- Set player job -Player.Functions.SetJob('mechanic', 2) - --- Check job permissions -if Player.PlayerData.job.name == 'police' and Player.PlayerData.job.grade.level >= 3 then - -- Allow police captain+ actions -end - --- Check if player is boss -if Player.PlayerData.job.isboss then - -- Boss-only actions -end - --- Client-side: Job checking -local PlayerData = QBCore.Functions.GetPlayerData() -if PlayerData.job.name == 'mechanic' then - -- Mechanic-specific functionality -end -``` - -## Vehicle System - -### Vehicle Management - -```lua --- Server-side: Vehicle functions -local Player = QBCore.Functions.GetPlayer(source) - --- Get player vehicles -local vehicles = MySQL.query.await('SELECT * FROM player_vehicles WHERE citizenid = ?', {Player.PlayerData.citizenid}) - --- Add vehicle to player -local vehicleData = { - citizenid = Player.PlayerData.citizenid, - vehicle = 'adder', - hash = GetHashKey('adder'), - mods = json.encode({}), - plate = 'ABC123', - garage = 'pillboxgarage', - fuel = 100, - engine = 1000.0, - body = 1000.0 -} - -MySQL.insert.await('INSERT INTO player_vehicles (citizenid, vehicle, hash, mods, plate, garage, fuel, engine, body) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', { - vehicleData.citizenid, - vehicleData.vehicle, - vehicleData.hash, - vehicleData.mods, - vehicleData.plate, - vehicleData.garage, - vehicleData.fuel, - vehicleData.engine, - vehicleData.body -}) -``` - -### Vehicle Keys Integration - -```lua --- Give keys to player -TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate) - --- Remove keys from player -TriggerEvent('qb-vehiclekeys:server:RemoveVehicleKeys', source, plate) - --- Client-side: Check if player has keys -local hasKeys = exports['qb-vehiclekeys']:HasKeys(plate) -``` - -## UI Development (NUI) - -### Basic NUI Setup - -```html - - - - - - - QBCore Resource UI - - - - - - - - -``` - -```css -/* html/style.css */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Arial', sans-serif; - background: transparent; - color: white; -} - -#container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 600px; - height: 400px; - background: rgba(0, 0, 0, 0.9); - border-radius: 10px; - border: 1px solid #333; -} - -#header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px; - border-bottom: 1px solid #333; -} - -#close-btn { - background: #ff4757; - color: white; - border: none; - border-radius: 50%; - width: 30px; - height: 30px; - cursor: pointer; - font-size: 16px; -} - -#content { - padding: 20px; -} -``` - -```javascript -// html/script.js -$(document).ready(function() { - // Hide UI on ESC key - document.onkeyup = function(data) { - if (data.which == 27) { - closeUI(); - } - } - - // Close button click - $('#close-btn').click(function() { - closeUI(); - }); - - // Listen for messages from Lua - window.addEventListener('message', function(event) { - const data = event.data; - - switch(data.action) { - case 'open': - openUI(data.data); - break; - case 'close': - closeUI(); - break; - case 'updateData': - updateUI(data.data); - break; - } - }); -}); - -function openUI(data) { - $('#container').fadeIn(300); - $.post('https://qb-myresource/uiLoaded', JSON.stringify({})); -} - -function closeUI() { - $('#container').fadeOut(300); - $.post('https://qb-myresource/closeUI', JSON.stringify({})); -} - -function updateUI(data) { - // Update UI with new data -} -``` - -### Lua NUI Integration - -```lua --- Client-side NUI management -local isUIOpen = false - --- Open UI -function OpenUI(data) - if isUIOpen then return end - - isUIOpen = true - SetNuiFocus(true, true) - SendNUIMessage({ - action = 'open', - data = data - }) -end - --- Close UI -function CloseUI() - if not isUIOpen then return end - - isUIOpen = false - SetNuiFocus(false, false) - SendNUIMessage({ - action = 'close' - }) -end - --- NUI Callbacks -RegisterNUICallback('uiLoaded', function(data, cb) - -- UI has loaded - cb('ok') -end) - -RegisterNUICallback('closeUI', function(data, cb) - CloseUI() - cb('ok') -end) - --- Export functions for other resources -exports('OpenUI', OpenUI) -exports('CloseUI', CloseUI) -``` - -## Event System - -### Custom Events - -```lua --- Server-side events -RegisterNetEvent('qb-myresource:server:doSomething', function(data) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then return end - - -- Validate data - if not data or not data.value then - return - end - - -- Process request - local success = processRequest(Player, data) - - -- Send response - TriggerClientEvent('qb-myresource:client:requestResult', src, success) -end) - --- Client-side events -RegisterNetEvent('qb-myresource:client:requestResult', function(success) - if success then - QBCore.Functions.Notify('Request successful!', 'success') - else - QBCore.Functions.Notify('Request failed!', 'error') - end -end) -``` - -### Callbacks - -```lua --- Server-side callback -QBCore.Functions.CreateCallback('qb-myresource:server:getData', function(source, cb, playerId) - local Player = QBCore.Functions.GetPlayer(playerId) - if Player then - cb(Player.PlayerData) - else - cb(false) - end -end) - --- Client-side callback usage -QBCore.Functions.TriggerCallback('qb-myresource:server:getData', function(playerData) - if playerData then - -- Use player data - end -end, GetPlayerServerId(PlayerId())) -``` - -## Commands - -### Creating Commands - -```lua --- Server-side command -QBCore.Commands.Add('mycommand', 'Command description', {{name = 'target', help = 'Target player ID'}}, true, function(source, args) - local Player = QBCore.Functions.GetPlayer(source) - if not Player then return end - - local targetId = tonumber(args[1]) - if not targetId then - TriggerClientEvent('QBCore:Notify', source, 'Invalid player ID', 'error') - return - end - - local TargetPlayer = QBCore.Functions.GetPlayer(targetId) - if not TargetPlayer then - TriggerClientEvent('QBCore:Notify', source, 'Player not found', 'error') - return - end - - -- Command logic here - TriggerClientEvent('QBCore:Notify', source, 'Command executed successfully', 'success') -end, 'admin') -- Permission level - --- Client-side command -RegisterCommand('clientcommand', function(source, args, rawCommand) - local PlayerData = QBCore.Functions.GetPlayerData() - if PlayerData.job.name == 'police' then - -- Police-only client command - end -end, false) -``` - -## Configuration Management - -### config.lua Template - -```lua -Config = {} - --- General settings -Config.Debug = false -Config.Locale = GetConvar('qb_locale', 'en') - --- Feature toggles -Config.EnableFeatureA = true -Config.EnableFeatureB = false - --- Timing settings -Config.Cooldowns = { - action1 = 5000, -- 5 seconds - action2 = 30000, -- 30 seconds -} - --- Job permissions -Config.JobPermissions = { - mechanic = { - repair = 0, -- Grade 0+ - advanced = 2, -- Grade 2+ - boss = 4 -- Grade 4+ - }, - police = { - arrest = 0, - impound = 1, - commander = 3 - } -} - --- Locations -Config.Locations = { - mechanic_shop = { - coords = vector3(123.45, 678.90, 12.34), - heading = 90.0, - radius = 2.0, - blip = { - sprite = 446, - color = 2, - scale = 0.8, - name = "Mechanic Shop" - } - } -} - --- Items and pricing -Config.Items = { - repair_kit = { - item = 'repair_kit', - price = 500, - requiredJob = 'mechanic' - } -} - --- Notifications -Config.Notifications = { - success_repair = 'Vehicle repaired successfully', - insufficient_funds = 'You don\'t have enough money', - wrong_job = 'You don\'t have the required job' -} -``` - -## Testing & Debugging - -### Debug Functions - -```lua --- Debug utility -local function DebugPrint(...) - if Config.Debug then - print('^3[QB-MYRESOURCE]^7', ...) - end -end - --- Server-side debugging -local function LogAction(player, action, data) - if Config.Debug then - print(string.format('^3[QB-MYRESOURCE]^7 Player: %s (%s) | Action: %s | Data: %s', - player.PlayerData.name, - player.PlayerData.citizenid, - action, - json.encode(data) - )) - end -end - --- Client-side debugging -local function DrawDebugText(text, x, y) - if Config.Debug then - SetTextFont(0) - SetTextProportional(1) - SetTextScale(0.0, 0.35) - SetTextDropshadow(0, 0, 0, 0, 255) - SetTextEdge(1, 0, 0, 0, 255) - SetTextDropShadow() - SetTextOutline() - SetTextEntry("STRING") - AddTextComponentString(text) - DrawText(x, y) - end -end -``` - -### Testing Checklist - -- [ ] Resource starts without errors -- [ ] Database connections work -- [ ] Player events trigger correctly -- [ ] Items can be used/given/removed -- [ ] UI opens/closes properly -- [ ] Commands execute with proper permissions -- [ ] No console errors or warnings -- [ ] Memory usage is reasonable -- [ ] Performance is acceptable - -## Best Practices - -### Code Organization - -1. **Separate Concerns**: Keep client, server, and shared code separate -2. **Use Callbacks**: For data requests between client and server -3. **Validate Everything**: Never trust client input -4. **Handle Errors**: Use pcall for database operations -5. **Performance**: Avoid unnecessary loops and timers - -### Security - -1. **Server Validation**: Always validate on the server -2. **Permission Checks**: Verify job/permissions before actions -3. **Rate Limiting**: Prevent spam/abuse -4. **Secure Events**: Use source validation - -### Performance - -1. **Efficient Queries**: Use proper database indexes -2. **Caching**: Cache frequently accessed data -3. **Resource Cleanup**: Clean up on resource stop -4. **Minimal UI**: Keep NUI lightweight - -This comprehensive development guide provides everything needed to create professional QBCore resources following best practices and framework conventions. + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/index.mdx b/content/docs/frameworks/qbcore/index.mdx index b729ddb..a40ec83 100644 --- a/content/docs/frameworks/qbcore/index.mdx +++ b/content/docs/frameworks/qbcore/index.mdx @@ -4,118 +4,8 @@ description: A comprehensive guide to using and developing for the QBCore framew icon: "Component" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -QBCore is a modern, feature-rich framework for FiveM that provides a solid foundation for roleplay servers. It offers a comprehensive set of features, excellent documentation, and an active community. - -## Quick Navigation - - - - - - - -## Overview - -QBCore is designed as a successor to ESX, addressing many common pain points while providing enhanced functionality and performance. The framework is built around a modular design philosophy, making it highly extensible and customizable. - -### Key Features - - - -## Getting Started - -New to QBCore? Follow this recommended path: - - - -## Community Resources - - - - - QBCore is actively developed with frequent updates. Always check the official documentation for the latest information. - - - - Always test resources thoroughly before deploying to a production server. Breaking changes in QBCore updates can affect resource compatibility. - \ No newline at end of file + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/setup.mdx b/content/docs/frameworks/qbcore/setup.mdx index a2c2864..70d8c7d 100644 --- a/content/docs/frameworks/qbcore/setup.mdx +++ b/content/docs/frameworks/qbcore/setup.mdx @@ -4,425 +4,8 @@ description: Complete guide to setting up and installing QBCore framework. icon: "Download" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers the complete installation and initial setup of QBCore framework for your FiveM server. - -## Prerequisites - -Before installing QBCore, ensure you have: - -- **FiveM Server**: A properly configured FiveM server -- **MySQL Database**: MySQL 8.0+ or MariaDB 10.6+ -- **Git**: For cloning repositories -- **Basic Command Line Knowledge**: Comfort with terminal commands - -## Installation Methods - - - -### Method 1: Fresh Installation (Recommended) - -#### 1. Database Setup - -Create a new MySQL database for your server: - -```sql -CREATE DATABASE qbcore_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_secure_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### 2. Server Structure - -Create the proper directory structure: - -```bash -# Navigate to your server's resources folder -cd /path/to/your/server/resources - -# Create QBCore folder structure -mkdir -p [qb] -mkdir -p [standalone] -mkdir -p [voice] -``` - -#### 3. Core Framework Installation - -```bash -# Clone the core framework -cd [qb] -git clone https://github.com/qbcore-framework/qb-core.git - -# Essential resources -git clone https://github.com/qbcore-framework/qb-multicharacter.git -git clone https://github.com/qbcore-framework/qb-spawn.git -git clone https://github.com/qbcore-framework/qb-inventory.git -git clone https://github.com/qbcore-framework/qb-target.git -git clone https://github.com/qbcore-framework/qb-clothing.git -git clone https://github.com/qbcore-framework/qb-weathersync.git -git clone https://github.com/qbcore-framework/qb-houses.git -git clone https://github.com/qbcore-framework/qb-garages.git -git clone https://github.com/qbcore-framework/qb-phone.git -git clone https://github.com/qbcore-framework/qb-vehicleshop.git -git clone https://github.com/qbcore-framework/qb-vehiclekeys.git -git clone https://github.com/qbcore-framework/qb-bankrobbery.git -git clone https://github.com/qbcore-framework/qb-policejob.git -git clone https://github.com/qbcore-framework/qb-ambulancejob.git -git clone https://github.com/qbcore-framework/qb-management.git -git clone https://github.com/qbcore-framework/qb-radialmenu.git -git clone https://github.com/qbcore-framework/qb-hud.git -git clone https://github.com/qbcore-framework/qb-menu.git -git clone https://github.com/qbcore-framework/qb-input.git -git clone https://github.com/qbcore-framework/qb-loading.git -``` - -#### 4. Database Dependencies - -Install required database resources: - -```bash -# Navigate to standalone folder -cd ../[standalone] - -# Clone database connector -git clone https://github.com/overextended/oxmysql.git -``` - -#### 5. Database Import - -Import the QBCore database structure: - -```bash -# Import base database (located in qb-core/server/database.sql) -mysql -u qbcore_user -p qbcore_server < [qb]/qb-core/server/database.sql - -# Import additional resource databases as needed -mysql -u qbcore_user -p qbcore_server < [qb]/qb-houses/database/qb-houses.sql -mysql -u qbcore_user -p qbcore_server < [qb]/qb-phone/database/qb-phone.sql -# Continue for other resources with database requirements -``` - -### Method 2: QBCore Template (Alternative) - -For a quicker start, use the QBCore server template: - -```bash -# Clone the complete template -git clone https://github.com/qbcore-framework/qb-txAdminRecipe.git - -# This includes: -# - Pre-configured server structure -# - All essential resources -# - Database files -# - Basic configuration -``` - -## Configuration - -### 1. Database Connection - - - -Configure your database connection in `qb-core/shared/config.lua`: - -```lua -QBConfig = {} - -QBConfig.MaxPlayers = GetConvarInt('sv_maxclients', 48) -QBConfig.DefaultSpawn = {x = -254.88, y = -982.14, z = 31.22, h = 207.5} -QBConfig.UpdateInterval = 5 -QBConfig.StatusInterval = 5000 -QBConfig.Money = {} -QBConfig.Money.MoneyTypes = {cash = 500, bank = 5000, crypto = 0} -QBConfig.Money.DontAllowMinus = {'cash', 'crypto'} -QBConfig.Money.PayCheckTimeOut = 10 -QBConfig.Money.PayCheckSociety = false - --- Player management -QBConfig.Player = {} -QBConfig.Player.HungerRate = 4.2 -QBConfig.Player.ThirstRate = 3.8 -QBConfig.Player.Bloodtypes = {"A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"} - --- Server configuration -QBConfig.Server = {} -QBConfig.Server.Closed = false -QBConfig.Server.ClosedReason = "Server Closed" -QBConfig.Server.Uptime = 0 -QBConfig.Server.Whitelist = false -QBConfig.Server.WhitelistPermission = 'admin' -QBConfig.Server.PVP = true -QBConfig.Server.Discord = "" -QBConfig.Server.CheckDuplicateLicense = true -QBConfig.Server.Permissions = {'god', 'admin', 'mod'} -``` - -### 2. Server.cfg Configuration - -Add QBCore resources to your `server.cfg`: - -```cfg -# Database -ensure oxmysql - -# QBCore Framework -ensure qb-core -ensure qb-multicharacter -ensure qb-spawn -ensure qb-inventory -ensure qb-target -ensure qb-clothing -ensure qb-weathersync - -# Jobs -ensure qb-policejob -ensure qb-ambulancejob -ensure qb-management - -# Properties -ensure qb-houses -ensure qb-garages - -# Vehicles -ensure qb-vehicleshop -ensure qb-vehiclekeys - -# UI/UX -ensure qb-hud -ensure qb-menu -ensure qb-input -ensure qb-phone -ensure qb-radialmenu -ensure qb-loading - -# Other essentials -ensure qb-bankrobbery - -# Database connection string -set mysql_connection_string "mysql://qbcore_user:your_secure_password@localhost/qbcore_server?charset=utf8mb4" - -# Server configuration -set sv_hostname "My QBCore Server" -set sv_maxclients 32 -set server_description "A QBCore FiveM Server" -set sv_projectName "QBCore Server" -set sv_projectDesc "QBCore FiveM Server" - -# QBCore specific -set qb_locale "en" -set qb_UseTarget true -set qb_inventory "qb-inventory" - -# Licensing -sv_licenseKey "your_license_key_here" -``` - -### 3. Resource Loading Order - -Proper loading order is crucial for QBCore: - -```cfg -# 1. Database connector -ensure oxmysql - -# 2. Core framework -ensure qb-core - -# 3. Character system -ensure qb-multicharacter -ensure qb-spawn - -# 4. Essential systems -ensure qb-inventory -ensure qb-target -ensure qb-clothing - -# 5. Environmental -ensure qb-weathersync - -# 6. UI Systems -ensure qb-hud -ensure qb-menu -ensure qb-input -ensure qb-radialmenu - -# 7. Properties & Vehicles -ensure qb-houses -ensure qb-garages -ensure qb-vehicleshop -ensure qb-vehiclekeys - -# 8. Jobs -ensure qb-policejob -ensure qb-ambulancejob -ensure qb-management - -# 9. Additional features -ensure qb-phone -ensure qb-bankrobbery -ensure qb-loading - -# 10. Custom resources (add your custom resources last) -``` - -## Verification & Testing - -### 1. Server Startup - -Start your server and verify QBCore loads properly: - -```bash -# Check the console for errors -# Look for messages like: -# [qb-core] QBCore Started! -# [qb-multicharacter] Multicharacter Started! -``` - -### 2. Database Verification - -Check that database tables were created: - -```sql -USE qbcore_server; -SHOW TABLES; - --- You should see tables like: --- players --- player_vehicles --- player_houses --- etc. -``` - -### 3. In-Game Testing - -1. **Connect to Server**: Join your server -2. **Character Creation**: Test the multicharacter system -3. **Basic Functions**: Try commands like `/me`, `/job`, `/inventory` -4. **Job System**: Test changing jobs with `/setjob [job] [grade]` - -## Common Installation Issues - -### Database Connection Errors - -**Error**: `Failed to execute query: Access denied` - -**Solution**: -```sql --- Recreate user with proper permissions -DROP USER 'qbcore_user'@'localhost'; -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -### Resource Loading Errors - -**Error**: `Resource [qb-core] couldn't be started` - -**Solutions**: -1. Check resource path: Ensure resources are in `[qb]` folder -2. Verify manifest: Check `fxmanifest.lua` syntax -3. Dependencies: Ensure oxmysql loads before qb-core - -### Permission Errors - -**Error**: `You don't have permissions to access this` - -**Solution**: -```lua --- Add yourself as admin in qb-core/server/player.lua --- Or use the database: -INSERT INTO players (license, name, money, job, position) -VALUES ('license:your_license_here', 'Your Name', '{"cash":5000,"bank":25000,"crypto":0}', '{"name":"admin","label":"Admin","payment":5000,"onduty":true,"isboss":true,"grade":{"name":"Admin","level":10}}', '{"x":-269.4,"y":-955.3,"z":31.22,"h":205.8}'); -``` - -## Post-Installation Steps - -### 1. Admin Setup - -Add yourself as admin: - -```sql --- Method 1: Direct database -UPDATE players SET job = '{"name":"admin","label":"Admin","payment":5000,"onduty":true,"isboss":true,"grade":{"name":"Admin","level":10}}' WHERE license = 'license:your_license_here'; - --- Method 2: In-game command (if you have permissions) -/setjob admin 4 -``` - -### 2. Basic Server Configuration - -Configure basic server settings: - -```lua --- In qb-core/shared/config.lua -QBConfig.DefaultSpawn = {x = -254.88, y = -982.14, z = 31.22, h = 207.5} -- Customize spawn location -QBConfig.Money.MoneyTypes = {cash = 1000, bank = 10000, crypto = 0} -- Starting money -``` - -### 3. Job Configuration - -Add or modify jobs in `qb-core/shared/jobs.lua`: - -```lua -QBShared.Jobs = { - ['unemployed'] = { - label = 'Civilian', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = { - name = 'Freelancer', - payment = 10 - }, - }, - }, - ['police'] = { - label = 'Law Enforcement', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = {name = 'Recruit', payment = 50}, - ['1'] = {name = 'Officer', payment = 75}, - ['2'] = {name = 'Sergeant', payment = 100}, - ['3'] = {name = 'Lieutenant', payment = 125}, - ['4'] = {name = 'Chief', isboss = true, payment = 150}, - }, - }, - -- Add more jobs as needed -} -``` - -## Next Steps - -After successful installation: - -1. **[QBCore Configuration](/docs/frameworks/qbcore/configuration)** - Learn about advanced configuration options -2. **[Development Guide](/docs/frameworks/qbcore/development)** - Start developing custom resources -3. **[Troubleshooting](/docs/frameworks/qbcore/troubleshooting)** - Common issues and solutions -4. **[Resource Management](/docs/frameworks/qbcore/resources)** - Managing and updating QBCore resources - - - Congratulations! You now have a working QBCore server. Take time to familiarize yourself with the framework before adding custom resources. - - - - Always backup your database and server files before making major changes or updates. - + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/frameworks/qbcore/troubleshooting.mdx b/content/docs/frameworks/qbcore/troubleshooting.mdx index 74646dc..e7f06dd 100644 --- a/content/docs/frameworks/qbcore/troubleshooting.mdx +++ b/content/docs/frameworks/qbcore/troubleshooting.mdx @@ -4,606 +4,8 @@ description: Common issues and solutions for QBCore framework. icon: "Bug" --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers common issues encountered when working with QBCore framework and their solutions. - -## Installation Issues - - - -### Database Connection Problems - -#### Error: "Access denied for user" - -**Symptoms:** -``` -[ERROR] Access denied for user 'qbcore_user'@'localhost' (using password: YES) -``` - -**Solutions:** - -1. **Check Database Credentials:** -```sql --- Verify user exists -SELECT User, Host FROM mysql.user WHERE User = 'qbcore_user'; - --- If user doesn't exist, create it -CREATE USER 'qbcore_user'@'localhost' IDENTIFIED BY 'your_password'; -GRANT ALL PRIVILEGES ON qbcore_server.* TO 'qbcore_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -2. **Verify Connection String:** -```cfg -# In server.cfg, ensure connection string is correct -set mysql_connection_string "mysql://qbcore_user:your_password@localhost/qbcore_server?charset=utf8mb4" -``` - -3. **Check MySQL Service:** -```bash -# Windows -net start mysql80 - -# Linux -sudo systemctl start mysql -sudo systemctl status mysql -``` - -#### Error: "Database 'qbcore_server' doesn't exist" - -**Solution:** -```sql -CREATE DATABASE qbcore_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -``` - -### Resource Loading Issues - -#### Error: "Resource [qb-core] couldn't be started" - -**Symptoms:** -``` -[ERROR] Could not load resource qb-core -[ERROR] Failed to start resource qb-core -``` - -**Solutions:** - -1. **Check Resource Path:** -``` -resources/ -├── [qb]/ -│ └── qb-core/ # Should be here -├── [standalone]/ -└── [voice]/ -``` - -2. **Verify fxmanifest.lua:** -```lua --- Check for syntax errors in fxmanifest.lua -fx_version 'cerulean' -game 'gta5' --- Ensure proper structure -``` - -3. **Check Dependencies:** -```cfg -# Ensure oxmysql loads before qb-core -ensure oxmysql -ensure qb-core -``` - -#### Error: "Script timeout" - -**Symptoms:** -``` -[ERROR] Script timeout: qb-core -``` - -**Solutions:** - -1. **Check for Infinite Loops:** -```lua --- Bad -while true do - -- No Wait() call - causes timeout -end - --- Good -while true do - Wait(1000) -- Always include Wait() -end -``` - -2. **Database Connection Issues:** -```lua --- Check if database is accessible -local result = MySQL.query.await('SELECT 1 as test') -if not result then - print('Database connection failed') -end -``` - -## Player Data Issues - -### Player Data Not Loading - -#### Error: "Player data is nil" - -**Symptoms:** -- Player spawns but has no data -- Commands don't work -- Money/job shows as nil - -**Solutions:** - -1. **Check Player Loading Events:** -```lua --- Client-side -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - PlayerData = QBCore.Functions.GetPlayerData() - print('Player data loaded:', json.encode(PlayerData)) -end) -``` - -2. **Verify Database Schema:** -```sql --- Check if players table exists and has data -SELECT COUNT(*) FROM players; -DESCRIBE players; -``` - -3. **Check for Database Corruption:** -```sql --- Look for corrupted player data -SELECT citizenid, name, LENGTH(money) as money_length -FROM players -WHERE money IS NULL OR money = '' OR JSON_VALID(money) = 0; -``` - -#### Error: "Player already exists" - -**Symptoms:** -- Cannot create new character -- Gets stuck on character creation - -**Solutions:** - -1. **Check for Duplicate Licenses:** -```sql --- Find duplicate licenses -SELECT license, COUNT(*) as count -FROM players -GROUP BY license -HAVING count > 1; - --- Remove duplicates (keep most recent) -DELETE p1 FROM players p1 -INNER JOIN players p2 -WHERE p1.id < p2.id AND p1.license = p2.license; -``` - -2. **Clear Character Data:** -```sql --- Remove specific player data -DELETE FROM players WHERE license = 'license:your_license_here'; -``` - -### Money/Job Issues - -#### Error: "Money is not updating" - -**Solutions:** - -1. **Check Money Format:** -```sql --- Verify money is valid JSON -SELECT citizenid, money, JSON_VALID(money) as is_valid -FROM players -WHERE JSON_VALID(money) = 0; - --- Fix invalid money data -UPDATE players -SET money = '{"cash":500,"bank":5000,"crypto":0}' -WHERE JSON_VALID(money) = 0; -``` - -2. **Check Money Functions:** -```lua --- Server-side: Proper money handling -local Player = QBCore.Functions.GetPlayer(source) -if Player then - local success = Player.Functions.AddMoney('cash', 1000) - print('Money added:', success) -end -``` - -#### Error: "Job permissions not working" - -**Solutions:** - -1. **Verify Job Data:** -```sql --- Check job format in database -SELECT citizenid, job, JSON_VALID(job) as is_valid -FROM players -WHERE JSON_VALID(job) = 0; -``` - -2. **Check Job Configuration:** -```lua --- In qb-core/shared/jobs.lua -QBShared.Jobs = { - ['police'] = { - label = 'Law Enforcement', - defaultDuty = true, - offDutyPay = false, - grades = { - ['0'] = {name = 'Recruit', payment = 50}, - -- Ensure grades are properly defined - }, - }, -} -``` - -## Resource-Specific Issues - -### Inventory Issues - -#### Error: "Items not showing in inventory" - -**Solutions:** - -1. **Check Item Registration:** -```lua --- In qb-core/shared/items.lua -QBShared.Items = { - ['my_item'] = { - name = 'my_item', - label = 'My Item', - weight = 100, - type = 'item', - image = 'my_item.png', - unique = false, - useable = true, - shouldClose = true, - combinable = nil, - description = 'Item description' - } -} -``` - -2. **Verify Item Images:** -``` -qb-inventory/html/images/ -└── my_item.png # Image must exist -``` - -3. **Check Inventory Database:** -```sql --- Verify inventory structure -SELECT citizenid, inventory -FROM players -WHERE JSON_VALID(inventory) = 0; -``` - -#### Error: "Cannot use items" - -**Solutions:** - -1. **Register Item Usage:** -```lua --- Server-side -QBCore.Functions.CreateUseableItem('my_item', function(source, item) - local Player = QBCore.Functions.GetPlayer(source) - if Player then - -- Item usage logic - TriggerClientEvent('qb-myresource:client:useItem', source, item) - end -end) -``` - -2. **Check Item Metadata:** -```lua --- Ensure item has proper metadata -local item = Player.Functions.GetItemByName('my_item') -if item and item.info then - -- Item has metadata -end -``` - -### Vehicle Issues - -#### Error: "Vehicles not spawning" - -**Solutions:** - -1. **Check Vehicle Hash:** -```lua --- Verify vehicle model exists -local model = GetHashKey('adder') -if not IsModelInCdimage(model) then - print('Vehicle model not found') - return -end -``` - -2. **Check Vehicle Database:** -```sql --- Verify vehicle data -SELECT plate, vehicle, hash -FROM player_vehicles -WHERE hash = 0 OR vehicle = ''; -``` - -3. **Vehicle Keys Issues:** -```lua --- Ensure proper key assignment -TriggerEvent('qb-vehiclekeys:server:GiveVehicleKeys', source, plate) -``` - -### Phone/UI Issues - -#### Error: "Phone not opening" - -**Solutions:** - -1. **Check Resource Dependencies:** -```cfg -# Ensure proper loading order -ensure qb-core -ensure qb-phone -``` - -2. **Check Phone Item:** -```lua --- Verify player has phone item -local Player = QBCore.Functions.GetPlayer(source) -local phone = Player.Functions.GetItemByName('phone') -if not phone then - Player.Functions.AddItem('phone', 1) -end -``` - -3. **UI/NUI Issues:** -```lua --- Check for JavaScript errors in F8 console --- Verify NUI focus -SetNuiFocus(true, true) -``` - -## Performance Issues - -### High Resource Usage - -#### Server Performance Problems - -**Symptoms:** -- High CPU usage -- Lag/stuttering -- Thread hitches - -**Solutions:** - -1. **Identify Resource Usage:** -``` -# In server console -resmon - -# Look for resources using high CPU/memory -``` - -2. **Optimize Database Queries:** -```lua --- Bad: Multiple queries in loop -for i = 1, #players do - MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', {players[i]}) -end - --- Good: Single query with IN clause -local citizenIds = {} -for i = 1, #players do - citizenIds[#citizenIds + 1] = players[i] -end -local placeholders = string.rep('?,', #citizenIds):sub(1, -2) -MySQL.query.await('SELECT * FROM players WHERE citizenid IN (' .. placeholders .. ')', citizenIds) -``` - -3. **Reduce Timer Frequency:** -```lua --- Bad: Too frequent -CreateThread(function() - while true do - Wait(0) -- Runs every frame - -- Heavy operation - end -end) - --- Good: Reasonable frequency -CreateThread(function() - while true do - Wait(5000) -- Runs every 5 seconds - -- Heavy operation - end -end) -``` - -### Memory Leaks - -#### Increasing Memory Usage - -**Solutions:** - -1. **Check for Event Listeners:** -```lua --- Ensure proper cleanup -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - -- Cleanup code here - for i = 1, #createdObjects do - DeleteEntity(createdObjects[i]) - end - end -end) -``` - -2. **Clear References:** -```lua --- Prevent memory leaks -local myData = {} - -AddEventHandler('onResourceStop', function(resourceName) - if GetCurrentResourceName() == resourceName then - myData = nil - end -end) -``` - -## Debug Techniques - -### Logging and Debugging - -#### Enable Debug Mode - -```lua --- In config.lua -Config.Debug = true - --- Debug function -local function DebugPrint(...) - if Config.Debug then - print('^3[DEBUG]^7', ...) - end -end - --- Usage -DebugPrint('Player data:', json.encode(PlayerData)) -``` - -#### Database Debugging - -```lua --- Check database operations -local function SafeQuery(query, parameters) - local success, result = pcall(MySQL.query.await, query, parameters) - if not success then - print('^1[ERROR]^7 Database query failed:', result) - return nil - end - return result -end -``` - -### Console Commands for Debugging - -```lua --- Server-side debug commands -RegisterCommand('qb-debug-player', function(source, args) - local playerId = tonumber(args[1]) or source - local Player = QBCore.Functions.GetPlayer(playerId) - - if Player then - print('=== PLAYER DEBUG ===') - print('CitizenID:', Player.PlayerData.citizenid) - print('Name:', Player.PlayerData.name) - print('Job:', json.encode(Player.PlayerData.job)) - print('Money:', json.encode(Player.PlayerData.money)) - print('Position:', json.encode(Player.PlayerData.position)) - else - print('Player not found') - end -end, true) - -RegisterCommand('qb-debug-db', function(source, args) - local playerId = tonumber(args[1]) or source - local Player = QBCore.Functions.GetPlayer(playerId) - - if Player then - local result = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', {Player.PlayerData.citizenid}) - if result[1] then - print('=== DATABASE DEBUG ===') - print('Raw data:', json.encode(result[1])) - end - end -end, true) -``` - -## Common Error Messages - -### Script Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `attempt to index a nil value` | Variable not initialized | Check if variable exists before using | -| `attempt to call a nil value` | Function doesn't exist | Verify function name and exports | -| `bad argument #1 to 'pairs'` | Trying to iterate nil value | Check if table exists before iteration | -| `string expected, got nil` | Passing nil to string function | Validate input parameters | - -### Database Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Table doesn't exist` | Missing database table | Import SQL files | -| `Column doesn't exist` | Database schema mismatch | Update database schema | -| `Duplicate entry` | Trying to insert duplicate key | Use UPDATE or INSERT IGNORE | -| `Data too long` | Value exceeds column length | Increase column size | - -## Prevention Best Practices - -### Code Quality - -1. **Always Check for Nil:** -```lua -local Player = QBCore.Functions.GetPlayer(source) -if not Player then return end -``` - -2. **Use Error Handling:** -```lua -local success, result = pcall(function() - return MySQL.query.await(query, params) -end) - -if not success then - print('Query failed:', result) - return -end -``` - -3. **Validate Inputs:** -```lua -RegisterNetEvent('myresource:server:action', function(data) - if not data or type(data) ~= 'table' then - return - end - - if not data.amount or type(data.amount) ~= 'number' then - return - end - - -- Process valid data -end) -``` - -### Testing - -1. **Test with Multiple Players** -2. **Test Resource Restart** -3. **Test Database Disconnection** -4. **Test Invalid Inputs** -5. **Monitor Resource Usage** - -### Monitoring - -1. **Regular Database Backups** -2. **Monitor Server Performance** -3. **Check Error Logs** -4. **Update Dependencies** - - - Regular maintenance and monitoring can prevent most issues before they become problems. - - - - Always test changes on a development server before applying to production. - + +We are working hard to get the full qbCore documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/advanced.mdx b/content/docs/txadmin/advanced.mdx index b1c0645..11ef6c4 100644 --- a/content/docs/txadmin/advanced.mdx +++ b/content/docs/txadmin/advanced.mdx @@ -3,848 +3,8 @@ title: Advanced Features description: Explore advanced txAdmin features including custom recipes, automation, monitoring, and enterprise-grade server management. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers advanced txAdmin features for experienced administrators who want to maximize their server management capabilities and implement enterprise-grade solutions. - -## Custom Recipes System - - - -### Recipe Development - -#### Recipe Structure -Create custom server deployment templates: - -```json -{ - "name": "Custom RP Server", - "version": "2.1.0", - "author": "YourName", - "description": "Complete roleplay server with custom features", - "fivemVersion": "latest", - "txAdminVersion": ">=5.0.0", - - "variables": { - "serverName": { - "type": "string", - "default": "My Roleplay Server", - "description": "The name of your server" - }, - "maxPlayers": { - "type": "number", - "default": 48, - "min": 1, - "max": 256, - "description": "Maximum number of players" - }, - "framework": { - "type": "select", - "options": ["esx", "qbcore", "custom"], - "default": "esx", - "description": "Choose your framework" - }, - "discordToken": { - "type": "string", - "sensitive": true, - "description": "Discord bot token for integrations" - } - }, - - "resources": [ - { - "name": "es_extended", - "source": "github", - "url": "https://github.com/esx-framework/esx-legacy", - "ref": "main", - "condition": "${framework} == 'esx'" - }, - { - "name": "qb-core", - "source": "github", - "url": "https://github.com/qbcore-framework/qb-core", - "ref": "main", - "condition": "${framework} == 'qbcore'" - }, - { - "name": "custom-scripts", - "source": "local", - "path": "./custom-resources", - "condition": "always" - } - ], - - "files": { - "server.cfg": { - "source": "template", - "template": "server.cfg.mustache" - }, - "database.sql": { - "source": "template", - "template": "database.sql.mustache" - } - }, - - "commands": [ - { - "name": "setup_database", - "command": "mysql -u root -p${db_password} < database.sql", - "description": "Initialize database" - }, - { - "name": "configure_resources", - "command": "node scripts/configure.js", - "description": "Configure installed resources" - } - ] -} -``` - -#### Template Files -Create dynamic configuration files: - -**server.cfg.mustache:** -```bash -## Server Configuration -sv_hostname "{{serverName}}" -set sv_maxclients {{maxPlayers}} - -## License Key -set sv_licenseKey "{{licenseKey}}" - -## Framework Configuration -{{#if framework == 'esx'}} -start es_extended -{{/if}} - -{{#if framework == 'qbcore'}} -start qb-core -{{/if}} - -## Custom Resources -{{#each customResources}} -start {{this}} -{{/each}} - -## Discord Integration -{{#if discordToken}} -set discord_token "{{discordToken}}" -{{/if}} -``` - -**database.sql.mustache:** -```sql -CREATE DATABASE IF NOT EXISTS {{databaseName}}; -USE {{databaseName}}; - -{{#if framework == 'esx'}} --- ESX Tables -SOURCE esx_tables.sql; -{{/if}} - -{{#if framework == 'qbcore'}} --- QBCore Tables -SOURCE qbcore_tables.sql; -{{/if}} - --- Custom Tables -CREATE TABLE IF NOT EXISTS server_config ( - id INT AUTO_INCREMENT PRIMARY KEY, - config_key VARCHAR(255) NOT NULL, - config_value TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); -``` - -### Recipe Deployment - - - -#### Automated Deployment -Deploy recipes programmatically: - -```javascript -// Deploy recipe via API -const deployRecipe = async (recipeId, variables) => { - const response = await fetch('/api/recipes/deploy', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - recipeId: recipeId, - variables: variables, - deploymentName: `deployment-${Date.now()}` - }) - }); - - return response.json(); -}; -``` - -#### Conditional Logic -Implement complex deployment logic: - -```json -{ - "conditions": { - "install_esx_jobs": "${framework} == 'esx' && ${includeJobs} == true", - "install_custom_ui": "${serverType} == 'roleplay' || ${serverType} == 'freeroam'", - "configure_whitelist": "${playerCount} <= 32" - }, - - "resources": [ - { - "name": "esx_jobs", - "condition": "install_esx_jobs" - }, - { - "name": "custom-ui", - "condition": "install_custom_ui" - } - ] -} -``` - -## API Integration - -### RESTful API - -#### Authentication -Secure API access with tokens: - -```javascript -// Generate API token -const generateApiToken = async () => { - const response = await fetch('/api/auth/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - username: 'admin', - password: 'secure_password', - scope: ['server:control', 'players:manage'] - }) - }); - - const { token } = await response.json(); - return token; -}; -``` - -#### Server Control API -Control server programmatically: - -```javascript -// Start server -const startServer = async (apiToken) => { - const response = await fetch('/api/server/start', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Get server status -const getServerStatus = async (apiToken) => { - const response = await fetch('/api/server/status', { - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Execute console command -const executeCommand = async (apiToken, command) => { - const response = await fetch('/api/server/console', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - command: command - }) - }); - - return response.json(); -}; -``` - -#### Player Management API -Manage players via API: - -```javascript -// Get player list -const getPlayers = async (apiToken) => { - const response = await fetch('/api/players', { - headers: { - 'Authorization': `Bearer ${apiToken}` - } - }); - - return response.json(); -}; - -// Kick player -const kickPlayer = async (apiToken, playerId, reason) => { - const response = await fetch(`/api/players/${playerId}/kick`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - reason: reason - }) - }); - - return response.json(); -}; - -// Ban player -const banPlayer = async (apiToken, playerId, duration, reason) => { - const response = await fetch(`/api/players/${playerId}/ban`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiToken}` - }, - body: JSON.stringify({ - duration: duration, - reason: reason - }) - }); - - return response.json(); -}; -``` - -### WebSocket Integration - -#### Real-time Events -Monitor server events in real-time: - -```javascript -// Connect to WebSocket -const ws = new WebSocket('ws://localhost:40120/ws'); - -// Authenticate WebSocket connection -ws.onopen = () => { - ws.send(JSON.stringify({ - type: 'auth', - token: apiToken - })); -}; - -// Handle incoming events -ws.onmessage = (event) => { - const data = JSON.parse(event.data); - - switch (data.type) { - case 'player_join': - console.log(`Player ${data.player.name} joined`); - break; - - case 'player_leave': - console.log(`Player ${data.player.name} left`); - break; - - case 'server_status': - updateServerStatus(data.status); - break; - - case 'console_output': - displayConsoleMessage(data.message); - break; - } -}; -``` - -## Automation Scripts - -### Server Maintenance - -#### Automated Restart Script -Schedule intelligent server restarts: - -```bash -#!/bin/bash -# smart-restart.sh - Intelligent server restart script - -API_TOKEN="your_api_token_here" -TXADMIN_URL="http://localhost:40120" - -# Check current player count -PLAYER_COUNT=$(curl -s -H "Authorization: Bearer $API_TOKEN" \ - "$TXADMIN_URL/api/players" | jq '.players | length') - -# Only restart if player count is low -if [ "$PLAYER_COUNT" -lt 5 ]; then - echo "Low player count ($PLAYER_COUNT), proceeding with restart" - - # Announce restart - curl -s -X POST -H "Authorization: Bearer $API_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"command":"say Server restart in 5 minutes!"}' \ - "$TXADMIN_URL/api/server/console" - - sleep 300 # Wait 5 minutes - - # Restart server - curl -s -X POST -H "Authorization: Bearer $API_TOKEN" \ - "$TXADMIN_URL/api/server/restart" - -else - echo "High player count ($PLAYER_COUNT), deferring restart" -fi -``` - -#### Resource Update Automation -Automatically update resources: - -```python -#!/usr/bin/env python3 -# resource-updater.py - -import requests -import json -import subprocess -import time - -class ResourceUpdater: - def __init__(self, api_token, txadmin_url): - self.api_token = api_token - self.txadmin_url = txadmin_url - self.headers = {'Authorization': f'Bearer {api_token}'} - - def check_updates(self): - """Check for resource updates""" - response = requests.get( - f'{self.txadmin_url}/api/resources/updates', - headers=self.headers - ) - return response.json() - - def update_resource(self, resource_name): - """Update a specific resource""" - # Create backup first - self.backup_resource(resource_name) - - # Update resource - response = requests.post( - f'{self.txadmin_url}/api/resources/{resource_name}/update', - headers=self.headers - ) - - if response.status_code == 200: - print(f"Successfully updated {resource_name}") - return True - else: - print(f"Failed to update {resource_name}") - return False - - def backup_resource(self, resource_name): - """Backup resource before update""" - backup_name = f"{resource_name}_backup_{int(time.time())}" - subprocess.run([ - 'cp', '-r', - f'resources/{resource_name}', - f'backups/{backup_name}' - ]) - -# Usage -updater = ResourceUpdater('your_token', 'http://localhost:40120') -updates = updater.check_updates() - -for resource in updates.get('available_updates', []): - updater.update_resource(resource['name']) -``` - -### Performance Monitoring - -#### Custom Monitoring Dashboard -Create external monitoring: - -```javascript -// monitoring-dashboard.js -const express = require('express'); -const axios = require('axios'); -const app = express(); - -class TxAdminMonitor { - constructor(apiToken, txadminUrl) { - this.apiToken = apiToken; - this.txadminUrl = txadminUrl; - this.metrics = {}; - } - - async collectMetrics() { - try { - // Server status - const statusResponse = await axios.get( - `${this.txadminUrl}/api/server/status`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - // Player count - const playersResponse = await axios.get( - `${this.txadminUrl}/api/players`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - // System metrics - const systemResponse = await axios.get( - `${this.txadminUrl}/api/system/metrics`, - { headers: { Authorization: `Bearer ${this.apiToken}` } } - ); - - this.metrics = { - timestamp: new Date(), - server: statusResponse.data, - players: playersResponse.data, - system: systemResponse.data - }; - - return this.metrics; - } catch (error) { - console.error('Error collecting metrics:', error); - return null; - } - } - - async checkAlerts() { - const metrics = await this.collectMetrics(); - if (!metrics) return; - - // Check CPU usage - if (metrics.system.cpu > 80) { - this.sendAlert('High CPU usage detected', `CPU: ${metrics.system.cpu}%`); - } - - // Check memory usage - if (metrics.system.memory > 90) { - this.sendAlert('High memory usage detected', `Memory: ${metrics.system.memory}%`); - } - - // Check player capacity - const playerPercentage = (metrics.players.count / metrics.server.maxPlayers) * 100; - if (playerPercentage > 95) { - this.sendAlert('Server near capacity', `Players: ${metrics.players.count}/${metrics.server.maxPlayers}`); - } - } - - sendAlert(title, message) { - // Send to Discord webhook - axios.post(process.env.DISCORD_WEBHOOK, { - embeds: [{ - title: title, - description: message, - color: 0xff0000, - timestamp: new Date() - }] - }); - } -} - -// Start monitoring -const monitor = new TxAdminMonitor(process.env.API_TOKEN, process.env.TXADMIN_URL); -setInterval(() => monitor.checkAlerts(), 60000); // Check every minute -``` - -## Multi-Server Management - -### Centralized Control - -#### Server Fleet Manager -Manage multiple servers from one interface: - -```javascript -// fleet-manager.js -class FleetManager { - constructor() { - this.servers = new Map(); - } - - addServer(name, config) { - this.servers.set(name, { - name: name, - url: config.url, - token: config.token, - status: 'unknown' - }); - } - - async getFleetStatus() { - const promises = Array.from(this.servers.entries()).map(async ([name, server]) => { - try { - const response = await axios.get(`${server.url}/api/server/status`, { - headers: { Authorization: `Bearer ${server.token}` } - }); - - return { - name: name, - status: response.data.status, - players: response.data.players, - uptime: response.data.uptime - }; - } catch (error) { - return { - name: name, - status: 'error', - error: error.message - }; - } - }); - - return Promise.all(promises); - } - - async executeFleetCommand(command) { - const results = []; - - for (const [name, server] of this.servers) { - try { - const response = await axios.post( - `${server.url}/api/server/console`, - { command: command }, - { headers: { Authorization: `Bearer ${server.token}` } } - ); - - results.push({ - server: name, - success: true, - response: response.data - }); - } catch (error) { - results.push({ - server: name, - success: false, - error: error.message - }); - } - } - - return results; - } -} - -// Usage -const fleet = new FleetManager(); -fleet.addServer('server1', { url: 'http://server1:40120', token: 'token1' }); -fleet.addServer('server2', { url: 'http://server2:40120', token: 'token2' }); - -// Get status of all servers -fleet.getFleetStatus().then(status => console.log(status)); - -// Execute command on all servers -fleet.executeFleetCommand('say Hello from fleet manager!'); -``` - -### Load Balancing - -#### Player Distribution -Distribute players across servers: - -```javascript -// load-balancer.js -class LoadBalancer { - constructor(servers) { - this.servers = servers; - this.roundRobinIndex = 0; - } - - async findBestServer() { - const statuses = await Promise.all( - this.servers.map(async (server) => { - const response = await axios.get(`${server.url}/api/server/status`, { - headers: { Authorization: `Bearer ${server.token}` } - }); - - return { - ...server, - playerCount: response.data.players, - maxPlayers: response.data.maxPlayers, - cpu: response.data.cpu, - ping: response.data.ping - }; - }) - ); - - // Find server with lowest load - return statuses - .filter(server => server.playerCount < server.maxPlayers) - .sort((a, b) => { - const loadA = (a.playerCount / a.maxPlayers) + (a.cpu / 100); - const loadB = (b.playerCount / b.maxPlayers) + (b.cpu / 100); - return loadA - loadB; - })[0]; - } - - async redirectPlayer(playerId, targetServer) { - // Implementation depends on your redirect system - const command = `redirect ${playerId} ${targetServer.url}`; - - // Execute on current server - await axios.post('/api/server/console', { - command: command - }, { - headers: { Authorization: `Bearer ${this.currentServerToken}` } - }); - } -} -``` - -## Enterprise Features - -### High Availability Setup - -#### Failover Configuration -Set up automatic failover: - -```yaml -# docker-compose.yml for HA setup -version: '3.8' -services: - txadmin-primary: - image: txadmin:latest - ports: - - "40120:40120" - environment: - - TXADMIN_ROLE=primary - - TXADMIN_CLUSTER_SECRET=your_secret - volumes: - - ./data:/app/data - depends_on: - - database - - txadmin-secondary: - image: txadmin:latest - ports: - - "40121:40120" - environment: - - TXADMIN_ROLE=secondary - - TXADMIN_PRIMARY_HOST=txadmin-primary - - TXADMIN_CLUSTER_SECRET=your_secret - volumes: - - ./data:/app/data - depends_on: - - database - - database: - image: mysql:8.0 - environment: - - MYSQL_ROOT_PASSWORD=secure_password - - MYSQL_DATABASE=txadmin - volumes: - - ./mysql-data:/var/lib/mysql - - redis: - image: redis:alpine - command: redis-server --requirepass redis_password -``` - -#### Health Checks -Implement comprehensive health monitoring: - -```javascript -// health-check.js -class HealthChecker { - constructor(config) { - this.config = config; - this.checks = []; - } - - addCheck(name, checkFunction, critical = false) { - this.checks.push({ - name, - check: checkFunction, - critical - }); - } - - async runChecks() { - const results = []; - - for (const check of this.checks) { - try { - const result = await check.check(); - results.push({ - name: check.name, - status: 'healthy', - result: result, - critical: check.critical - }); - } catch (error) { - results.push({ - name: check.name, - status: 'unhealthy', - error: error.message, - critical: check.critical - }); - } - } - - return { - timestamp: new Date(), - overall: this.calculateOverallHealth(results), - checks: results - }; - } - - calculateOverallHealth(results) { - const criticalFailed = results.some(r => r.critical && r.status === 'unhealthy'); - const anyFailed = results.some(r => r.status === 'unhealthy'); - - if (criticalFailed) return 'critical'; - if (anyFailed) return 'degraded'; - return 'healthy'; - } -} - -// Setup health checks -const healthChecker = new HealthChecker(); - -healthChecker.addCheck('database', async () => { - const result = await mysql.query('SELECT 1'); - return { connected: true, responseTime: result.duration }; -}, true); - -healthChecker.addCheck('server_process', async () => { - const response = await axios.get('/api/server/status'); - return { running: response.data.status === 'online' }; -}, true); - -healthChecker.addCheck('disk_space', async () => { - const stats = await fs.promises.statfs('/'); - const freePercent = (stats.free / stats.size) * 100; - return { freePercent: freePercent, adequate: freePercent > 10 }; -}, false); -``` - ---- - - -**Pro Tip:** Use these advanced features gradually. Start with basic automation and build up to complex multi-server setups. - - - -**Security Note:** Always secure API endpoints and use proper authentication for advanced integrations. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/api-events.mdx b/content/docs/txadmin/api-events.mdx new file mode 100644 index 0000000..8b1ca29 --- /dev/null +++ b/content/docs/txadmin/api-events.mdx @@ -0,0 +1,550 @@ +--- +title: "API Events" +description: "Learn about txAdmin Events and how to integrate them with your resources." +--- + +import { FeatureList, DefinitionList, StepList, PropertyCard, CommandCard } from "@ui/components"; +import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; + +# txAdmin Events API + +txAdmin broadcasts **server events** that allow you to integrate functionalities with other resources. Events follow a consistent naming pattern: `txAdmin:events:` and include a table parameter with relevant data. + +## Overview + + + +
+ +
+

CFX Events, Not txAdmin API

+

These events are part of the CFX event system and are broadcast by txAdmin into the server's game environment. txAdmin does not currently expose a separate HTTP/REST API. To integrate with these events, you must listen for them using game event handlers in your server-side resources.

+
+
+ +
+ +
+

Important Notice

+

Do not fully rely on events where consistency is key since they may be executed while the server is not online. For example, while the server is stopped, one could whitelist or ban player identifiers without triggering events.

+
+
+ +--- + +## Server-Related Events + +### txAdmin:events:announcement + +Broadcasted when an announcement is made using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin or `txAdmin` +- `message` *(string)* - The message content of the announcement + +**Customization:** You can hide the default notification in `txAdmin → Settings → Game → Notifications`. + +**Example:** +```lua +AddEventHandler('txAdmin:events:announcement', function(eventData) + print("Announcement from " .. eventData.author .. ": " .. eventData.message) + -- Custom announcement handling +end) +``` + +--- + +### txAdmin:events:serverShuttingDown + +Broadcasted when the server is about to shut down. This can be triggered by scheduled/unscheduled stops or restarts, by an admin, or by the system. + + + +**Event Data:** +- `delay` *(number)* - Milliseconds txAdmin will wait before killing the server process +- `author` *(string)* - The name of the admin or `txAdmin` +- `message` *(string)* - The shutdown message + +**Example:** +```lua +AddEventHandler('txAdmin:events:serverShuttingDown', function(eventData) + print("Server shutting down in " .. (eventData.delay / 1000) .. " seconds") + -- Perform cleanup tasks +end) +``` + +--- + +### txAdmin:events:scheduledRestart + +Broadcasted automatically `[30, 15, 10, 5, 4, 3, 2, 1]` minutes before a scheduled restart. + + + +**Event Data:** +- `secondsRemaining` *(number)* - Seconds before the scheduled restart +- `translatedMessage` *(string)* - Translated message to show on the announcement + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +**Example (ESX v1.2):** +```lua +ESX = nil +TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) + +AddEventHandler('txAdmin:events:scheduledRestart', function(eventData) + if eventData.secondsRemaining == 60 then + CreateThread(function() + Wait(45000) + print("15 seconds before restart... saving all players!") + ESX.SavePlayers(function() + -- Save player data + end) + end) + end +end) +``` + +--- + +### txAdmin:events:scheduledRestartSkipped + +Broadcasted when an admin skips the next scheduled restart. + + + +**Event Data:** +- `secondsRemaining` *(number)* - Seconds before the previously scheduled restart +- `temporary` *(boolean)* - Whether it was a temporary or configured restart +- `author` *(string)* - Name of the admin that skipped the restart + +--- + +## Player-Related Events + +### txAdmin:events:playerBanned + +Broadcasted when a player is banned using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the ban +- `actionId` *(string)* - The ID of this action +- `expiration` *(number|boolean)* - Unix timestamp for ban expiration, or `false` if permanent (added in v4.9) +- `durationInput` *(string)* - Duration input (added in v5.0) +- `durationTranslated` *(string|null)* - Translated duration or `null` (added in v5.0) +- `targetNetId` *(number|null)* - Network ID of banned player, or `null` if identifiers-only ban (added in v5.0) +- `targetIds` *(table)* - Identifiers that were banned (added in v5.0) +- `targetHwids` *(table)* - Hardware identifiers that were banned (added in v6.0) +- `targetName` *(string)* - Clean name of banned player or `identifiers` for legacy bans (added in v5.0) +- `kickMessage` *(string)* - Message shown to player as kick reason (added in v5.0) + +--- + +### txAdmin:events:playerWarned + +Broadcasted when a player is warned using txAdmin. + + + +**Event Data:** +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the warning +- `actionId` *(string)* - The ID of this action +- `targetNetId` *(number|null)* - Network ID of warned player, or `null` if offline (added in v7.3) +- `targetIds` *(table)* - Identifiers that were warned (added in v7.3) +- `targetName` *(string)* - Clean name of warned player (added in v7.3) + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +--- + +### txAdmin:events:playerKicked + +Broadcasted when a player is kicked using txAdmin. Note: Starting on v8.0, the `target` parameter might be `-1`. + + + +**Event Data:** +- `target` *(number)* - Player ID that was kicked, or `-1` if kicking everyone +- `author` *(string)* - The name of the admin +- `reason` *(string)* - The reason for the kick +- `dropMessage` *(string)* - Translated message players will see when kicked (added in v8.0) + +--- + +### txAdmin:events:playerHealed + +Broadcasted when a heal event is triggered for a player or the whole server. This is useful for servers running ambulance jobs or resources that keep players unconscious even after health restoration. + + + +**Event Data:** +- `target` *(number)* - Player ID that was healed, or `-1` if the entire server was healed +- `author` *(string)* - Name of the admin that triggered the heal + +--- + +### txAdmin:events:playerDirectMessage + +Broadcasted when an admin DMs a player. + + + +**Event Data:** +- `target` *(number)* - ID of the player to receive the DM +- `author` *(string)* - The name of the admin +- `message` *(string)* - The message content + +**Customization:** Hide the default notification in `txAdmin → Settings → Game → Notifications`. + +--- + +## Whitelist-Related Events + +### txAdmin:events:whitelistPlayer + +Broadcasted when a player is whitelisted or has their whitelisted status revoked. This event is only fired when the player is already registered and is not related to whitelist requests. + + + +**Event Data:** +- `action` *(string)* - `added` or `removed` +- `license` *(string)* - The license of the player +- `playerName` *(string)* - The player display name +- `adminName` *(string)* - Name of the admin that performed the action + +--- + +### txAdmin:events:whitelistPreApproval + +Broadcasted when manually adding identifiers to whitelist pre-approvals. Players with pre-approved identifiers will be automatically saved to the database upon connecting. + + + +**Event Data:** +- `action` *(string)* - `added` or `removed` +- `identifier` *(string)* - The identifier that was pre-approved (e.g., `discord:xxxxxx`) +- `playerName?` *(string)* - The player display name (except when action is `removed`) +- `adminName` *(string)* - Name of the admin that performed the action + +**Note:** This event is NOT triggered when a whitelist request is approved. Use `txAdmin:events:whitelistRequest` for that. + +--- + +### txAdmin:events:whitelistRequest + +Broadcasted whenever an event related to whitelist requests occurs. + + + +**Event Data:** +- `action` *(string)* - `requested`, `approved`, `denied`, or `deniedAll` +- `playerName?` *(string)* - The player display name (except when action is `deniedAll`) +- `requestId?` *(string)* - The request ID (e.g., `Rxxxx`), except when action is `deniedAll` +- `license?` *(string)* - The license of the player/requester (except when action is `deniedAll`) +- `adminName?` *(string)* - Name of the admin that performed the action (except when action is `requested`) + +--- + +## Admin & Other Events + +### txAdmin:events:adminAuth + +Broadcasted whenever an admin is authenticated in-game or loses admin permissions. This event is particularly useful for anti-cheats to ignore txAdmin admins. + + + +**Event Data:** +- `netid` *(number)* - Player ID or `-1` when revoking all admin permissions (forced reauth) +- `isAdmin` *(boolean)* - Whether the player is an admin +- `username?` *(string)* - The txAdmin username of the authenticated admin + +--- + +### txAdmin:events:adminsUpdated + +Broadcasted whenever the admin list changes, including permission or identifier changes. Used by txAdmin to force admin re-authentication. + + + +**Event Data:** +- Array of Network IDs of admins currently online + +--- + +### txAdmin:events:actionRevoked + +Broadcasted when an admin revokes a database action (ban, warn, etc.). + + + +**Event Data:** +- `actionId` *(string)* - The ID of the revoked action +- `actionType` *(string)* - The type of action that was revoked (ban, warn, etc.) +- `actionReason` *(string)* - The action reason +- `actionAuthor` *(string)* - Name of the admin that issued the action +- `playerName` *(string|boolean)* - Name of the player that received the action, or `false` if not applicable +- `playerIds` *(table)* - Array of identifiers the action applied to (license, discord, etc.) +- `playerHwids` *(table)* - Array of hardware ID tokens the action applied to (added in v6.0) +- `revokedBy` *(string)* - Name of the admin that revoked the action + +--- + +### txAdmin:events:configChanged + +Broadcasted when txAdmin settings change in a way that could affect the server. + + + +**Event Data:** This event has no data. + +**Usage:** Currently used to signal the txAdmin in-game menu when the language changes. Can be used to test custom language files without server restart. + +--- + +### txAdmin:events:consoleCommand + +Broadcasted whenever an admin sends a command through the Live Console. + + + +**Event Data:** +- `author` *(string)* - The txAdmin username of the admin who sent the command +- `channel` *(string)* - Currently always `txAdmin`, but may be `rcon` or `game` in the future +- `command` *(string)* - The command that was executed + +--- + +## Deprecated Events + +The following events have been deprecated and should not be used in new resources: + + + +--- + +## Implementation Example + +Here's a complete example of listening to multiple events in a resource: + +```lua +-- Initialize event listeners +local eventLog = {} + +-- Server announcements +AddEventHandler('txAdmin:events:announcement', function(eventData) + table.insert(eventLog, { + type = 'announcement', + author = eventData.author, + message = eventData.message, + timestamp = os.time() + }) + print("^2[txAdmin Announcement]^7 " .. eventData.author .. ": " .. eventData.message) +end) + +-- Player actions +AddEventHandler('txAdmin:events:playerBanned', function(eventData) + table.insert(eventLog, { + type = 'ban', + author = eventData.author, + target = eventData.targetName or 'Unknown', + reason = eventData.reason, + expiration = eventData.expiration, + timestamp = os.time() + }) + print("^1[txAdmin Ban]^7 " .. eventData.targetName .. " banned by " .. eventData.author) +end) + +AddEventHandler('txAdmin:events:playerWarned', function(eventData) + table.insert(eventLog, { + type = 'warn', + author = eventData.author, + target = eventData.targetName, + reason = eventData.reason, + timestamp = os.time() + }) + print("^3[txAdmin Warning]^7 " .. eventData.targetName .. " warned by " .. eventData.author) +end) + +-- Admin authentication +AddEventHandler('txAdmin:events:adminAuth', function(eventData) + if eventData.isAdmin then + print("^5[txAdmin Auth]^7 " .. eventData.username .. " authenticated (netid: " .. eventData.netid .. ")") + else + print("^5[txAdmin Auth]^7 Admin permissions revoked for netid: " .. eventData.netid) + end +end) + +-- Server shutdown +AddEventHandler('txAdmin:events:serverShuttingDown', function(eventData) + print("^1[txAdmin]^7 Server shutting down in " .. (eventData.delay / 1000) .. " seconds") + print("Reason: " .. eventData.message) +end) +``` + +--- + +## Best Practices + + + +--- + +## Related Documentation + +- [txAdmin Official Repository](https://github.com/citizenfx/txAdmin) +- [FiveM Development Docs](https://docs.fivem.net/) +- [Event System Guide](./natives) diff --git a/content/docs/txadmin/backup-system.mdx b/content/docs/txadmin/backup-system.mdx index 526c0e3..24740f4 100644 --- a/content/docs/txadmin/backup-system.mdx +++ b/content/docs/txadmin/backup-system.mdx @@ -3,559 +3,8 @@ title: Backup System description: Configure automated backups, manage backup storage, and implement disaster recovery for your FiveM/RedM server. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers txAdmin's comprehensive backup system, helping you protect your server data and implement reliable disaster recovery procedures. - -## Backup Overview - -### What Gets Backed Up -txAdmin's backup system protects critical server components: - -#### Server Files -- **Resources Folder:** All custom and framework resources -- **Server Configuration:** server.cfg and related config files -- **Data Folder:** Server data including logs and cache -- **Database:** MySQL/MariaDB database content (if configured) -- **User Data:** Player profiles and save data - -#### Exclusions (Configurable) -- **Cache Files:** Temporary files that can be regenerated -- **Log Files:** Large log files (configurable retention) -- **Temp Directories:** Temporary processing folders -- **System Files:** Operating system and txAdmin installation files - -### Backup Types - - - - - - - -#### Full Backups -Complete server backup including all data: -- **Frequency:** Weekly or monthly -- **Size:** Largest backup type -- **Restore:** Complete server restoration possible -- **Use Case:** Major updates, migrations, fresh installs - -#### Incremental Backups -Only changed files since last backup: -- **Frequency:** Daily or hourly -- **Size:** Smaller, faster backups -- **Restore:** Requires full backup + incrementals -- **Use Case:** Regular protection with minimal storage - -#### Resource-Only Backups -Backup only the resources folder: -- **Frequency:** Before resource updates -- **Size:** Medium size backups -- **Restore:** Resource-specific restoration -- **Use Case:** Development servers, resource testing - -## Configuring Automated Backups - - - -### Basic Backup Setup - -#### Enable Automated Backups -1. Navigate to **Settings → Backup** -2. Enable **Automatic Backups** -3. Configure basic settings: - -```json -{ - "backup_config": { - "enabled": true, - "frequency": "daily", - "time": "03:00", - "timezone": "UTC", - "retention_days": 14, - "compression": true - } -} -``` - -#### Backup Schedule Options -Configure when backups run: - -**Daily Backups:** -```json -{ - "schedule": { - "type": "daily", - "time": "03:00", - "timezone": "UTC" - } -} -``` - -**Weekly Backups:** -```json -{ - "schedule": { - "type": "weekly", - "day": "sunday", - "time": "02:00", - "timezone": "UTC" - } -} -``` - -**Custom Schedule (Cron):** -```json -{ - "schedule": { - "type": "cron", - "expression": "0 3 * * 0", - "description": "Every Sunday at 3 AM" - } -} -``` - -### Advanced Backup Configuration - -#### Retention Policies -Manage backup storage efficiently: - -```json -{ - "retention_policy": { - "daily_backups": { - "keep": 7, - "description": "Keep 7 daily backups" - }, - "weekly_backups": { - "keep": 4, - "description": "Keep 4 weekly backups" - }, - "monthly_backups": { - "keep": 12, - "description": "Keep 12 monthly backups" - } - } -} -``` - -#### Include/Exclude Rules -Customize what gets backed up: - -```json -{ - "backup_rules": { - "include": [ - "resources/", - "server.cfg", - "data/", - "database/" - ], - "exclude": [ - "cache/", - "logs/*.log", - "temp/", - "node_modules/", - "*.tmp" - ] - } -} -``` - -#### Compression Settings -Optimize backup size and speed: - -```json -{ - "compression": { - "enabled": true, - "algorithm": "gzip", - "level": 6, - "exclude_already_compressed": true - } -} -``` - -## Backup Storage Options - -### Local Storage -Default backup storage on the server: - -#### Configuration -```json -{ - "storage": { - "type": "local", - "path": "/opt/txadmin/backups", - "max_size": "50GB", - "cleanup_enabled": true - } -} -``` - -#### Advantages -- **Fast Access:** Quick backup and restore operations -- **No Dependencies:** Works without internet connection -- **Low Latency:** Immediate access to backup files - -#### Considerations -- **Single Point of Failure:** Vulnerable to hardware failure -- **Limited Space:** Constrained by local storage -- **Physical Security:** Relies on server physical security - -### Cloud Storage Integration - -#### Amazon S3 -Configure AWS S3 for backup storage: - -```json -{ - "storage": { - "type": "s3", - "bucket": "your-backup-bucket", - "region": "us-west-2", - "access_key": "YOUR_ACCESS_KEY", - "secret_key": "YOUR_SECRET_KEY", - "encryption": true, - "storage_class": "STANDARD_IA" - } -} -``` - -#### Google Cloud Storage -Set up Google Cloud for backups: - -```json -{ - "storage": { - "type": "gcs", - "bucket": "your-backup-bucket", - "project_id": "your-project-id", - "key_file": "/path/to/service-account.json", - "storage_class": "NEARLINE" - } -} -``` - -#### Azure Blob Storage -Configure Azure for backup storage: - -```json -{ - "storage": { - "type": "azure", - "container": "backups", - "account_name": "yourstorageaccount", - "account_key": "YOUR_ACCOUNT_KEY", - "tier": "Cool" - } -} -``` - -### Network Storage - -#### SFTP/SSH Storage -Use remote servers via SSH: - -```json -{ - "storage": { - "type": "sftp", - "host": "backup-server.com", - "port": 22, - "username": "backup_user", - "private_key": "/path/to/private_key", - "remote_path": "/backups/server1" - } -} -``` - -#### SMB/CIFS Shares -Network file sharing: - -```json -{ - "storage": { - "type": "smb", - "share": "//backup-server/backups", - "username": "backup_user", - "password": "secure_password", - "domain": "your_domain" - } -} -``` - -## Manual Backup Operations - -### Creating Manual Backups - -#### Via Web Interface -1. Go to **System → Backups** -2. Click **Create Backup** -3. Select backup options: - - **Backup Type:** Full, Resources Only, Custom - - **Description:** Custom backup description - - **Include Database:** Include database dump -4. Start backup process - -#### Via Console Commands -```bash -# Create full backup -backup create --type=full --description="Pre-update backup" - -# Create resource-only backup -backup create --type=resources --description="Before resource update" - -# Create custom backup -backup create --include="resources,data" --exclude="cache" --description="Custom backup" -``` - -#### Backup Verification -Verify backup integrity: - -```bash -# Verify backup -backup verify --file="backup_20240115_030000.tar.gz" - -# List backup contents -backup list --file="backup_20240115_030000.tar.gz" - -# Test backup extraction -backup test --file="backup_20240115_030000.tar.gz" --test-path="/tmp/backup_test" -``` - -### Backup Management - -#### Viewing Backups -Monitor existing backups: - -- **Backup List:** View all available backups -- **Size Information:** Backup file sizes and compression ratios -- **Creation Dates:** When each backup was created -- **Verification Status:** Backup integrity status -- **Storage Location:** Where backup is stored - -#### Backup Actions -Available actions for each backup: - -- **Download:** Download backup to local machine -- **Verify:** Check backup integrity -- **Delete:** Remove backup from storage -- **Restore:** Restore server from backup -- **Clone:** Create new server from backup - -## Disaster Recovery - -### Restoration Procedures - -#### Full Server Restore -Complete server restoration from backup: - -1. **Preparation:** - - Stop current server - - Backup current state (if possible) - - Ensure sufficient disk space - -2. **Restoration Process:** - ```bash - # Via txAdmin interface - 1. Navigate to System → Backups - 2. Select backup to restore - 3. Choose restore options - 4. Confirm restoration - - # Via command line - backup restore --file="backup_20240115_030000.tar.gz" --type=full - ``` - -3. **Post-Restore Steps:** - - Verify resource integrity - - Update configuration if needed - - Test server startup - - Verify player data - -#### Partial Restoration -Restore specific components: - -**Resource-Only Restore:** -```bash -backup restore --file="backup.tar.gz" --type=resources --target="/opt/server/resources" -``` - -**Database Restore:** -```bash -backup restore --file="backup.tar.gz" --type=database --target="server_database" -``` - -**Configuration Restore:** -```bash -backup restore --file="backup.tar.gz" --type=config --target="/opt/server" -``` - -### Recovery Scenarios - -#### Server Corruption -When server files become corrupted: - -1. **Assessment:** - - Identify corrupted components - - Determine extent of damage - - Select appropriate backup - -2. **Recovery Steps:** - - Stop server immediately - - Backup current state for analysis - - Restore from last known good backup - - Verify restoration success - -#### Data Loss Events -Recovering from data loss: - -1. **Player Data Loss:** - - Identify affected players - - Restore from database backup - - Merge with current data if possible - - Communicate with affected players - -2. **Resource Loss:** - - Identify missing resources - - Restore from resource backup - - Update resource configurations - - Test functionality - -#### Hardware Failure -Recovering on new hardware: - -1. **Preparation:** - - Set up new server environment - - Install txAdmin and dependencies - - Configure network settings - -2. **Migration:** - - Download backup from cloud storage - - Restore complete server state - - Update network configurations - - Test all functionality - -## Monitoring and Alerts - -### Backup Monitoring - -#### Success/Failure Tracking -Monitor backup operations: - -```json -{ - "monitoring": { - "track_duration": true, - "alert_on_failure": true, - "alert_on_long_duration": true, - "max_duration_minutes": 60, - "notification_channels": ["email", "discord"] - } -} -``` - -#### Storage Monitoring -Track backup storage usage: - -- **Storage Usage:** Monitor used vs available space -- **Growth Trends:** Track storage usage over time -- **Retention Compliance:** Ensure retention policies are followed -- **Cost Monitoring:** Track cloud storage costs - -### Alerting System - -#### Backup Failure Alerts -Get notified when backups fail: - -```json -{ - "alerts": { - "backup_failure": { - "enabled": true, - "channels": ["email", "discord", "slack"], - "retry_attempts": 3, - "escalation": true - } - } -} -``` - -#### Storage Alerts -Monitor storage issues: - -```json -{ - "storage_alerts": { - "low_space": { - "threshold": "85%", - "action": "cleanup_old_backups" - }, - "backup_too_large": { - "threshold": "10GB", - "action": "investigate_growth" - } - } -} -``` - -## Best Practices - -### Backup Strategy - -#### 3-2-1 Rule -Implement the industry-standard backup strategy: -- **3 Copies:** Keep 3 copies of important data -- **2 Different Media:** Store on 2 different storage types -- **1 Offsite:** Keep 1 copy in a different location - -#### Testing Procedures -Regularly test backup integrity: - -1. **Monthly Tests:** Verify random backups -2. **Quarterly Restores:** Perform full restoration tests -3. **Documentation:** Document test procedures and results -4. **Improvement:** Update procedures based on test results - -### Security Considerations - -#### Backup Encryption -Protect sensitive data in backups: - -```json -{ - "encryption": { - "enabled": true, - "algorithm": "AES-256", - "key_management": "auto", - "encrypt_in_transit": true, - "encrypt_at_rest": true - } -} -``` - -#### Access Control -Limit backup access: - -- **Role-Based Access:** Limit who can create/restore backups -- **Audit Logging:** Log all backup operations -- **Secure Storage:** Use encrypted storage solutions -- **Key Management:** Secure encryption key storage - ---- - - -**Backup Tip:** Test your disaster recovery procedures regularly to ensure they work when needed. - - - -**Important:** Store backups in multiple locations to protect against site-wide disasters. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/configuration.mdx b/content/docs/txadmin/configuration.mdx index 6abd309..4807f6b 100644 --- a/content/docs/txadmin/configuration.mdx +++ b/content/docs/txadmin/configuration.mdx @@ -3,206 +3,8 @@ title: Config Editor description: Learn about the txAdmin Config Editor and server configuration. --- -import { Step, Steps } from 'fumadocs-ui/components/steps'; -import { Callout } from 'fumadocs-ui/components/callout'; -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid, FileTree } from '@ui/components/mdx-components' - -# txAdmin Config Editor - -The Config Editor is a web-based interface within txAdmin that allows you to edit your server's configuration file (`server.cfg`) without needing to access the file system directly. This guide explains how to use it and what each configuration option does. - -## Accessing the Config Editor - - - -## Config File Sections - -The default configuration file is divided into three main sections: - -### 1. MUST EDIT - Server Identity - - -These settings define your server's basic information and are essential for operation. - - - - - - - - - - - - -### 2. CAN EDIT - Server Configuration - -These settings control server behavior and performance: - - - - - - - - - - -- **Default Port:** 30120 (game server) -- **Important:** Must match your firewall configuration -- **Note:** `0.0.0.0` means listen on all available network interfaces - -#### `set steam_webApiKey` -- **What it does:** Steam API key for additional Steam integration features -- **Example:** `set steam_webApiKey "none"` -- **Type:** String -- **Default:** "none" (optional feature) -- **How to Get:** Register at [Steam Developer](https://steamcommunity.com/dev/apikey) - -#### `set resources_useSystemChat` -- **What it does:** Whether to use the system chat or let resources handle chat -- **Values:** `true` or `false` -- **Example:** `set resources_useSystemChat true` -- **Default:** true - -## Resources Section - -This section lists which resources automatically start when the server boots: - -``` -ensure mapmanager # Manages maps/worlds -ensure chat # In-game chat system -ensure spawnmanager # Player spawn management -ensure sessionmanager-rdr3 # Session management for RedM -ensure redm-map-one # RedM map resource -ensure hardcap # Prevents exceeding sv_maxclients -``` - -To add or remove resources: -1. Add new lines with `ensure resourcename` -2. Comment out lines with `#` to disable them -3. Save the file - - -**Pro Tip:** Add essential resources at the beginning and optional ones at the end. This helps with startup troubleshooting. - - -## Admin Management Section - -This section defines which players have admin permissions: - -#### `add_ace group.admin command allow` -- **What it does:** Gives the "admin" group permission to use all commands -- **Example:** `add_ace group.admin command allow` - -#### `add_ace group.admin command.quit deny` -- **What it does:** Denies the admin group the ability to quit the server (safety measure) -- **Example:** `add_ace group.admin command.quit deny` - -#### `add_principal identifier.fivem:XXXXX group.admin` -- **What it does:** Adds a specific FiveM user to the admin group -- **Example:** `add_principal identifier.fivem:739337 group.admin #CodeMeAPixel` -- **How to Find Your ID:** Check txAdmin console or use `/getid` in-game -- **Format:** `add_principal identifier.fivem:[YOUR_ID] group.admin` - -#### `add_principal identifier.discord:XXXXX group.admin` -- **What it does:** Adds a specific Discord user to the admin group (if Discord integration enabled) -- **Example:** `add_principal identifier.discord:510065483693817867 group.admin` -- **How to Find Your ID:** Enable Discord integration in txAdmin and check your ID - -## Using the Config Editor - - - - ### Make Your Changes - Edit the configuration file directly in the web editor. The editor provides syntax highlighting for easier reading. - - - ### Save the File - Click the **Save File** button (typically at the bottom right: `CTRL + S`) - - - ### Restart the Server - Changes take effect after a server restart. Use txAdmin's restart function to apply changes. - - - ### Check the Logs - After restart, check the console logs for any errors or warnings related to your changes. - - - - -**Important:** Some changes require a full server restart to take effect. Test configuration changes on a test server first if possible. - - -## Common Configuration Scenarios - -### Setting up a Whitelisted Server -``` -# Add these tags -sets tags "whitelisted, roleplay" - -# Reduce max clients for exclusivity -sv_maxclients 32 - -# Add ACE rules for whitelist checking -add_ace builtin.everyone command deny -add_ace group.admin command allow -``` - -### Setting up a Large Public Server -``` -# Increase player limit -sv_maxclients 128 - -# Enable relevant tags -sets tags "roleplay, economy, factions, pvp" - -# Ensure enough resources -ensure mapmanager -ensure chat -ensure spawnmanager -ensure sessionmanager-rdr3 -``` - -### Setting up a Development/Testing Server -``` -# Clear identifying info -sv_hostname "DEV - Test Server" -sets tags "dev, testing" - -# Lower player limit -sv_maxclients 16 - -# Enable verbose logging -set txAdmin-verbose true -``` - -## Troubleshooting Configuration Issues - -**Server won't start:** -- Check the license key is correct -- Verify all resource names are valid -- Check for syntax errors (missing quotes, brackets) - -**Players can't join:** -- Verify firewall rules for port 30120 -- Check `sv_maxclients` is appropriate -- Ensure license key is valid and active - -**Resources not starting:** -- Check resource names are spelled correctly -- Look at server console for error messages -- Verify the resource folder exists - -## References - -For more detailed information, refer to the official FiveM documentation: -- [FiveM Server Commands](https://aka.cfx.re/server-commands) -- [Server Manual](https://docs.fivem.net/docs/server-manual/setting-up-a-server/) -- [CFX Portal](https://portal.cfx.re) - License key management +import { InfoBanner } from '@ui/components/mdx-components'; + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/custom-server-log.mdx b/content/docs/txadmin/custom-server-log.mdx new file mode 100644 index 0000000..c68ab78 --- /dev/null +++ b/content/docs/txadmin/custom-server-log.mdx @@ -0,0 +1,48 @@ +--- +title: "Custom Server Logs" +description: "Add logging for custom commands to txAdmin." +--- + +import { Info } from "lucide-react"; +import { StepList, PropertyCard } from "@ui/components"; + +# Logging Extra Data + +This feature allows you to add logging for custom commands like `/car` and `/tp` to txAdmin's server logs. To do that, you will need to edit the scripts of those commands to trigger a `txaLogger:CommandExecuted` event. + +> **Note:** For now this only supports client commands! + +
+ +
+

Event Trigger

+

Add the following event call inside your command function: `TriggerServerEvent('txaLogger:CommandExecuted', rawCommand)` where `rawCommand` is the full command with parameters. You don't NEED to pass `rawCommand`, you can edit this string or pass anything you want.

+
+
+ +## How to Enable + +In the client script, add the following event call inside the command function: + +```lua +TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) +``` + +Where `rawCommand` is a variable containing the full command with parameters. + +## Example + +In this example, we will log data from the `/car` command from the `CarCommand` script. + +```lua +RegisterCommand('car', function(source, args, rawCommand) + -- txAdmin logging callback + TriggerServerEvent('txaLogger:CommandExecuted', rawCommand) + + local x,y,z = table.unpack(GetOffsetFromEntityInWorldCoords(PlayerPedId(), 0.0, 8.0, 0.5)) + + -- there is more code here, no need to edit +end) +``` + +This will add an entry to txAdmin's server logs showing that the command was executed along with the parameters used. diff --git a/content/docs/txadmin/development.mdx b/content/docs/txadmin/development.mdx new file mode 100644 index 0000000..8218917 --- /dev/null +++ b/content/docs/txadmin/development.mdx @@ -0,0 +1,167 @@ +--- +title: "Development" +description: "Guide to set up txAdmin development environment." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, FeatureList, CommandCard } from "@ui/components"; + +# txAdmin Development + +If you are interested in development of txAdmin, this guide will help set up your environment. Before starting, please make sure you are familiar with the basics of NodeJS & ecosystem. + +> **Note:** This guide does not cover translations, [which are very easy to do!](./translation) + +## Requirements + +- Windows (the builder doesn't work for other OSs) +- NodeJS v22.9 or newer +- FXServer + +## Project Structure + +- **core** - Node Backend & Modules. Transpiled by `tsc` and bundled with `esbuild` + - **boot** - Code used/triggered during the boot process + - **deployer** - Responsible for deploying new servers + - **lib** - Collection of stateless utils, helpers and business logic + - **modules** - Classes that compose the txAdmin instance, stateful with specific functionalities + - **routes** - All web routes containing HTTP router logic + - **testing** - Top-level testing utilities + +- **resource** - The in-game resource running under the `monitor` name. Files synchronized with deploy path during development. + +- **menu** - React source code for txAdmin's NUI Menu. Transpiled & built using Vite. + +- **web** - Legacy SSR templates & static assets. Uses EJS templating. Will be deprecated in favor of `panel`. + +- **panel** - New UI built with React and Vite. + +- **scripts** - Scripts used for development only. + +- **shared** - Stuff used across workspaces like functions and type definitions. + +## Preparing the Environment + + + +## Development Workflows + +### Core/Panel/Resource + +This workflow is controlled by `scripts/build/*`, which is responsible for: +- Watching and copying static files (resource, docs, license, entry file, etc) to the deploy path +- Watching and re-transpiling the core files, bundling and deploying it +- Running FXServer in the same terminal, restarting when core is modified (like `nodemon`, but enhanced) + +In dev mode, core redirects the panel `index.html` to use Vite, so you first need to start it, then start the builder: + +```bash +# run vite +cd panel +npm run dev + +# In a new terminal - run the builder +cd core +npm run dev +``` + +### NUI Menu + +```bash +cd nui + +# To run Vite on game dev mode: +npm run dev + +# To run Vite on browser dev mode: +npm run browser +``` + +Keep in mind that for every change you will need to restart the `monitor` resource. Unless you started the server with `+setr txAdmin-debugMode true`, txAdmin will detect that as a crash and restart your server. + +When running in game mode, it takes between 10 and 30 seconds for Vite to finish building before you can restart the `monitor` resource in-game. + +### Resource Event Naming Rules + +- Event prefix must be `tx:` indicating where it is registered +- Events that request something (like permission) from the server start with `txsv:req` +- Events can have verbs like `txsv:checkAdminStatus` or `txcl:setServerCtx` +- Most events are menu-related, so scoping events to menu is not required + +### Testing & Building + +The building process is normally done in the GitHub Action workflow only, but if you must build locally: + +```bash +npm run test --workspaces +GITHUB_REF="refs/tags/v9.9.9" npm run build +``` + +The output will be in the `dist/` folder. + +> **Note:** Linting & typechecking need to be re-added into the workflow. + +## Notes Regarding the Settings System + +- `config.json` now only contains changed, non-default values +- `DEFAULT_NULL` is only for values that cannot and should not have defaults (like `fxRunner.dataPath`, `discordBot.token`) +- Note how `fxRunner.cfgPath` does have a default +- All schemas must have a default, even if `null` +- The objective of the `schema.fixer` is to fix invalid values, not apply defaults for missing values +- The `schema.fixer` is only used during boot, not during any saves +- Only use `SYM_FIXER_FATAL` for very important settings so txAdmin rather not boot than boot with an unexpected config +- The objective of the schema is to guarantee correct type values (shouldn't cause TypeErrors), but does not validate dynamic things like file existence +- Validator transformers are only to "polish" the value (removing duplicates, sorting) not to fix invalid values + +## Legacy UI Reference + +
+ +
+

Legacy Warning

+

The `/web/` UI is considered legacy and will be migrated to `/panel/`. **DO NOT** modify `css/coreui.css`. Either do a patch in `custom.css` or modify the SCSS variables.

+
+
+ +### Building CoreUI CSS + +This doc is a reference if you are trying to re-build the `css/coreui.css` from the SCSS source. The only change from CoreUI was the `aside-menu` size from 200px to 300px in `scss/_variables.scss : $aside-menu-width`. + +You can find other variable names in `node_modules/@coreui/coreui/scss/coreui`. + +```bash +git clone https://github.com/coreui/coreui-free-bootstrap-admin-template.git coreui +cd coreui +npm i + +# If you want to make sure you used the same version of CoreUI +git checkout 0cb1d81a8471ff4b6eb80c41b45c61a8e2ab3ef6 + +# Edit your stuff and then compile: +npx node-sass \ + --output-style expanded \ + --source-map true \ + --source-map-contents true \ + --precision 6 \ + src/scss/style.scss src/css/style.css +``` + +Then copy the `src/css/style.css` to txAdmin's folder. diff --git a/content/docs/txadmin/discord-bot.mdx b/content/docs/txadmin/discord-bot.mdx index 4ffb00b..8244dd5 100644 --- a/content/docs/txadmin/discord-bot.mdx +++ b/content/docs/txadmin/discord-bot.mdx @@ -254,7 +254,7 @@ The Status Config JSON controls the appearance for each server state. { "emoji": "1062339910654246964", "label": "txAdmin Discord", - "url": "https://discord.gg/txAdmin" + "url": "https://discord.gg/eWhDDVCpPn" } ] } diff --git a/content/docs/txadmin/discord-status.mdx b/content/docs/txadmin/discord-status.mdx new file mode 100644 index 0000000..967ca59 --- /dev/null +++ b/content/docs/txadmin/discord-status.mdx @@ -0,0 +1,125 @@ +--- +title: "Discord Status Embed" +description: "Customize txAdmin's Discord persistent status embed." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard } from "@ui/components"; + +# Custom Discord Status Embed + +Starting in v5.1, **txAdmin** has a Discord Persistent Status Embed feature. This is a Discord embed that txAdmin will update every minute, and you can configure it to display server status, and any other random thing that you can normally do with a Discord embed. + +To add the embed, type `/status add` on a channel that the txAdmin bot has Send Message permission. + +To modify the embed, navigate to `txAdmin > Settings > Discord Bot`, and click on the two JSON editor buttons. + +
+ +
+

JSON Editing Tip

+

If you are having issues with JSON encoding, we recommend you use [jsoneditoronline.org](https://jsoneditoronline.org/) to modify your JSON.

+
+
+ +## Available Placeholders + +To add dynamic data to the embed, you can use the built-in placeholders, which txAdmin will replace at runtime. + +- `{{serverCfxId}}` - The Cfx.re id of your server, tied to your `sv_licenseKey` and detected at runtime. +- `{{serverJoinUrl}}` - The direct join URL of your server. Example: `https://cfx.re/join/xxxxxx` +- `{{serverBrowserUrl}}` - The FiveM Server browser URL. Example: `https://servers.fivem.net/servers/detail/xxxxxx` +- `{{serverClients}}` - The number of players online in your server. +- `{{serverMaxClients}}` - The `sv_maxclients` of your server, detected at runtime. +- `{{serverName}}` - The txAdmin-given name for this server. Can be changed in `txAdmin > Settings > Global`. +- `{{statusColor}}` - A hex-encoded color from the Config JSON. +- `{{statusString}}` - A text to be displayed with the server status from the Config JSON. +- `{{uptime}}` - For how long the server is online. Example: `1 hr, 50 mins` +- `{{nextScheduledRestart}}` - String with when the next scheduled restart is. Example: `in 2 hrs, 48 mins` + +## Embed JSON + +This is the JSON of the Embed that will be sent to Discord. This MUST be a valid Discord embed JSON. + +We recommend you use a tool like [discohook.org](https://discohook.org/) to edit the embed. To do so, click `JSON Data Editor` at the bottom and paste the JSON inside the `embeds: [...]` array. + +
+ +
+

Important Notice

+

At save time, txAdmin cannot validate if the embed is correct without sending it to the Discord API. If it does not work, check the `System Logs` page in txAdmin and see if there are any errors related to it. You don't need to set `color` or `footer` as txAdmin will replace those. You can modify the color in the config JSON, but the footer is generated by txAdmin.

+
+
+ +### Example Embed JSON + +```json +{ + "title": "{{serverName}}", + "url": "{{serverBrowserUrl}}", + "description": "You can configure this embed in `txAdmin > Settings > Discord Bot`, and edit everything from it (except footer).", + "fields": [ + { + "name": "> STATUS", + "value": "```\n{{statusString}}\n```", + "inline": true + }, + { + "name": "> PLAYERS", + "value": "```\n{{serverClients}}/{{serverMaxClients}}\n```", + "inline": true + }, + { + "name": "> F8 CONNECT COMMAND", + "value": "```\nconnect 123.123.123.123\n```" + }, + { + "name": "> NEXT RESTART", + "value": "```\n{{nextScheduledRestart}}\n```", + "inline": true + }, + { + "name": "> UPTIME", + "value": "```\n{{uptime}}\n```", + "inline": true + } + ], + "image": { + "url": "https://forum-cfx-re.akamaized.net/original/5X/e/e/c/b/eecb4664ee03d39e34fcd82a075a18c24add91ed.png" + }, + "thumbnail": { + "url": "https://forum-cfx-re.akamaized.net/original/5X/9/b/d/7/9bd744dc2b21804e18c3bb331e8902c930624e44.png" + } +} +``` + +## Embed Configuration + +The configuration of the embed, where you can change the status texts, as well as the embed color. You can set up to 5 buttons. + +For emojis, you can use an actual unicode emoji character, or the emoji ID. To get the emoji ID, insert it into discord, add `\` before it, then send the message to get the full name (e.g., `<:txicon:1062339910654246964>`). + +### Example Configuration + +```json +{ + "onlineString": "🟢 Online", + "onlineColor": "#0BA70B", + "partialString": "🟡 Partial", + "partialColor": "#FFF100", + "offlineString": "🔴 Offline", + "offlineColor": "#A70B28", + "buttons": [ + { + "emoji": "1062338355909640233", + "label": "Connect", + "url": "{{serverJoinUrl}}" + }, + { + "emoji": "1062339910654246964", + "label": "txAdmin Discord", + "url": "https://discord.gg/eWhDDVCpPn" + } + ] +} +``` diff --git a/content/docs/txadmin/env-config.mdx b/content/docs/txadmin/env-config.mdx new file mode 100644 index 0000000..fe116b9 --- /dev/null +++ b/content/docs/txadmin/env-config.mdx @@ -0,0 +1,204 @@ +--- +title: "Environment Configuration" +description: "Configure txAdmin through TXHOST_* environment variables." +--- + +import { AlertTriangle, CheckCircle2, Info } from "lucide-react"; +import { FeatureList, PropertyCard, CommandCard, StepList } from "@ui/components"; + +# Environment Configuration + +Starting from txAdmin v8.0.0, you can now customize txAdmin through `TXHOST_*` environment variables documented in this page. + +Those configurations are usually required for Game Server Providers (GSPs) and advanced server owners, and allow them to force txAdmin and FXServer to use specific ports/interfaces, customize the location of the txData directory, force a max player slot count, etc. + +
+ +
+

Deprecated Configuration

+

The `txAdminPort`, `txAdminInterface`, and `txDataPath` ConVars, as well as the `txAdminZapConfig.json` file are now considered deprecated and will cease to work in an upcoming update. If the new and old configs are present at the same time, the new one will take priority. Set `TXHOST_IGNORE_DEPRECATED_CONFIGS` to `true` to disable the old config and silence warnings.

+
+
+ +## Setup + +The specific way to set up those variables vary from system to system, and there are usually multiple ways even within the same system. But these should work for most people: + +**Windows:** +- Edit your existing `start__.bat` to add the `set VAR_NAME=VALUE` commands before the `./<...>/FXServer.exe` line. +- Alternatively, create a `env.bat` file with the `set` commands, then start your server with `call env.bat && FXServer.exe`. + +**Linux:** +- Edit your existing `run.sh` to add the `export VAR_NAME=VALUE` commands before the `exec $SCRIPTPATH/[...]` line. +- Alternatively, create a `env.sh` file with the `export` commands, then start with `source env.sh && ./run.sh`. + +**Docker:** +- Create a `.env` file with the vars like: `VAR_NAME=VALUE` +- Load it using the `--env-file=.env` flag in your docker run command. + +**Pterodactyl:** +- You will likely need to contact your GSP or edit the "egg" being used. + +
+ +
+

Security Recommendation

+

For security reasons, those environment variables should be set specifically for the boot process and must not be widely available for other processes. If they are to be written to a file (such as `.env`), the file should only be readable for the txAdmin process and not its children processes.

+
+
+ +## General Configuration + +**TXHOST_DATA_PATH** +- **Default value:** + - **Windows:** `/../txData` — sits in the folder parent of the folder containing `fxserver.exe` + - **Linux:** `/../../../txData` — sits in the folder that contains your `run.sh` +- The path to the txData folder, which contains the txAdmin logs, configs, and data. +- This is also the default place suggested for deploying new servers as a subfolder. +- Usually set to `/home/container` when running on Pterodactyl. +- **Note:** This variable takes priority over the deprecated `txDataPath` ConVar. + +**TXHOST_GAME_NAME** +- **Default value:** _undefined_ +- **Options:** `fivem`, `redm` +- Restricts to only running either FiveM or RedM servers. +- The setup page will only show recipes for the game specified. + +**TXHOST_INTERFACE** +- **Default value:** `0.0.0.0` +- Which interface txAdmin will bind and enforce FXServer to bind to. +- **Note:** This variable takes priority over the deprecated `txAdminInterface` ConVar. + +**TXHOST_MAX_SLOTS** +- **Default value:** _undefined_ +- Enforces the server `sv_maxClients` is set to a number less than or equal to the variable value. + +**TXHOST_QUIET_MODE** +- **Default value:** `false` +- If true, do not pipe the FXServer's stdout/stderr to txAdmin's stdout. +- You will only be able to see server output by visiting the txAdmin Live Console page. +- If enabled, server owners won't be able to disable it in `txAdmin → Settings → FXServer`. +- **Note:** Game Server Providers should enable this option. + +## Networking Configuration + +**TXHOST_TXA_PORT** +- **Default value:** `40120` +- Which TCP port txAdmin should bind & listen to. +- This variable cannot be `30120` to prevent user confusion. +- **Note:** This variable takes priority over the deprecated `txAdminPort` ConVar. + +**TXHOST_TXA_URL** +- **Default value:** _undefined_ +- If present, that is the URL that will show on txAdmin as its public URL on the boot message. +- Useful for when running inside a container using `0.0.0.0:40120` as interface/port. + +**TXHOST_FXS_PORT** +- **Default value:** _undefined_ +- Forces the FXServer to bind to the specified port. +- Enforces or replaces the `endpoint_add_*` commands in `server.cfg`. +- This variable cannot be `40120` to prevent user confusion. + +## API & Hosting Configuration + +**TXHOST_API_TOKEN** +- **Default value:** _undefined_ +- **Options:** `disabled` or a string matching `/^[A-Za-z0-9_-]{16,48}$/` +- The token to access the `/host/status` endpoint via the `x-txadmin-envtoken` HTTP header or `?envtoken=` URL parameter. +- If _undefined_: endpoint disabled & unavailable +- If token is string `disabled`: endpoint will be publicly available without restrictions +- If token is present: endpoint requires the token to be present + +**TXHOST_PROVIDER_NAME** +- **Default value:** `Host Config` +- A short name to identify this hosting provider. +- Must be between 2 and 16 characters long. +- Can only contain letters, numbers, underscores, periods, hyphens, and spaces. +- Must not start or end with special chars, and must not have two subsequent special chars. + +**TXHOST_PROVIDER_LOGO** +- **Default value:** _undefined_ +- The URL for the hosting provider logo which will appear at the login page. +- Maximum image size is **224x96**. +- You can create a theme-aware URL by including a `{theme}` placeholder, which will be replaced by `light` or `dark`. +- Example: `https://.../logo_{theme}.png` + +## Deployer Defaults + +These variables are used only for auto-filling the config steps when deploying a new server: + +**TXHOST_DEFAULT_DBHOST, TXHOST_DEFAULT_DBPORT, TXHOST_DEFAULT_DBUSER, TXHOST_DEFAULT_DBPASS, TXHOST_DEFAULT_DBNAME** +- **Default value:** _undefined_ +- Used for auto-filling database configuration during deployment. +- All values are considered strings, and no validation is done. +- Can be overwritten during manual deployment or after by modifying `server.cfg`. + +**TXHOST_DEFAULT_CFXKEY** +- **Default value:** _undefined_ +- Used for auto-filling the Cfx.re key during deployment. +- Should be a `cfxk_xxxxxxxxxxxxxxxxxxxxx_xxxxxx` key from the [Cfx.re Portal](https://portal.cfx.re/). +- Very useful for developers who need to go through txAdmin Setup & Deployer frequently. + +**TXHOST_DEFAULT_ACCOUNT** +- **Default value:** _undefined_ +- Used by GSPs for setting up an `admins.json` automatically on first boot. +- Format: `username:fivemId:bcryptHash` (separated by colons) + - **Username:** Must match the FiveM account username if FiveM ID is provided + - **FiveM ID:** Numeric ID of a FiveM account (e.g., `271816` for `fivem:271816`) + - **Password:** Bcrypt-hashed password as "backup password" +- The account must have at least either FiveM ID or password set. +- Examples: + - `tabarra:271816` + - `tabarra:271816:$2a$11$K3HwDzkoUfhU6.W.tScfhOLEtR5uNc9qpQ685emtERx3dZ7fmgXCy` + - `tabarra::$2a$11$K3HwDzkoUfhU6.W.tScfhOLEtR5uNc9qpQ685emtERx3dZ7fmgXCy` + +## Examples + +### Windows Development Server with env.bat + +```batch +@REM Deployer defaults +set TXHOST_DEFAULT_CFXKEY=cfxk_11hIT156dX0F0ekFVsuda_fQ0ZYS +set TXHOST_DEFAULT_DBHOST=127.0.0.1 +set TXHOST_DEFAULT_DBPORT=3306 +set TXHOST_DEFAULT_DBUSER=root +set TXHOST_DEFAULT_DBPASS=4b6c3_1919_ab04df6 +set TXHOST_DEFAULT_DBNAME=coolrp_dev + +@REM Prevent conflicting with main server +set TXHOST_DATA_PATH=C:/test-server/txData +set TXHOST_FXS_PORT=30125 +set TXHOST_MAX_SLOTS=8 +``` + +### Docker GSP Configuration + +```dotenv +# So txAdmin suggests the right path during setup +TXHOST_DATA_PATH=/home/container + +# Deployer defaults +TXHOST_DEFAULT_DBHOST=123.123.123.123 +TXHOST_DEFAULT_DBPORT=3306 +TXHOST_DEFAULT_DBUSER=u538241 +TXHOST_DEFAULT_DBPASS=4b6c3_1919_ab04df6 +TXHOST_DEFAULT_DBNAME=db538241 + +# Customer FiveM-linked account +TXHOST_DEFAULT_ACCOUNT=tabarra:271816 + +# Provider details +TXHOST_PROVIDER_NAME=ExampleHosting +TXHOST_PROVIDER_LOGO=https://github.com/citizenfx/txAdmin/raw/master/docs/banner.png +``` + +### Migrating from Old Config + +```diff + @echo off ++set TXHOST_DATA_PATH=E:\FiveM\txData-dev ++set TXHOST_TXA_PORT=40125 +-FXServer.exe +set serverProfile "server2" +set txAdminPort "40125" ++FXServer.exe + pause +``` diff --git a/content/docs/txadmin/linux/install.mdx b/content/docs/txadmin/linux/install.mdx new file mode 100644 index 0000000..1c01a9f --- /dev/null +++ b/content/docs/txadmin/linux/install.mdx @@ -0,0 +1,10 @@ +--- +title: Install and Setup +description: Complete guide to installing txAdmin on Linux. +--- + +import { InfoBanner } from '@ui/components/mdx-components'; + + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/linux/meta.json b/content/docs/txadmin/linux/meta.json index 82ab099..93611a8 100644 --- a/content/docs/txadmin/linux/meta.json +++ b/content/docs/txadmin/linux/meta.json @@ -1,4 +1,6 @@ { - "open": true, - "pages": [] + "defaultOpen": true, + "pages": [ + "install" + ] } \ No newline at end of file diff --git a/content/docs/txadmin/logs.mdx b/content/docs/txadmin/logs.mdx new file mode 100644 index 0000000..e52bdc4 --- /dev/null +++ b/content/docs/txadmin/logs.mdx @@ -0,0 +1,104 @@ +--- +title: "Logging" +description: "Understand txAdmin's persistent logging system with file rotation." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard } from "@ui/components"; + +# Logging + +In version v4.6.0, **txAdmin** added support for persistent logging with file rotation, meaning you will have an organized folder (`txData//logs/`) containing your log files up to a maximum size and number of days. + +> **Note:** Player warn/ban/whitelist actions are not just stored in the Admin Logs, but also on the players database. + +## Log Types + +### Admin Logs + +Contains log of administrative actions as well as some automated ones like server restarts, bans, warns, settings change, and live console input. + +- **Recent Buffer:** None. Methods will read the entire file. +- **Interval:** 7d +- **maxFiles:** false +- **maxSize:** false +- **Note:** Does not log the user IP unless from an authentication endpoint. + +### FXServer Console Log + +Contains the log of everything that happens in the FXServer console (`stdin`, `stdout`, `stderr`). Any live console input is prefixed with `> `. + +- **Recent Buffer:** 64~128kb +- **Interval:** 1d +- **maxFiles:** 7 +- **maxSize:** 5G + +### Server Logs + +Contains all actions that happen inside the server, such as player join/leave/die, chat messages, explosions, menu events, and commands. Player sources are kept in the format `[mutex#id] name` where the mutex is an identifier of that server execution. If you search the file for a `[mutex#id]`, the first result will be the player join with all his identifiers available. + +- **Recent Buffer:** 32k events +- **Interval:** 1d +- **maxFiles:** 7 +- **maxSize:** 10G + +## Configuring Log Rotation + +The log rotation can be configured, so you can choose to store more or less logs according to your needs. + +To configure it, edit your `txData//config.json` and add an object inside `logger` with the key being one of `[admin, fxserver, server]`. Then add option keys according with the [rotating-file-stream library reference](https://github.com/iccicci/rotating-file-stream#options). + +### Example: Custom FXServer Log Rotation + +```json +{ + "logger": { + "fxserver": { + "interval": "1d", + "maxSize": "2G", + "maxFiles": 14 + } + } +} +``` + +### Example: Disable Server Logs + +To completely disable one of the log types, set its value to `false`. + +```json +{ + "logger": { + "server": false + } +} +``` + +### Available Options + +The rotating-file-stream library supports various options: + +- **interval** - Rotation interval (e.g., `1d`, `7d`, `30d`) +- **maxSize** - Maximum size of rotated files to keep (e.g., `2G`, `500M`) +- **maxFiles** - Maximum number of rotated files to keep (e.g., `14`, `30`) +- **maxDays** - Maximum number of days to keep files +- **rotate** - Maximum number of files to keep in the main directory + +Refer to the [library documentation](https://github.com/iccicci/rotating-file-stream#options) for a complete list of available options. + +## Accessing Logs + +All logs are stored in the `txData//logs/` directory with the following structure: + +``` +logs/ +├── admin.log +├── fxserver.log +├── server.log +└── [rotated files with timestamps] +``` + +You can access these logs directly from the file system or through the txAdmin web panel under: +- **Admin Logs:** `txAdmin > Logs > Admin Logs` +- **FXServer Console:** `txAdmin > Logs > FXServer Console` +- **Server Logs:** `txAdmin > Logs > Server Logs` diff --git a/content/docs/txadmin/menu.mdx b/content/docs/txadmin/menu.mdx new file mode 100644 index 0000000..f7aac7b --- /dev/null +++ b/content/docs/txadmin/menu.mdx @@ -0,0 +1,125 @@ +--- +title: "In-Game Menu" +description: "Learn about txAdmin's in-game menu with player management tools." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { FeatureList, PropertyCard, CommandCard } from "@ui/components"; + +# In-Game Menu + +txAdmin v4.0.0 introduced an in-game menu equipped with common admin functionality, an online player browser, and a slightly trimmed down version of the web panel. + +You can find a short preview video [here](https://www.youtube.com/watch?v=jWKg0VQK0sc). + +## Accessing the Menu + +You can access the menu in-game by using the command `/tx` or `/txadmin`, or alternatively you can also use a keybind by going to `Game Settings > Key Bindings > FiveM` and setting the `(txAdmin) Menu: Open Main Page` option. + +### Permissions + +Anybody who you would like to give permissions to open the menu in-game must have a txAdmin account with either their Discord or Cfx.re identifiers tied to it. + +**If you do not have any of these identifiers attached, you will not be able to access the menu.** + +You can further control the menu options accessible to admins by changing their permissions in the admin manager. + +## Server ConVars + +The txAdmin menu has various ConVars that can alter the default behavior of the menu. ConVars configured in the settings page should not be set manually. + +### Settings Page Options + +**txAdmin-menuEnabled** +- **Description:** Whether the menu is enabled or not. Changing it requires server restart. +- **Default:** `true` + +**txAdmin-menuAlignRight** +- **Description:** Whether to align the menu to the right of the screen instead of the left. +- **Default:** `false` + +**txAdmin-menuPageKey** +- **Description:** Will change the key used for changing pages in the menu. +- **Value:** Must be the exact browser key code. Use [keycode.info](https://keycode.info/) and check the `event.code` section. +- **Default:** `Tab` + +**txAdmin-playerModePtfx** +- **Description:** Determine whether to play particles effects and sound whenever an admin's player mode is changed (god mode, noclip, etc). +- **Default:** `true` + +**txAdmin-hideAdminInPunishments** +- **Description:** Never show to the players the admin name on Bans or Warns. +- **Default:** `true` + +**txAdmin-hideAdminInMessages** +- **Description:** Do not show the admin name on Announcements or DMs. +- **Default:** `false` + +**txAdmin-hideDefaultAnnouncement** +- **Description:** Suppresses the display of announcements, allowing you to implement your own via the event `txAdmin:events:announcement`. +- **Default:** `false` + +**txAdmin-hideDefaultDirectMessage** +- **Description:** Suppresses the display of direct messages, allowing you to implement your own via the event `txAdmin:events:playerDirectMessage`. +- **Default:** `false` + +**txAdmin-hideDefaultWarning** +- **Description:** Suppresses the display of warnings, allowing you to implement your own via the event `txAdmin:events:playerWarned`. +- **Default:** `false` + +**txAdmin-hideDefaultScheduledRestartWarning** +- **Description:** Suppresses the display of scheduled restart warnings, allowing you to implement your own via the event `txAdmin:events:scheduledRestart`. +- **Default:** `false` + +### ConVar Only (Not in Settings Page) + +**txAdmin-debugMode** +- **Description:** Will toggle debug printing on the server and client. +- **Default:** `false` +- **Usage:** `setr txAdmin-debugMode true` + +**txAdmin-menuPlayerIdDistance** +- **Description:** The distance in which Player IDs become visible, if toggled on. Game engine limits tag display to ~300m. +- **Default:** `150` +- **Usage:** `setr txAdmin-menuPlayerIdDistance 100` + +**txAdmin-menuDrunkDuration** +- **Description:** How many seconds the drunk effect (troll action) should last. +- **Default:** `30` +- **Usage:** `setr txAdmin-menuDrunkDuration 120` + +**txAdmin-menuAnnounceNotiPos** +- **Description:** Determines the location of the txAdmin announcement notification. +- **Valid Positions:** `top-center`, `top-left`, `top-right`, `bottom-center`, `bottom-left`, `bottom-right` +- **Default:** `top-center` +- **Usage:** `set txAdmin-menuAnnounceNotiPos top-right` + +## Commands + +**tx | txadmin** +- **Description:** Will toggle the in-game menu. Optional argument to open a specific player's info. +- **Usage:** `/tx (playerID)`, `/txadmin (playerID)` +- **Required Permission:** Must be an admin registered in the Admin Manager + +**txAdmin-reauth** +- **Description:** Will retrigger the re-authentication process. +- **Usage:** `/txAdmin-reauth` +- **Required Permission:** None + +## Troubleshooting Menu Access + +- **If you type `/tx` and nothing happens:** Your menu is probably disabled. + +- **If you see a red message and are registered on txAdmin:** You can type `/txAdmin-reauth` in the chat to retry the authentication. + +- **If you can't authenticate with error "Invalid Request: source":** This means the source IP of the HTTP request made by FXServer to txAdmin is not a "localhost" one, which might occur if your host has multiple IPs. To disable this protection, edit your `config.json` file and add `webServer.disableNuiSourceCheck` with value `true` then restart txAdmin. + +## Development + +You can find development instructions regarding the menu [here](./development#nui-menu). + +## FAQ + +**Q: Why don't the 'Heal' options revive a player when using ESX/QBCore/etc?** + +**A:** Many frameworks independently handle a "dead" state for a player, meaning the menu is unable to reset this state in a framework-agnostic form. To establish compatibility with any framework, txAdmin will emit an [`txAdmin:events:playerHealed`](./api-events) event for developers to handle. diff --git a/content/docs/txadmin/meta.json b/content/docs/txadmin/meta.json index 7ffe89b..7d9f2f8 100644 --- a/content/docs/txadmin/meta.json +++ b/content/docs/txadmin/meta.json @@ -1,13 +1,24 @@ { "root": true, "pages": [ - "windows", "advanced", + "api-events", "backup-system", "configuration", + "custom-server-log", + "development", + "discord-bot", + "discord-status", + "env-config", + "logs", + "menu", + "palettes", "permissions", + "recipe", "server-management", + "translation", "troubleshooting", - "web-panel" + "windows", + "linux" ] } \ No newline at end of file diff --git a/content/docs/txadmin/palettes.mdx b/content/docs/txadmin/palettes.mdx new file mode 100644 index 0000000..b96374a --- /dev/null +++ b/content/docs/txadmin/palettes.mdx @@ -0,0 +1,107 @@ +--- +title: "Palettes Configuration" +description: "Color palettes and theming options for txAdmin." +--- + +import { FeatureList } from "@ui/components"; + +# Color Palettes + +txAdmin supports multiple color palette configurations for theming the interface. Palettes define the colors used for semantic states, backgrounds, and interactive elements. + +## Available Palettes + +### Semantic Palettes + +These palettes define semantic colors for different states and purposes: + +- **Dark Semantic** - Color scheme for dark mode semantic states + - Accent, Danger, Warning, Success, Info colors + +- **Light Semantic** - Color scheme for light mode semantic states + - Accent, Danger, Warning, Success, Info colors + +- **FiveM Semantic** - FiveM-specific semantic colors + - Primary, Error, Warning, Success, Info colors + +### Background Palettes + +These palettes define background, card, and surface colors: + +- **Dark Backgrounds** - Dark mode background colors + - New background, card, border, muted, secondary, and primary colors + +- **Light Backgrounds** - Light mode background colors + - New background, card, border, muted, secondary, and primary colors + +- **Dark v2** - Updated dark mode palette + - Background, card, border, muted, secondary, and primary variations + +- **Light v2** - Updated light mode palette + - Background, card, border, muted, secondary, and primary variations + +### FiveM Palettes + +- **FiveM Light** - Light theme based on FiveM branding +- **FiveM Dark** - Dark theme based on FiveM branding + +## Palette Structure + +Each palette contains a set of color swatches with the following structure: + +```json +{ + "paletteName": "palette-name", + "swatches": [ + { + "name": "color-name", + "color": "HEXCODE" + } + ] +} +``` + +## Default Colors + +### Semantic Colors + +| Color | Dark Hex | Light Hex | Purpose | +|-------|----------|-----------|---------| +| Accent | F50551 | F50551 | Primary action color | +| Danger | F86565 | EF4141 | Danger/destructive actions | +| Warning | E8C957 | E6C13A | Warning messages | +| Success | 51E47A | 39E669 | Success messages | +| Info | 5AC8E1 | 39C6E6 | Info messages | + +### Background Colors (Dark Mode) + +| Element | Color | +|---------|-------| +| New Background | 171516 | +| Card | 1F1C1E | +| Border | 322D31 | +| Muted | 2A2629 | +| Secondary | 463F44 | +| Primary Text | F9F3F8 | + +### Background Colors (Light Mode) + +| Element | Color | +|---------|-------| +| New Background | F9F4F8 | +| Card | EFEAEE | +| Border | D6D2D5 | +| Muted | DCD8DB | +| Secondary | B7B3B6 | +| Primary Text | 1B161A | + +## Customizing Palettes + +To customize the color palette for your txAdmin installation: + +1. Navigate to **txAdmin > Settings > Interface** +2. Select your preferred palette from the available options +3. The interface will update in real-time with the new colors +4. Changes are persisted in your `config.json` + +For advanced customization, you can directly edit the palette JSON in the configuration file or use the web interface color picker tools. diff --git a/content/docs/txadmin/permissions.mdx b/content/docs/txadmin/permissions.mdx index 864daca..6f4380c 100644 --- a/content/docs/txadmin/permissions.mdx +++ b/content/docs/txadmin/permissions.mdx @@ -3,401 +3,8 @@ title: Permissions System description: Configure user roles, permissions, and access control for your txAdmin server management team. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This guide covers txAdmin's comprehensive permission system, allowing you to create custom roles and manage access control for your administrative team. - -## Understanding Permissions - -### Permission Levels - -txAdmin uses a role-based permission system with several predefined levels: - - - - - - - - - -## Setting Up Admin Accounts - -### Creating New Administrators - -#### Through Web Interface -1. Navigate to **Settings → Admins** -2. Click **Add New Admin** -3. Fill in account details: - ``` - Username: admin_username - Password: secure_password_123! - Display Name: Administrator Name - Role: Select appropriate role - ``` - -#### Account Configuration Options -- **Username:** Unique identifier for login -- **Display Name:** Name shown in logs and interface -- **Email:** For notifications and password reset -- **Discord ID:** Link Discord account for notifications -- **Two-Factor Auth:** Enable additional security - -### Permission Assignment - -#### Role Selection -Choose appropriate role based on responsibilities: - -**Server Owner/Lead Admin:** -- Assign **Master Admin** role -- Full access to all features -- Can manage other administrators - -**Technical Administrators:** -- Assign **Admin** role -- Server management and configuration -- Resource installation and updates - -**Community Moderators:** -- Assign **Moderator** role -- Player management and moderation -- Chat supervision and warnings - -**Support Staff:** -- Assign **View Only** role -- Access to help players -- Cannot make administrative changes - -## Custom Roles - -### Creating Custom Roles - -For advanced permission management, create custom roles: - - -```json -{ - "role_name": "custom_moderator", - "display_name": "Community Moderator", - "description": "Enhanced moderator with limited admin access", - "permissions": { - "server": { - "view_status": true, - "restart": false, - "console_access": true - }, - "players": { - "view_list": true, - "kick": true, - "ban_temporary": true, - "ban_permanent": false, - "warn": true, - "whitelist_manage": false - }, - "resources": { - "view_list": true, - "start_stop": false, - "install": false, - "configure": false - } - } -} -``` - - -### Permission Categories - - - - - - - - - -## Advanced Permission Management - -### Time-Based Permissions -Set up temporary permission changes: - -#### Temporary Promotions -```json -{ - "temp_permissions": { - "user_id": "admin_123", - "elevated_role": "admin", - "duration": "24h", - "reason": "Covering for absent admin", - "granted_by": "master_admin", - "auto_revert": true - } -} -``` - -#### Scheduled Access -Configure permissions that activate at specific times: - -```json -{ - "scheduled_access": { - "user_id": "night_moderator", - "schedule": "0 22 * * *", - "duration": "8h", - "permissions": ["enhanced_moderation"], - "timezone": "UTC" - } -} -``` - -### IP-Based Restrictions -Add additional security with IP restrictions: - -#### Admin IP Whitelisting -```json -{ - "ip_restrictions": { - "admin_123": { - "allowed_ips": [ - "192.168.1.100", - "203.0.113.0/24" - ], - "deny_other_ips": true, - "notify_on_violation": true - } - } -} -``` - -#### Location-Based Access -- **Office Network:** Full access from office IPs -- **Home Network:** Limited access from home -- **Mobile/Public:** View-only access for security -- **VPN Required:** Require VPN for external access - -## Security Features - -### Two-Factor Authentication - -#### Enabling 2FA -1. Go to **Settings → Security** -2. Enable **Two-Factor Authentication** -3. Scan QR code with authenticator app -4. Enter verification code to confirm - -#### 2FA Management -- **Recovery Codes:** Generate backup codes -- **App Selection:** Google Authenticator, Authy, etc. -- **Mandatory 2FA:** Require for all admin accounts -- **Grace Period:** Time before 2FA enforcement - -### Session Management - -#### Session Security -Configure secure session handling: - -```json -{ - "session_config": { - "timeout": 3600, - "max_concurrent": 2, - "secure_cookies": true, - "ip_binding": true, - "activity_timeout": 1800 - } -} -``` - -#### Login Monitoring -Track administrative access: - -- **Login Attempts:** Monitor failed login attempts -- **Session History:** Track active and past sessions -- **Location Tracking:** Monitor login locations -- **Device Tracking:** Identify login devices -- **Suspicious Activity:** Alert on unusual patterns - -### Audit Logging - -#### Administrative Actions -All administrative actions are logged: - -```json -{ - "audit_log": { - "timestamp": "2024-01-15T10:30:00Z", - "admin_id": "admin_123", - "action": "player_ban", - "target": "player_456", - "details": { - "reason": "Griefing", - "duration": "7d", - "ip_address": "192.168.1.100" - } - } -} -``` - -#### Log Categories -- **User Management:** Admin account changes -- **Permission Changes:** Role and permission modifications -- **Server Actions:** Server control operations -- **Player Actions:** Moderation activities -- **System Changes:** Configuration modifications - -## Permission Templates - -### Pre-configured Templates -Quick setup for common scenarios: - -#### Roleplay Server Template -```json -{ - "roles": { - "server_owner": { - "permissions": ["all"], - "description": "Server owner with full access" - }, - "head_admin": { - "permissions": ["server_management", "admin_management", "full_moderation"], - "description": "Senior administrator role" - }, - "roleplay_admin": { - "permissions": ["player_management", "resource_basic", "whitelist_control"], - "description": "Roleplay focused administrator" - }, - "chat_moderator": { - "permissions": ["player_view", "chat_moderation", "basic_kicks"], - "description": "Chat and community moderator" - } - } -} -``` - -#### Development Server Template -```json -{ - "roles": { - "lead_developer": { - "permissions": ["all"], - "description": "Lead developer with full access" - }, - "developer": { - "permissions": ["resource_full", "server_restart", "console_access"], - "description": "Resource developer role" - }, - "tester": { - "permissions": ["resource_view", "player_basic", "console_view"], - "description": "Testing and QA role" - } - } -} -``` - -## Best Practices - -### Permission Assignment Guidelines - -#### Principle of Least Privilege -- **Minimum Required:** Grant only necessary permissions -- **Role-Based:** Use roles rather than individual permissions -- **Regular Review:** Periodically audit permission assignments -- **Temporary Access:** Use time-limited permissions when possible - -#### Security Recommendations -- **Strong Passwords:** Enforce password complexity requirements -- **2FA Required:** Make two-factor authentication mandatory -- **IP Restrictions:** Limit access to known IP addresses -- **Session Monitoring:** Track and alert on unusual activity - -### Common Permission Scenarios - -#### Community Server Setup -1. **Server Owner:** Master admin with full control -2. **Community Managers:** Admin role for daily operations -3. **Moderators:** Limited moderation permissions -4. **Supporters:** View-only access for helping players - -#### Development Team Structure -1. **Project Lead:** Master admin for project oversight -2. **Senior Developers:** Full resource and server control -3. **Junior Developers:** Limited resource permissions -4. **QA Testers:** Testing-focused permissions - ---- - - -**Security Tip:** Regularly review and audit admin permissions to maintain server security. - - - -**Important:** Never share admin credentials. Create individual accounts for each team member. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/recipe.mdx b/content/docs/txadmin/recipe.mdx new file mode 100644 index 0000000..6b7f340 --- /dev/null +++ b/content/docs/txadmin/recipe.mdx @@ -0,0 +1,304 @@ +--- +title: "Recipe Files" +description: "Learn about server deployment recipes for txAdmin." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, PropertyCard, CommandCard } from "@ui/components"; + +# Recipe Files + +A Recipe is a YAML document that describes how to deploy a server properly: from downloading resources, to configuring the `server.cfg` file. + +You can run a recipe from txAdmin's built-in Server Deployer. Recipes will be "jailed" to the target folder, so for example they won't be able to execute `write_file` to your `admins.json`. + +At the end of the deployment process, your target folder will be checked for the presence of a `server.cfg` and a `resources` folder to make sure everything went correctly. + +On the setup page you will be able to import a recipe via its URL or by selecting one of the recommended ones from the community. + +## Meta Data + +The recipe accepts the following default meta data: + +### Engine Specific Metadata *(optional)* + +- `$engine` - The recipe's target engine version +- `$minFxVersion` - The minimum required FXserver version for this recipe +- `$onesync` - The required onesync value to be set after deployment. Supports only `off`, `legacy`, `on` +- `$steamRequired` - Boolean declaring that the `steam_webApiKey` context variable MUST be set + +### General Tags *(strongly-recommended)* + +- `name` - The short name for your recipe. Recommended to be under 24 characters +- `version` - The version of your recipe +- `author` - The short name of the author. Recommended to be under 24 characters +- `description` - A single or multiline description. Recommended to be under 256 characters. On YAML you can use multiline strings in many ways, check [yaml-multiline.info](https://yaml-multiline.info) + +## Context Variables + +The deployer has a shared context between tasks, initially populated by the `variables` and the deployer step 2 (user input) for things like database configuration and string replacements. + +### Default Variables + +- `deploymentID` - Composed by the shortened recipe name with a hex timestamp (e.g., `PlumeESX_BBC957`) +- `serverName` - The name of the server specified in the setup page +- `recipeName`, `recipeAuthor`, `recipeVersion`, `recipeDescription` - Populated from recipe metadata if available +- `dbHost`, `dbPort`, `dbUsername`, `dbPassword`, `dbName`, `dbDelete`, `dbConnectionString` - Populated from database configuration +- `svLicense` - Required variable, inputted in deployer step 2. Will automatically replace `{{svLicense}}` in `server.cfg` +- `serverEndpoints` - The deployer will set this with endpoint_add_xxxx for the server +- `maxClients` - The number of max clients. Defaults to 48 or uses `TXHOST_MAX_SLOTS` variable + +### Custom Variables + +You can set custom variables in the recipe: + +```yaml +variables: + aaa: bbbb + ccc: dddd +``` + +## Tasks + +Tasks/actions are executed sequentially, and any failure in the chain stops the process. + +> **Attention:** Careful with the number of spaces used in the indentation. + +Every task can contain a `timeoutSeconds` option to increase its default value. + +### download_github + +Downloads a GitHub repository with an optional reference (branch, tag, commit hash) or subpath. If the directory structure does not exist, it is created. + +- `src` - The repository to be downloaded. Can be a URL or `repo_owner/repo_name` +- `ref` *(optional)* - Git reference (branch, tag, or commit hash). If none is set, queries GitHub API for default branch +- `subpath` *(optional)* - When specified, copies a subpath of the repository +- `dest` - The destination path for the downloaded file + +> **Note:** If you have more than 30 of this action, it is recommended to set the ref to avoid download errors. + +#### Examples + +```yaml +# Example with subpath and reference +- action: download_github + src: https://github.com/citizenfx/cfx-server-data + ref: 6eaa3525a6858a83546dc9c4ce621e59eae7085c + subpath: resources + dest: ./resources + +# Simple example +- action: download_github + src: esx-framework/es_extended + dest: ./resources/[esx]/es_extended +``` + +### download_file + +Downloads a file to a specific path. + +- `url` - The URL of the file +- `path` - The destination path for the downloaded file (must be a file name, not a path) + +```yaml +- action: download_file + url: https://github.com/citizenfx/cfx-server-data/archive/master.zip + path: ./tmp/cfx-server-data.zip +``` + +### unzip + +Extracts a ZIP file to a target folder. This will not work for tar files. + +- `src` - The source path +- `dest` - The destination path + +```yaml +- action: unzip + src: ./tmp/cfx-server-data.zip + dest: ./tmp +``` + +### move_path + +Moves a file or directory. The directory can have contents. This is an implementation of [fs-extra.move()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/move.md). + +- `src` - The source path (can be file or folder, cannot be root `./`) +- `dest` - The destination path (cannot be root `./`) +- `overwrite` *(optional, boolean)* - When true, replaces destination if it exists + +```yaml +- action: move_path + src: ./tmp/cfx-server-data-master/resources + dest: ./resources + overwrite: true +``` + +### copy_path + +Copy a file or directory. The directory can have contents. This is an implementation of [fs-extra.copy()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/copy.md). + +- `src` - The source path. If `src` is a directory, copies everything inside, not the directory itself +- `dest` - The destination path. If `src` is a file, `dest` cannot be a directory +- `overwrite` *(optional, boolean)* - When true, overwrite existing file or directory. Default is `true` +- `errorOnExist` *(optional, boolean)* - When overwrite is `false` and destination exists, throw an error. Default is `false` + +```yaml +- action: copy_path + src: ./tmp/cfx-server-data-master/resources/ + dest: ./resources +``` + +### remove_path + +Removes a file or directory. The directory can have contents. If the path does not exist, silently does nothing. This is an implementation of [fs-extra.remove()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/remove.md). + +- `path` - The path to be removed (cannot be root `./`) + +```yaml +- action: remove_path + path: ./tmp +``` + +### ensure_dir + +Ensures that the directory exists. If the directory structure does not exist, it is created. This is an implementation of [fs-extra.ensureDir()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/ensureDir.md). + +- `path` - The path to be created (cannot be root `./`) + +```yaml +- action: ensure_dir + path: ./resources +``` + +### write_file + +Writes or appends data to a file. If not in append mode, the file will be overwritten and the directory structure will be created if it doesn't exist. This is an implementation of [fs-extra.outputFile()](https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/outputFile.md) and Node's default `fs.appendFile()`. + +- `file` - The path of the file to be written to +- `append` *(optional, boolean)* - When true, appends to the end of the file instead of overwriting +- `data` - The data to be written to the target path + +```yaml +# Append example +- action: write_file + file: ./server.cfg + append: true + data: | + ensure example1 + ensure example2 + +# Write file example +- action: write_file + file: ./doesntexist/config.json + data: | + { + "someVariable": true, + "heyLookAnArray": [123, 456] + } +``` + +### replace_string + +Replaces a string in the target file or files array based on a search string and/or context variables. + +- `file` - String or array containing the file(s) to be checked +- `mode` *(optional)* - Specify the behavior of the replacer + - `template` *(default)* - The `replace` string option processed for context variables in the `{{varName}}` format + - `all_vars` - All variables (`{{varName}}`) will be replaced in the target file + - `literal` - Normal string search/replace without any vars +- `search` - The string to be searched for +- `replace` - The string that will replace the `search` one + +```yaml +# Single file - template mode is implicit +- action: replace_string + file: ./server.cfg + search: 'FXServer, but unconfigured' + replace: '{{serverName}} built with {{recipeName}} by {{recipeAuthor}}!' + +# Multiple files +- action: replace_string + mode: all_vars + file: + - ./resources/blah.cfg + - ./something/config.json + +# Replace all variables +- action: replace_string + file: ./configs.cfg + search: 'omg_replace_this' + replace: 'got_it!' +``` + +### connect_database + +Connects to a MySQL/MariaDB server and creates a database if the dbName variable is null. You need to execute this action before the `query_database` to prepare the deployer context. + +This action does not have any direct attributes attached to it. Instead it uses Context Variables set in the deployer step 2 (user input). + +```yaml +- action: connect_database +``` + +### query_database + +Runs a SQL query in the previously connected database. This query can be a file path **OR** a string, but not both at the same time! You need to execute the `connect_database` before this action. + +- `file` - The path of the SQL file to be executed +- `query` - The query string to be executed + +```yaml +# Running a query from a file +- action: query_database + file: ./tmp/create_tables.sql + +# Running a query from a string +- action: query_database + query: | + CREATE TABLE IF NOT EXISTS `users` ( + `id` int(10) unsigned NOT NULL, + `name` tinytext NOT NULL + ); + INSERT INTO `users` (`name`) VALUES ('tabarra'); +``` + +### load_vars + +Loads variables from a JSON file to the deployer context. + +- `src` - The path of the JSON file to be loaded + +```yaml +- action: load_vars + src: ./toload.json +``` + +## Example Recipe + +```yaml +name: PlumeESX2 +version: v1.2.3 +author: Toybarra +description: A full featured (8 jobs) and highly configurable yet lightweight ESX v2 base that can be easily extendable. + +variables: + dbHost: localhost + dbUsername: root + dbPassword: "" + dbName: null + +tasks: + - action: download_file + url: https://github.com/citizenfx/cfx-server-data/archive/master.zip + path: ./tmp/cfx-server-data.zip + + - action: unzip + src: ./tmp/cfx-server-data.zip + dest: ./tmp + + - action: move_path + src: ./tmp/cfx-server-data-master/resources + dest: ./resources + overwrite: true +``` diff --git a/content/docs/txadmin/server-management.mdx b/content/docs/txadmin/server-management.mdx index 961b97e..47fb784 100644 --- a/content/docs/txadmin/server-management.mdx +++ b/content/docs/txadmin/server-management.mdx @@ -7,7 +7,7 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; import { TypeTable, SimpleTypeTable } from '@ui/components/type-table'; import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable, RoleCard, PermissionCodeBlock, KeyboardShortcutTable, ActionTable, InfoBanner, CategoryGrid } from '@ui/components/mdx-components'; -import { ImageModal } from '@/app/components/image-modal'; +import { ImageModal } from '@ui/components/image-modal'; ## Dashboard diff --git a/content/docs/txadmin/translation.mdx b/content/docs/txadmin/translation.mdx new file mode 100644 index 0000000..700d7db --- /dev/null +++ b/content/docs/txadmin/translation.mdx @@ -0,0 +1,122 @@ +--- +title: "Translation Support" +description: "Contribute translations to txAdmin or create custom locales." +--- + +import { AlertTriangle, Info } from "lucide-react"; +import { StepList, FeatureList } from "@ui/components"; + +# Translation Support + +txAdmin supports translation for over 30 languages for the in-game interface (menu/warn) and chat messages, as well as Discord warnings. + +## Custom Locales + +If your language is not available, or you want to customize the messages, you can create a `locale.json` file inside the `txData` folder based on any language file found on [our repository](https://github.com/citizenfx/txAdmin/tree/master/locale). Then go to the settings and select the "Custom" language option. + +The `$meta.humanizer_language` key must be compatible with the library [humanize-duration](https://www.npmjs.com/package/humanize-duration), check their page for a list of compatible languages. + +### Creating a Custom Locale + + + +## Contributing Translations + +We need the community help to translate, and keep the translations updated and high-quality. + +To contribute, you will need to: + +1. **Make a custom locale file** with the instructions above +2. **Name the file** using the language code from [this page](https://www.science.co.il/language/Locale-codes.php) +3. **Set the $meta.label** to the language name in English (e.g., `Spanish` instead of `Español`) +4. **Add to shared/localeMap.ts** if creating a new translation, maintaining alphabetical order +5. **Submit a Pull Request** with a few screenshots showing you tested the changes in-game +6. **Automatic checks** will run - read the output in case of any errors + +### Translation Checklist + +- [ ] Language code is correct from locale codes list +- [ ] `$meta.label` is in English +- [ ] Added to `shared/localeMap.ts` in alphabetical order +- [ ] All keys translated (none left in English) +- [ ] Screenshots of in-game testing attached +- [ ] Pull request description clearly states what language was translated + +
+ +
+

Quick Testing Tip

+

To quickly test your changes, edit the `locale.json` file and then in the settings page click "Save Global Settings" again to see the changes in the game menu without needing to restart txAdmin or the server.

+
+
+ +### Validate Your Translation + +To make sure you didn't miss anything in the locale file, you can: + +1. Download the txAdmin source code +2. Execute `npm i` +3. Move your `locale.json` to inside the `txAdmin/locale` folder +4. Run `npm run locale:check` to validate completeness + +This will tell you about missing or extra keys. + +
+ +
+

Performance Note

+

The performance of custom locale files for big servers may not be ideal due to the way we need to sync dynamic content to clients. So it is strongly encouraged that you contribute with translations in our GitHub so they get packed with the rest of txAdmin.

+
+
+ +## Submitting to Community + +Once your translation is complete and tested: + +1. Fork the [txAdmin repository](https://github.com/citizenfx/txAdmin) +2. Add your language file to the `locale` folder +3. Update `shared/localeMap.ts` with your language +4. Submit a Pull Request with evidence of testing +5. Wait for review and merge + +## Language Support + +Currently supported languages include: +- English (en) +- Spanish (es) +- French (fr) +- German (de) +- Italian (it) +- Portuguese (pt) +- Russian (ru) +- Chinese (zh) +- Japanese (ja) +- Korean (ko) +- And 20+ more... + +Check the [locale folder](https://github.com/citizenfx/txAdmin/tree/master/locale) for a complete list. + +## Translation Resources + +- **Language Codes:** [ISO 639 Language Codes](https://www.science.co.il/language/Locale-codes.php) +- **Duration Formatting:** [humanize-duration Library](https://www.npmjs.com/package/humanize-duration) +- **txAdmin Locale Files:** [GitHub Locale Folder](https://github.com/citizenfx/txAdmin/tree/master/locale) diff --git a/content/docs/txadmin/troubleshooting.mdx b/content/docs/txadmin/troubleshooting.mdx index c16ee1c..f096300 100644 --- a/content/docs/txadmin/troubleshooting.mdx +++ b/content/docs/txadmin/troubleshooting.mdx @@ -3,590 +3,8 @@ title: Troubleshooting description: Diagnose and resolve common txAdmin issues, server problems, and configuration challenges. --- -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' +import { InfoBanner } from '@ui/components/mdx-components'; -This comprehensive troubleshooting guide helps you identify and resolve common issues with txAdmin, server management, and configuration problems. - -## Common Issues - -### Installation Problems - -#### txAdmin Won't Start - - - -**Port Conflict:** -```bash -# Check what's using the port -netstat -tulpn | grep :40120 - -# Kill conflicting process -sudo kill -9 - -# Or change txAdmin port -export TXADMIN_PORT=40121 -``` - -**Permission Issues (Linux):** -```bash -# Fix file permissions -chmod -R 755 /opt/fivem-server/ -chown -R $(whoami):$(whoami) /opt/fivem-server/ - -# Check SELinux (if applicable) -setsebool -P httpd_can_network_connect 1 -``` - -**Missing Dependencies:** -```bash -# Update Node.js -curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - -sudo apt-get install -y nodejs - -# Verify installation -node --version -npm --version -``` - -#### Web Interface Loading Issues -**Symptoms:** -- Blank page or loading errors -- CSS/JavaScript not loading -- Authentication problems - -**Solutions:** - -**Clear Browser Cache:** -1. Press Ctrl+Shift+Delete -2. Select "All time" as time range -3. Clear cookies, cache, and site data -4. Restart browser - -**Check Network Configuration:** -```bash -# Test local connection -curl http://localhost:40120 - -# Check firewall settings -sudo ufw status -sudo iptables -L - -# Open required ports -sudo ufw allow 40120/tcp -``` - -**HTTPS Configuration Issues:** -```bash -# Verify SSL certificate -openssl x509 -in /path/to/cert.pem -text -noout - -# Check certificate expiry -openssl x509 -in /path/to/cert.pem -noout -dates - -# Generate new self-signed certificate -openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -``` - -### Server Management Issues - -#### Server Won't Start -**Symptoms:** -- Server fails to start from txAdmin -- Error messages in console -- Resources fail to load - -**Diagnosis:** -1. Check server.cfg syntax -2. Verify license key validity -3. Review resource dependencies -4. Check available system resources - -**Common Solutions:** - -**Invalid License Key:** -```bash -# Verify license key format -set sv_licenseKey "your_valid_license_key_here" - -# Check key status at https://keymaster.fivem.net/ -# Ensure key isn't expired or banned -``` - -**Resource Conflicts:** -```bash -# Start server in safe mode -start_minimal_resources_only - -# Test resources individually -start resource_name -stop resource_name - -# Check resource dependencies -cd resources/resource_name/ -cat fxmanifest.lua | grep dependency -``` - -**Port Conflicts:** -```bash -# Check if ports are in use -netstat -tulpn | grep :30120 - -# Change server port in server.cfg -endpoint_add_tcp "0.0.0.0:30121" -endpoint_add_udp "0.0.0.0:30121" -``` - -#### Server Crashes Frequently -**Symptoms:** -- Server stops unexpectedly -- High CPU or memory usage -- Performance degradation - -**Troubleshooting Steps:** - -**Memory Issues:** -```bash -# Monitor memory usage -free -h -top -p $(pgrep FXServer) - -# Increase server memory limit (if VPS) -# Optimize resource loading -set sv_maxClients 32 # Reduce if needed -``` - -**Resource Problems:** -```bash -# Enable resource debugging -set developer 1 - -# Monitor resource performance -profiler record 30 -profiler view - -# Identify problematic resources -``` - -**Network Issues:** -```bash -# Monitor network connections -ss -tuln | grep :30120 - -# Check bandwidth usage -iftop -i eth0 - -# Optimize network settings -set sv_maxClients 32 -set onesync on -``` - -### Player Management Problems - -#### Players Can't Connect -**Symptoms:** -- Connection timeouts -- Authentication failures -- Whitelist issues - -**Solutions:** - -**Whitelist Problems:** -1. Check if whitelist is enabled -2. Verify player identifiers are correct -3. Add player to whitelist: - ```bash - whitelist add steam:110000100000000 - whitelist add license:1234567890abcdef - ``` - -**Server Full Issues:** -```bash -# Check current player count -status - -# Increase server capacity -set sv_maxClients 48 # Increase limit - -# Enable connection queue -set txAdmin-checkPlayerJoin true -``` - -**Network Connectivity:** -```bash -# Test server accessibility -telnet your-server-ip 30120 - -# Check firewall rules -sudo ufw status verbose - -# Verify port forwarding (if behind NAT) -``` - -#### Moderation Tools Not Working -**Symptoms:** -- Commands don't execute -- Player actions fail -- Permission errors - -**Fixes:** - -**Permission Issues:** -1. Verify admin permissions in txAdmin -2. Check role assignments -3. Test with Master Admin account - -**Command Syntax:** -```bash -# Correct command formats -kick [player_id] [reason] -ban [player_id] [duration] [reason] - -# Check player ID is correct -status # List all players with IDs -``` - -### Performance Issues - -#### High CPU Usage -**Symptoms:** -- Server FPS below 50 -- High CPU percentage -- Lag and latency issues - -**Optimization Steps:** - -**Resource Optimization:** -```bash -# Profile resource usage -profiler record 60 - -# Identify CPU-heavy resources -resmon - -# Optimize heavy resources or remove them -``` - -**Server Configuration:** -```bash -# Limit concurrent players -set sv_maxClients 32 - -# Optimize tick rate -set sv_fivemTickRate 50 - -# Enable OneSync if not already enabled -set onesync on -``` - -#### Memory Leaks -**Symptoms:** -- Gradually increasing memory usage -- Server becomes unstable over time -- Need frequent restarts - -**Solutions:** - -**Memory Monitoring:** -```bash -# Monitor memory usage over time -watch -n 5 'free -h && ps aux | grep FXServer' - -# Enable memory debugging -set developer 1 -``` - -**Resource Investigation:** -```bash -# Check resource memory usage -resmon - -# Restart problematic resources -restart resource_name - -# Update or replace leaky resources -``` - -### Database Issues - -#### Database Connection Problems -**Symptoms:** -- Resources can't connect to database -- "Connection refused" errors -- Data not saving - -**Solutions:** - -**MySQL/MariaDB Issues:** -```bash -# Check if MySQL is running -sudo systemctl status mysql - -# Start MySQL service -sudo systemctl start mysql - -# Test connection -mysql -u username -p database_name -``` - -**Connection String Problems:** -```lua --- Correct connection string format -set mysql_connection_string "mysql://username:password@localhost/database?charset=utf8mb4" - --- Alternative format -set mysql_connection_string "user=root;password=yourpass;host=localhost;port=3306;database=fivem;SslMode=none;" -``` - -**Permission Issues:** -```sql --- Grant proper permissions -GRANT ALL PRIVILEGES ON database_name.* TO 'username'@'localhost'; -FLUSH PRIVILEGES; - --- Check user permissions -SHOW GRANTS FOR 'username'@'localhost'; -``` - -## Diagnostic Tools - -### Built-in Diagnostics - -#### txAdmin Health Check -Access comprehensive health diagnostics: - -1. Navigate to **System → Diagnostics** -2. Run **Health Check** -3. Review results: - - Port accessibility - - File permissions - - Resource integrity - - Database connectivity - -#### Performance Profiler -Monitor server performance: - -```bash -# Start profiling -profiler record 30 - -# View results -profiler view - -# Save profile data -profiler save profile_$(date +%Y%m%d_%H%M%S).json -``` - -#### Resource Monitor -Track resource performance: - -```bash -# View resource statistics -resmon - -# Monitor specific resource -resmon resource_name - -# Export resource data -resmon export -``` - -### External Tools - -#### System Monitoring -**Linux Tools:** -```bash -# Monitor system resources -htop -iotop -nethogs - -# Check disk usage -df -h -du -sh /opt/fivem-server/* - -# Monitor network -ss -tuln -tcpdump -i eth0 port 30120 -``` - -**Windows Tools:** -```powershell -# Performance Monitor -perfmon - -# Resource Monitor -resmon - -# Network monitoring -netstat -an | findstr :30120 -``` - -#### Database Tools -```sql --- Check database performance -SHOW PROCESSLIST; -SHOW STATUS LIKE '%connection%'; - --- Monitor slow queries -SET GLOBAL slow_query_log = 'ON'; -SET GLOBAL long_query_time = 2; - --- Optimize tables -OPTIMIZE TABLE table_name; -``` - -## Log Analysis - -### txAdmin Logs -Understanding txAdmin log messages: - -#### Log Locations -- **System Logs:** `data/logs/system.log` -- **Admin Actions:** `data/logs/admin.log` -- **Player Events:** `data/logs/players.log` -- **Error Logs:** `data/logs/errors.log` - -#### Common Error Messages -**"Port already in use":** -``` -[ERROR] Port 40120 is already in use -Solution: Change port or kill conflicting process -``` - -**"Failed to load resource":** -``` -[ERROR] Failed to load resource 'resource_name': manifest not found -Solution: Check resource files and fxmanifest.lua -``` - -**"Database connection failed":** -``` -[ERROR] MySQL connection failed: Access denied -Solution: Check credentials and database permissions -``` - -### FXServer Logs -Interpreting server console output: - -#### Resource Errors -``` -[ERROR] [resource_name] script error: attempt to index nil value -Solution: Check Lua syntax in resource scripts -``` - -#### Network Issues -``` -[WARN] Dropped client connection (timeout) -Solution: Check network stability and firewall rules -``` - -#### Performance Warnings -``` -[WARN] Server thread hitch warning: 150ms -Solution: Optimize resources or increase server resources -``` - -## Recovery Procedures - -### Emergency Recovery - -#### Server Unresponsive -When server becomes completely unresponsive: - -1. **Force Stop:** - ```bash - # Find FXServer process - ps aux | grep FXServer - - # Kill process - sudo kill -9 - ``` - -2. **Clean Start:** - ```bash - # Clear cache - rm -rf cache/* - - # Start with minimal resources - # Comment out most resources in server.cfg temporarily - ``` - -#### Corrupted Configuration -When server.cfg or other configs are corrupted: - -1. **Backup Current Config:** - ```bash - cp server.cfg server.cfg.backup - ``` - -2. **Restore from Backup:** - ```bash - # Use txAdmin backup system - backup restore --file=latest_backup.tar.gz --type=config - ``` - -3. **Manual Recovery:** - ```bash - # Start with minimal config - echo "endpoint_add_tcp 0.0.0.0:30120" > server.cfg - echo "endpoint_add_udp 0.0.0.0:30120" >> server.cfg - echo "sv_maxclients 32" >> server.cfg - echo "set sv_licenseKey YOUR_KEY" >> server.cfg - ``` - -### Data Recovery - -#### Player Data Loss -If player data is lost or corrupted: - -1. **Identify Scope:** - - Check which players are affected - - Determine what data is missing - - Find most recent good backup - -2. **Database Recovery:** - ```sql - -- Restore from backup - mysql -u username -p database_name < backup.sql - - -- Or restore specific tables - mysql -u username -p -e "DROP TABLE users" - mysql -u username -p database_name < users_backup.sql - ``` - -3. **Communicate with Players:** - - Inform affected players - - Explain recovery process - - Provide compensation if needed - ---- - - -**Debug Tip:** Enable `set developer 1` for more detailed error messages and debugging information. - - - -**Recovery Warning:** Always backup current state before attempting major fixes or recovery procedures. - \ No newline at end of file + +We are working hard to get the full txAdmin documentation done as soon as possible! + \ No newline at end of file diff --git a/content/docs/txadmin/web-panel.mdx b/content/docs/txadmin/web-panel.mdx deleted file mode 100644 index d00cfde..0000000 --- a/content/docs/txadmin/web-panel.mdx +++ /dev/null @@ -1,360 +0,0 @@ ---- -title: Web Panel Guide -description: Complete tour of the txAdmin web interface and all its features for effective server management. ---- - -import { FeatureList, CheckList, QuickLinks, DefinitionList, CommandCard, CommandTable, PropertyCard, IconGrid, Shortcut, StatusBadge, TroubleshootingCard, PermissionTable, ConfigBlock, StepList, ComparisonTable } from '@ui/components/mdx-components' - -This comprehensive guide covers every aspect of the txAdmin web panel, helping you navigate and utilize all available features effectively. - -## Dashboard Overview - - - -### Main Dashboard -The txAdmin dashboard provides an at-a-glance view of your server's status: - -#### Server Status Card -- **Status Indicator:** Green (running), Red (stopped), Yellow (starting/stopping) -- **Uptime:** Current server uptime -- **Players:** Current/maximum players -- **Quick Actions:** Start, stop, restart buttons - -#### Performance Metrics -- **CPU Usage:** Real-time processor utilization -- **RAM Usage:** Memory consumption with available memory -- **FPS:** Server frames per second (target: 50 FPS) -- **Tick Time:** Server tick processing time - -#### Recent Activity -- **Player Joins/Leaves:** Latest connection activity -- **Admin Actions:** Recent moderation activities -- **System Events:** Server starts, stops, crashes -- **Resource Changes:** Recently started/stopped resources - -### Navigation Menu - -#### Primary Sections -- **Dashboard:** Main overview and server status -- **Server:** Server control and management tools -- **Players:** Player management and moderation -- **Resources:** Resource management and installation -- **System:** System logs and diagnostics -- **Settings:** Configuration and preferences - -#### Quick Access Toolbar -- **Server Controls:** Start/stop/restart buttons -- **Console Toggle:** Quick access to live console -- **Player Count:** Current player statistics -- **Admin Notifications:** System alerts and messages - -## Server Section - -### Live Console -The heart of server management: - -#### Console Features -- **Live Output:** Real-time server console display -- **Command Input:** Direct server command execution -- **Output Filtering:** Filter by message type (info, warning, error) -- **Search Function:** Find specific messages in output -- **Export Logs:** Download console output for analysis - -#### Console Commands -Common administrative commands: - -```bash -# Player Management -kick [id] [reason] # Kick a player -ban [id] [duration] [reason] # Ban a player -unban [identifier] # Unban a player -whitelist add [identifier] # Add to whitelist - -# Resource Management -start [resource] # Start a resource -stop [resource] # Stop a resource -restart [resource] # Restart a resource -refresh # Refresh resource list - -# Server Control -saveall # Save all player data -quit # Stop the server -restart [seconds] # Restart with countdown -``` - -### Server Controls - -#### Power Management -- **Start Server:** Launch server with full initialization -- **Graceful Stop:** Save data and shutdown cleanly -- **Force Stop:** Immediate shutdown (emergency use only) -- **Restart Server:** Stop and start sequence -- **Kill Server:** Terminate server process forcefully - -#### Advanced Controls -- **Scheduled Restart:** Set timed server restarts -- **Maintenance Mode:** Prevent new connections -- **Emergency Stop:** Immediate shutdown with alerts -- **Safe Mode:** Start server with minimal resources - -### Server Logs -Comprehensive logging system: - -#### Log Categories -- **Server Logs:** FXServer output and system messages -- **Admin Actions:** Administrative command history -- **Player Actions:** Player connections and activities -- **Resource Logs:** Resource-specific messages -- **Error Logs:** Error messages and stack traces - -#### Log Management -- **Real-time Viewing:** Live log updates -- **Historical Search:** Search through archived logs -- **Export Options:** Download logs in various formats -- **Automatic Cleanup:** Configurable log retention - -## Players Section - -### Player List -Comprehensive player management interface: - -#### Player Information Display -- **ID:** Server-assigned player identifier -- **Name:** Current player name -- **Identifiers:** Steam, license, discord, IP -- **Ping:** Connection latency in milliseconds -- **Playtime:** Current session duration -- **Status:** Online status and activity - -#### Sorting and Filtering -- **Sort Options:** By name, ID, ping, playtime -- **Filter Tools:** Search by name or identifier -- **Status Filters:** Online, recently disconnected -- **Advanced Filters:** By ping range, playtime, location - -### Player Actions - -#### Moderation Tools -Available for each player: - -**Kick Player:** -- Immediate removal from server -- Custom reason message -- Visible to kicked player -- Logged in admin actions - -**Ban Player:** -- Temporary or permanent bans -- Multiple identifier types -- Ban reason and admin notes -- Appeal system integration - -**Warn Player:** -- Issue warnings without removal -- Warning categories and severity levels -- Cumulative warning tracking -- Automatic escalation rules - -#### Communication Tools -- **Direct Message:** Send private messages to players -- **Server Announcement:** Broadcast messages to all players -- **Admin Chat:** Private communication channel -- **MOTD Updates:** Update message of the day - -### Player History -Detailed player tracking: - -#### Connection History -- **Login Records:** Connection timestamps -- **Session Duration:** Time spent per session -- **IP History:** Previous IP addresses used -- **Identifier Changes:** Steam/license changes - -#### Moderation History -- **Kick Records:** Previous kicks with reasons -- **Ban History:** Past bans and durations -- **Warning Log:** All issued warnings -- **Appeal Status:** Ban appeal information - -## Resources Section - -### Resource Management -Complete control over server resources: - -#### Resource List View -- **Name:** Resource identifier -- **Status:** Running, stopped, error state -- **Version:** Resource version information -- **Description:** Resource purpose and features -- **Actions:** Start, stop, restart, configure - -#### Resource Categories -- **Framework:** Core framework resources (ESX, QBCore) -- **Jobs:** Employment and roleplay systems -- **Vehicles:** Car dealerships, garages, modifications -- **Housing:** Property and real estate systems -- **Economy:** Banking, shops, money systems -- **Custom:** Server-specific resources - -### Resource Installation - -#### Manual Upload -- **ZIP Upload:** Upload resource archives -- **Directory Creation:** Automatic folder structure -- **Dependency Detection:** Identify required resources -- **Auto-configuration:** Basic setup automation - -#### Recipe System -Pre-configured resource packages: - -**Popular Recipes:** -- **ESX Complete:** Full ESX framework setup -- **QBCore Bundle:** Complete QBCore installation -- **Roleplay Starter:** Basic RP server resources -- **Racing Server:** Street racing focused setup -- **Custom Builds:** Community-created recipes - -#### GitHub Integration -- **Repository Cloning:** Direct GitHub downloads -- **Update Tracking:** Monitor for resource updates -- **Version Control:** Manage resource versions -- **Automated Updates:** Schedule update checks - -### Resource Monitoring - -#### Performance Metrics -- **CPU Usage:** Processor time per resource -- **Memory Usage:** RAM consumption tracking -- **Network I/O:** Data transfer statistics -- **Execution Time:** Code execution profiling -- **Error Rate:** Error frequency monitoring - -#### Health Checks -- **Startup Verification:** Ensure clean resource startup -- **Dependency Validation:** Check required dependencies -- **Conflict Detection:** Identify resource conflicts -- **Performance Warnings:** Alert on resource issues - -## System Section - -### System Information -Hardware and software monitoring: - -#### Server Hardware -- **CPU Information:** Processor details and usage -- **Memory Stats:** Total, used, available RAM -- **Storage Space:** Disk usage and availability -- **Network Interfaces:** Available network connections - -#### Software Environment -- **Operating System:** OS version and build -- **FXServer Version:** Server artifact information -- **txAdmin Version:** Panel version and updates -- **Node.js Version:** Runtime environment details - -### Diagnostics Tools - -#### Health Checks -Comprehensive system health monitoring: - -- **Port Accessibility:** Verify server ports are open -- **Database Connectivity:** Test database connections -- **File System Health:** Check disk and file permissions -- **Network Latency:** Monitor connection quality -- **Resource Integrity:** Verify resource file integrity - -#### Performance Analysis -- **FPS Monitoring:** Server performance tracking -- **Memory Leak Detection:** Identify memory issues -- **CPU Bottleneck Analysis:** Find performance issues -- **Network Congestion:** Monitor bandwidth usage - -### Backup Management - -#### Backup Operations -- **Manual Backup:** Create immediate backups -- **Scheduled Backups:** Automated backup creation -- **Backup Verification:** Ensure backup integrity -- **Restoration Tools:** Restore from backups - -#### Backup Storage -- **Local Storage:** Server-based backup storage -- **Cloud Integration:** AWS, Google Cloud, Azure -- **Remote Storage:** FTP, SFTP, network drives -- **Compression:** Reduce backup file sizes - -## Settings Section - -### Global Settings - -#### Interface Preferences -- **Theme:** Dark/light mode selection -- **Language:** Interface language options -- **Timezone:** Display timezone configuration -- **Auto-refresh:** Automatic page update intervals - -#### Notification Settings -- **Email Alerts:** System notification emails -- **Discord Integration:** Discord webhook notifications -- **Sound Alerts:** Browser notification sounds -- **Alert Thresholds:** Configure alert triggers - -### Server Configuration - -#### Basic Settings -- **Server Name:** Public server display name -- **Max Players:** Maximum concurrent players -- **Password:** Server access password (optional) -- **Whitelist:** Enable/disable whitelist mode - -#### Advanced Configuration -- **OneSync Settings:** Enable/configure OneSync -- **Steam Integration:** Steam API configuration -- **Database Settings:** MySQL/MariaDB configuration -- **Security Options:** Various security features - -### Admin Management - -#### Admin Accounts -- **User Creation:** Add new administrator accounts -- **Permission Levels:** Assign access permissions -- **Role Management:** Create custom admin roles -- **Session Control:** Manage active admin sessions - -#### Access Control -- **IP Restrictions:** Limit access by IP address -- **Two-Factor Auth:** Enhanced security options -- **Login Auditing:** Track admin login activities -- **Password Policies:** Enforce password requirements - -## Mobile Interface - -### Mobile Optimization -txAdmin is fully responsive for mobile management: - -#### Touch-Friendly Controls -- **Large Buttons:** Easy touch interaction -- **Swipe Navigation:** Mobile-native navigation -- **Responsive Layout:** Adapts to screen sizes -- **Quick Actions:** Essential tools readily accessible - -#### Mobile-Specific Features -- **Push Notifications:** Receive alerts on mobile -- **Offline Caching:** Basic functionality when offline -- **Touch Gestures:** Intuitive mobile interactions -- **Portrait/Landscape:** Optimized for both orientations - ---- - - -**Navigation Tip:** Use keyboard shortcuts for quick navigation: Ctrl+1 for Dashboard, Ctrl+2 for Server, etc. - - - -**Pro Tip:** Customize your dashboard by pinning frequently used tools and metrics for quick access. - \ No newline at end of file diff --git a/content/docs/txadmin/windows/index.mdx b/content/docs/txadmin/windows/index.mdx index 9a1c1c0..62d914b 100644 --- a/content/docs/txadmin/windows/index.mdx +++ b/content/docs/txadmin/windows/index.mdx @@ -18,10 +18,10 @@ Before you begin, ensure your Windows system meets these requirements for optima @@ -38,12 +38,16 @@ Before you begin, ensure your Windows system meets these requirements for optima ### Software Requirements + +FiveM requires a **fully updated** version of Windows. An outdated operating system may not work correctly. Windows 10 or newer is recommended for the best experience. + + ## Installation Options { return providers; } - - for (const file of jsonFiles) { - try { - const filePath = path.join(providersDir, file); - const content = fs.readFileSync(filePath, "utf-8"); - const provider = JSON.parse(content) as HostingProvider; - - // Basic validation - if (provider.id && provider.name && provider.description && provider.discount && provider.links) { - providers.push(provider); - } else { - console.warn(`Invalid provider file: ${file} - missing required fields`); - } - } catch (error) { - console.error(`Error loading provider file ${file}:`, error); - } - } - - // Sort by priority (highest first), then by name - return providers.sort((a, b) => { - const priorityA = a.priority ?? 0; - const priorityB = b.priority ?? 0; - if (priorityB !== priorityA) { - return priorityB - priorityA; - } - return a.name.localeCompare(b.name); - }); -} diff --git a/package.json b/package.json index 377a22c..14dd22e 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,13 @@ "version:json": "node .github/scripts/get-version.js --json", "analyze:commits": "node .github/scripts/analyze-commits.js", "analyze:commits:json": "node .github/scripts/analyze-commits.js --json", - "knip": "npx knip", - "knip:exports": "npx knip --exports", - "knip:deps": "npx knip --dependencies", - "knip:files": "npx knip --files", - "knip:prod": "npx knip --production", - "knip:fix": "npx knip --fix", + "trusted-hosts:update": "node .github/scripts/update-trusted-hosts.js", + "trusted-hosts:validate": "node .github/scripts/validate-trusted-hosts.js", + "knip:exports": "knip --exports", + "knip:deps": "knip --dependencies", + "knip:files": "knip --files", + "knip:prod": "knip --production", + "knip:fix": "knip --fix", "postinstall": "fumadocs-mdx", "prepare": "husky install" }, @@ -29,6 +30,7 @@ "@ai-sdk/openai": "^0.0.38", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-dialog": "^1.1.4", @@ -39,11 +41,12 @@ "@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-tooltip": "^1.1.7", "@radix-ui/react-switch": "^1.1.2", - "@scalar/api-client-react": "1.2.12", "@vercel/analytics": "^1.3.2", "@vercel/functions": "^2.0.0", "ai": "^3.2.34", "axios": "^1.7.9", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", "cheerio": "^1.0.0", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -62,9 +65,6 @@ "react-markdown": "^10.1.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", - "rimraf": "^6.0.1", - "sugar-high": "^0.9.3", - "tsup": "^8.3.6", "shiki": "^1.26.0", "uuid": "^13.0.0", "zod": "^3.24.2" @@ -72,7 +72,6 @@ "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "19.8.1", - "@fumadocs/cli": "^0.0.4", "@types/mdx": "^2.0.13", "@types/node": "22.5.4", "@types/react": "^18.3.5", @@ -83,14 +82,12 @@ "commitlint": "19.8.1", "husky": "^8.0.3", "knip": "^5.78.0", - "lint-staged": "^16.2.7", "postcss": "^8.4.45", "prettier": "^3.7.4", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "tailwind-merge": "^2.6.0", - "typescript": "^5.5.4", - "ua-parser-js": "^2.0.3" + "typescript": "^5.5.4" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -101,7 +98,7 @@ "prettier -w ." ], "*.{json,md}": [ - "prettier --w ." + "prettier -w ." ] } } \ No newline at end of file diff --git a/packages/providers/GUIDELINES.md b/packages/providers/GUIDELINES.md index d5e9e1a..a66b94b 100644 --- a/packages/providers/GUIDELINES.md +++ b/packages/providers/GUIDELINES.md @@ -115,6 +115,6 @@ Partners will receive 30-day notice of termination unless the violation is sever ## Questions? Have questions about becoming a provider or these guidelines? -- Open an issue in the FixFX repository -- Email: partnerships@fixfx.dev -- Check the [Comprehensive Provider README](./README.md) for detailed technical requirements +- Open an issue in the [FixFX repository](https://github.com/CodeMeAPixel/FixFX/issues) +- Email: hey@codemeapixel.dev +- Check the [Comprehensive Provider README](https://github.com/CodeMeAPixel/FixFX/blob/develop/packages/providers/README.md) for detailed technical requirements diff --git a/packages/providers/README.md b/packages/providers/README.md index aa63ca7..a4cf330 100644 --- a/packages/providers/README.md +++ b/packages/providers/README.md @@ -210,7 +210,7 @@ Include this information in your PR: **Need help?** - 📖 Review the [JSON Schema](./schema.json) for technical specs -- 💬 Ask on our [Discord](https://discord.gg/Vv2bdC44Ge) +- 💬 Ask on our [Discord](https://discord.gg/cYauqJfnNK) - 🐛 [Open an issue](https://github.com/CodeMeAPixel/FixFX/issues) for technical problems **Partnership inquiries:** diff --git a/packages/providers/trusted-hosts.json b/packages/providers/trusted-hosts.json index 7818f1e..091e1b5 100644 --- a/packages/providers/trusted-hosts.json +++ b/packages/providers/trusted-hosts.json @@ -1,30 +1,54 @@ { - "lastUpdated": "2026-01-26T00:00:00Z", + "lastUpdated": "2026-01-27T01:48:05.934Z", "source": "https://fivem.net/server-hosting", "hosts": [ { - "id": "zap-hosting", - "name": "ZAP-Hosting", - "url": "https://zap-hosting.com", - "description": "Premium game server hosting with DDoS protection and 24/7 support", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "id": "firestorm-servers", + "name": "Firestorm Servers", + "url": "https://firestormservers.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "g-portal", + "name": "G-Portal", + "url": "https://www.g-portal.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "gameservers", + "name": "GameServers", + "url": "https://www.gameservers.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" }, { "id": "gtxgaming", "name": "GTXGaming", "url": "https://gtxgaming.co.uk", - "description": "Reliable FiveM and RedM server hosting with excellent uptime", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" }, { "id": "nitrado", "name": "Nitrado", "url": "https://nitrado.net", - "description": "Global game server hosting provider with multi-region support", - "verified": true, - "lastVerified": "2026-01-26T00:00:00Z" + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.933Z" + }, + { + "id": "zap-hosting", + "name": "ZAP-Hosting", + "url": "https://zap-hosting.com", + "description": "Trusted FiveM/RedM hosting provider", + "verified": false, + "lastVerified": "2026-01-27T01:48:05.931Z" } ] -} +} \ No newline at end of file diff --git a/packages/providers/zap-hosting/provider.json b/packages/providers/zap-hosting/provider.json index ac30d7d..65d7d1a 100644 --- a/packages/providers/zap-hosting/provider.json +++ b/packages/providers/zap-hosting/provider.json @@ -36,6 +36,5 @@ "Multiple server locations", "SSD storage" ], - "highlight": "Official Partner", "priority": 100 } diff --git a/app/components/file-source.tsx b/packages/ui/src/components/file-source.tsx similarity index 100% rename from app/components/file-source.tsx rename to packages/ui/src/components/file-source.tsx diff --git a/packages/ui/src/components/guidelines-modal.tsx b/packages/ui/src/components/guidelines-modal.tsx new file mode 100644 index 0000000..0d735d9 --- /dev/null +++ b/packages/ui/src/components/guidelines-modal.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { createPortal } from "react-dom"; +import { X } from "lucide-react"; + +interface GuidelinesModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function GuidelinesModal({ open, onOpenChange }: GuidelinesModalProps) { + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!open) return; + + const fetchGuidelines = async () => { + setLoading(true); + try { + const response = await fetch("/api/guidelines"); + const data = await response.json(); + if (data.success) { + setContent(data.content); + } + } catch (error) { + console.error("Failed to fetch guidelines:", error); + } finally { + setLoading(false); + } + }; + + fetchGuidelines(); + }, [open]); + + useEffect(() => { + if (open) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [open]); + + if (!mounted) return null; + + const modal = ( + <> + {/* Backdrop */} +
onOpenChange(false)} + /> + + {/* Modal Content */} +
+ {/* Close Button */} + + + {/* Dialog Container */} +
+
+

+ Partnership Guidelines & Code of Conduct +

+ + {loading ? ( +
+
+
+

+ Loading guidelines... +

+
+
+ ) : ( +
+ {content.split("\n").map((line, idx) => { + if (!line.trim()) return
; + + // Headings + if (line.startsWith("# ")) { + return ( +

+ {line.replace(/^# /, "")} +

+ ); + } + if (line.startsWith("## ")) { + return ( +

+ {line.replace(/^## /, "")} +

+ ); + } + if (line.startsWith("### ")) { + return ( +

+ {line.replace(/^### /, "")} +

+ ); + } + + // Lists + if (line.startsWith("- ")) { + return ( +
+ + • + +

{formatMarkdownText(line.replace(/^- /, ""))}

+
+ ); + } + + // Regular paragraphs + return ( +

+ {formatMarkdownText(line)} +

+ ); + })} +
+ )} +
+
+
+ + ); + + return createPortal(open ? modal : null, document.body); +} + +function formatMarkdownText(text: string): React.ReactNode { + const parts: React.ReactNode[] = []; + let lastIndex = 0; + + // Handle [links](url), **bold**, _italic_, and `code` + const regex = /\[([^\]]+)\]\(([^)]+)\)|\*\*([^*]+)\*\*|_([^_]+)_|`([^`]+)`/g; + let match; + + while ((match = regex.exec(text)) !== null) { + // Add text before match + if (match.index > lastIndex) { + parts.push(text.substring(lastIndex, match.index)); + } + + // Add formatted text + if (match[1] && match[2]) { + // Link: [text](url) + parts.push( + + {match[1]} + + ); + } else if (match[3]) { + parts.push( + + {match[3]} + + ); + } else if (match[4]) { + parts.push( + + {match[4]} + + ); + } else if (match[5]) { + parts.push( + + {match[5]} + + ); + } + + lastIndex = match.index + match[0].length; + } + + // Add remaining text + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)); + } + + return parts.length > 0 ? parts : text; +} diff --git a/app/components/image-modal.tsx b/packages/ui/src/components/image-modal.tsx similarity index 100% rename from app/components/image-modal.tsx rename to packages/ui/src/components/image-modal.tsx diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index f47627d..debe120 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -3,9 +3,13 @@ export * from "./bento"; export * from "./button"; export * from "./card"; export * from "./code-editor"; +export * from "./file-source"; export * from "./grid"; +export * from "./guidelines-modal"; +export * from "./image-modal"; export * from "./marquee"; export * from "./mdx-components"; +export * from "./provider-card"; export * from "./retro"; export * from "./shimmer"; export * from "./shine"; diff --git a/app/hosting/provider-card.tsx b/packages/ui/src/components/provider-card.tsx similarity index 89% rename from app/hosting/provider-card.tsx rename to packages/ui/src/components/provider-card.tsx index 7e4064e..fef2697 100644 --- a/app/hosting/provider-card.tsx +++ b/packages/ui/src/components/provider-card.tsx @@ -4,7 +4,6 @@ import { useState } from "react"; import Link from "next/link"; import { Server, - Zap, Shield, Copy, Check, @@ -12,8 +11,9 @@ import { Percent, Clock, Globe, + Award, } from "lucide-react"; -import { cn } from "@/app/lib/utils"; +import { cn } from "@utils/functions/"; import type { HostingProvider } from "@/lib/providers"; function CopyButton({ text, className }: { text: string; className?: string }) { @@ -52,12 +52,13 @@ function CopyButton({ text, className }: { text: string; className?: string }) { export function ProviderCard({ provider }: { provider: HostingProvider }) { return (
- {/* Highlight badge */} - {provider.highlight && ( -
- - - {provider.highlight} + {/* CFX Trusted Badge */} + {provider.isTrusted && ( +
+ + + CFX Trusted + Trusted
)} diff --git a/packages/utils/src/constants/link.ts b/packages/utils/src/constants/link.ts index 753c200..7f032b8 100644 --- a/packages/utils/src/constants/link.ts +++ b/packages/utils/src/constants/link.ts @@ -5,7 +5,7 @@ export const ENV_URL = process.env.NODE_ENV === "development" ? "http://localhos // Go backend API endpoint - always use production by default // For local development with local backend, set API_URL environment variable to http://localhost:3001 export const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://core.fixfx.wiki"; -export const DISCORD_LINK = "https://discord.gg/Vv2bdC44Ge"; +export const DISCORD_LINK = "https://discord.gg/cYauqJfnNK"; export const GITHUB_ORG = "https://github.com/CodeMeAPixel"; export const GITHUB_LINK = "https://github.com/CodeMeAPixel/FixFX"; export const DOCS_URL = "https://fixfx.wiki"; diff --git a/public/llms.txt b/public/llms.txt index cc818cd..6ff6fcc 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -56,7 +56,7 @@ FixFX is a documentation hub for the CitizenFX ecosystem, providing guides and r ## Contact - GitHub: https://github.com/CodeMeAPixel -- Discord: https://discord.gg/fixfx +- Discord: https://discord.gg/cYauqJfnNK ## Optional