mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51ca6d3669 | ||
|
|
9cee55c05a | ||
|
|
a0e8c097f7 | ||
|
|
aee64ac137 | ||
|
|
bbda8eaca1 | ||
|
|
18a525e438 | ||
|
|
6c92985707 | ||
|
|
d4691bc700 | ||
|
|
74b258b12d | ||
|
|
d5939dc5a2 | ||
|
|
c95c8b08ea |
@@ -1,5 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.env
|
||||
node_modules
|
||||
deploy_package.tar.gz
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
APP_NAME=MIVO
|
||||
APP_ENV=production
|
||||
APP_KEY=mikhmonv3remake_secret_key_32bytes
|
||||
APP_KEY=mivo_official_secret_key_32bytes
|
||||
APP_DEBUG=true
|
||||
|
||||
# Database
|
||||
|
||||
9
.github/release_template.md
vendored
Normal file
9
.github/release_template.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
MIVO is a Modern, Lightweight, and Efficient. Built for low-end devices with premium UX.
|
||||
|
||||
## Installation
|
||||
For the best experience, we recommend using **Docker**.
|
||||
[Read the full Docker Installation Guide](https://mivodev.github.io/docs/guide/docker)
|
||||
|
||||
## Notes
|
||||
- Ensure your server runs **PHP 8.0+** with `sqlite3` extension enabled.
|
||||
- Default installation will guide you to create an Admin account.
|
||||
97
.github/scripts/generate-release-notes.js
vendored
Normal file
97
.github/scripts/generate-release-notes.js
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
const { GoogleGenerativeAI } = require("@google/generative-ai");
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Configuration
|
||||
const API_KEY = process.env.GEMINI_API_KEY;
|
||||
const MODEL_NAME = process.env.GEMINI_MODEL || "gemini-2.5-flash";
|
||||
const VERSION_TAG = process.argv[2]; // e.g., v1.2.0
|
||||
// Fix for Windows: Avoid 2>/dev/null, handle error in try-catch block instead
|
||||
const PREVIOUS_TAG_CMD = `git describe --abbrev=0 --tags ${VERSION_TAG}~1`;
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error("Error: GEMINI_API_KEY is not set.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!VERSION_TAG) {
|
||||
console.error("Error: Version tag must be provided as the first argument.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
console.log(`Generating release notes for ${VERSION_TAG} using ${MODEL_NAME}...`);
|
||||
|
||||
// 1. Get Previous Tag
|
||||
let previousTag;
|
||||
try {
|
||||
previousTag = execSync(PREVIOUS_TAG_CMD).toString().trim();
|
||||
} catch (e) {
|
||||
console.log("No previous tag found, assuming first release.");
|
||||
previousTag = execSync("git rev-list --max-parents=0 HEAD").toString().trim();
|
||||
}
|
||||
console.log(`Comparing from ${previousTag} to ${VERSION_TAG}`);
|
||||
|
||||
// 2. Get Commit Messages
|
||||
const commits = execSync(`git log ${previousTag}..${VERSION_TAG} --pretty=format:"- %s (%h)" --no-merges`).toString();
|
||||
|
||||
if (!commits) {
|
||||
console.log("No commits found between tags.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Generate Content with Gemini
|
||||
const genAI = new GoogleGenerativeAI(API_KEY);
|
||||
const model = genAI.getGenerativeModel({ model: MODEL_NAME });
|
||||
|
||||
const prompt = `
|
||||
You are a release note generator for a software project named 'Mivo'.
|
||||
|
||||
Here are the commits for the new version ${VERSION_TAG}:
|
||||
${commits}
|
||||
|
||||
Please generate a clean, professional release note in Markdown format.
|
||||
|
||||
Strict Rules:
|
||||
1. **NO EMOJIS**: Do not use any emojis in headers, bullet points, or text.
|
||||
2. **Structure**: Group changes strictly into these headers (if applicable):
|
||||
- ### Features
|
||||
- ### Bug Fixes
|
||||
- ### Improvements
|
||||
- ### Maintenance
|
||||
3. **Format**: Use simple bullet points (-) for each item.
|
||||
4. **Content**: Keep it concise but descriptive. Do not mention 'Merge pull request' commits.
|
||||
5. **Header**: Start with a simple header: "# Release Notes ${VERSION_TAG}"
|
||||
6. **Output**: Output ONLY the markdown content.
|
||||
`;
|
||||
|
||||
const result = await model.generateContent(prompt);
|
||||
const response = await result.response;
|
||||
const text = response.text();
|
||||
|
||||
// 4. Read Template (Optional) and Merge
|
||||
// For now, we just output the AI text. You can append this to a template if needed.
|
||||
|
||||
// Write to file
|
||||
const outputPath = path.join(process.cwd(), ".github", "release_notes.md");
|
||||
fs.writeFileSync(outputPath, text);
|
||||
|
||||
console.log(`Release notes generated at ${outputPath}`);
|
||||
console.log(text);
|
||||
|
||||
// Export for GitHub Actions
|
||||
const githubOutput = process.env.GITHUB_OUTPUT;
|
||||
if (githubOutput) {
|
||||
// Multiline string for GitHub Output
|
||||
fs.appendFileSync(githubOutput, `RELEASE_NOTES<<EOF\n${text}\nEOF\n`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to generate release notes:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
55
.github/workflows/deploy-docs.yml
vendored
55
.github/workflows/deploy-docs.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 // fetch all history for lastUpdated
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build with VitePress
|
||||
run: npm run docs:build
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs/.vitepress/dist
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
26
.github/workflows/docker-publish.yml
vendored
26
.github/workflows/docker-publish.yml
vendored
@@ -10,9 +10,12 @@ on:
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: docker.io
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: dyzulk/mivo
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
# Map secrets to env for availability in 'if' conditions
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -30,13 +33,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
# Login against GHCR
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Login against Docker Hub (Optional fallback if secrets exist)
|
||||
- name: Log into Docker Hub
|
||||
if: github.event_name != 'pull_request' && env.DOCKER_USERNAME != ''
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
@@ -46,13 +57,12 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
docker.io/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# Branch main -> dyzulk/mivo:edge
|
||||
type=raw,value=edge,enable={{is_default_branch}}
|
||||
# Tag v1.0.0 -> dyzulk/mivo:1.0.0
|
||||
type=ref,event=tag
|
||||
# Tag v1.0.0 -> dyzulk/mivo:latest
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
# Build and push Docker image with Buildx (don't push on PR)
|
||||
|
||||
25
.github/workflows/release.yml
vendored
25
.github/workflows/release.yml
vendored
@@ -14,6 +14,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -32,11 +34,31 @@ jobs:
|
||||
# Export source using git archive (respects .gitattributes)
|
||||
git archive --format=tar HEAD | tar -x -C release_temp
|
||||
|
||||
- name: Install Development Dependencies (for Build & AI)
|
||||
run: npm install
|
||||
|
||||
- name: Build Localized Assets & Editor Bundle
|
||||
run: |
|
||||
npm run sync:assets
|
||||
npm run build:editor
|
||||
|
||||
- name: Generate AI Release Notes
|
||||
env:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
run: |
|
||||
node .github/scripts/generate-release-notes.js ${{ github.ref_name }}
|
||||
|
||||
- name: Install Production Dependencies
|
||||
run: |
|
||||
cd release_temp
|
||||
composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs
|
||||
|
||||
- name: Copy Build Artifacts to Release
|
||||
run: |
|
||||
cp -r public/assets/vendor/ release_temp/public/assets/
|
||||
mkdir -p release_temp/public/assets/js/vendor/
|
||||
cp public/assets/js/vendor/editor.bundle.js release_temp/public/assets/js/vendor/
|
||||
|
||||
- name: Build Zip Artifact
|
||||
run: |
|
||||
cd release_temp
|
||||
@@ -46,7 +68,8 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: mivo-v${{ steps.get_version.outputs.VERSION }}.zip
|
||||
generate_release_notes: true
|
||||
body_path: .github/release_notes.md
|
||||
generate_release_notes: false
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -22,14 +22,15 @@ Thumbs.db
|
||||
# Secrets and Environment
|
||||
.env
|
||||
|
||||
# VitePress
|
||||
docs/.vitepress/dist
|
||||
docs/.vitepress/cache
|
||||
|
||||
# Build Scripts & Artifacts
|
||||
build_release.ps1
|
||||
deploy.ps1
|
||||
.github/release_notes.md
|
||||
|
||||
# User Uploads
|
||||
/public/uploads/*
|
||||
!/public/uploads/.gitignore
|
||||
|
||||
# Plugins
|
||||
/plugins/*
|
||||
!/plugins/.gitkeep
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/dyzulk/mivo/main/public/assets/img/logo.png" alt="MIVO Logo" width="200" />
|
||||
<img src="https://raw.githubusercontent.com/mivodev/mivo/main/public/assets/img/logo.png" alt="MIVO Logo" width="200" />
|
||||
</p>
|
||||
|
||||
# MIVO (Mikrotik Voucher) Docker Image
|
||||
@@ -24,7 +24,7 @@ docker run -d \
|
||||
-e APP_ENV=production \
|
||||
-v mivo_data:/var/www/html/app/Database \
|
||||
-v mivo_config:/var/www/html/.env \
|
||||
dyzulk/mivo:latest
|
||||
mivodev/mivo:latest
|
||||
```
|
||||
|
||||
Open your browser and navigate to `http://localhost:8080`.
|
||||
@@ -39,7 +39,7 @@ For a more permanent setup, use `docker-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:latest
|
||||
image: mivodev/mivo:latest
|
||||
container_name: mivo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -111,4 +111,4 @@ If you find MIVO useful, please consider supporting its development. Your contri
|
||||
[](https://sociabuzz.com/dyzulkdev/tribe)
|
||||
|
||||
---
|
||||
*Created by DyzulkDev*
|
||||
*Created by MivoDev*
|
||||
|
||||
@@ -29,8 +29,15 @@ RUN mkdir -p /var/www/html/app/Database && \
|
||||
chown -R www-data:www-data /var/www/html && \
|
||||
chmod -R 755 /var/www/html
|
||||
|
||||
# Copy Entrypoint
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Start Supervisor (which starts Nginx & PHP-FPM)
|
||||
# Use Entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
# Start Supervisor
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||
|
||||
@@ -30,15 +30,15 @@ MIVO is a next-generation **Mikrotik Voucher Management System** with a modern M
|
||||
|
||||
1. **Install via Composer**
|
||||
```bash
|
||||
composer create-project dyzulk/mivo
|
||||
composer create-project mivodev/mivo
|
||||
cd mivo
|
||||
```
|
||||
|
||||
> **Alternative (Docker):**
|
||||
> ```bash
|
||||
> docker pull dyzulk/mivo
|
||||
> docker pull mivodev/mivo
|
||||
> ```
|
||||
> *See [INSTALLATION.md](docs/INSTALLATION.md) for more tags.*
|
||||
> *See [DOCKER_README.md](DOCKER_README.md) for more tags.*
|
||||
|
||||
2. **Setup Environment**
|
||||
```bash
|
||||
@@ -83,4 +83,4 @@ If you find MIVO useful, please consider supporting its development. Your contri
|
||||
This project is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
---
|
||||
*Created by DyzulkDev*
|
||||
*Created by MivoDev*
|
||||
|
||||
@@ -3,17 +3,17 @@ namespace App\Config;
|
||||
|
||||
class SiteConfig {
|
||||
const APP_NAME = 'MIVO';
|
||||
const APP_VERSION = 'v1.1.0';
|
||||
const APP_VERSION = 'v1.2.2';
|
||||
const APP_FULL_NAME = 'MIVO - Mikrotik Voucher';
|
||||
const CREDIT_NAME = 'DyzulkDev';
|
||||
const CREDIT_URL = 'https://dyzulk.com';
|
||||
const CREDIT_NAME = 'MivoDev';
|
||||
const CREDIT_URL = 'https://github.com/mivodev';
|
||||
const YEAR = '2026';
|
||||
const REPO_URL = 'https://github.com/dyzulk/mivo';
|
||||
const REPO_URL = 'https://github.com/mivodev/mivo';
|
||||
|
||||
// Security Keys
|
||||
// Fetched from .env or fallback to default
|
||||
public static function getSecretKey() {
|
||||
return getenv('APP_KEY') ?: 'mikhmonv3remake_secret_key_32bytes';
|
||||
return getenv('APP_KEY') ?: 'mivo_official_secret_key_32bytes';
|
||||
}
|
||||
|
||||
const IS_DEV = true; // Still useful for code logic not relying on env yet, or can be refactored too.
|
||||
|
||||
@@ -44,7 +44,7 @@ class InstallController extends Controller {
|
||||
Migrations::up();
|
||||
|
||||
// 2. Generate Key if default
|
||||
if (SiteConfig::getSecretKey() === 'mikhmonv3remake_secret_key_32bytes') {
|
||||
if (SiteConfig::getSecretKey() === 'mivo_official_secret_key_32bytes') {
|
||||
$this->generateKey();
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ class InstallController extends Controller {
|
||||
$envPath = ROOT . '/.env';
|
||||
if (!file_exists($envPath)) {
|
||||
// Check if SiteConfig has a manual override (legacy)
|
||||
return SiteConfig::getSecretKey() !== 'mikhmonv3remake_secret_key_32bytes';
|
||||
return SiteConfig::getSecretKey() !== 'mivo_official_secret_key_32bytes';
|
||||
}
|
||||
|
||||
$key = getenv('APP_KEY');
|
||||
$keyChanged = ($key && $key !== 'mikhmonv3remake_secret_key_32bytes');
|
||||
$keyChanged = ($key && $key !== 'mivo_official_secret_key_32bytes');
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
@@ -191,10 +191,10 @@ class QuickPrintController extends Controller {
|
||||
// Check if M or G
|
||||
// Simple logic for now, assuming raw if number, or passing string if Mikrotik accepts it (usually requires bytes)
|
||||
// Let's assume user inputs "100M" or "1G" which usually needs parsing.
|
||||
// For now, let's assume input is NUMBER in MB as per standard Mikhmon practice, OR generic string.
|
||||
// For now, let's assume input is NUMBER in MB as per standard Mivo practice, OR generic string.
|
||||
// We'll pass as is for strings, or multiply if strictly numeric?
|
||||
// Let's rely on standard Mikrotik parsing if string passed, or convert.
|
||||
// Mikhmon v3 usually uses dropdown "MB/GB".
|
||||
// Mivo usually uses dropdown "MB/GB".
|
||||
// Implementing simple conversion:
|
||||
$val = intval($package['data_limit']);
|
||||
if (strpos(strtolower($package['data_limit']), 'g') !== false) {
|
||||
|
||||
@@ -11,22 +11,66 @@ class ReportController extends Controller
|
||||
{
|
||||
public function index($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) {
|
||||
$data = $this->getSellingReportData($session);
|
||||
if (!$data) {
|
||||
header('Location: /');
|
||||
exit;
|
||||
}
|
||||
|
||||
return $this->view('reports/selling', $data);
|
||||
}
|
||||
|
||||
public function sellingExport($session, $type)
|
||||
{
|
||||
$data = $this->getSellingReportData($session);
|
||||
if (!$data) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'No data found']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$report = $data['report'];
|
||||
$exportData = [];
|
||||
|
||||
foreach ($report as $row) {
|
||||
$exportData[] = [
|
||||
'Date/Batch' => $row['date'],
|
||||
'Status' => $row['status'] ?? '-',
|
||||
'Qty (Stock)' => $row['count'],
|
||||
'Used' => $row['realized_count'],
|
||||
'Realized Income' => $row['realized_total'],
|
||||
'Total Stock' => $row['total']
|
||||
];
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($exportData);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function getSellingReportData($session)
|
||||
{
|
||||
$configModel = new Config();
|
||||
$config = $configModel->getSession($session);
|
||||
|
||||
if (!$config) return null;
|
||||
|
||||
$API = new RouterOSAPI();
|
||||
$users = [];
|
||||
|
||||
$profilePriceMap = [];
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
// Fetch All Users
|
||||
// Optimized print: get .id, name, price, comment
|
||||
$users = $API->comm("/ip/hotspot/user/print");
|
||||
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
||||
$API->disconnect();
|
||||
|
||||
// Build Price Map from Profile Scripts
|
||||
foreach ($profiles as $p) {
|
||||
$meta = HotspotHelper::parseProfileMetadata($p['on-login'] ?? '');
|
||||
if (!empty($meta['price'])) {
|
||||
$profilePriceMap[$p['name']] = intval($meta['price']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate Data
|
||||
@@ -34,21 +78,22 @@ class ReportController extends Controller
|
||||
$totalIncome = 0;
|
||||
$totalVouchers = 0;
|
||||
|
||||
// Realized (Used) Metrics
|
||||
$totalRealizedIncome = 0;
|
||||
$totalUsedVouchers = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
// Skip if no price
|
||||
if (empty($user['price']) || $user['price'] == '0') continue;
|
||||
// Smart Price Detection
|
||||
$price = $this->detectPrice($user, $profilePriceMap);
|
||||
if ($price <= 0) continue;
|
||||
|
||||
// Inject price back to user array for downstream logic
|
||||
$user['price'] = $price;
|
||||
|
||||
// Determine Date from Comment
|
||||
// Mikhmon format usually: "mikhmon-MM/DD/YYYY" or just "MM/DD/YYYY" or plain comment
|
||||
// We will try to parse a date from the comment, or use "Unknown Date"
|
||||
$date = 'Unknown Date';
|
||||
$comment = $user['comment'] ?? '';
|
||||
|
||||
// Regex for date patterns (d-m-Y or m/d/Y or Y-m-d)
|
||||
// Simplify: Group by Comment content itself if it looks like a date/batch
|
||||
// Or try to extract M-Y.
|
||||
|
||||
// For feature parity, Mikhmon often groups by the exact comment string as the "Batch/Date"
|
||||
if (!empty($comment)) {
|
||||
$date = $comment;
|
||||
}
|
||||
@@ -57,28 +102,59 @@ class ReportController extends Controller
|
||||
$report[$date] = [
|
||||
'date' => $date,
|
||||
'count' => 0,
|
||||
'total' => 0
|
||||
'total' => 0,
|
||||
'realized_total' => 0,
|
||||
'realized_count' => 0
|
||||
];
|
||||
}
|
||||
|
||||
$price = intval($user['price']);
|
||||
|
||||
// Check if Used
|
||||
// Criteria: uptime != 0s OR bytes-out > 0 OR bytes-in > 0
|
||||
$isUsed = false;
|
||||
if ((isset($user['uptime']) && $user['uptime'] != '0s') ||
|
||||
(isset($user['bytes-out']) && $user['bytes-out'] > 0)) {
|
||||
$isUsed = true;
|
||||
}
|
||||
|
||||
$report[$date]['count']++;
|
||||
$report[$date]['total'] += $price;
|
||||
|
||||
$totalIncome += $price;
|
||||
$totalVouchers++;
|
||||
|
||||
if ($isUsed) {
|
||||
$report[$date]['realized_count']++;
|
||||
$report[$date]['realized_total'] += $price;
|
||||
$totalRealizedIncome += $price;
|
||||
$totalUsedVouchers++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Status for each batch
|
||||
foreach ($report as &$row) {
|
||||
if ($row['realized_count'] === 0) {
|
||||
$row['status'] = 'New';
|
||||
} elseif ($row['realized_count'] >= $row['count']) {
|
||||
$row['status'] = 'Sold Out';
|
||||
} else {
|
||||
$row['status'] = 'Selling';
|
||||
}
|
||||
}
|
||||
unset($row);
|
||||
|
||||
// Sort by key (Date/Comment) desc
|
||||
krsort($report);
|
||||
|
||||
return $this->view('reports/selling', [
|
||||
return [
|
||||
'session' => $session,
|
||||
'report' => $report,
|
||||
'totalIncome' => $totalIncome,
|
||||
'totalVouchers' => $totalVouchers,
|
||||
'totalRealizedIncome' => $totalRealizedIncome,
|
||||
'totalUsedVouchers' => $totalUsedVouchers,
|
||||
'currency' => $config['currency'] ?? 'Rp'
|
||||
]);
|
||||
];
|
||||
}
|
||||
public function resume($session)
|
||||
{
|
||||
@@ -93,9 +169,18 @@ class ReportController extends Controller
|
||||
$API = new RouterOSAPI();
|
||||
$users = [];
|
||||
|
||||
$profilePriceMap = [];
|
||||
if ($API->connect($config['ip_address'], $config['username'], $config['password'])) {
|
||||
$users = $API->comm("/ip/hotspot/user/print");
|
||||
$profiles = $API->comm("/ip/hotspot/user/profile/print");
|
||||
$API->disconnect();
|
||||
|
||||
foreach ($profiles as $p) {
|
||||
$meta = HotspotHelper::parseProfileMetadata($p['on-login'] ?? '');
|
||||
if (!empty($meta['price'])) {
|
||||
$profilePriceMap[$p['name']] = intval($meta['price']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Aggregates
|
||||
@@ -104,27 +189,68 @@ class ReportController extends Controller
|
||||
$yearly = [];
|
||||
$totalIncome = 0;
|
||||
|
||||
foreach ($users as $user) {
|
||||
if (empty($user['price']) || $user['price'] == '0') continue;
|
||||
// Realized Metrics for Resume?
|
||||
// Usually Resume is just general financial overview.
|
||||
// We'll stick to Stock for now unless requested, as Resume mimics Mikhmon's logic closer.
|
||||
// Or we can just calculate standard revenue based on Stock if that's what user expects for "Resume",
|
||||
// OR we can add Realized. Let's keep Resume simple first, focus on Selling Report.
|
||||
|
||||
// Try to parse Date from Comment (Mikhmon format: mikhmon-10/25/2023 or just 10/25/2023)
|
||||
foreach ($users as $user) {
|
||||
$price = $this->detectPrice($user, $profilePriceMap);
|
||||
if ($price <= 0) continue;
|
||||
|
||||
$user['price'] = $price;
|
||||
|
||||
// Try to parse Date from Comment
|
||||
// Supported formats:
|
||||
// - MM/DD/YYYY or MM.DD.YYYY (US)
|
||||
// - DD-MM-YYYY (EU/ID)
|
||||
// - YYYY-MM-DD (ISO)
|
||||
// Regex explanations:
|
||||
// 1. \b starts word boundary to avoid matching parts of batch IDs (e.g. 711-...)
|
||||
// 2. We look for 3 groups of digits separated by / . or -
|
||||
$comment = $user['comment'] ?? '';
|
||||
$dateObj = null;
|
||||
|
||||
// Simple parser: try to find MM/DD/YYYY
|
||||
if (preg_match('/(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{2,4})/', $comment, $matches)) {
|
||||
// Assuming MM/DD/YYYY based on typical Mikhmon, but could be DD-MM-YYYY
|
||||
// Let's standardise on checking valid date.
|
||||
// Standard Mikhmon V3 is MM/DD/YYYY.
|
||||
$m = $matches[1];
|
||||
$d = $matches[2];
|
||||
$y = $matches[3];
|
||||
if (strlen($y) == 2) $y = '20' . $y;
|
||||
$dateObj = \DateTime::createFromFormat('m/d/Y', "$m/$d/$y");
|
||||
if (preg_match('/\b(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{2,4})\b/', $comment, $matches)) {
|
||||
// Heuristic: If 3rd part is year (4 digits or > 31), use it.
|
||||
// If 1st part > 12, it's likely Day (DD-MM-YYYY).
|
||||
// Mivo Generator format often: MM.DD.YY or DD.MM.YY
|
||||
|
||||
$p1 = intval($matches[1]);
|
||||
$p2 = intval($matches[2]);
|
||||
$p3 = intval($matches[3]);
|
||||
|
||||
$year = $p3;
|
||||
$month = $p1;
|
||||
$day = $p2;
|
||||
|
||||
// Adjust 2-digit year
|
||||
if ($year < 100) $year += 2000;
|
||||
|
||||
// Guess format
|
||||
// If p1 > 12, it must be Day. (DD-MM-YYYY)
|
||||
if ($p1 > 12) {
|
||||
$day = $p1;
|
||||
$month = $p2;
|
||||
}
|
||||
|
||||
// Fallback: If no date found in comment, maybe created at?
|
||||
// Usually Mikhmon relies strictly on comment.
|
||||
// Validate date
|
||||
if (checkdate($month, $day, $year)) {
|
||||
$dateObj = (new \DateTime())->setDate($year, $month, $day);
|
||||
}
|
||||
}
|
||||
// Check for ISO YYYY-MM-DD
|
||||
elseif (preg_match('/\b(\d{4})[\/.-](\d{1,2})[\/.-](\d{1,2})\b/', $comment, $matches)) {
|
||||
if (checkdate($matches[2], $matches[3], $matches[1])) {
|
||||
$dateObj = (new \DateTime())->setDate($matches[1], $matches[2], $matches[3]);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: If no date found -> "Unknown Date" in resume?
|
||||
// Resume requires Month/Year keys. If we can't parse date, we can't add to daily/monthly.
|
||||
// We'll skip or add to "Unknown"?
|
||||
// Current logic skips if !$dateObj
|
||||
if (!$dateObj) continue;
|
||||
|
||||
$price = intval($user['price']);
|
||||
@@ -162,4 +288,38 @@ class ReportController extends Controller
|
||||
'currency' => $config['currency'] ?? 'Rp'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart Price Detection Logic
|
||||
* Hierarchy:
|
||||
* 1. Comment Override (p:5000)
|
||||
* 2. Profile Script (Standard Profile)
|
||||
* 3. Profile Name Fallback (50K) -- REMOVED loose number matching to avoid garbage data
|
||||
*/
|
||||
private function detectPrice($user, $profileMap)
|
||||
{
|
||||
$comment = $user['comment'] ?? '';
|
||||
|
||||
// 1. Comment Override (p:5000 or price:5000)
|
||||
// Updated: Added \b to prevent matching "up-123" as "p-123"
|
||||
if (preg_match('/\b(?:p|price)[:-]\s*(\d+)/i', $comment, $matches)) {
|
||||
return intval($matches[1]);
|
||||
}
|
||||
|
||||
// 2. Profile Script
|
||||
$profile = $user['profile'] ?? 'default';
|
||||
if (isset($profileMap[$profile])) {
|
||||
return $profileMap[$profile];
|
||||
}
|
||||
|
||||
// 3. Fallback: Parse Profile Name (Strict "K" notation only)
|
||||
// Matches "5K", "5k" -> 5000
|
||||
if (preg_match('/(\d+)k\b/i', $profile, $m)) {
|
||||
return intval($m[1]) * 1000;
|
||||
}
|
||||
|
||||
// DEPRECATED: Loose number matching caused garbage data (e.g. "up-311" -> 311)
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class SettingsController extends Controller {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
$hash = password_hash($newPassword, PASSWORD_DEFAULT);
|
||||
// Assuming we are updating the default 'admin' user or the currently logged in user
|
||||
// Original Mikhmon usually has one main user. Let's update 'admin' for now.
|
||||
// Original Mivo usually has one main user. Let's update 'admin' for now.
|
||||
$db->query("UPDATE users SET password = ? WHERE username = 'admin'", [$hash]);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.password_updated', 'toasts.password_updated_desc', [], true);
|
||||
}
|
||||
@@ -431,4 +431,194 @@ class SettingsController extends Controller {
|
||||
}
|
||||
header('Location: /settings/api-cors');
|
||||
}
|
||||
|
||||
// --- Plugin Management ---
|
||||
|
||||
public function plugins() {
|
||||
$pluginManager = new \App\Core\PluginManager();
|
||||
// Since PluginManager loads everything in constructor/loadPlugins,
|
||||
// we can just scan the directory to list them and check status (implied active for now)
|
||||
$pluginsDir = ROOT . '/plugins';
|
||||
$plugins = [];
|
||||
|
||||
if (is_dir($pluginsDir)) {
|
||||
$folders = scandir($pluginsDir);
|
||||
foreach ($folders as $folder) {
|
||||
if ($folder === '.' || $folder === '..') continue;
|
||||
|
||||
$path = $pluginsDir . '/' . $folder;
|
||||
if (is_dir($path) && file_exists($path . '/plugin.php')) {
|
||||
// Try to read header from plugin.php
|
||||
$content = file_get_contents($path . '/plugin.php', false, null, 0, 1024); // Read first 1KB
|
||||
preg_match('/Plugin Name:\s*(.*)$/mi', $content, $nameMatch);
|
||||
preg_match('/Version:\s*(.*)$/mi', $content, $verMatch);
|
||||
preg_match('/Description:\s*(.*)$/mi', $content, $descMatch);
|
||||
preg_match('/Author:\s*(.*)$/mi', $content, $authMatch);
|
||||
|
||||
$plugins[] = [
|
||||
'id' => $folder,
|
||||
'name' => trim($nameMatch[1] ?? $folder),
|
||||
'version' => trim($verMatch[1] ?? '1.0.0'),
|
||||
'description' => trim($descMatch[1] ?? '-'),
|
||||
'author' => trim($authMatch[1] ?? '-'),
|
||||
'path' => $path
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->view('settings/plugins', ['plugins' => $plugins]);
|
||||
}
|
||||
|
||||
public function uploadPlugin() {
|
||||
if (!isset($_FILES['plugin_file']) || $_FILES['plugin_file']['error'] !== UPLOAD_ERR_OK) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'toasts.no_file_selected', [], true);
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['plugin_file'];
|
||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if ($ext !== 'zip') {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'Only .zip files are allowed', [], true);
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ($zip->open($file['tmp_name']) === TRUE) {
|
||||
$extractPath = ROOT . '/plugins/';
|
||||
if (!is_dir($extractPath)) mkdir($extractPath, 0755, true);
|
||||
|
||||
// TODO: Better validation to prevent overwriting existing plugins without confirmation?
|
||||
// For now, extraction overwrites.
|
||||
|
||||
// Validate content before extracting everything
|
||||
// Check if zip has a root folder or just files
|
||||
// Logic:
|
||||
// 1. Extract to temp.
|
||||
// 2. Find plugin.php
|
||||
// 3. Move to plugins dir.
|
||||
|
||||
$tempExtract = sys_get_temp_dir() . '/mivo_plugin_' . uniqid();
|
||||
if (!mkdir($tempExtract, 0755, true)) {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'Failed to create temp dir', [], true);
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$zip->extractTo($tempExtract);
|
||||
$zip->close();
|
||||
|
||||
// Search for plugin.php
|
||||
$pluginFile = null;
|
||||
$pluginRoot = $tempExtract;
|
||||
|
||||
// Recursive iterator to find plugin.php (max depth 2 to avoid deep scanning)
|
||||
$rii = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($tempExtract));
|
||||
foreach ($rii as $file) {
|
||||
if ($file->isDir()) continue;
|
||||
if ($file->getFilename() === 'plugin.php') {
|
||||
$pluginFile = $file->getPathname();
|
||||
$pluginRoot = dirname($pluginFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($pluginFile) {
|
||||
// Determine destination name
|
||||
// If the immediate parent of plugin.php is NOT the temp dir, use that folder name.
|
||||
// Else use the zip name.
|
||||
$folderName = basename($pluginRoot);
|
||||
if ($pluginRoot === $tempExtract) {
|
||||
$folderName = pathinfo($_FILES['plugin_file']['name'], PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
$dest = $extractPath . $folderName;
|
||||
|
||||
// Move/Copy
|
||||
// Using helper or rename. Rename might fail across volumes (temp to project).
|
||||
// Use custom recursive copy then delete temp.
|
||||
$this->recurseCopy($pluginRoot, $dest);
|
||||
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.plugin_installed', 'toasts.plugin_installed_desc', ['name' => $folderName], true);
|
||||
} else {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.install_failed', 'toasts.invalid_plugin_desc', [], true);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
$this->recurseDelete($tempExtract);
|
||||
|
||||
} else {
|
||||
\App\Helpers\FlashHelper::set('error', 'toasts.upload_failed', 'toasts.zip_open_failed_desc', [], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/plugins');
|
||||
}
|
||||
|
||||
public function deletePlugin() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = $_POST['plugin_id'] ?? '';
|
||||
if (empty($id)) {
|
||||
\App\Helpers\FlashHelper::set('error', 'common.error', 'Invalid plugin ID', [], true);
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Security check: validate id is just a folder name, no path traversal
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $id)) {
|
||||
\App\Helpers\FlashHelper::set('error', 'common.error', 'Invalid plugin ID format', [], true);
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
$pluginDir = ROOT . '/plugins/' . $id;
|
||||
|
||||
if (is_dir($pluginDir)) {
|
||||
$this->recurseDelete($pluginDir);
|
||||
\App\Helpers\FlashHelper::set('success', 'toasts.plugin_deleted', 'toasts.plugin_deleted_desc', [], true);
|
||||
} else {
|
||||
\App\Helpers\FlashHelper::set('error', 'common.error', 'Plugin directory not found', [], true);
|
||||
}
|
||||
|
||||
header('Location: /settings/plugins');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Helper for recursive copy (since rename/move_uploaded_file limit across partitions)
|
||||
private function recurseCopy($src, $dst) {
|
||||
$dir = opendir($src);
|
||||
@mkdir($dst);
|
||||
while(false !== ( $file = readdir($dir)) ) {
|
||||
if (( $file != '.' ) && ( $file != '..' )) {
|
||||
if ( is_dir($src . '/' . $file) ) {
|
||||
$this->recurseCopy($src . '/' . $file,$dst . '/' . $file);
|
||||
}
|
||||
else {
|
||||
copy($src . '/' . $file,$dst . '/' . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir($dir);
|
||||
}
|
||||
|
||||
private function recurseDelete($dir) {
|
||||
if (!is_dir($dir)) return;
|
||||
$scan = scandir($dir);
|
||||
foreach ($scan as $file) {
|
||||
if ($file == '.' || $file == '..') continue;
|
||||
if (is_dir($dir . "/" . $file)) {
|
||||
$this->recurseDelete($dir . "/" . $file);
|
||||
} else {
|
||||
unlink($dir . "/" . $file);
|
||||
}
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class Console {
|
||||
|
||||
private function printBanner() {
|
||||
echo "\n";
|
||||
echo self::COLOR_BOLD . " MIVO Helper " . self::COLOR_RESET . self::COLOR_GRAY . "v1.1.0" . self::COLOR_RESET . "\n\n";
|
||||
echo self::COLOR_BOLD . " MIVO Helper " . self::COLOR_RESET . self::COLOR_GRAY . \App\Config\SiteConfig::APP_VERSION . self::COLOR_RESET . "\n\n";
|
||||
}
|
||||
|
||||
private function commandServe($args) {
|
||||
@@ -171,7 +171,7 @@ class Console {
|
||||
|
||||
if (file_exists($envPath)) {
|
||||
$envIds = parse_ini_file($envPath);
|
||||
if (!empty($envIds['APP_KEY']) && $envIds['APP_KEY'] !== 'mikhmonv3remake_secret_key_32bytes') {
|
||||
if (!empty($envIds['APP_KEY']) && $envIds['APP_KEY'] !== 'mivo_official_secret_key_32bytes') {
|
||||
$keyExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
120
app/Core/Hooks.php
Normal file
120
app/Core/Hooks.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Hooks
|
||||
{
|
||||
/**
|
||||
* @var array Stores all registered actions
|
||||
*/
|
||||
private static $actions = [];
|
||||
|
||||
/**
|
||||
* @var array Stores all registered filters
|
||||
*/
|
||||
private static $filters = [];
|
||||
|
||||
/**
|
||||
* Register a new action
|
||||
*
|
||||
* @param string $tag The name of the action hook
|
||||
* @param callable $callback The function to call
|
||||
* @param int $priority Lower numbers correspond to earlier execution
|
||||
* @param int $accepted_args The number of arguments the function accepts
|
||||
*/
|
||||
public static function addAction($tag, $callback, $priority = 10, $accepted_args = 1)
|
||||
{
|
||||
self::$actions[$tag][$priority][] = [
|
||||
'function' => $callback,
|
||||
'accepted_args' => $accepted_args
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an action
|
||||
*
|
||||
* @param string $tag The name of the action hook
|
||||
* @param mixed ...$args Optional arguments to pass to the callback
|
||||
*/
|
||||
public static function doAction($tag, ...$args)
|
||||
{
|
||||
if (empty(self::$actions[$tag])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
ksort(self::$actions[$tag]);
|
||||
|
||||
foreach (self::$actions[$tag] as $priority => $callbacks) {
|
||||
foreach ($callbacks as $callbackData) {
|
||||
call_user_func_array($callbackData['function'], array_slice($args, 0, $callbackData['accepted_args']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new filter
|
||||
*
|
||||
* @param string $tag The name of the filter hook
|
||||
* @param callable $callback The function to call
|
||||
* @param int $priority Lower numbers correspond to earlier execution
|
||||
* @param int $accepted_args The number of arguments the function accepts
|
||||
*/
|
||||
public static function addFilter($tag, $callback, $priority = 10, $accepted_args = 1)
|
||||
{
|
||||
self::$filters[$tag][$priority][] = [
|
||||
'function' => $callback,
|
||||
'accepted_args' => $accepted_args
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to a value
|
||||
*
|
||||
* @param string $tag The name of the filter hook
|
||||
* @param mixed $value The value to be filtered
|
||||
* @param mixed ...$args Optional extra arguments
|
||||
* @return mixed The filtered value
|
||||
*/
|
||||
public static function applyFilters($tag, $value, ...$args)
|
||||
{
|
||||
if (empty(self::$filters[$tag])) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Sort by priority
|
||||
ksort(self::$filters[$tag]);
|
||||
|
||||
foreach (self::$filters[$tag] as $priority => $callbacks) {
|
||||
foreach ($callbacks as $callbackData) {
|
||||
// Prepend value to args
|
||||
$params = array_merge([$value], array_slice($args, 0, $callbackData['accepted_args'] - 1));
|
||||
$value = call_user_func_array($callbackData['function'], $params);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any action has been registered for a hook.
|
||||
*
|
||||
* @param string $tag The name of the action hook.
|
||||
* @return bool True if action exists, false otherwise.
|
||||
*/
|
||||
public static function hasAction($tag)
|
||||
{
|
||||
return isset(self::$actions[$tag]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any filter has been registered for a hook.
|
||||
*
|
||||
* @param string $tag The name of the filter hook.
|
||||
* @return bool True if filter exists, false otherwise.
|
||||
*/
|
||||
public static function hasFilter($tag)
|
||||
{
|
||||
return isset(self::$filters[$tag]);
|
||||
}
|
||||
}
|
||||
78
app/Core/PluginManager.php
Normal file
78
app/Core/PluginManager.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class PluginManager
|
||||
{
|
||||
/**
|
||||
* @var string Path to plugins directory
|
||||
*/
|
||||
private $pluginsDir;
|
||||
|
||||
/**
|
||||
* @var array List of active plugins
|
||||
*/
|
||||
private $activePlugins = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->pluginsDir = dirname(__DIR__, 2) . '/plugins'; // Root/plugins
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all active plugins
|
||||
*/
|
||||
public function loadPlugins()
|
||||
{
|
||||
// Ensure plugins directory exists
|
||||
if (!is_dir($this->pluginsDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Get List of Active Plugins (For now, we load ALL folders as active)
|
||||
// TODO: Implement database/config check for active status
|
||||
$plugins = scandir($this->pluginsDir);
|
||||
|
||||
foreach ($plugins as $pluginName) {
|
||||
if ($pluginName === '.' || $pluginName === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pluginPath = $this->pluginsDir . '/' . $pluginName;
|
||||
|
||||
// Check if it is a directory and has specific plugin file
|
||||
if (is_dir($pluginPath) && file_exists($pluginPath . '/plugin.php')) {
|
||||
$this->loadPlugin($pluginName, $pluginPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Fire 'plugins_loaded' action after all plugins are loaded
|
||||
Hooks::doAction('plugins_loaded');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a single plugin
|
||||
*
|
||||
* @param string $name Plugin folder name
|
||||
* @param string $path Full path to plugin directory
|
||||
*/
|
||||
private function loadPlugin($name, $path)
|
||||
{
|
||||
try {
|
||||
require_once $path . '/plugin.php';
|
||||
$this->activePlugins[] = $name;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Failed to load plugin [$name]: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of loaded plugins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getActivePlugins()
|
||||
{
|
||||
return $this->activePlugins;
|
||||
}
|
||||
}
|
||||
@@ -92,13 +92,22 @@ class Router {
|
||||
}
|
||||
|
||||
public function dispatch($uri, $method) {
|
||||
// Fire hook to allow plugins to register routes
|
||||
\App\Core\Hooks::doAction('router_init', $this);
|
||||
|
||||
$path = parse_url($uri, PHP_URL_PATH);
|
||||
|
||||
// Handle subdirectory
|
||||
// Handle subdirectory (SKIP for PHP Built-in Server to avoid SCRIPT_NAME issues)
|
||||
if (php_sapi_name() !== 'cli-server') {
|
||||
$scriptName = dirname($_SERVER['SCRIPT_NAME']);
|
||||
if (strpos($path, $scriptName) === 0) {
|
||||
// Normalize backslashes (Windows)
|
||||
$scriptName = str_replace('\\', '/', $scriptName);
|
||||
|
||||
// Ensure we don't strip root slash
|
||||
if ($scriptName !== '/' && strpos($path, $scriptName) === 0) {
|
||||
$path = substr($path, strlen($scriptName));
|
||||
}
|
||||
}
|
||||
$path = $this->normalizePath($path);
|
||||
|
||||
// Global Install Check
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace App\Helpers;
|
||||
class HotspotHelper
|
||||
{
|
||||
/**
|
||||
* Parse profile on-login script metadata (Mikhmon format)
|
||||
* Parse profile on-login script metadata (Standard format)
|
||||
* Format: :put (",mode,price,validity,selling_price,lock_user,")
|
||||
*/
|
||||
public static function parseProfileMetadata($script) {
|
||||
|
||||
@@ -40,6 +40,6 @@ class LanguageHelper
|
||||
}
|
||||
}
|
||||
|
||||
return $languages;
|
||||
return \App\Core\Hooks::applyFilters('get_available_languages', $languages);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@ class TemplateHelper {
|
||||
'{{ip_address}}' => '192.168.88.254',
|
||||
'{{mac_address}}' => 'AA:BB:CC:DD:EE:FF',
|
||||
'{{comment}}' => 'Thank You',
|
||||
'{{copyright}}' => 'Mikhmon',
|
||||
'{{copyright}}' => 'Mivo',
|
||||
];
|
||||
|
||||
$content = str_replace(array_keys($dummyData), array_values($dummyData), $content);
|
||||
|
||||
// QR Code replacement
|
||||
$content = preg_replace('/\{\{\s*qrcode.*?\}\}/i', '<img src="https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=http://hotspot.lan/login?user=u-5829" style="width:80px;height:80px;display:inline-block;">', $content);
|
||||
// QR Code replacement - Using canvas for client-side rendering with QRious
|
||||
$content = preg_replace('/\{\{\s*qrcode\s*(.*?)\s*\}\}/i', '<canvas class="qrcode-placeholder" data-options=\'$1\' style="width:80px;height:80px;display:inline-block;"></canvas>', $content);
|
||||
|
||||
return $content;
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class TemplateHelper {
|
||||
body { display: flex; align-items: center; justify-content: center; background-color: transparent; }
|
||||
#wrapper { display: inline-block; transform-origin: center center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
||||
</style>
|
||||
<script src="/assets/js/qrious.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">' . $mockContent . '</div>
|
||||
@@ -77,6 +78,48 @@ class TemplateHelper {
|
||||
const wrap = document.getElementById("wrapper");
|
||||
if(!wrap) return;
|
||||
|
||||
// Render QR Codes
|
||||
document.querySelectorAll(".qrcode-placeholder").forEach(canvas => {
|
||||
const optionsStr = canvas.dataset.options || "";
|
||||
const options = {};
|
||||
|
||||
// Robust parser for "fg=red bg=#fff size=100" format
|
||||
const regex = /([a-z]+)=([^ \t\r\n\f\v"]+|"[^"]*"|\'[^\']*\')/gi;
|
||||
let match;
|
||||
while ((match = regex.exec(optionsStr)) !== null) {
|
||||
let key = match[1].toLowerCase();
|
||||
let val = match[2].replace(/["\']/g, "");
|
||||
options[key] = val;
|
||||
}
|
||||
|
||||
new QRious({
|
||||
element: canvas,
|
||||
value: "http://hotspot.lan/login?username=u-5829&password=5912",
|
||||
size: parseInt(options.size) || 100,
|
||||
foreground: options.fg || "#000000",
|
||||
backgroundAlpha: 0,
|
||||
level: "M"
|
||||
});
|
||||
|
||||
// Handle styles via CSS for better compatibility with rounding and padding
|
||||
if (options.size) {
|
||||
canvas.style.width = options.size + "px";
|
||||
canvas.style.height = options.size + "px";
|
||||
}
|
||||
|
||||
if (options.bg) {
|
||||
canvas.style.backgroundColor = options.bg;
|
||||
}
|
||||
|
||||
if (options.padding) {
|
||||
canvas.style.padding = options.padding + "px";
|
||||
}
|
||||
|
||||
if (options.rounded) {
|
||||
canvas.style.borderRadius = options.rounded + "px";
|
||||
}
|
||||
});
|
||||
|
||||
const updateScale = () => {
|
||||
const w = wrap.offsetWidth;
|
||||
const h = wrap.offsetHeight;
|
||||
|
||||
@@ -326,6 +326,30 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
setInterval(fetchTraffic, reloadInterval);
|
||||
fetchTraffic();
|
||||
});
|
||||
|
||||
// Localization Support
|
||||
const updateChartLabels = () => {
|
||||
if (window.i18n && window.i18n.isLoaded) {
|
||||
const rxLabel = window.i18n.t('dashboard.rx_download');
|
||||
const txLabel = window.i18n.t('dashboard.tx_upload');
|
||||
|
||||
// Only update if changed
|
||||
if (chart.data.datasets[0].label !== rxLabel || chart.data.datasets[1].label !== txLabel) {
|
||||
chart.data.datasets[0].label = rxLabel;
|
||||
chart.data.datasets[1].label = txLabel;
|
||||
chart.update('none');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for language changes
|
||||
if (window.Mivo) {
|
||||
window.Mivo.on('languageChanged', updateChartLabels);
|
||||
}
|
||||
window.addEventListener('languageChanged', updateChartLabels);
|
||||
|
||||
// Try initial update after a short delay to ensure i18n is ready if race condition
|
||||
setTimeout(updateChartLabels, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Design System</h1>
|
||||
<p class="text-accents-5">Component library and style guide for Mikhmon v3.</p>
|
||||
<p class="text-accents-5">Component library and style guide for Mivo.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="document.documentElement.classList.remove('dark')" class="btn bg-gray-200 text-gray-800">Light</button>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div class="w-full max-w-4xl mx-auto py-8 md:py-16 px-4 sm:px-6 text-center">
|
||||
<div class="mb-8 flex justify-center">
|
||||
<div class="h-16 w-16 bg-transparent rounded-full flex items-center justify-center">
|
||||
<img src="/assets/img/logo-m.svg" alt="Mikhmon Logo" class="h-16 w-auto block dark:hidden">
|
||||
<img src="/assets/img/logo-m-dark.svg" alt="Mikhmon Logo" class="h-16 w-auto hidden dark:block">
|
||||
<img src="/assets/img/logo-m.svg" alt="Mivo Logo" class="h-16 w-auto block dark:hidden">
|
||||
<img src="/assets/img/logo-m-dark.svg" alt="Mivo Logo" class="h-16 w-auto hidden dark:block">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<footer class="border-t border-accents-2 bg-background mt-auto transition-colors duration-200 py-8 text-center space-y-4">
|
||||
<!-- Links Row -->
|
||||
<div class="flex justify-center items-center gap-6 text-sm font-medium text-accents-5">
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://mivodev.github.io" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span>Repo</span>
|
||||
</a>
|
||||
@@ -306,5 +306,6 @@
|
||||
}, 300); // 300ms delay to prevent accidental closure
|
||||
}
|
||||
</script>
|
||||
<?php \App\Core\Hooks::doAction('mivo_footer'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<footer class="mt-auto py-8 text-center space-y-4">
|
||||
<div class="flex justify-center items-center gap-6 text-sm font-medium text-accents-5">
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://mivodev.github.io" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span>Docs</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span>Community</span>
|
||||
</a>
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<a href="https://github.com/mivodev/mivo" target="_blank" class="hover:text-foreground transition-colors flex items-center gap-2">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span>Repo</span>
|
||||
</a>
|
||||
@@ -79,5 +79,6 @@
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<?php \App\Core\Hooks::doAction('mivo_footer'); ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,8 +19,8 @@ $title = isset($title) ? $title : \App\Config\SiteConfig::APP_NAME;
|
||||
<!-- Tailwind CSS (Local) -->
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
|
||||
<!-- Flag Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" />
|
||||
<!-- Flag Icons (Local) -->
|
||||
<link rel="stylesheet" href="/assets/vendor/flag-icons/css/flag-icons.min.css" />
|
||||
|
||||
|
||||
<style>
|
||||
@@ -114,6 +114,8 @@ $title = isset($title) ? $title : \App\Config\SiteConfig::APP_NAME;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<?php \App\Core\Hooks::doAction('mivo_head'); ?>
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen bg-background text-foreground anti-aliased relative">
|
||||
<!-- Background Elements (Global Sci-Fi Grid) -->
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
</script>
|
||||
<?php \App\Core\Hooks::doAction('mivo_head'); ?>
|
||||
</head>
|
||||
<body class="bg-background text-foreground antialiased min-h-screen relative overflow-hidden font-sans selection:bg-accents-2 selection:text-foreground flex flex-col">
|
||||
|
||||
|
||||
@@ -53,8 +53,9 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<?php
|
||||
$languages = \App\Helpers\LanguageHelper::getAvailableLanguages();
|
||||
foreach ($languages as $lang):
|
||||
$pathArg = isset($lang['path']) ? "', '" . $lang['path'] : "";
|
||||
?>
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $lang['code'] ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<button onclick="Mivo.modules.I18n.loadLanguage('<?= $lang['code'] ?><?= $pathArg ?>')" class="w-full text-left flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-accents-1 transition-colors text-accents-6 hover:text-foreground group/lang">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-sm shadow-sm transition-transform group-hover/lang:scale-110"></span>
|
||||
<span><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
@@ -123,8 +124,10 @@ $uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||
<span class="text-xs font-bold text-accents-4 uppercase tracking-wider">Select Language</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide snap-x">
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?>')" class="flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full border border-accents-2 bg-background hover:border-foreground transition-all text-sm font-medium snap-start shadow-sm">
|
||||
<?php foreach ($languages as $lang):
|
||||
$pathArg = isset($lang['path']) ? "', '" . $lang['path'] : "";
|
||||
?>
|
||||
<button onclick="changeLanguage('<?= $lang['code'] ?><?= $pathArg ?>')" class="flex-shrink-0 flex items-center gap-2 px-4 py-2 rounded-full border border-accents-2 bg-background hover:border-foreground transition-all text-sm font-medium snap-start shadow-sm">
|
||||
<span class="fi fi-<?= $lang['flag'] ?> rounded-full shadow-sm"></span>
|
||||
<span class="whitespace-nowrap"><?= $lang['name'] ?></span>
|
||||
</button>
|
||||
|
||||
@@ -390,21 +390,21 @@ $getInitials = function($name) {
|
||||
</div>
|
||||
|
||||
<!-- Docs -->
|
||||
<a href="https://docs.mivo.dyzulk.com" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<a href="https://mivodev.github.io" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="book-open" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.docs">Documentation</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
</a>
|
||||
|
||||
<!-- Community -->
|
||||
<a href="https://github.com/dyzulk/mivo/issues" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<a href="https://github.com/mivodev/mivo/discussions" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="message-circle" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.community">Community</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
</a>
|
||||
|
||||
<!-- Repo -->
|
||||
<a href="https://github.com/dyzulk/mivo" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<a href="https://github.com/mivodev/mivo" target="_blank" class="flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors text-accents-6 hover:text-foreground hover:bg-white/5">
|
||||
<i data-lucide="github" class="w-4 h-4"></i>
|
||||
<span data-i18n="sidebar.repo">Repository</span>
|
||||
<i data-lucide="external-link" class="w-3 h-3 ml-auto opacity-50"></i>
|
||||
|
||||
@@ -14,6 +14,7 @@ $menu = [
|
||||
['label' => 'templates_title', 'url' => '/settings/voucher-templates', 'namespace' => 'settings'],
|
||||
['label' => 'logos_title', 'url' => '/settings/logos', 'namespace' => 'settings'],
|
||||
['label' => 'api_cors_title', 'url' => '/settings/api-cors', 'namespace' => 'settings'],
|
||||
['label' => 'plugins_title', 'url' => '/settings/plugins', 'namespace' => 'settings'],
|
||||
];
|
||||
?>
|
||||
<nav id="settings-sidebar" class="w-full sticky top-[64px] z-40 bg-background/95 backdrop-blur border-b border-accents-2 transition-all duration-300">
|
||||
|
||||
@@ -26,8 +26,7 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
|
||||
<!-- Daily Tab -->
|
||||
<div id="content-daily" class="tab-content">
|
||||
<div class="table-container">
|
||||
<table class="table-glass">
|
||||
<table class="table-glass" id="table-daily">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="reports.date">Date</th>
|
||||
@@ -38,18 +37,16 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<?php foreach ($daily as $date => $total): ?>
|
||||
<tr>
|
||||
<td><?= $date ?></td>
|
||||
<td class="text-right"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
<td class="text-right font-mono"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monthly Tab -->
|
||||
<div id="content-monthly" class="tab-content hidden">
|
||||
<div class="table-container">
|
||||
<table class="table-glass">
|
||||
<table class="table-glass" id="table-monthly">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="reports.month">Month</th>
|
||||
@@ -60,18 +57,16 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<?php foreach ($monthly as $date => $total): ?>
|
||||
<tr>
|
||||
<td><?= $date ?></td>
|
||||
<td class="text-right"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
<td class="text-right font-mono"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Tab -->
|
||||
<div id="content-yearly" class="tab-content hidden">
|
||||
<div class="table-container">
|
||||
<table class="table-glass">
|
||||
<table class="table-glass" id="table-yearly">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="reports.year">Year</th>
|
||||
@@ -82,15 +77,24 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<?php foreach ($yearly as $date => $total): ?>
|
||||
<tr>
|
||||
<td><?= $date ?></td>
|
||||
<td class="text-right"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
<td class="text-right font-mono"><?= $currency ?> <?= number_format($total, 0, ',', '.') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/components/datatable.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Init Datatables
|
||||
if (typeof SimpleDataTable !== 'undefined') {
|
||||
new SimpleDataTable('#table-daily', { itemsPerPage: 10, searchable: true });
|
||||
new SimpleDataTable('#table-monthly', { itemsPerPage: 10, searchable: true });
|
||||
new SimpleDataTable('#table-yearly', { itemsPerPage: 10, searchable: true });
|
||||
}
|
||||
});
|
||||
|
||||
function switchTab(tabName) {
|
||||
// Hide all contents
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
|
||||
@@ -9,6 +9,20 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<p class="text-accents-5"><span data-i18n="reports.selling_subtitle">Sales summary and details for:</span> <span class="text-foreground font-medium"><?= htmlspecialchars($session) ?></span></p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="dropdown dropdown-end relative" id="export-dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" onclick="document.getElementById('export-menu').classList.toggle('hidden')">
|
||||
<i data-lucide="download" class="w-4 h-4 mr-2"></i> <span data-i18n="reports.export">Export</span>
|
||||
<i data-lucide="chevron-down" class="w-4 h-4 ml-1"></i>
|
||||
</button>
|
||||
<div id="export-menu" class="dropdown-menu hidden absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white dark:bg-black border border-accents-2 z-50 p-1">
|
||||
<button onclick="exportReport('csv')" class="block w-full text-left px-4 py-2 text-sm text-foreground hover:bg-accents-1 rounded flex items-center">
|
||||
<i data-lucide="file-text" class="w-4 h-4 mr-2 text-green-600"></i> Export CSV
|
||||
</button>
|
||||
<button onclick="exportReport('xlsx')" class="block w-full text-left px-4 py-2 text-sm text-foreground hover:bg-accents-1 rounded flex items-center">
|
||||
<i data-lucide="sheet" class="w-4 h-4 mr-2 text-green-600"></i> Export Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="location.reload()" class="btn btn-secondary">
|
||||
<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> <span data-i18n="reports.refresh">Refresh</span>
|
||||
</button>
|
||||
@@ -17,57 +31,74 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div class="card bg-accents-1 border-accents-2">
|
||||
<div class="text-sm text-accents-5 uppercase font-bold tracking-wide" data-i18n="reports.total_income">Total Income</div>
|
||||
<div class="text-3xl font-bold text-green-500 mt-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Stock / Potential -->
|
||||
<div class="card">
|
||||
<div class="text-sm text-accents-5 uppercase font-bold tracking-wide" data-i18n="reports.generated_stock">Generated Stock</div>
|
||||
<div class="text-3xl font-bold text-accents-6 mt-2">
|
||||
<?= \App\Helpers\FormatHelper::formatCurrency($totalIncome, $currency) ?>
|
||||
</div>
|
||||
<div class="text-xs text-accents-5 mt-1">
|
||||
<?= number_format($totalVouchers) ?> vouchers
|
||||
</div>
|
||||
<div class="card bg-accents-1 border-accents-2">
|
||||
<div class="text-sm text-accents-5 uppercase font-bold tracking-wide" data-i18n="reports.total_vouchers">Total Vouchers Sold</div>
|
||||
<div class="text-3xl font-bold text-blue-500 mt-2">
|
||||
<?= number_format($totalVouchers, 0, ',', '.') ?>
|
||||
</div>
|
||||
|
||||
<!-- Realized / Actual -->
|
||||
<div class="card !bg-green-500/10 !border-green-500/20">
|
||||
<div class="text-sm text-green-600 dark:text-green-400 uppercase font-bold tracking-wide" data-i18n="reports.realized_income">Realized Income</div>
|
||||
<div class="text-3xl font-bold text-green-600 dark:text-green-400 mt-2">
|
||||
<?= \App\Helpers\FormatHelper::formatCurrency($totalRealizedIncome ?? 0, $currency) ?>
|
||||
</div>
|
||||
<div class="text-xs text-green-600/70 dark:text-green-400/70 mt-1">
|
||||
<?= number_format($totalUsedVouchers ?? 0) ?> used
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Filter Bar -->
|
||||
<div class="flex flex-col md:flex-row gap-4 justify-between items-center no-print">
|
||||
<!-- Search -->
|
||||
<div class="relative w-full md:w-64">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i data-lucide="search" class="h-4 w-4 text-accents-5"></i>
|
||||
</div>
|
||||
<input type="text" id="global-search" class="form-input pl-10 w-full" placeholder="Search date..." data-i18n-placeholder="common.table.search_placeholder">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Table -->
|
||||
<div class="table-container">
|
||||
<table class="table-glass" id="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sort="date" class="sortable cursor-pointer hover:text-foreground select-none" data-i18n="reports.date_batch">Date / Batch (Comment)</th>
|
||||
<th class="text-right" data-i18n="reports.qty">Qty</th>
|
||||
<th data-sort="total" class="sortable text-right cursor-pointer hover:text-foreground select-none" data-i18n="reports.total">Total</th>
|
||||
<th data-sort="date" data-i18n="reports.date_batch">Date / Batch (Comment)</th>
|
||||
<th data-i18n="reports.status">Status</th>
|
||||
<th class="text-right" data-i18n="reports.qty">Qty (Stock)</th>
|
||||
<th class="text-right text-green-500" data-i18n="reports.used">Used</th>
|
||||
<th data-sort="total" class="text-right" data-i18n="reports.total_stock">Total Stock</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<?php if (empty($report)): ?>
|
||||
<tr>
|
||||
<td colspan="3" class="p-8 text-center text-accents-5" data-i18n="reports.no_data">No sales data found.</td>
|
||||
<td colspan="5" class="p-8 text-center text-accents-5" data-i18n="reports.no_data">No sales data found.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($report as $row): ?>
|
||||
<tr class="table-row-item"
|
||||
data-date="<?= strtolower($row['date']) ?>"
|
||||
data-total="<?= $row['total'] ?>">
|
||||
<td class="font-medium"><?= htmlspecialchars($row['date']) ?></td>
|
||||
<td class="text-right font-mono"><?= number_format($row['count']) ?></td>
|
||||
<tr class="table-row-item">
|
||||
<td class="font-medium">
|
||||
<?= htmlspecialchars($row['date']) ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if($row['status'] === 'New'): ?>
|
||||
<span class="px-2 py-1 text-xs font-bold rounded-md bg-accents-2 text-accents-6">NEW</span>
|
||||
<?php elseif($row['status'] === 'Selling'): ?>
|
||||
<span class="px-2 py-1 text-xs font-bold rounded-md bg-blue-500/10 text-blue-500 border border-blue-500/20">SELLING</span>
|
||||
<?php elseif($row['status'] === 'Sold Out'): ?>
|
||||
<span class="px-2 py-1 text-xs font-bold rounded-md bg-green-500/10 text-green-500 border border-green-500/20">SOLD OUT</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-right font-mono text-accents-6">
|
||||
<?= number_format($row['count']) ?>
|
||||
</td>
|
||||
<td class="text-right font-mono text-green-500 font-medium">
|
||||
<?= number_format($row['realized_count']) ?>
|
||||
<span class="text-xs opacity-70 block">
|
||||
<?= \App\Helpers\FormatHelper::formatCurrency($row['realized_total'], $currency) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right font-mono font-bold text-foreground">
|
||||
<?= \App\Helpers\FormatHelper::formatCurrency($row['total'], $currency) ?>
|
||||
</td>
|
||||
@@ -76,130 +107,84 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 border-t border-white/10 flex items-center justify-between no-print" id="pagination-controls">
|
||||
<div class="text-sm text-accents-5">
|
||||
Showing <span id="start-idx" class="font-medium text-foreground">0</span> to <span id="end-idx" class="font-medium text-foreground">0</span> of <span id="total-count" class="font-medium text-foreground">0</span> rows
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button id="prev-btn" class="btn btn-sm btn-secondary" disabled data-i18n="common.previous">Previous</button>
|
||||
<div id="page-numbers" class="flex gap-1"></div>
|
||||
<button id="next-btn" class="btn btn-sm btn-secondary" disabled data-i18n="common.next">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/components/datatable.js"></script>
|
||||
<!-- Local SheetJS Library -->
|
||||
<script src="/assets/vendor/xlsx/xlsx.full.min.js"></script>
|
||||
|
||||
<script>
|
||||
class TableManager {
|
||||
constructor(rows, itemsPerPage = 15) {
|
||||
this.allRows = Array.from(rows);
|
||||
this.filteredRows = this.allRows;
|
||||
this.itemsPerPage = itemsPerPage;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.elements = {
|
||||
body: document.getElementById('table-body'),
|
||||
startIdx: document.getElementById('start-idx'),
|
||||
endIdx: document.getElementById('end-idx'),
|
||||
totalCount: document.getElementById('total-count'),
|
||||
prevBtn: document.getElementById('prev-btn'),
|
||||
nextBtn: document.getElementById('next-btn'),
|
||||
pageNumbers: document.getElementById('page-numbers')
|
||||
};
|
||||
|
||||
this.filters = { search: '' };
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Translate placeholder
|
||||
const searchInput = document.getElementById('global-search');
|
||||
if (searchInput && window.i18n) {
|
||||
searchInput.placeholder = window.i18n.t('common.table.search_placeholder');
|
||||
}
|
||||
document.getElementById('global-search').addEventListener('input', (e) => {
|
||||
this.filters.search = e.target.value.toLowerCase();
|
||||
this.currentPage = 1;
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.elements.prevBtn.addEventListener('click', () => { if(this.currentPage > 1) { this.currentPage--; this.render(); } });
|
||||
this.elements.nextBtn.addEventListener('click', () => {
|
||||
const max = Math.ceil(this.filteredRows.length / this.itemsPerPage);
|
||||
if(this.currentPage < max) { this.currentPage++; this.render(); }
|
||||
});
|
||||
|
||||
this.update();
|
||||
|
||||
// Listen for language change
|
||||
window.addEventListener('languageChanged', () => {
|
||||
const searchInput = document.getElementById('global-search');
|
||||
if (searchInput && window.i18n) {
|
||||
searchInput.placeholder = window.i18n.t('common.table.search_placeholder');
|
||||
}
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.filteredRows = this.allRows.filter(row => {
|
||||
const date = row.dataset.date || '';
|
||||
|
||||
if (this.filters.search && !date.includes(this.filters.search)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const total = this.filteredRows.length;
|
||||
const maxPage = Math.ceil(total / this.itemsPerPage) || 1;
|
||||
if (this.currentPage > maxPage) this.currentPage = maxPage;
|
||||
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
const end = Math.min(start + this.itemsPerPage, total);
|
||||
|
||||
this.elements.startIdx.textContent = total === 0 ? 0 : start + 1;
|
||||
this.elements.endIdx.textContent = end;
|
||||
this.elements.totalCount.textContent = total;
|
||||
|
||||
// Update Text (Use Translation)
|
||||
if (window.i18n && document.getElementById('pagination-controls')) {
|
||||
const text = window.i18n.t('common.table.showing', {
|
||||
start: total === 0 ? 0 : start + 1,
|
||||
end: end,
|
||||
total: total
|
||||
});
|
||||
// Find and update the text node if possible
|
||||
const container = document.getElementById('pagination-controls').querySelector('.text-accents-5');
|
||||
if(container) {
|
||||
container.innerHTML = text.replace('{start}', `<span class="font-medium text-foreground">${total === 0 ? 0 : start + 1}</span>`)
|
||||
.replace('{end}', `<span class="font-medium text-foreground">${end}</span>`)
|
||||
.replace('{total}', `<span class="font-medium text-foreground">${total}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
this.elements.body.innerHTML = '';
|
||||
this.filteredRows.slice(start, end).forEach(row => this.elements.body.appendChild(row));
|
||||
|
||||
this.elements.prevBtn.disabled = this.currentPage === 1;
|
||||
this.elements.nextBtn.disabled = this.currentPage === maxPage || total === 0;
|
||||
|
||||
if (this.elements.pageNumbers) {
|
||||
const pageText = window.i18n ? window.i18n.t('common.page_of', {current: this.currentPage, total: maxPage}) : `Page ${this.currentPage} of ${maxPage}`;
|
||||
this.elements.pageNumbers.innerHTML = `<span class="px-3 py-1 text-sm font-medium bg-accents-2 rounded text-accents-6">${pageText}</span>`;
|
||||
}
|
||||
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new TableManager(document.querySelectorAll('.table-row-item'), 15);
|
||||
if (typeof SimpleDataTable !== 'undefined') {
|
||||
new SimpleDataTable('#report-table', {
|
||||
itemsPerPage: 15,
|
||||
searchable: true,
|
||||
pagination: true,
|
||||
// Add Filter for Status Column (Index 1)
|
||||
filters: [
|
||||
{ index: 1, label: 'Status: All' }
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function exportReport(type) {
|
||||
const url = '/<?= $session ?>/reports/selling/export/' + type;
|
||||
const btn = document.querySelector('.dropdown-toggle');
|
||||
const originalText = btn.innerHTML;
|
||||
|
||||
// Show Loading State
|
||||
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 mr-2 animate-spin"></i> Processing...`;
|
||||
lucide.createIcons();
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
alert('Export Failed: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = `selling-report-<?= date('Y-m-d') ?>-${type}.` + (type === 'csv' ? 'csv' : 'xlsx');
|
||||
|
||||
if (type === 'csv') {
|
||||
// Convert JSON to CSV
|
||||
const header = Object.keys(data[0]);
|
||||
const csv = [
|
||||
header.join(','), // header row first
|
||||
...data.map(row => header.map(fieldName => JSON.stringify(row[fieldName])).join(','))
|
||||
].join('\r\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('hidden', '');
|
||||
a.setAttribute('href', url);
|
||||
a.setAttribute('download', filename);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
else if (type === 'xlsx') {
|
||||
// Use SheetJS for Real Excel
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, "Selling Report");
|
||||
XLSX.writeFile(wb, filename);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Export Error:', error);
|
||||
alert('Failed to export data. Check console for details.');
|
||||
} finally {
|
||||
// Restore Button
|
||||
btn.innerHTML = originalText;
|
||||
lucide.createIcons();
|
||||
document.getElementById('export-menu').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
|
||||
129
app/Views/settings/plugins.php
Normal file
129
app/Views/settings/plugins.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
// Plugins View
|
||||
$title = "Plugins";
|
||||
$no_main_container = true;
|
||||
require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
?>
|
||||
|
||||
<!-- Sub-Navbar Navigation -->
|
||||
<?php include ROOT . '/app/Views/layouts/sidebar_settings.php'; ?>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 flex-grow w-full flex flex-col">
|
||||
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight" data-i18n="settings.plugins">Plugins</h1>
|
||||
<p class="text-accents-5 mt-2" data-i18n="settings.plugins_desc">Manage and extend functionality with plugins.</p>
|
||||
</div>
|
||||
<button onclick="openUploadModal()" class="btn btn-primary">
|
||||
<i data-lucide="upload" class="w-4 h-4 mr-2"></i>
|
||||
<span data-i18n="settings.upload_plugin">Upload Plugin</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="mt-8 flex-1 min-w-0" id="settings-content-area">
|
||||
|
||||
<div class="card overflow-hidden p-0">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs text-accents-5 uppercase bg-accents-1/50 border-b border-accents-2 font-semibold tracking-wider">
|
||||
<tr>
|
||||
<th class="px-6 py-4 w-[250px]" data-i18n="common.name">Name</th>
|
||||
<th class="px-6 py-4" data-i18n="common.description">Description</th>
|
||||
<th class="px-6 py-4 w-[100px]" data-i18n="common.version">Version</th>
|
||||
<th class="px-6 py-4 w-[150px]" data-i18n="common.author">Author</th>
|
||||
<th class="px-6 py-4 w-[100px] text-right" data-i18n="common.status">Status</th>
|
||||
<th class="px-6 py-4 w-[100px] text-right" data-i18n="common.actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-accents-2">
|
||||
<?php if(empty($plugins)): ?>
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-12 text-center text-accents-5">
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
<div class="p-3 rounded-full bg-accents-1">
|
||||
<i data-lucide="package-search" class="w-6 h-6 text-accents-4"></i>
|
||||
</div>
|
||||
<span class="font-medium" data-i18n="settings.no_plugins">No plugins installed</span>
|
||||
<span class="text-xs" data-i18n="settings.no_plugins_desc">Upload a .zip file to get started.</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach($plugins as $plugin): ?>
|
||||
<tr class="group hover:bg-accents-1/30 transition-colors">
|
||||
<td class="px-6 py-4 font-medium text-foreground">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 rounded bg-primary/10 flex items-center justify-center text-primary">
|
||||
<i data-lucide="plug" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span><?= htmlspecialchars($plugin['name']) ?></span>
|
||||
<span class="text-[10px] text-accents-4 font-normal font-mono"><?= htmlspecialchars($plugin['id']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-accents-6">
|
||||
<?= htmlspecialchars($plugin['description']) ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-accents-6 font-mono text-xs">
|
||||
<?= htmlspecialchars($plugin['version']) ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-accents-6">
|
||||
<?= htmlspecialchars($plugin['author']) ?>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-500/10 text-green-600 dark:text-green-400">
|
||||
Active
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<form action="/settings/plugins/delete" method="POST" class="inline" onsubmit="event.preventDefault();
|
||||
const title = window.i18n ? window.i18n.t('settings.delete_plugin') : 'Delete Plugin?';
|
||||
const msg = window.i18n ? window.i18n.t('settings.delete_plugin_confirm', {name: '<?= htmlspecialchars($plugin['name']) ?>'}) : 'Delete this plugin?';
|
||||
|
||||
Mivo.confirm(title, msg, window.i18n ? window.i18n.t('common.delete') : 'Delete', window.i18n ? window.i18n.t('common.cancel') : 'Cancel').then(res => {
|
||||
if(res) this.submit();
|
||||
});">
|
||||
<input type="hidden" name="plugin_id" value="<?= htmlspecialchars($plugin['id']) ?>">
|
||||
<button type="submit" class="btn-icon-danger" title="Delete">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openUploadModal() {
|
||||
const title = window.i18n ? window.i18n.t('settings.upload_plugin') : 'Upload Plugin';
|
||||
const html = `
|
||||
<form id="upload-plugin-form" action="/settings/plugins/upload" method="POST" enctype="multipart/form-data" class="space-y-4">
|
||||
<div class="text-sm text-accents-5">
|
||||
<p class="mb-4" data-i18n="settings.upload_plugin_desc">Select a plugin .zip file to install.</p>
|
||||
<input type="file" name="plugin_file" accept=".zip" required class="form-control-file w-full">
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
Mivo.modal.form(title, html, window.i18n ? window.i18n.t('common.install') : 'Install', () => {
|
||||
const form = document.getElementById('upload-plugin-form');
|
||||
if (form.reportValidity()) {
|
||||
form.submit();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
@@ -12,6 +12,13 @@ $initialContent = $template['content'] ?? '<div style="border: 1px solid #000; p
|
||||
require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
/* Make CodeMirror fill the entire container height */
|
||||
#editorContainer .cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="flex flex-col lg:h-[calc(100vh-8rem)] gap-6">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4 flex-shrink-0">
|
||||
@@ -64,7 +71,9 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="codeEditor" name="content" form="templateForm" class="form-control flex-1 w-full font-mono text-sm resize-none h-[500px]" spellcheck="false"><?= htmlspecialchars($initialContent) ?></textarea>
|
||||
<div id="editorContainer" class="flex-1 w-full font-mono text-sm h-full min-h-0 border-none outline-none overflow-hidden bg-background"></div>
|
||||
<!-- Hidden textarea for form submission -->
|
||||
<textarea id="codeEditor" name="content" form="templateForm" class="hidden"><?= htmlspecialchars($initialContent) ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Right: Preview -->
|
||||
@@ -84,6 +93,7 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/qrious.min.js"></script>
|
||||
<script src="/assets/js/vendor/editor.bundle.js"></script>
|
||||
</div>
|
||||
|
||||
<!-- Documentation Modal -->
|
||||
@@ -104,6 +114,18 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
<div class="prose dark:prose-invert max-w-none">
|
||||
<p class="text-sm text-accents-5 mb-4" data-i18n="settings.variables_desc">Use these variables in your HTML source. They will be replaced with actual user data during printing.</p>
|
||||
|
||||
<!-- NEW: Editor Shortcuts & Emmet -->
|
||||
<h3 class="text-sm font-bold uppercase text-accents-5 mb-2" data-i18n="settings.editor_shortcuts">Editor Shortcuts & Emmet</h3>
|
||||
<div class="p-4 rounded bg-accents-1 border border-accents-2 mb-6">
|
||||
<p class="text-sm text-accents-6 mb-4" data-i18n="settings.emmet_desc">Use Emmet abbreviations for fast coding. Look for the dotted underline, then press Tab.</p>
|
||||
<ul class="text-sm space-y-4 list-disc list-inside text-accents-6">
|
||||
<li data-i18n="settings.tip_emmet_html"><strong>HTML Boilerplate</strong>: Type <code>!</code> then <code>Tab</code>.</li>
|
||||
<li data-i18n="settings.tip_emmet_tag"><strong>Auto-Tag</strong>: Type <code>.container</code> then <code>Tab</code> for <code><div class="container"></code>.</li>
|
||||
<li data-i18n="settings.tip_color_picker"><strong>Color Picker</strong>: Click the color box next to hex codes (e.g., #ff0000) to open the picker.</li>
|
||||
<li data-i18n="settings.tip_syntax_error"><strong>Syntax Error</strong>: Red squiggles (and dots in the gutter) show structure errors like mismatched tags.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3 class="text-sm font-bold uppercase text-accents-5 mb-2">Basic Variables</h3>
|
||||
<div class="grid grid-cols-1 gap-2 mb-6">
|
||||
<div class="flex items-center justify-between p-2 rounded bg-accents-1 border border-accents-2">
|
||||
@@ -201,148 +223,43 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Editor Logic ---
|
||||
const editor = document.getElementById('codeEditor');
|
||||
// --- Editor Logic (CodeMirror 6) ---
|
||||
const textarea = document.getElementById('codeEditor');
|
||||
const container = document.getElementById('editorContainer');
|
||||
const preview = document.getElementById('previewContainer');
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
// History Stack for Undo/Redo
|
||||
let historyStack = [];
|
||||
let redoStack = [];
|
||||
let isTyping = false;
|
||||
let typingTimer = null;
|
||||
let cmView = null;
|
||||
|
||||
// Initial State
|
||||
historyStack.push({ value: editor.value, selectionStart: 0, selectionEnd: 0 });
|
||||
|
||||
function saveState() {
|
||||
// Limit stack size
|
||||
if (historyStack.length > 50) historyStack.shift();
|
||||
|
||||
const lastState = historyStack[historyStack.length - 1];
|
||||
if (lastState && lastState.value === editor.value) return; // No change
|
||||
|
||||
historyStack.push({
|
||||
value: editor.value,
|
||||
selectionStart: editor.selectionStart,
|
||||
selectionEnd: editor.selectionEnd
|
||||
});
|
||||
redoStack = []; // Clear redo on new change
|
||||
}
|
||||
|
||||
// Debounced save for typing
|
||||
editor.addEventListener('input', (e) => {
|
||||
if (!isTyping) {
|
||||
// Save state *before* a burst of typing starts?
|
||||
// Actually usually we save *after*.
|
||||
// For robust undo: save state Before modification if possible, or assume previous state is safe.
|
||||
// Simplified: Save debounced.
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(saveState, 500);
|
||||
}
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// --- Keyboard Shortcuts (Undo/Redo, Tab, Enter) ---
|
||||
editor.addEventListener('keydown', function(e) {
|
||||
// Undo: Ctrl+Z
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
undo();
|
||||
return;
|
||||
}
|
||||
// Redo: Ctrl+Y or Ctrl+Shift+Z
|
||||
if (((e.ctrlKey || e.metaKey) && e.key === 'y') || ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey)) {
|
||||
e.preventDefault();
|
||||
redo();
|
||||
function initEditor() {
|
||||
if (typeof MivoEditor === 'undefined') {
|
||||
console.error('CodeMirror bundle not loaded yet.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Tab: Insert/Remove Indent
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
const val = this.value;
|
||||
const tabChar = " "; // 4 spaces
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Un-indent (naive single line)
|
||||
// TODO: Multiline support if needed. For now simple cursor unindent.
|
||||
// Checking previous chars
|
||||
// Not implemented for simplicity, just preventing focus loss.
|
||||
} else {
|
||||
// Insert Tab
|
||||
// Use setRangeText to preserve browser undo buffer if mixed usage?
|
||||
// But we have custom undo.
|
||||
this.value = val.substring(0, start) + tabChar + val.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + tabChar.length;
|
||||
saveState();
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
// Enter: Auto-indent checking previous line
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
const val = this.value;
|
||||
|
||||
// Find start of current line
|
||||
const lineStart = val.lastIndexOf('\n', start - 1) + 1;
|
||||
const currentLine = val.substring(lineStart, start);
|
||||
|
||||
// Calculate indentation
|
||||
const match = currentLine.match(/^\s*/);
|
||||
const indent = match ? match[0] : '';
|
||||
|
||||
const insert = '\n' + indent;
|
||||
|
||||
this.value = val.substring(0, start) + insert + val.substring(end);
|
||||
this.selectionStart = this.selectionEnd = start + insert.length;
|
||||
|
||||
saveState(); // Immediate save on Enter
|
||||
cmView = MivoEditor.init({
|
||||
parent: container,
|
||||
initialValue: textarea.value,
|
||||
dark: isDark,
|
||||
onChange: (val) => {
|
||||
textarea.value = val;
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
|
||||
function undo() {
|
||||
if (historyStack.length > 1) { // Keep initial state
|
||||
const current = historyStack.pop();
|
||||
redoStack.push(current);
|
||||
|
||||
const prev = historyStack[historyStack.length - 1];
|
||||
editor.value = prev.value;
|
||||
editor.selectionStart = prev.selectionStart;
|
||||
editor.selectionEnd = prev.selectionEnd;
|
||||
updatePreview();
|
||||
}
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (redoStack.length > 0) {
|
||||
const next = redoStack.pop();
|
||||
historyStack.push(next);
|
||||
|
||||
editor.value = next.value;
|
||||
editor.selectionStart = next.selectionStart;
|
||||
editor.selectionEnd = next.selectionEnd;
|
||||
updatePreview();
|
||||
}
|
||||
// Set focus
|
||||
cmView.focus();
|
||||
}
|
||||
|
||||
function insertVar(text) {
|
||||
saveState(); // Save state before insertion
|
||||
if (!cmView) return;
|
||||
|
||||
const start = editor.selectionStart;
|
||||
const end = editor.selectionEnd;
|
||||
const val = editor.value;
|
||||
editor.value = val.substring(0, start) + text + val.substring(end);
|
||||
editor.selectionStart = editor.selectionEnd = start + text.length;
|
||||
editor.focus();
|
||||
|
||||
saveState(); // Save state after insertion
|
||||
updatePreview();
|
||||
const selection = cmView.state.selection.main;
|
||||
cmView.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: text },
|
||||
selection: { anchor: selection.from + text.length }
|
||||
});
|
||||
cmView.focus();
|
||||
}
|
||||
|
||||
// Live Preview Logic
|
||||
@@ -359,16 +276,16 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
'{{timelimit}}': ' 3 Hours',
|
||||
'{{datalimit}}': '500 MB',
|
||||
'{{profile}}': 'General',
|
||||
'{{comment}}': 'mikhmon',
|
||||
'{{hotspotname}}': 'Mikhmon Hotspot',
|
||||
'{{comment}}': 'mivo',
|
||||
'{{hotspotname}}': 'Mivo Hotspot',
|
||||
'{{num}}': '1',
|
||||
'{{logo}}': '<img src="/assets/img/logo.png" style="height:30px;border:0;">', // Default placeholder
|
||||
'{{dns_name}}': 'hotspot.mikhmon',
|
||||
'{{login_url}}': 'http://hotspot.mikhmon/login',
|
||||
'{{dns_name}}': 'hotspot.mivo',
|
||||
'{{login_url}}': 'http://hotspot.mivo/login',
|
||||
};
|
||||
|
||||
function updatePreview() {
|
||||
let content = editor.value;
|
||||
let content = textarea.value;
|
||||
|
||||
// 1. Handle {{logo id=...}}
|
||||
content = content.replace(/\{\{logo\s+id=['"]?([^'"\s]+)['"]?\}\}/gi, (match, id) => {
|
||||
@@ -479,10 +396,39 @@ require_once ROOT . '/app/Views/layouts/header_main.php';
|
||||
preview.innerHTML = content;
|
||||
}
|
||||
|
||||
editor.addEventListener('input', updatePreview); // Handled by debouncer above too, but OK.
|
||||
|
||||
// Init
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initEditor();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
// Theme Switch Recognition
|
||||
window.addEventListener('languageChanged', () => {
|
||||
// Not language, but theme toggle button often triggers layout shifts.
|
||||
// We might need a MutationObserver if we want to live-toggle CM theme.
|
||||
// For now, reload or manual re-init on theme toggle could work.
|
||||
});
|
||||
|
||||
// Watch for theme changes globally
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'class' && mutation.target === document.documentElement) {
|
||||
// Theme changed
|
||||
// CodeMirror 6 themes are extensions, changing them requires re-configuring the state.
|
||||
// For simplicity, let's just re-init everything if theme changes.
|
||||
const newIsDark = document.documentElement.classList.contains('dark');
|
||||
if (cmView) {
|
||||
const content = cmView.state.doc.toString();
|
||||
container.innerHTML = '';
|
||||
cmView = null;
|
||||
textarea.value = content;
|
||||
initEditor();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true });
|
||||
|
||||
</script>
|
||||
|
||||
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dyzulk/mivo",
|
||||
"name": "mivodev/mivo",
|
||||
"description": "MIVO - Modern Mikrotik Voucher Management System",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "DyzulkDev",
|
||||
"email": "dev@dyzulk.com"
|
||||
"name": "MivoDev",
|
||||
"email": "mivo@dev.dyzulk.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
|
||||
53
deploy.ps1
53
deploy.ps1
@@ -1,53 +0,0 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Configuration
|
||||
$RemotePath = "/www/wwwroot/app.mivo.dyzulk.com"
|
||||
|
||||
Write-Host "Starting Deployment to app.mivo.dyzulk.com..." -ForegroundColor Green
|
||||
|
||||
# 1. Build Assets
|
||||
Write-Host "Building assets..." -ForegroundColor Cyan
|
||||
cmd /c "npm run build"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Build failed!"
|
||||
}
|
||||
|
||||
# 2. Create Archive
|
||||
Write-Host "Creating deployment package..." -ForegroundColor Cyan
|
||||
# Excluding potential garbage
|
||||
$excludeParams = @("--exclude", "node_modules", "--exclude", ".git", "--exclude", ".github", "--exclude", "temp_debug", "--exclude", "deploy.ps1", "--exclude", "*.tar.gz")
|
||||
tar -czf deploy_package.tar.gz @excludeParams app public routes mivo src package.json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to create archive!"
|
||||
}
|
||||
|
||||
# 3. Upload
|
||||
Write-Host "Uploading to server ($RemotePath)..." -ForegroundColor Cyan
|
||||
scp deploy_package.tar.gz "aapanel:$RemotePath/"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "SCP upload failed!"
|
||||
}
|
||||
|
||||
# 4. Extract and Cleanup on Server
|
||||
Write-Host "Extracting and configuring permissions..." -ForegroundColor Cyan
|
||||
# Commands:
|
||||
# 1. cd to remote path
|
||||
# 2. Extract
|
||||
# 3. Set ownership to www:www
|
||||
# 4. Set mivo executable
|
||||
# 5. Set public folder to 755 (Laravel recommendation)
|
||||
# 6. Cleanup archive
|
||||
$remoteCommands = "cd $RemotePath && tar -xzf deploy_package.tar.gz && chown -R www:www . && chmod +x mivo && chmod -R 755 public && rm deploy_package.tar.gz"
|
||||
|
||||
ssh aapanel $remoteCommands
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Remote deployment failed!"
|
||||
}
|
||||
|
||||
# 5. Local Cleanup
|
||||
Write-Host "Cleaning up local package..." -ForegroundColor Cyan
|
||||
if (Test-Path deploy_package.tar.gz) {
|
||||
Remove-Item deploy_package.tar.gz
|
||||
}
|
||||
|
||||
Write-Host "Deployment successfully completed!" -ForegroundColor Green
|
||||
@@ -2,7 +2,7 @@ version: '3.8'
|
||||
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:${VERSION:-latest}
|
||||
image: ghcr.io/mivodev/mivo:${VERSION:-latest}
|
||||
container_name: ${CONTAINER_NAME:-mivo}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
25
docker/entrypoint.sh
Normal file
25
docker/entrypoint.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Ensure Database directory exists
|
||||
mkdir -p /var/www/html/app/Database
|
||||
|
||||
# Fix permissions for the Database directory
|
||||
# This is crucial for SQLite when volumes are mounted from host
|
||||
if [ -d "/var/www/html/app/Database" ]; then
|
||||
chown -R www-data:www-data /var/www/html/app/Database
|
||||
chmod -R 775 /var/www/html/app/Database
|
||||
fi
|
||||
|
||||
# Also ensure .env is writable if it exists, or create it from example
|
||||
if [ ! -f "/var/www/html/.env" ] && [ -f "/var/www/html/.env.example" ]; then
|
||||
cp /var/www/html/.env.example /var/www/html/.env
|
||||
chown www-data:www-data /var/www/html/.env
|
||||
fi
|
||||
|
||||
if [ -f "/var/www/html/.env" ]; then
|
||||
chmod 664 /var/www/html/.env
|
||||
fi
|
||||
|
||||
# Execute the command passed to docker run (usually supervisor)
|
||||
exec "$@"
|
||||
61
docs/.vitepress/cache/deps/_metadata.json
vendored
61
docs/.vitepress/cache/deps/_metadata.json
vendored
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"hash": "351ebb31",
|
||||
"configHash": "9f61585a",
|
||||
"lockfileHash": "a8ce03f4",
|
||||
"browserHash": "3a62ec11",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "5b1d304c",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "b482c46a",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "a5c5890d",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/integrations/useFocusTrap": {
|
||||
"src": "../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs",
|
||||
"file": "vitepress___@vueuse_integrations_useFocusTrap.js",
|
||||
"fileHash": "95f44bf0",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > mark.js/src/vanilla.js": {
|
||||
"src": "../../../../node_modules/mark.js/src/vanilla.js",
|
||||
"file": "vitepress___mark__js_src_vanilla__js.js",
|
||||
"fileHash": "9dc093d4",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > minisearch": {
|
||||
"src": "../../../../node_modules/minisearch/dist/es/index.js",
|
||||
"file": "vitepress___minisearch.js",
|
||||
"fileHash": "55d2ce5f",
|
||||
"needsInterop": false
|
||||
},
|
||||
"lucide-vue-next": {
|
||||
"src": "../../../../node_modules/lucide-vue-next/dist/esm/lucide-vue-next.js",
|
||||
"file": "lucide-vue-next.js",
|
||||
"fileHash": "d70b9ca5",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-2CLQ7TTZ": {
|
||||
"file": "chunk-2CLQ7TTZ.js"
|
||||
},
|
||||
"chunk-LE5NDSFD": {
|
||||
"file": "chunk-LE5NDSFD.js"
|
||||
},
|
||||
"chunk-PZ5AY32C": {
|
||||
"file": "chunk-PZ5AY32C.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
9719
docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js
vendored
9719
docs/.vitepress/cache/deps/chunk-2CLQ7TTZ.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
12824
docs/.vitepress/cache/deps/chunk-LE5NDSFD.js
vendored
12824
docs/.vitepress/cache/deps/chunk-LE5NDSFD.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
10
docs/.vitepress/cache/deps/chunk-PZ5AY32C.js
vendored
10
docs/.vitepress/cache/deps/chunk-PZ5AY32C.js
vendored
@@ -1,10 +0,0 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
|
||||
export {
|
||||
__export
|
||||
};
|
||||
//# sourceMappingURL=chunk-PZ5AY32C.js.map
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
39046
docs/.vitepress/cache/deps/lucide-vue-next.js
vendored
39046
docs/.vitepress/cache/deps/lucide-vue-next.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
3
docs/.vitepress/cache/deps/package.json
vendored
3
docs/.vitepress/cache/deps/package.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,584 +0,0 @@
|
||||
import {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
} from "./chunk-2CLQ7TTZ.js";
|
||||
import "./chunk-LE5NDSFD.js";
|
||||
import "./chunk-PZ5AY32C.js";
|
||||
export {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
computedAsync as asyncComputed,
|
||||
refAutoReset as autoResetRef,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
computedWithControl as controlledComputed,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
reactify as createReactiveFn,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
refDebounced as debouncedRef,
|
||||
watchDebounced as debouncedWatch,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
computedEager as eagerComputed,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
watchIgnorable as ignorableWatch,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
watchPausable as pausableWatch,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
refThrottled as throttledRef,
|
||||
watchThrottled as throttledWatch,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
refDebounced as useDebounce,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
refThrottled as useThrottle,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
};
|
||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
1815
docs/.vitepress/cache/deps/vitepress___minisearch.js
vendored
1815
docs/.vitepress/cache/deps/vitepress___minisearch.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
348
docs/.vitepress/cache/deps/vue.js
vendored
348
docs/.vitepress/cache/deps/vue.js
vendored
@@ -1,348 +0,0 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-LE5NDSFD.js";
|
||||
import "./chunk-PZ5AY32C.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
nodeOps,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
patchProp,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
||||
7
docs/.vitepress/cache/deps/vue.js.map
vendored
7
docs/.vitepress/cache/deps/vue.js.map
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { sidebarEn, sidebarId } from './config/sidebars'
|
||||
import { navEn, navId } from './config/nav'
|
||||
|
||||
export default defineConfig({
|
||||
title: "MIVO",
|
||||
description: "Modern Mikrotik Voucher Management System",
|
||||
lang: 'en-US',
|
||||
cleanUrls: true,
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://docs.mivo.dyzulk.com'
|
||||
},
|
||||
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/logo-m.svg' }],
|
||||
['meta', { name: 'theme-color', content: '#0ea5e9' }],
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:site_name', content: 'MIVO' }],
|
||||
['meta', { property: 'og:image', content: 'https://docs.mivo.dyzulk.com/og-image.png' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:image', content: 'https://docs.mivo.dyzulk.com/og-image.png' }],
|
||||
['meta', { name: 'twitter:site', content: '@dyzulkdev' }]
|
||||
],
|
||||
|
||||
transformHead: ({ pageData }) => {
|
||||
const title = pageData.title ? `${pageData.title} | MIVO` : 'MIVO'
|
||||
const description = pageData.description || "Modern Mikrotik Voucher Management System"
|
||||
const url = `https://docs.mivo.dyzulk.com/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2')}`
|
||||
|
||||
return [
|
||||
['meta', { property: 'og:title', content: title }],
|
||||
['meta', { property: 'og:description', content: description }],
|
||||
['meta', { property: 'og:url', content: url }],
|
||||
['meta', { name: 'twitter:title', content: title }],
|
||||
['meta', { name: 'twitter:description', content: description }],
|
||||
]
|
||||
},
|
||||
|
||||
// Shared theme config
|
||||
themeConfig: {
|
||||
logo: {
|
||||
light: '/logo-m.svg',
|
||||
dark: '/logo-m-dark.svg'
|
||||
},
|
||||
siteTitle: 'MIVO',
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/dyzulk/mivo' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: 'Released under the MIT License.',
|
||||
copyright: `Copyright © 2026${new Date().getFullYear() > 2026 ? ' - ' + new Date().getFullYear() : ''} DyzulkDev`
|
||||
},
|
||||
|
||||
search: {
|
||||
provider: 'local'
|
||||
}
|
||||
},
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
themeConfig: {
|
||||
nav: navEn,
|
||||
sidebar: sidebarEn
|
||||
}
|
||||
},
|
||||
id: {
|
||||
label: 'Indonesia',
|
||||
lang: 'id',
|
||||
themeConfig: {
|
||||
nav: navId,
|
||||
sidebar: sidebarId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
|
||||
export const navEn: DefaultTheme.NavItem[] = [
|
||||
{ text: 'Home', link: '/' },
|
||||
{ text: 'Guide', link: '/guide/installation' },
|
||||
{ text: 'Manual', link: '/manual/' },
|
||||
{
|
||||
text: 'Community',
|
||||
items: [
|
||||
{ text: 'Changelog', link: 'https://github.com/dyzulk/mivo/releases' },
|
||||
{ text: 'Contributing', link: 'https://github.com/dyzulk/mivo/blob/main/CONTRIBUTING.md' },
|
||||
{ text: 'Forum (Issues)', link: 'https://github.com/dyzulk/mivo/issues' },
|
||||
{ text: 'Star on GitHub', link: 'https://github.com/dyzulk/mivo/stargazers' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export const navId: DefaultTheme.NavItem[] = [
|
||||
{ text: 'Beranda', link: '/id/' },
|
||||
{ text: 'Panduan', link: '/id/guide/installation' },
|
||||
{ text: 'Buku Manual', link: '/id/manual/' },
|
||||
{
|
||||
text: 'Komunitas',
|
||||
items: [
|
||||
{ text: 'Catatan Rilis', link: 'https://github.com/dyzulk/mivo/releases' },
|
||||
{ text: 'Kontribusi', link: 'https://github.com/dyzulk/mivo/blob/main/CONTRIBUTING.md' },
|
||||
{ text: 'Forum (Issues)', link: 'https://github.com/dyzulk/mivo/issues' },
|
||||
{ text: 'Beri Bintang', link: 'https://github.com/dyzulk/mivo/stargazers' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,147 +0,0 @@
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
|
||||
// English Sidebars
|
||||
export const sidebarEn: DefaultTheme.Sidebar = {
|
||||
// Sidebar for /guide/ path
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/guide/' },
|
||||
{ text: 'Requirements', link: '/guide/installation#requirements' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Installation',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Docker', link: '/guide/docker' },
|
||||
{ text: 'aaPanel (Docker)', link: '/guide/docker-aapanel' },
|
||||
{ text: 'Web Server', link: '/guide/installation#web-servers' },
|
||||
{ text: 'Shared Hosting', link: '/guide/installation#shared-hosting' },
|
||||
{ text: 'VPS & Cloud', link: '/guide/installation#vps-cloud' },
|
||||
{ text: 'Mobile & STB', link: '/guide/installation#mobile-stb' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Configuration',
|
||||
items: [
|
||||
{ text: 'Post-Installation', link: '/guide/installation#post-installation' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Support',
|
||||
items: [
|
||||
{ text: 'Contribution', link: 'https://github.com/dyzulk/mivo/blob/main/CONTRIBUTING.md' },
|
||||
{ text: 'Donate', link: 'https://sociabuzz.com/dyzulkdev/tribe' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// Sidebar for /manual/ path
|
||||
'/manual/': [
|
||||
{
|
||||
text: 'User Manual',
|
||||
items: [
|
||||
{ text: 'Overview', link: '/manual/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Global Settings',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/manual/settings/' },
|
||||
{ text: 'Routers', link: '/manual/settings/routers' },
|
||||
{ text: 'Templates', link: '/manual/settings/templates' },
|
||||
{ text: 'Logos', link: '/manual/settings/logos' },
|
||||
{ text: 'API & CORS', link: '/manual/settings/api-cors' },
|
||||
{ text: 'System', link: '/manual/settings/system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Router Operations',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/manual/router/' },
|
||||
{ text: 'Dashboard', link: '/manual/router/dashboard' },
|
||||
{ text: 'Quick Print', link: '/manual/router/quick-print' },
|
||||
{ text: 'Hotspot Management', link: '/manual/router/hotspot' },
|
||||
{ text: 'Reports & Logs', link: '/manual/router/reports' },
|
||||
{ text: 'Network & System', link: '/manual/router/tools' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Indonesian Sidebars
|
||||
export const sidebarId: DefaultTheme.Sidebar = {
|
||||
// Sidebar for /id/guide/ path
|
||||
'/id/guide/': [
|
||||
{
|
||||
text: 'Pengenalan',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Apa itu MIVO?', link: '/id/guide/' },
|
||||
{ text: 'Persyaratan', link: '/id/guide/installation#persyaratan' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Instalasi',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Docker', link: '/id/guide/docker' },
|
||||
{ text: 'aaPanel (Docker)', link: '/id/guide/docker-aapanel' },
|
||||
{ text: 'Web Server', link: '/id/guide/installation#web-server' },
|
||||
{ text: 'Shared Hosting', link: '/id/guide/installation#shared-hosting' },
|
||||
{ text: 'VPS & Cloud', link: '/id/guide/installation#vps-cloud' },
|
||||
{ text: 'Mobile & STB', link: '/id/guide/installation#mobile-stb' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Konfigurasi',
|
||||
items: [
|
||||
{ text: 'Pasca-Instalasi', link: '/id/guide/installation#pasca-instalasi' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Dukungan',
|
||||
items: [
|
||||
{ text: 'Kontribusi', link: 'https://github.com/dyzulk/mivo/blob/main/CONTRIBUTING.md' },
|
||||
{ text: 'Donasi', link: 'https://sociabuzz.com/dyzulkdev/tribe' }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
// Sidebar for /id/manual/ path
|
||||
'/id/manual/': [
|
||||
{
|
||||
text: 'Buku Manual',
|
||||
items: [
|
||||
{ text: 'Ringkasan', link: '/id/manual/' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Pengaturan Global',
|
||||
items: [
|
||||
{ text: 'Pendahuluan', link: '/id/manual/settings/' },
|
||||
{ text: 'Router', link: '/id/manual/settings/routers' },
|
||||
{ text: 'Template', link: '/id/manual/settings/templates' },
|
||||
{ text: 'Logo', link: '/id/manual/settings/logos' },
|
||||
{ text: 'API & CORS', link: '/id/manual/settings/api-cors' },
|
||||
{ text: 'Sistem', link: '/id/manual/settings/system' }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Operasional Router',
|
||||
items: [
|
||||
{ text: 'Pendahuluan', link: '/id/manual/router/' },
|
||||
{ text: 'Dashboard', link: '/id/manual/router/dashboard' },
|
||||
{ text: 'Cetak Cepat', link: '/id/manual/router/quick-print' },
|
||||
{ text: 'Manajemen Hotspot', link: '/id/manual/router/hotspot' },
|
||||
{ text: 'Laporan & Log', link: '/id/manual/router/reports' },
|
||||
{ text: 'Jaringan & Sistem', link: '/id/manual/router/tools' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup>
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #layout-bottom>
|
||||
<div class="mivo-bg">
|
||||
<!-- Subtle Grid Pattern -->
|
||||
<div class="mivo-grid"></div>
|
||||
<!-- Glowing Orbs -->
|
||||
<div class="mivo-orb orb-1"></div>
|
||||
<div class="mivo-orb orb-2"></div>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mivo-bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mivo-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMCwwLDAsMC4zKSIvPjwvc3ZnPg==');
|
||||
mask-image: linear-gradient(to bottom, white, transparent);
|
||||
-webkit-mask-image: linear-gradient(to bottom, white, transparent);
|
||||
}
|
||||
|
||||
:root.dark .mivo-grid {
|
||||
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wNSkiLz48L3N2Zz4=');
|
||||
}
|
||||
|
||||
.mivo-orb {
|
||||
position: absolute;
|
||||
border-radius: 9999px;
|
||||
filter: blur(100px);
|
||||
opacity: 0.2;
|
||||
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
:root.dark .mivo-orb {
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
.orb-1 {
|
||||
top: -20%;
|
||||
left: -10%;
|
||||
width: 70vw;
|
||||
height: 70vw;
|
||||
background-color: #3b82f6; /* blue-500 */
|
||||
animation-duration: 4s;
|
||||
}
|
||||
|
||||
.orb-2 {
|
||||
top: 30%;
|
||||
right: -15%;
|
||||
width: 60vw;
|
||||
height: 60vw;
|
||||
background-color: #a855f7; /* purple-500 */
|
||||
animation-duration: 6s;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.2; }
|
||||
50% { opacity: 0.15; }
|
||||
}
|
||||
|
||||
:root.dark @keyframes pulse {
|
||||
0%, 100% { opacity: 0.05; }
|
||||
50% { opacity: 0.03; }
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<component
|
||||
:is="iconComponent"
|
||||
v-if="iconComponent"
|
||||
:size="size || 20"
|
||||
:stroke-width="strokeWidth || 2"
|
||||
class="lucide-icon"
|
||||
:style="{ color: resolvedColor }"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import * as icons from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: [Number, String],
|
||||
strokeWidth: [Number, String],
|
||||
color: {
|
||||
type: String,
|
||||
default: 'base'
|
||||
}
|
||||
})
|
||||
|
||||
const semanticColors = {
|
||||
base: 'var(--mivo-icon-base)',
|
||||
muted: 'var(--mivo-icon-muted)',
|
||||
primary: 'var(--mivo-icon-primary)',
|
||||
info: 'var(--mivo-icon-info)',
|
||||
success: 'var(--mivo-icon-success)',
|
||||
warning: 'var(--mivo-icon-warning)',
|
||||
danger: 'var(--mivo-icon-danger)'
|
||||
}
|
||||
|
||||
const resolvedColor = computed(() => {
|
||||
return semanticColors[props.color] || props.color
|
||||
})
|
||||
|
||||
const iconComponent = computed(() => {
|
||||
// Handle both PascalCase (Search) and kebab-case (search-icon)
|
||||
const pascalName = props.name
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('')
|
||||
|
||||
return icons[pascalName] || icons[props.name]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lucide-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import Layout from './Layout.vue'
|
||||
import Icon from './components/Icon.vue'
|
||||
import 'flag-icons/css/flag-icons.min.css'
|
||||
import './style.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: Layout,
|
||||
enhanceApp({ app }) {
|
||||
app.component('Icon', Icon)
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
/* Fonts Setup */
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('/assets/fonts/Geist-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Geist';
|
||||
src: url('/assets/fonts/Geist-Bold.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Geist Mono';
|
||||
src: url('/assets/fonts/GeistMono-Regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Fonts */
|
||||
--vp-font-family-base: "Geist", sans-serif;
|
||||
--vp-font-family-mono: "Geist Mono", monospace;
|
||||
|
||||
/* Colors Override to match MIVO */
|
||||
--vp-c-bg: #ffffff;
|
||||
--vp-c-bg-alt: #fafafa;
|
||||
--vp-c-bg-elv: #ffffff;
|
||||
--vp-c-text-1: #000000;
|
||||
--vp-c-text-2: #666666;
|
||||
|
||||
--vp-c-brand-1: #000000;
|
||||
--vp-c-brand-2: #333333;
|
||||
--vp-c-brand-3: #666666;
|
||||
|
||||
/* Icon Colors - Light */
|
||||
--mivo-icon-base: var(--vp-c-text-1);
|
||||
--mivo-icon-muted: var(--vp-c-text-2);
|
||||
--mivo-icon-primary: var(--vp-c-brand-1);
|
||||
--mivo-icon-info: #3b82f6;
|
||||
--mivo-icon-success: #22c55e;
|
||||
--mivo-icon-warning: #eab308;
|
||||
--mivo-icon-danger: #ef4444;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: #000000;
|
||||
--vp-c-bg-alt: #111111;
|
||||
--vp-c-bg-elv: #111111;
|
||||
--vp-c-text-1: #ffffff;
|
||||
--vp-c-text-2: #888888;
|
||||
|
||||
--vp-c-brand-1: #ffffff;
|
||||
--vp-c-brand-2: #eaeaea;
|
||||
--vp-c-brand-3: #999999;
|
||||
|
||||
/* Icon Colors - Dark */
|
||||
--mivo-icon-base: var(--vp-c-text-1);
|
||||
--mivo-icon-muted: var(--vp-c-text-2);
|
||||
--mivo-icon-primary: var(--vp-c-brand-1);
|
||||
--mivo-icon-info: #60a5fa;
|
||||
--mivo-icon-success: #4ade80;
|
||||
--mivo-icon-warning: #facc15;
|
||||
--mivo-icon-danger: #f87171;
|
||||
}
|
||||
|
||||
/* Glassmorphism Overrides */
|
||||
|
||||
/* Navbar Glass */
|
||||
.VPNav {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.dark .VPNav {
|
||||
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Sidebar Glass */
|
||||
.VPSidebar {
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
border-right: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.dark .VPSidebar {
|
||||
background-color: rgba(0, 0, 0, 0.3) !important;
|
||||
border-right: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Content Container - Transparent to show particles */
|
||||
.VPContent {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Footer Glass */
|
||||
.VPFooter {
|
||||
background-color: transparent !important;
|
||||
border-top: 1px solid rgba(0,0,0,0.05);
|
||||
}
|
||||
.dark .VPFooter {
|
||||
border-top: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/* Local Search Box Glass */
|
||||
.VPLocalSearchBox {
|
||||
background-color: rgba(255, 255, 255, 0.8) !important;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.dark .VPLocalSearchBox {
|
||||
background-color: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
|
||||
/* Glassmorphism Cards (Feature Cards) */
|
||||
.VPFeature {
|
||||
background-color: rgba(255, 255, 255, 0.25) !important;
|
||||
backdrop-filter: blur(40px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.2) !important;
|
||||
border-radius: 1rem !important; /* rounded-2xl */
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .VPFeature {
|
||||
background-color: rgba(0, 0, 0, 0.4) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.VPFeature:hover {
|
||||
border-color: rgba(255, 255, 255, 0.4) !important;
|
||||
background-color: rgba(255, 255, 255, 0.4) !important;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dark .VPFeature:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||
background-color: rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
|
||||
/* Fix text colors inside cards if needed */
|
||||
/* Global Button Styling (MIVO Style) */
|
||||
.VPButton {
|
||||
border-radius: 0.375rem !important; /* rounded-md */
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
font-weight: 500 !important;
|
||||
text-transform: none !important;
|
||||
letter-spacing: normal !important;
|
||||
}
|
||||
|
||||
.VPButton:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.VPButton:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Primary Button (Brand) */
|
||||
.VPButton.brand {
|
||||
background-color: var(--vp-c-brand-1) !important;
|
||||
border-color: var(--vp-c-brand-1) !important;
|
||||
color: var(--vp-c-bg) !important;
|
||||
}
|
||||
|
||||
.dark .VPButton.brand {
|
||||
color: #000 !important; /* Ensure black text on white button in dark mode */
|
||||
}
|
||||
|
||||
/* Secondary Button (Alt) */
|
||||
.VPButton.alt {
|
||||
background-color: transparent !important;
|
||||
border: 1px solid var(--vp-c-brand-2) !important;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
}
|
||||
|
||||
/* Mobile Navigation (Glass Overlay) */
|
||||
.VPNavScreen {
|
||||
background-color: rgba(255, 255, 255, 0.95) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
position: fixed !important;
|
||||
top: var(--vp-nav-height, 64px) !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
width: 100% !important;
|
||||
height: calc(100vh - var(--vp-nav-height, 64px)) !important; /* Force Height */
|
||||
z-index: 90 !important;
|
||||
overflow-y: auto !important;
|
||||
opacity: 1 !important; /* Always opacity 1 if visible */
|
||||
pointer-events: auto !important;
|
||||
/* Remove transition that might delay visibility */
|
||||
transition: background-color 0.25s !important;
|
||||
}
|
||||
|
||||
/* Rely on VitePress/Vue to toggle 'display' property */
|
||||
/* We just ensure that IF it is displayed, it looks right */
|
||||
|
||||
.dark .VPNavScreen {
|
||||
background-color: rgba(0, 0, 0, 0.95) !important;
|
||||
}
|
||||
|
||||
/* Mobile Sidebar (Slide-out) */
|
||||
@media (max-width: 960px) {
|
||||
.VPSidebar {
|
||||
background-color: rgba(255, 255, 255, 0.9) !important; /* Less transparent on mobile for readability */
|
||||
backdrop-filter: blur(20px);
|
||||
border-right: none !important;
|
||||
box-shadow: 10px 0 30px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dark .VPSidebar {
|
||||
background-color: rgba(20, 20, 20, 0.9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fix Hamburger Menu Icon Color & Interaction */
|
||||
.VPNavBarHamburger {
|
||||
cursor: pointer !important;
|
||||
pointer-events: auto !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
.VPNavBarHamburger .container .top,
|
||||
.VPNavBarHamburger .container .middle,
|
||||
.VPNavBarHamburger .container .bottom {
|
||||
background-color: var(--vp-c-text-1) !important;
|
||||
}
|
||||
|
||||
/* Ensure Logo Text is Visible on Mobile */
|
||||
/* High Contrast Sidebar & TOC */
|
||||
/* High Contrast Sidebar & TOC */
|
||||
.VPSidebarItem .text {
|
||||
color: var(--vp-c-text-2) !important;
|
||||
opacity: 1 !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* Level 0 Parent Headers (Collapsible) */
|
||||
.VPSidebarItem.level-0 > .item > .text,
|
||||
.VPSidebarItem.level-0 > .item > .VPLink > .text {
|
||||
color: var(--vp-c-text-1) !important;
|
||||
font-weight: 700 !important; /* Bold for headers */
|
||||
}
|
||||
|
||||
/* Level 1+ Sub-items explicitly regular */
|
||||
.VPSidebarItem.level-1 .text,
|
||||
.VPSidebarItem.level-2 .text {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
/* Sidebar Lines (GitBook Style) */
|
||||
.VPSidebarItem.level-0 > .items {
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.05); /* Light vertical line */
|
||||
margin-left: 1.15rem;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dark .VPSidebarItem.level-0 > .items {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Active Sidebar Item Differentiation */
|
||||
.VPSidebarItem.is-active > .item {
|
||||
border-left: 2px solid var(--vp-c-brand-1); /* Bold indicator line */
|
||||
margin-left: calc(-0.5rem - 1px); /* Overlap the group line */
|
||||
padding-left: calc(0.5rem - 1px);
|
||||
}
|
||||
|
||||
.VPSidebarItem.is-active > .item > .text,
|
||||
.VPSidebarItem.is-active > .item > .VPLink > .text {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
font-weight: 400 !important; /* Keep active sub-item thinner than collapsible header */
|
||||
background-color: transparent !important; /* Clean style */
|
||||
}
|
||||
|
||||
/* Navbar Active Underline */
|
||||
.VPNavBarMenuLink.active {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.VPNavBarMenuLink.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.VPDocOutlineItem .text {
|
||||
color: var(--vp-c-text-2) !important;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.VPDocOutlineItem.active .text {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Dark Mode Specific Contrast Boost */
|
||||
.dark .VPSidebarItem .text,
|
||||
.dark .VPDocOutlineItem .text {
|
||||
color: #b0b0b0 !important; /* Brighter than default gray */
|
||||
}
|
||||
|
||||
.dark .VPSidebarItem.is-active > .item > .text,
|
||||
.dark .VPSidebarItem.is-active > .item > .VPLink > .text,
|
||||
.dark .VPDocOutlineItem.active .text,
|
||||
.dark .VPSidebarItem.level-0 > .item > .text,
|
||||
.dark .VPSidebarItem.level-0 > .item > .VPLink > .text {
|
||||
color: #ffffff !important;
|
||||
/* Font weights are already inherited or set above */
|
||||
}
|
||||
|
||||
/* Fix Code Block Background to be Glassy too */
|
||||
.vp-code-group .tabs {
|
||||
background-color: rgba(255,255,255,0.5) !important;
|
||||
}
|
||||
.dark .vp-code-group .tabs {
|
||||
background-color: rgba(0,0,0,0.5) !important;
|
||||
}
|
||||
|
||||
/* Markdown Divider Contrast */
|
||||
.vp-doc hr {
|
||||
border: none;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.15); /* Stronger in light mode */
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.dark .vp-doc hr {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2); /* Stronger in dark mode */
|
||||
}
|
||||
|
||||
/* Pagination Cards */
|
||||
.pager-link {
|
||||
background-color: rgba(255, 255, 255, 0.4) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05) !important;
|
||||
border-radius: 0.75rem !important; /* rounded-xl */
|
||||
padding: 1.5rem !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
||||
text-decoration: none !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.dark .pager-link {
|
||||
background-color: rgba(255, 255, 255, 0.03) !important;
|
||||
border-color: rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.pager-link:hover {
|
||||
border-color: var(--vp-c-brand-1) !important;
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
}
|
||||
|
||||
.dark .pager-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.pager-link .desc {
|
||||
color: var(--vp-c-text-2) !important;
|
||||
font-size: 0.8rem !important;
|
||||
font-weight: 500 !important;
|
||||
margin-bottom: 4px !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.pager-link .title {
|
||||
color: var(--vp-c-brand-1) !important;
|
||||
font-size: 1.1rem !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.pager-link.next {
|
||||
text-align: right !important;
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
/* Language Switcher Flags Integration */
|
||||
.VPNavBarTranslations .items .title::before,
|
||||
.VPNavBarTranslations .items .VPMenuLink .VPLink.link span::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 1.33333333em;
|
||||
height: 1em;
|
||||
margin-right: 0.6rem;
|
||||
vertical-align: middle;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* --- English Flag (US) --- */
|
||||
|
||||
/* 1. Target the link to English (Any link NOT starting with /id/) */
|
||||
.VPNavBarTranslations a:not([href^="/id/"])::before,
|
||||
.VPNavBarTranslations a:not([href^="/id/"]) span::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3Cpath fill='%23bd3d44' d='M0 0h640v480H0z'/%3E%3Cpath stroke='%23fff' stroke-width='37' d='M0 74h640M0 148h640M0 222h640M0 296h640M0 370h640M0 444h640'/%3E%3Cpath fill='%23192f5d' d='M0 0h364v258.5H0z'/%3E%3Cg fill='%23fff'%3E%3Cg id='a'%3E%3Cg id='b'%3E%3Cpath id='c' d='M31 23l5 15.5-13.1-9.5H44l-13.1 9.5'/%3E%3Cuse href='%23c' x='62'/%3E%3Cuse href='%23c' x='124'/%3E%3Cuse href='%23c' x='186'/%3E%3Cuse href='%23c' x='248'/%3E%3C/g%3E%3Cuse href='%23b' x='31' y='21'/%3E%3C/g%3E%3Cuse href='%23a' y='42'/%3E%3Cuse href='%23a' y='84'/%3E%3Cuse href='%23a' y='126'/%3E%3Cuse href='%23a' y='168'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 2. Target the active English title (when on /) */
|
||||
html[lang="en"] .VPNavBarTranslations .items .title::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3Cpath fill='%23bd3d44' d='M0 0h640v480H0z'/%3E%3Cpath stroke='%23fff' stroke-width='37' d='M0 74h640M0 148h640M0 222h640M0 296h640M0 370h640M0 444h640'/%3E%3Cpath fill='%23192f5d' d='M0 0h364v258.5H0z'/%3E%3Cg fill='%23fff'%3E%3Cg id='a'%3E%3Cg id='b'%3E%3Cpath id='c' d='M31 23l5 15.5-13.1-9.5H44l-13.1 9.5'/%3E%3Cuse href='%23c' x='62'/%3E%3Cuse href='%23c' x='124'/%3E%3Cuse href='%23c' x='186'/%3E%3Cuse href='%23c' x='248'/%3E%3C/g%3E%3Cuse href='%23b' x='31' y='21'/%3E%3C/g%3E%3Cuse href='%23a' y='42'/%3E%3Cuse href='%23a' y='84'/%3E%3Cuse href='%23a' y='126'/%3E%3Cuse href='%23a' y='168'/%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* --- Indonesia Flag (ID) --- */
|
||||
|
||||
/* 1. Target the link to Indonesia (Any link starting with /id/) */
|
||||
.VPNavBarTranslations a[href^="/id/"]::before,
|
||||
.VPNavBarTranslations a[href^="/id/"] span::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3Cpath fill='%23e12127' d='M0 0h640v240H0z'/%3E%3Cpath fill='%23fff' d='M0 240h640v240H0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 2. Target the active Indonesia title (when on /id/) */
|
||||
html[lang="id"] .VPNavBarTranslations .items .title::before {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 480'%3E%3Cpath fill='%23e12127' d='M0 0h640v240H0z'/%3E%3Cpath fill='%23fff' d='M0 240h640v240H0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ The easiest way to run MIVO.
|
||||
2. **Manual Pull (Alternative)**
|
||||
If you prefer to pull the image manually:
|
||||
```bash
|
||||
docker pull dyzulk/mivo:latest # Stable
|
||||
docker pull dyzulk/mivo:v1.0.0 # Specific Version
|
||||
docker pull dyzulk/mivo:edge # Bleeding Edge
|
||||
docker pull ghcr.io/mivodev/mivo:latest # Stable
|
||||
docker pull ghcr.io/mivodev/mivo:v1.0.0 # Specific Version
|
||||
docker pull ghcr.io/mivodev/mivo:edge # Bleeding Edge
|
||||
```
|
||||
|
||||
*Note: The database is persisted in `app/Database` via volumes.*
|
||||
|
||||
1
docs/README.md
Normal file
1
docs/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Documentation has moved to https://mivodev.github.io
|
||||
@@ -1,104 +0,0 @@
|
||||
# Deploy on aaPanel (Docker)
|
||||
|
||||
aaPanel makes it easiest to manage Docker projects using its native **Docker Manager** module. We recommend following the standard "Quick Install" pattern which uses environment variables for better maintainability.
|
||||
|
||||
::: tip PREREQUISITES
|
||||
Ensure you have the **Docker** module installed from the aaPanel App Store.
|
||||
:::
|
||||
|
||||
## 1. Prepare Configuration
|
||||
|
||||
We use a "Native aaPanel Style" setup where configuration is separated from the template.
|
||||
|
||||
**Environment File (`.env`)**
|
||||
Create a new file named `.env` and configure your preferences:
|
||||
|
||||
```ini
|
||||
VERSION=latest
|
||||
CONTAINER_NAME=mivo
|
||||
HOST_IP=0.0.0.0
|
||||
APP_PORT=8085
|
||||
APP_PATH=/www/dk_project/mivo
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
TZ=Asia/Jakarta
|
||||
CPUS=1.0
|
||||
MEMORY_LIMIT=512M
|
||||
```
|
||||
|
||||
**Attribute Explanation:**
|
||||
- `APP_PATH`: **Crucial**. This must match your project directory in aaPanel (default is `/www/dk_project/<project_name>`).
|
||||
- `APP_PORT`: The port you want to expose (default `8080`).
|
||||
- `TZ`: Your timezone (e.g., `Asia/Jakarta`).
|
||||
|
||||
## 2. Create Project in aaPanel
|
||||
|
||||
1. Login to your aaPanel dashboard.
|
||||
2. Navigate to **Docker** > **Project**.
|
||||
3. Click **Add Project**.
|
||||
4. Fill in the details:
|
||||
- **Name**: `mivo` (or your preferred name)
|
||||
- **Path**: This will auto-fill (usually `/www/dk_project/mivo`). **Update `APP_PATH` in your .env to match this!**
|
||||
- **Compose Template**: Paste the following YAML content:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:${VERSION:-latest}
|
||||
container_name: ${CONTAINER_NAME:-mivo}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HOST_IP:-0.0.0.0}:${APP_PORT:-8085}:80"
|
||||
volumes:
|
||||
# Database & Sessions
|
||||
- ${APP_PATH:-.}/mivo_data:/var/www/html/app/Database
|
||||
|
||||
# Custom Logos
|
||||
- ${APP_PATH:-.}/mivo_logos:/var/www/html/public/assets/img/logos
|
||||
|
||||
# Environment file (Optional - mapped from host)
|
||||
# - ${APP_PATH:-.}/.env:/var/www/html/.env
|
||||
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- APP_DEBUG=${APP_DEBUG:-false}
|
||||
- TZ=${TZ:-Asia/Jakarta}
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '${CPUS:-1.0}'
|
||||
memory: ${MEMORY_LIMIT:-512M}
|
||||
|
||||
networks:
|
||||
- mivo_net
|
||||
|
||||
networks:
|
||||
mivo_net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
5. **Wait!** Before clicking "Confirm" or "Add":
|
||||
- Look for the **.env** config section (usually a tab or sidebar in the Add Project modal).
|
||||
- Paste your `.env` content there.
|
||||
6. Click **Confirm** to deploy.
|
||||
|
||||
## 3. Verify Deployment
|
||||
|
||||
aaPanel will pull the image and start the container. Once running:
|
||||
|
||||
- **Database Location**: Your data is safe at `/www/dk_project/mivo/mivo_data`.
|
||||
- **Logos Location**: Upload custom logos to `/www/dk_project/mivo/mivo_logos`.
|
||||
|
||||
## 4. Setup Domain (Reverse Proxy)
|
||||
|
||||
To access MIVO via a domain (e.g., `mivo.yourdomain.com`):
|
||||
|
||||
1. Go to **Website** > **Add Site**.
|
||||
2. Enter your domain name.
|
||||
3. For **PHP Version**, select **Reverse Proxy** (or create as Static and set up proxy later).
|
||||
4. After creation, open the site settings > **Reverse Proxy** > **Add Reverse Proxy**.
|
||||
5. **Target URL**: `http://127.0.0.1:8085` (Replace `8085` with your `APP_PORT`).
|
||||
6. Save and secure with SSL.
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
title: Docker Guide
|
||||
---
|
||||
|
||||
# Docker Guide
|
||||
|
||||
This Docker image is built on **Alpine Linux** and **Nginx**, optimized for high performance and low resource usage.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Quick Start
|
||||
|
||||
Run MIVO in a single command:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mivo \
|
||||
-p 8080:80 \
|
||||
-e APP_KEY=base64:YOUR_GENERATED_KEY \
|
||||
-e APP_ENV=production \
|
||||
-v mivo_data:/var/www/html/app/Database \
|
||||
-v mivo_config:/var/www/html/.env \
|
||||
dyzulk/mivo:latest
|
||||
```
|
||||
|
||||
Open your browser and navigate to `http://localhost:8080`.
|
||||
|
||||
**Initial Setup:**
|
||||
If this is your first run, you will be redirected to the **Web Installer**. Follow the on-screen instructions to create the database and admin account.
|
||||
|
||||
## <Icon name="Wrench" color="primary" /> Docker Compose
|
||||
|
||||
For a more permanent setup, use `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:latest
|
||||
container_name: mivo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- TZ=Asia/Jakarta
|
||||
volumes:
|
||||
- ./mivo-data:/var/www/html/app/Database
|
||||
```
|
||||
|
||||
## <Icon name="Tags" color="info" /> Tags
|
||||
|
||||
- `latest`: Stable release (recommended).
|
||||
- `edge`: Bleeding edge build from the `main` branch.
|
||||
- `v1.x.x`: Specific released versions.
|
||||
|
||||
## <Icon name="Sliders" color="success" /> Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| `APP_ENV` | Application environment (`production` or `local`). | `production` |
|
||||
| `APP_DEBUG` | Enable debug mode (`true` or `false`). | `false` |
|
||||
| `APP_KEY` | 32-character random string (base64). Auto-generated on first install if not provided. | |
|
||||
| `TZ` | Timezone for the container. | `UTC` |
|
||||
|
||||
## <Icon name="Folder" color="primary" /> Volumes
|
||||
|
||||
Persist your data by mounting these paths:
|
||||
|
||||
- `/var/www/html/app/Database`: Stores the SQLite database and session files. **(Critical)**
|
||||
- `/var/www/html/public/assets/img/logos`: Stores uploaded custom logos.
|
||||
|
||||
## <Icon name="Heart" color="danger" /> Support the Project
|
||||
|
||||
If you find MIVO useful, please consider supporting its development. Your contribution helps keep the project alive!
|
||||
|
||||
[](https://sociabuzz.com/dyzulkdev/tribe)
|
||||
|
||||
---
|
||||
*Created with <Icon name="Heart" color="danger" /> by DyzulkDev*
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
Welcome to the MIVO Guide. This section will help you understand what MIVO is and how to get it running on your system.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> What is MIVO?
|
||||
|
||||
MIVO is a modern, lightweight Mikrotik Voucher Management system. It is designed to be efficient, fast, and user-friendly, providing a seamless experience for Hotspot management.
|
||||
|
||||
## <Icon name="BookOpen" color="primary" /> Navigation
|
||||
|
||||
Explore the following sections to get started:
|
||||
|
||||
- **[Installation Guide](/guide/installation)**: Learn how to install MIVO on various platforms.
|
||||
- **[Docker Guide](/guide/docker)**: The recommended way to run MIVO using containers.
|
||||
- **[Manual](/manual/)**: Detailed instructions on how to use MIVO features.
|
||||
|
||||
## <Icon name="Heart" color="danger" /> Support
|
||||
|
||||
MIVO is an open-source project. If you find it useful, please consider supporting the development through [donations](https://sociabuzz.com/dyzulkdev/tribe) or [contributing](https://github.com/dyzulk/mivo) to the codebase.
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
title: Installation Guide
|
||||
---
|
||||
|
||||
# Installation Guide
|
||||
|
||||
This guide covers installation on various platforms. MIVO is designed to be lightweight and runs on almost any PHP-capable server.
|
||||
|
||||
## <Icon name="ClipboardList" color="primary" /> General Requirements {#requirements}
|
||||
* **PHP**: 8.0 or higher
|
||||
* **Extensions**: `sqlite3`, `openssl`, `mbstring`, `json`
|
||||
* **Database**: SQLite (File based, no server needed)
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Container" color="info" /> Docker (Recommended)
|
||||
The easiest way to run MIVO.
|
||||
|
||||
1. **Build & Run**
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
Go to `http://localhost:8080`
|
||||
|
||||
2. **Manual Pull (Alternative)**
|
||||
If you prefer to pull the image manually:
|
||||
```bash
|
||||
docker pull dyzulk/mivo:latest # Stable
|
||||
docker pull dyzulk/mivo:v1.0.0 # Specific Version
|
||||
docker pull dyzulk/mivo:edge # Bleeding Edge
|
||||
```
|
||||
|
||||
*Note: The database is persisted in `app/Database` via volumes.*
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Server" color="success" /> Web Servers {#web-servers}
|
||||
|
||||
### Apache / OpenLiteSpeed
|
||||
1. **Document Root**: Set your web server's document root to the `public/` folder.
|
||||
2. **Rewrite Rules**: Ensure `mod_rewrite` is enabled. MIVO includes a `.htaccess` file in `public/` that handles URL routing automatically.
|
||||
3. **Permissions**: Ensure the web server user (e.g., `www-data`) has **write** access to:
|
||||
* `app/Database/` (directory and file)
|
||||
* `app/Config/` (if using installer)
|
||||
* `.env` file
|
||||
|
||||
### Nginx
|
||||
Nginx does not read `.htaccess`. Use this configuration block in your `server` block:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /path/to/mivo/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Adjust version
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IIS (Windows)
|
||||
1. **Document Root**: Point the site to the `public/` folder.
|
||||
2. **Web Config**: A `web.config` file has been provided in `public/` to handle URL Rewriting.
|
||||
3. **Requirements**: Ensure **URL Rewrite Module 2.0** is installed on IIS.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Smartphone" color="warning" /> Mobile / STB {#mobile-stb}
|
||||
|
||||
### Awebserver
|
||||
1. Copy the MIVO files to `/htdocs`.
|
||||
2. Point the document root to `public` if supported, or access via `http://localhost:8080/public`.
|
||||
3. Ensure PHP version is compatible.
|
||||
|
||||
### Termux
|
||||
1. Install PHP: `pkg install php`
|
||||
2. Navigate to MIVO directory: `cd mivo`
|
||||
3. Use the built-in server:
|
||||
```bash
|
||||
php mivo serve --host=0.0.0.0 --port=8080
|
||||
```
|
||||
4. Access via browser.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Globe" color="info" /> Shared Hosting {#shared-hosting}
|
||||
Most shared hosting uses Apache or OpenLiteSpeed, which is fully compatible.
|
||||
|
||||
1. **Upload Files**: Upload the MIVO files to `public_html/mivo` (or a subdomain folder).
|
||||
2. **Point Domain**:
|
||||
* **Recommended**: Go to "Domains" or "Subdomains" in cPanel and set the **Document Root** to point strictly to the `public/` folder (e.g., `public_html/mivo/public`).
|
||||
* **Alternative**: If you cannot change Document Root, you can move contents of `public/` to the root `public_html` and move `app/`, `routes/`, etc. one level up (not recommended for security).
|
||||
3. **PHP Version**: Select PHP 8.0+ in "Select PHP Version" menu.
|
||||
4. **Extensions**: Ensure `sqlite3` and `fileinfo` are checked.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Cloud" color="primary" /> VPS & Cloud {#vps-cloud}
|
||||
|
||||
### aaPanel
|
||||
1. **Create Website**: Add site -> PHP-8.x.
|
||||
2. **Site Directory**:
|
||||
* Set **Running Directory** (bukan Site Directory) to `/public`.
|
||||
* Uncheck "Anti-XSS" (sometimes blocks config saving).
|
||||
3. **URL Rewrite**: Select `thinkphp` or `laravel` template (compatible) OR just use the Nginx config provided above.
|
||||
4. **Permissions**: Chown `www` user to the site directory.
|
||||
|
||||
### PaaS Cloud (Railway / Render / Heroku)
|
||||
> [!WARNING]
|
||||
> MIVO uses SQLite (File Database). Most PaaS cloud have **Ephemeral Filesytem** (Reset on restart).
|
||||
|
||||
* **Requirement**: You MUST mount a **Persistent Volume/Disk**.
|
||||
* **Mount Path**: Mount your volume to `/var/www/html/app/Database` (or wherever you put MIVO).
|
||||
* **Docker**: Use the Docker deployment method, it works natively on these platforms.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Settings" color="success" /> Post-Installation {#post-installation}
|
||||
After setting up the server:
|
||||
1. Copy `.env.example` to `.env` (if not already done).
|
||||
2. **Install Application**
|
||||
* **Option A: CLI**
|
||||
Run `php mivo install` in your terminal.
|
||||
* **Option B: Web Installer**
|
||||
Open `http://your-domain.com/install` in your browser.
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Icons Test Page
|
||||
|
||||
This page verifies that **Lucide Icons** and **Flag Icons** are correctly integrated.
|
||||
|
||||
## Lucide Icons
|
||||
|
||||
Using the global `<Icon />` component:
|
||||
|
||||
- <Icon name="Search" /> Search (Default)
|
||||
- <Icon name="Settings" :size="32" stroke-width="3" color="danger" /> Settings (Semantic: danger)
|
||||
- <Icon name="check-circle" color="success" /> Check Circle (Semantic: success)
|
||||
- <Icon name="Github" color="primary" /> Github (Semantic: primary)
|
||||
- <Icon name="Zap" color="warning" /> Zap (Semantic: warning)
|
||||
- <Icon name="Info" color="info" /> Info (Semantic: info)
|
||||
- <Icon name="HelpCircle" color="muted" /> Help (Semantic: muted)
|
||||
|
||||
## Flag Icons
|
||||
|
||||
Using standard `flag-icons` CSS classes:
|
||||
|
||||
- <span class="fi fi-id"></span> Indonesia
|
||||
- <span class="fi fi-us"></span> United States
|
||||
- <span class="fi fi-gb"></span> Great Britain
|
||||
- <span class="fi fi-jp"></span> Japan
|
||||
|
||||
## Combined Usage
|
||||
|
||||
<div style="display: flex; gap: 10px; align-items: center; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 8px;">
|
||||
<Icon name="Globe" />
|
||||
<span>Selected Language:</span>
|
||||
<span class="fi fi-id"></span>
|
||||
<strong>Bahasa Indonesia</strong>
|
||||
</div>
|
||||
@@ -1,104 +0,0 @@
|
||||
# Deploy di aaPanel (Docker)
|
||||
|
||||
aaPanel adalah salah satu panel hosting paling populer yang memiliki modul **Docker Manager** yang sangat memudahkan manajemen container. Kami merekomendasikan penggunaan gaya "Native" aaPanel agar manajemen resource dan env lebih rapi.
|
||||
|
||||
::: tip PRASYARAT
|
||||
Pastikan Anda sudah menginstall modul **Docker** dari App Store di dalam aaPanel Anda.
|
||||
:::
|
||||
|
||||
## 1. Siapkan Konfigurasi
|
||||
|
||||
Metode ini memisahkan konfigurasi (di file `.env`) dari template logic, sehingga Anda bisa dengan mudah mengubah port atau resource limit tanpa mengedit file YAML yang rumit.
|
||||
|
||||
**File Environment (`.env`)**
|
||||
Buat file baru bernama `.env` (atau simpan teks ini untuk nanti):
|
||||
|
||||
```ini
|
||||
VERSION=latest
|
||||
CONTAINER_NAME=mivo
|
||||
HOST_IP=0.0.0.0
|
||||
APP_PORT=8085
|
||||
APP_PATH=/www/dk_project/mivo
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
TZ=Asia/Jakarta
|
||||
CPUS=1.0
|
||||
MEMORY_LIMIT=512M
|
||||
```
|
||||
|
||||
**Penjelasan Atribut:**
|
||||
- `APP_PATH`: **Penting**. Ini harus sama persis dengan lokasi project di aaPanel Anda (defaultnya `/www/dk_project/<nama_project>`).
|
||||
- `APP_PORT`: Port host yang ingin dibuka (default `8080`).
|
||||
- `CPUS` & `MEMORY_LIMIT`: Batasan resource agar container tidak membebani server/VPS Anda.
|
||||
|
||||
## 2. Buat Project di aaPanel
|
||||
|
||||
1. Login ke dashboard aaPanel.
|
||||
2. Masuk ke menu **Docker** > **Project** (atau **Compose** di versi lama).
|
||||
3. Klik tombol **Add Project**.
|
||||
4. Isi form sebagai berikut:
|
||||
- **Name**: `mivo` (atau nama lain yang Anda suka)
|
||||
- **Path**: Perhatikan path yang muncul otomatis (biasanya `/www/dk_project/mivo`). **Pastikan `APP_PATH` di .env Anda sesuai dengan path ini!**
|
||||
- **Compose Template**: Copy-paste kode YAML berikut:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:${VERSION:-latest}
|
||||
container_name: ${CONTAINER_NAME:-mivo}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${HOST_IP:-0.0.0.0}:${APP_PORT:-8085}:80"
|
||||
volumes:
|
||||
# Database & Sessions
|
||||
- ${APP_PATH:-.}/mivo_data:/var/www/html/app/Database
|
||||
|
||||
# Custom Logos
|
||||
- ${APP_PATH:-.}/mivo_logos:/var/www/html/public/assets/img/logos
|
||||
|
||||
# Environment file (Optional - mapped from host)
|
||||
# - ${APP_PATH:-.}/.env:/var/www/html/.env
|
||||
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-production}
|
||||
- APP_DEBUG=${APP_DEBUG:-false}
|
||||
- TZ=${TZ:-Asia/Jakarta}
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '${CPUS:-1.0}'
|
||||
memory: ${MEMORY_LIMIT:-512M}
|
||||
|
||||
networks:
|
||||
- mivo_net
|
||||
|
||||
networks:
|
||||
mivo_net:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
5. **Tunggu!** Sebelum klik "Confirm":
|
||||
- Cari bagian konfigurasi **.env** (biasanya berupa tab atau input area di samping/bawah editor YAML).
|
||||
- Paste konten `.env` yang sudah Anda siapkan di langkah 1 ke sana.
|
||||
6. Klik **Confirm** untuk memulai deployment.
|
||||
|
||||
## 3. Verifikasi Deployment
|
||||
|
||||
aaPanel akan otomatis mendownload image dan menjalankan container.
|
||||
|
||||
- **Lokasi Data**: Database Anda aman tersimpan di `/www/dk_project/mivo/mivo_data`. folder ini tidak akan hilang walau container dihapus.
|
||||
- **Lokasi Logo**: Upload logo kustom Anda ke `/www/dk_project/mivo/mivo_logos`.
|
||||
|
||||
## 4. Setup Domain (Reverse Proxy)
|
||||
|
||||
Agar MIVO bisa diakses menggunakan domain (contoh: `mivo.domainanda.com`):
|
||||
|
||||
1. Ke menu **Website** > **Add Site**.
|
||||
2. Masukkan nama domain Anda.
|
||||
3. Pada **PHP Version**, pilih **Static** (atau langsung Reverse Proxy jika ada opsinya).
|
||||
4. Setelah site dibuat, buka settingannya > **Reverse Proxy** > **Add Reverse Proxy**.
|
||||
5. **Target URL**: `http://127.0.0.1:8085` (Ganti `8085` sesuai dengan `APP_PORT` Anda).
|
||||
6. Simpan dan aktifkan SSL agar lebih aman.
|
||||
@@ -1,77 +0,0 @@
|
||||
---
|
||||
title: Panduan Docker
|
||||
---
|
||||
|
||||
# Panduan Docker
|
||||
|
||||
Image Docker ini dibangun di atas **Alpine Linux** dan **Nginx**, dioptimalkan untuk performa tinggi dan penggunaan sumber daya rendah.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Mulai Cepat
|
||||
|
||||
Jalankan MIVO dengan satu perintah:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mivo \
|
||||
-p 8080:80 \
|
||||
-e APP_KEY=base64:YOUR_GENERATED_KEY \
|
||||
-e APP_ENV=production \
|
||||
-v mivo_data:/var/www/html/app/Database \
|
||||
-v mivo_config:/var/www/html/.env \
|
||||
dyzulk/mivo:latest
|
||||
```
|
||||
|
||||
Buka browser Anda dan navigasi ke `http://localhost:8080`.
|
||||
|
||||
**Pengaturan Awal:**
|
||||
Jika ini adalah pertama kali dijalankan, Anda akan diarahkan ke **Web Installer**. Ikuti instruksi di layar untuk membuat database dan akun admin.
|
||||
|
||||
## <Icon name="Wrench" color="primary" /> Docker Compose
|
||||
|
||||
Untuk pengaturan yang lebih permanen, gunakan `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mivo:
|
||||
image: dyzulk/mivo:latest
|
||||
container_name: mivo
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
environment:
|
||||
- APP_ENV=production
|
||||
- TZ=Asia/Jakarta
|
||||
volumes:
|
||||
- ./mivo-data:/var/www/html/app/Database
|
||||
```
|
||||
|
||||
## <Icon name="Tags" color="info" /> Tags
|
||||
|
||||
- `latest`: Rilis stabil (direkomendasikan).
|
||||
- `edge`: Build terbaru dari branch `main` (bleeding edge).
|
||||
- `v1.x.x`: Versi rilis spesifik.
|
||||
|
||||
## <Icon name="Sliders" color="success" /> Variabel Lingkungan
|
||||
|
||||
| Variabel | Deskripsi | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| `APP_ENV` | Lingkungan aplikasi (`production` atau `local`). | `production` |
|
||||
| `APP_DEBUG` | Aktifkan mode debug (`true` atau `false`). | `false` |
|
||||
| `APP_KEY` | String acak ca 32-karakter (base64). Dibuat otomatis saat install pertama kali jika kosong. | |
|
||||
| `TZ` | Zona waktu untuk container. | `UTC` |
|
||||
|
||||
## <Icon name="Folder" color="primary" /> Volume
|
||||
|
||||
Persist data Anda dengan me-mount path ini:
|
||||
|
||||
- `/var/www/html/app/Database`: Menyimpan database SQLite dan file sesi. **(Penting)**
|
||||
- `/var/www/html/public/assets/img/logos`: Menyimpan logo kustom yang diupload.
|
||||
|
||||
## <Icon name="Heart" color="danger" /> Dukung Proyek Ini
|
||||
|
||||
Jika Anda merasa MIVO bermanfaat, harap pertimbangkan untuk mendukung pengembangannya. Kontribusi Anda sangat berarti untuk kelangsungan proyek ini!
|
||||
|
||||
[](https://sociabuzz.com/dyzulkdev/tribe)
|
||||
|
||||
---
|
||||
*Dibuat dengan <Icon name="Heart" color="danger" /> oleh DyzulkDev*
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
title: Pengenalan
|
||||
---
|
||||
|
||||
# Pengenalan
|
||||
|
||||
Selamat datang di Panduan MIVO. Bagian ini akan membantu Anda memahami apa itu MIVO dan bagaimana cara menjalankannya di sistem Anda.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Apa itu MIVO?
|
||||
|
||||
MIVO adalah sistem Manajemen Voucher Mikrotik yang modern dan ringan. Sistem ini dirancang agar efisien, cepat, dan mudah digunakan, memberikan pengalaman yang mulus untuk pengelolaan Hotspot.
|
||||
|
||||
## <Icon name="BookOpen" color="primary" /> Navigasi
|
||||
|
||||
Jelajahi bagian berikut untuk memulai:
|
||||
|
||||
- **[Panduan Instalasi](/id/guide/installation)**: Pelajari cara menginstal MIVO di berbagai platform.
|
||||
- **[Panduan Docker](/id/guide/docker)**: Cara yang direkomendasikan untuk menjalankan MIVO menggunakan container.
|
||||
- **[Buku Manual](/id/manual/)**: Instruksi detail tentang cara menggunakan fitur-fitur MIVO.
|
||||
|
||||
## <Icon name="Heart" color="danger" /> Dukungan
|
||||
|
||||
MIVO adalah proyek open-source. Jika Anda merasa MIVO bermanfaat, harap pertimbangkan untuk mendukung pengembangannya melalui [donasi](https://sociabuzz.com/dyzulkdev/tribe) atau [berkontribusi](https://github.com/dyzulk/mivo) langsung ke kode program.
|
||||
@@ -1,121 +0,0 @@
|
||||
---
|
||||
title: Panduan Instalasi
|
||||
---
|
||||
|
||||
# Panduan Instalasi
|
||||
|
||||
Panduan ini mencakup instalasi di berbagai platform. MIVO dirancang agar ringan dan berjalan di hampir semua server yang mendukung PHP.
|
||||
|
||||
## <Icon name="ClipboardList" color="primary" /> Persyaratan Umum {#persyaratan}
|
||||
* **PHP**: 8.0 atau lebih tinggi
|
||||
* **Ekstensi**: `sqlite3`, `openssl`, `mbstring`, `json`
|
||||
* **Database**: SQLite (Berbasis file, tidak perlu server database)
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Container" color="info" /> Docker (Direkomendasikan) {#docker}
|
||||
Cara termudah untuk menjalankan MIVO.
|
||||
|
||||
1. **Build & Run**
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
Buka `http://localhost:8080`
|
||||
|
||||
2. **Manual Pull (Alternatif)**
|
||||
Jika Anda lebih suka menarik image secara manual:
|
||||
```bash
|
||||
docker pull dyzulk/mivo:latest # Stable
|
||||
docker pull dyzulk/mivo:v1.0.0 # Versi Spesifik
|
||||
docker pull dyzulk/mivo:edge # Bleeding Edge
|
||||
```
|
||||
|
||||
*Catatan: Database disimpan secara permanen di `app/Database` melalui volume.*
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Server" color="success" /> Web Server {#web-server}
|
||||
|
||||
### Nginx
|
||||
Nginx tidak membaca `.htaccess`. Gunakan blok konfigurasi ini di blok `server` Anda:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name domain-anda.com;
|
||||
root /path/to/mivo/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # Sesuaikan versi
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Smartphone" color="warning" /> Mobile / STB {#mobile-stb}
|
||||
|
||||
### Awebserver
|
||||
1. Salin file MIVO ke folder `/htdocs`.
|
||||
2. Arahkan document root ke `public` jika didukung, atau akses via `http://localhost:8080/public`.
|
||||
3. Pastikan versi PHP kompatibel.
|
||||
|
||||
### Termux
|
||||
1. Install PHP: `pkg install php`
|
||||
2. Masuk ke direktori MIVO: `cd mivo`
|
||||
3. Gunakan built-in server:
|
||||
```bash
|
||||
php mivo serve --host=0.0.0.0 --port=8080
|
||||
```
|
||||
4. Akses melalui browser.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Globe" color="info" /> Shared Hosting {#shared-hosting}
|
||||
Kebanyakan shared hosting menggunakan Apache atau OpenLiteSpeed, yang didukung penuh.
|
||||
|
||||
1. **Upload File**: Upload file MIVO ke `public_html/mivo` (atau folder subdomain).
|
||||
2. **Arahkan Domain**:
|
||||
* **Direkomendasikan**: Pergi ke "Domains" atau "Subdomains" di cPanel dan set **Document Root** agar menunjuk langsung ke folder `public/` (contoh: `public_html/mivo/public`).
|
||||
* **Alternatif**: Jika tidak bisa mengubah Document Root, Anda bisa memindahkan isi `public/` ke root `public_html` dan memindahkan `app/`, `routes/`, dll satu level ke atas (tidak disarankan untuk keamanan).
|
||||
3. **Versi PHP**: Pilih PHP 8.0+ di menu "Select PHP Version".
|
||||
4. **Ekstensi**: Pastikan `sqlite3` dan `fileinfo` dicentang.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Cloud" color="primary" /> VPS & Cloud {#vps-cloud}
|
||||
|
||||
### aaPanel
|
||||
1. **Buat Website**: Tambah situs -> PHP-8.x.
|
||||
2. **Direktori Situs**:
|
||||
* Set **Running Directory** ke `/public`.
|
||||
* Hapus centang "Anti-XSS" (terkadang memblokir penyimpanan konfigurasi).
|
||||
3. **URL Rewrite**: Pilih template `thinkphp` atau `laravel` yang kompatibel.
|
||||
4. **Izin File**: Chown user `www` ke direktori situs.
|
||||
|
||||
### PaaS Cloud (Railway / Render / Heroku)
|
||||
> [!WARNING]
|
||||
> MIVO menggunakan SQLite. Kebanyakan PaaS Cloud menggunakan **Ephemeral Filesytem** (Data hilang saat restart).
|
||||
> Anda WAJIB menggunakan **Persistent Volume/Disk**.
|
||||
|
||||
---
|
||||
|
||||
## <Icon name="Settings" color="success" /> Pasca-Instalasi {#pasca-instalasi}
|
||||
Setelah menyiapkan server:
|
||||
1. Salin `.env.example` ke `.env` (jika belum dilakukan).
|
||||
2. **Install Aplikasi**
|
||||
* **Opsi A: CLI**
|
||||
Jalankan `php mivo install` di terminal Anda.
|
||||
* **Opsi B: Web Installer**
|
||||
Buka `http://domain-anda.com/install` di browser.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "MIVO"
|
||||
text: "Manajemen Voucher Mikrotik"
|
||||
tagline: Modern, Ringan, dan Efisien. Dibuat untuk perangkat spesifikasi rendah dengan UX premium.
|
||||
image:
|
||||
light: /logo-m.svg
|
||||
dark: /logo-m-dark.svg
|
||||
alt: Logo MIVO
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Mulai Sekarang
|
||||
link: /id/guide/installation
|
||||
- theme: alt
|
||||
text: Docker Image
|
||||
link: /id/guide/docker
|
||||
|
||||
features:
|
||||
- title: Core Ringan
|
||||
details: Dibangun di atas framework MVC minimal (~50KB core) yang dioptimalkan untuk STB/Android.
|
||||
- title: UI/UX Modern
|
||||
details: Desain Glassmorphism segar menggunakan TailwindCSS dan Alpine.js.
|
||||
- title: Docker Ready
|
||||
details: Image resmi berbasis Alpine (~50MB) dengan Nginx dan Supervisor.
|
||||
- title: Aman
|
||||
details: Konfigurasi berbasis environment (.env), kredensial terenkripsi, dan sesi aman.
|
||||
---
|
||||
|
||||
## Mengapa MIVO?
|
||||
|
||||
MIVO adalah **Sistem Manajemen Voucher Mikrotik** generasi baru, dirancang untuk memberikan pengalaman pengguna premium bahkan pada perangkat keras spesifikasi rendah.
|
||||
|
||||
### Sorotan Utama
|
||||
|
||||
- <Icon name="Zap" color="warning" /> **Sangat Cepat**: Tanpa framework berat seperti Laravel. Murni performa PHP 8.0+.
|
||||
- <Icon name="Smartphone" color="info" /> **Mobile First**: Desain responsif sepenuhnya yang terasa seperti aplikasi native.
|
||||
- <Icon name="Plug" color="success" /> **API First**: REST API bawaan dengan dukungan CORS untuk integrasi pihak ketiga.
|
||||
- <Icon name="Wrench" color="primary" /> **Ramah Developer**: Arsitektur bersih, CLI tools (`php mivo`), dan mudah dikembangkan.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
title: Buku Panduan
|
||||
---
|
||||
|
||||
# Buku Panduan
|
||||
|
||||
Selamat datang di **Buku Panduan MIVO**. Bagian ini mencakup aspek fungsional penggunaan aplikasi untuk mengelola jaringan Anda.
|
||||
|
||||
## <Icon name="BookOpen" color="primary" /> Topik
|
||||
|
||||
### <Icon name="Settings" color="info" /> [Pengaturan Global](/id/manual/settings/)
|
||||
Konfigurasikan pengaturan tingkat sistem:
|
||||
- **Manajemen Router**: Hubungkan dan kelola perangkat Mikrotik Anda.
|
||||
- **Template Voucher**: Desain dan sesuaikan tata letak voucher Anda.
|
||||
- **Logo Brand**: Unggah logo khusus untuk hotspot Anda.
|
||||
- **API & CORS**: Ekspos data router Anda dengan aman ke aplikasi pihak ketiga.
|
||||
|
||||
### <Icon name="Activity" color="success" /> [Operasional Router](/id/manual/router/)
|
||||
Kelola tugas harian router Anda setelah terhubung:
|
||||
- **Dashboard**: Pantau traffic real-time dan kesehatan sistem.
|
||||
- **Manajemen Hotspot**: Buat user, profil, dan generate voucher.
|
||||
- **Laporan**: Lacak penjualan Anda dan lihat log sistem.
|
||||
- **Tools Sistem**: Reboot, scheduler, dan manajemen DHCP.
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> Panduan ini fokus pada **penggunaan** aplikasi. Untuk instalasi dan konfigurasi server, silakan merujuk ke [Panduan](/id/guide/installation).
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Dashboard & Pemantauan
|
||||
|
||||
Dashboard berfungsi sebagai pusat kendali real-time untuk router Mikrotik Anda. Fitur ini mengumpulkan data penting dari API Mikrotik untuk memberikan gambaran instan tentang kesehatan jaringan Anda.
|
||||
|
||||
## <Icon name="LineChart" color="primary" /> Monitor Traffic Real-time
|
||||
|
||||
MIVO dilengkapi dengan monitor trafik langsung yang berkomunikasi langsung dengan interface Mikrotik Anda.
|
||||
- **Pemilihan Interface**: Pilih interface fisik atau virtual apa pun (contoh: `ether1`, `wlan1`, `bridge-hotspot`).
|
||||
- **Grafik Langsung**: Lihat trafik masuk dan keluar dalam satuan bits/sec atau bytes/sec.
|
||||
- **Pelacakan Puncak**: Identifikasi lonjakan bandwidth dengan cepat.
|
||||
|
||||
## <Icon name="Cpu" color="warning" /> Sumber Daya Router
|
||||
|
||||
Pantau kesehatan fisik perangkat keras Mikrotik Anda:
|
||||
- **Beban CPU**: Ditampilkan dalam persentase. Beban CPU yang tinggi mungkin menunjukkan perlunya pemutakhiran perangkat keras atau optimasi konfigurasi.
|
||||
- **Memori**: Melacak RAM yang bebas vs total RAM.
|
||||
- **Uptime**: Menunjukkan berapa lama router telah berjalan sejak reboot terakhir.
|
||||
- **Penyimpanan**: Pantau ruang yang tersedia pada memori flash router Anda.
|
||||
|
||||
## <Icon name="Smartphone" color="success" /> Sesi Aktif
|
||||
|
||||
Ringkasan cepat pengguna yang saat ini terautentikasi:
|
||||
- **Total Online**: Hitungan real-time pengguna yang sedang menggunakan hotspot.
|
||||
- **IP/MAC Aktif**: Pantau perangkat yang terhubung secara garis besar.
|
||||
|
||||
> [!TIP]
|
||||
> Biarkan Dashboard tetap terbuka selama jam sibuk untuk memantau kepadatan atau upaya akses yang tidak sah.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Manajemen Hotspot
|
||||
|
||||
Alat komprehensif untuk mengelola server Hotspot Mikrotik Anda, mulai dari pembuatan user hingga kontrol akses tingkat lanjut.
|
||||
|
||||
## <Icon name="Users" color="primary" /> User Hotspot {#users}
|
||||
|
||||
Halaman User (`/hotspot/users`) adalah database pusat untuk semua akun wifi Anda.
|
||||
- **Pembuatan Manual**: Tambah user tunggal dengan username dan password spesifik.
|
||||
- **Cetak Satuan**: Arahkan kursor ke user untuk melihat ikon cetak. Ini menggunakan template default yang ditetapkan pada profil mereka.
|
||||
- **Cetak Massal**: Pilih beberapa user dan gunakan menu **Batch Actions** untuk mencetak semuanya sekaligus dalam satu halaman.
|
||||
- **Monitor Status**: Lihat apakah user sedang login (Aktif) secara langsung di dalam daftar.
|
||||
|
||||
## <Icon name="Layers" color="info" /> Profil User {#profiles}
|
||||
|
||||
Profil User (`/hotspot/profiles`) menentukan aturan untuk setiap jenis voucher (contoh: 1 Jam, 1 Hari).
|
||||
- **Rate Limit**: Kontrol kecepatan unggah dan unduh (contoh: `512k/1M`).
|
||||
- **Shared Users**: Batasi berapa banyak perangkat yang dapat menggunakan akun yang sama secara bersamaan.
|
||||
- **Validity**: Atur berapa lama akun tetap aktif setelah login pertama.
|
||||
- **Harga**: Simpan harga jual untuk keperluan laporan.
|
||||
|
||||
## <Icon name="Ticket" color="success" /> Generator Voucher {#generate}
|
||||
|
||||
Generate ratusan voucher dalam hitungan detik (`/hotspot/generate`).
|
||||
1. **Jumlah**: Pilih berapa banyak voucher yang akan dibuat.
|
||||
2. **Server**: Pilih server hotspot mana yang dituju (biasanya `all`).
|
||||
3. **User Mode**: Pilih antara `Username & Password` atau `Username = Password`.
|
||||
4. **Prefix**: Tambahkan awalan (prefix) tetap pada setiap username yang dibuat.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Sesi Aktif & Cookies {#active}
|
||||
|
||||
Pantau dan kontrol koneksi saat ini (`/hotspot/active` dan `/hotspot/cookies`).
|
||||
- **Kick User**: Putuskan sesi user yang sedang aktif secara paksa.
|
||||
- **Cookies**: Kelola token 'remember me'. Menghapus cookie memaksa user untuk login kembali pada koneksi berikutnya.
|
||||
|
||||
## <Icon name="ShieldCheck" color="danger" /> Keamanan & Akses {#security}
|
||||
|
||||
Pengaturan lanjutan untuk akses jaringan tanpa persyaratan voucher biasa.
|
||||
- **IP Bindings**: Lewati login hotspot untuk alamat MAC atau IP tertentu (contoh: untuk printer kantor atau server).
|
||||
- **Walled Garden**: Izinkan akses ke situs web atau domain tertentu (contoh: portal bank Anda) bahkan sebelum user login.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Operasional Router
|
||||
|
||||
Operasional router adalah tugas-tugas spesifik dalam sesi setelah terhubung ke perangkat Mikrotik. Pengaturan ini bervariasi tergantung pada sesi router mana yang sedang Anda gunakan.
|
||||
|
||||
## <Icon name="Activity" color="primary" /> Ikhtisar
|
||||
|
||||
Setelah Anda memilih sesi dari sidebar, Anda mendapatkan akses ke alat-alat berikut:
|
||||
|
||||
- **[Dashboard](/id/manual/router/dashboard)**: Pemantauan traffic secara real-time.
|
||||
- **[Manajemen Hotspot](/id/manual/router/hotspot)**: User, Profil, dan Voucher.
|
||||
- **[Laporan](/id/manual/router/reports)**: Laporan penjualan dan log sistem.
|
||||
- **[Tools Sistem](/id/manual/router/tools)**: Reboot, Scheduler, dan DHCP.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Sinkronisasi Real-time
|
||||
|
||||
MIVO berkomunikasi langsung dengan API Mikrotik Anda. Sebagian besar perubahan akan langsung diterapkan pada perangkat Anda.
|
||||
@@ -1,27 +0,0 @@
|
||||
# Cetak Cepat (Quick Print)
|
||||
|
||||
Quick Print adalah modul khusus untuk penjualan voucher kecepatan tinggi. Fitur ini memungkinkan Anda untuk menampilkan paket tertentu yang ingin dijual dan mencetaknya dengan satu klik.
|
||||
|
||||
## <Icon name="BarChart2" color="primary" /> Dashboard Penjualan
|
||||
|
||||
Halaman utama (`/quick-print`) menampilkan "Paket" Anda dalam bentuk kartu besar yang dapat diklik.
|
||||
- **Generate Instan**: Mengklik paket akan langsung memerintahkan router untuk membuat user baru.
|
||||
- **Cetak Otomatis**: Setelah user dibuat, dialog cetak untuk voucher tersebut akan terbuka secara otomatis.
|
||||
|
||||
## <Icon name="Library" color="info" /> Manajemen Paket
|
||||
|
||||
Akses bagian **Kelola** (`/quick-print/manage`) untuk mempersonalisasi dashboard penjualan Anda.
|
||||
|
||||
### <Icon name="PlusCircle" color="success" /> Menambah Paket
|
||||
- **Profil**: Pilih profil user Mikrotik.
|
||||
- **Harga**: Tentukan harga tampilan (bisa berbeda dengan komentar di Mikrotik).
|
||||
- **Template**: Tetapkan template voucher khusus untuk paket ini.
|
||||
|
||||
### <Icon name="Trash2" color="danger" /> Menghapus Paket
|
||||
Menghapus paket di sini hanya menghapusnya dari dashboard Quick Print; **tidak** menghapus profil dari router Mikrotik Anda.
|
||||
|
||||
## <Icon name="Printer" color="warning" /> Alur Kerja
|
||||
1. **Admin** memilih paket dari dashboard.
|
||||
2. **MIVO** membuat akun username/password acak di Mikrotik.
|
||||
3. **MIVO** mengambil template yang ditentukan dan mengirimkannya ke engine cetak browser.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Laporan & Log
|
||||
|
||||
Analisis performa bisnis Anda dan pantau aktivitas sistem melalui alat pelaporan yang mendetail.
|
||||
|
||||
## <Icon name="BarChart" color="success" /> Laporan Penjualan {#selling}
|
||||
|
||||
Halaman Laporan Penjualan (`/reports/selling`) memberikan rincian pendapatan Anda secara mendetail.
|
||||
- **Garis Waktu**: Lihat penjualan berdasarkan hari, bulan, atau rentang tanggal khusus.
|
||||
- **Detail**: Lihat profil mana yang terjual, stempel waktu (timestamp), dan harganya.
|
||||
- **Ekspor**: (Jika tersedia) Ekspor data Anda untuk keperluan akuntansi offline.
|
||||
|
||||
## <Icon name="BarChart2" color="primary" /> Resume Penjualan {#resume}
|
||||
|
||||
Halaman Resume (`/reports/resume`) menawarkan pandangan tingkat tinggi yang disederhanakan tentang pertumbuhan bisnis Anda.
|
||||
- **Total Pendapatan**: Gabungan pendapatan dari seluruh penjualan voucher.
|
||||
- **Jumlah Voucher**: Total voucher yang terjual vs yang dibuat (generated).
|
||||
- **Perbandingan Sesi**: Bandingkan performa di berbagai sesi router yang berbeda.
|
||||
|
||||
## <Icon name="ClipboardList" color="info" /> Log Sistem {#logs}
|
||||
|
||||
Pantau peristiwa real-time dari router Mikrotik Anda (`/reports/user-log`).
|
||||
- **Peristiwa**: Lacak login user, logout, eksekusi script, dan error sistem.
|
||||
- **Pemecahan Masalah**: Gunakan log ini untuk mengidentifikasi mengapa user tidak dapat terhubung atau kapan sesi terputus.
|
||||
- **Live Stream**: Log diperbarui secara otomatis saat peristiwa terjadi di router.
|
||||
|
||||
> [!NOTE]
|
||||
> MIVO mengambil log ini langsung dari circular buffer Mikrotik. Bersihkan log Anda pada terminal Mikrotik jika buffer menjadi terlalu besar.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Tools Sistem
|
||||
|
||||
Utilitas penting untuk memelihara, menjadwalkan, dan memantau fungsi inti router Mikrotik Anda.
|
||||
|
||||
## <Icon name="Network" color="info" /> DHCP Leases
|
||||
|
||||
Halaman DHCP Leases (`/network/dhcp`) memungkinkan Anda memantau semua perangkat yang terhubung ke jaringan LAN atau Hotspot Anda, bahkan sebelum mereka login.
|
||||
- **Pelacakan Lease**: Lihat penetapan IP, alamat MAC, dan hostname dari perangkat yang terhubung.
|
||||
- **Monitor Pra-Login**: Berguna untuk mengidentifikasi perangkat yang terhubung tetapi kesulitan mencapai halaman login hotspot.
|
||||
|
||||
## <Icon name="Clock" color="primary" /> Scheduler Router
|
||||
|
||||
MIVO menyediakan antarmuka lengkap (`/system/scheduler`) untuk mengelola script dan jadwal internal Mikrotik.
|
||||
- **Daftar Tugas**: Lihat semua tugas terjadwal yang aktif dan dinonaktifkan di router Anda.
|
||||
- **Kelola Tugas**: Tambah, ubah, atau hapus tugas langsung dari MIVO.
|
||||
- **Otomatisasi**: Gunakan ini untuk eksekusi script berkala, seperti menghapus user yang kedaluwarsa atau menghasilkan laporan otomatis.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Tindakan Kritis
|
||||
|
||||
Picukan perintah tingkat sistem secara langsung dari antarmuka MIVO:
|
||||
- **<Icon name="RefreshCw" color="info" /> Reboot**: Muat ulang perangkat keras Mikrotik Anda dengan aman.
|
||||
- **<Icon name="Power" color="danger" /> Shutdown**: Matikan perangkat. Perhatikan bahwa Anda memerlukan akses fisik ke router untuk menghidupkannya kembali.
|
||||
|
||||
> [!WARNING]
|
||||
> Tindakan ini segera dieksekusi pada router Mikrotik Anda. Pastikan tidak ada operasi kritis yang sedang berjalan sebelum melakukan reboot.
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# API & CORS
|
||||
|
||||
MIVO memungkinkan aplikasi eksternal untuk mengakses data router Anda secara aman melalui REST API.
|
||||
|
||||
## <Icon name="Unlock" color="warning" /> Kebijakan CORS
|
||||
|
||||
Untuk memungkinkan situs web Anda (misal: pengecek status) memanggil API MIVO, Anda harus memasukkan domain tersebut ke daftar putih (whitelist).
|
||||
|
||||
1. Buka **Pengaturan** > **API & CORS**.
|
||||
2. Tambahkan URL situs web Anda (termasuk `https://`).
|
||||
3. Simpan perubahan.
|
||||
|
||||
## <Icon name="Terminal" color="info" /> Dokumentasi
|
||||
|
||||
Dokumentasi API mendetail tersedia di bagian pengembang.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Pengaturan Global
|
||||
|
||||
Pengaturan global mengontrol instance MIVO Anda secara sistem lunak. Ini adalah konfigurasi tingkat sistem yang tidak bergantung pada koneksi Mikrotik tertentu.
|
||||
|
||||
## <Icon name="Settings" color="primary" /> Ikhtisar
|
||||
|
||||
Akses pengaturan ini dari menu kanan atas atau sidebar utama.
|
||||
|
||||
- **[Router](/id/manual/settings/routers)**: Kelola koneksi Mikrotik Anda.
|
||||
- **[Template](/id/manual/settings/templates)**: Kustomisasi desain voucher.
|
||||
- **[Logo](/id/manual/settings/logos)**: Unggah logo brand.
|
||||
- **[API & CORS](/id/manual/settings/api-cors)**: Konfigurasi akses API.
|
||||
|
||||
## <Icon name="Shield" color="info" /> Administrasi
|
||||
|
||||
Anda juga dapat mengelola administrator MIVO dan perilaku sistem umum (Zona Waktu, Bahasa) di sini.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Logo Brand
|
||||
|
||||
Manajemen Logo memungkinkan Anda untuk mengelola galeri aset brand yang digunakan untuk kustomisasi voucher Hotspot Mikrotik dan antarmuka aplikasi.
|
||||
|
||||
## <Icon name="Image" color="primary" /> Tujuan
|
||||
|
||||
Dengan mengunggah logo bisnis Anda di sini, Anda dapat menciptakan pengalaman brand yang profesional bagi pelanggan Anda. Logo-logo ini disimpan terpusat dan dapat dipanggil secara dinamis di berbagai bagian MIVO.
|
||||
|
||||
## <Icon name="UploadCloud" color="success" /> Proses Mengunggah
|
||||
|
||||
1. Buka **Pengaturan** > **Logo**.
|
||||
2. **Drag & Drop** atau klik area unggah untuk memilih file Anda.
|
||||
3. **Format yang Didukung**: PNG, JPG, SVG, dan GIF didukung. Disarankan menggunakan PNG transparan atau SVG untuk tampilan terbaik di voucher.
|
||||
|
||||
## <Icon name="Hash" color="info" /> Logo ID & Fitur Copy
|
||||
|
||||
Setiap logo yang Anda unggah akan diberikan **Short ID** yang unik (contoh: `lg01`, `logo_wifi`).
|
||||
|
||||
### <Icon name="Copy" color="primary" /> Cara Menggunakan Copy ID
|
||||
Di galeri logo, arahkan kursor ke logo apapun untuk memunculkan tombol **Copy ID**.
|
||||
- **Fungsi**: Mengklik tombol ini akan menyalin ID unik tersebut ke clipboard Anda.
|
||||
- **Integrasi**: Gunakan ID ini di dalam **Voucher Template Editor** (contoh: `{{logo:lg01}}`) untuk menampilkan logo spesifik tersebut pada voucher yang dicetak.
|
||||
|
||||
## <Icon name="Trash2" color="danger" /> Manajemen
|
||||
|
||||
Anda dapat menghapus logo yang tidak digunakan kapan saja.
|
||||
> [!WARNING]
|
||||
> Menghapus logo akan menyebabkan template voucher yang mereferensikan ID-nya menampilkan gambar yang rusak (placeholder).
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Manajemen Router
|
||||
|
||||
Untuk mulai mengelola Mikrotik Anda, pertama-tama Anda perlu menghubungkannya ke MIVO.
|
||||
|
||||
## <Icon name="PlusCircle" color="success" /> Menghubungkan Router
|
||||
|
||||
1. Buka **Pengaturan** > **Router**.
|
||||
2. Klik **Tambah Router**.
|
||||
3. Isi detailnya:
|
||||
- **Nama Sesi**: Identitas unik untuk koneksi ini.
|
||||
- **Alamat IP**: IP Mikrotik atau nama DNS Anda.
|
||||
- **Username/Password**: Akun API Mikrotik Anda.
|
||||
- **Port**: Biasanya 8728 (API).
|
||||
|
||||
## <Icon name="Activity" color="info" /> Status Koneksi
|
||||
|
||||
MIVO akan mencoba terhubung ke router secara real-time. Jika status berwarna hijau, Anda siap memulai pengelolaan!
|
||||
|
||||
## <Icon name="Database" color="warning" /> Backup & Restore
|
||||
|
||||
Lindungi konfigurasi Anda dengan membuat cadangan (backup) database MIVO Anda.
|
||||
|
||||
- **Backup**: Membuat file JSON yang berisi semua sesi router dan pengaturan global.
|
||||
- **Restore**: Unggah file cadangan MIVO yang telah disimpan sebelumnya untuk memulihkan data Anda.
|
||||
|
||||
> [!CAUTION]
|
||||
> Melakukan restore akan menimpa pengaturan dan sesi Anda yang ada saat ini.
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# Pengaturan Sistem
|
||||
|
||||
Kelola akun administrator MIVO Anda dan perilaku aplikasi secara global.
|
||||
|
||||
## <Icon name="UserCheck" color="primary" /> Akun Admin
|
||||
|
||||
Ubah kredensial administrator MIVO Anda untuk menjaga keamanan sistem.
|
||||
- **Username**: Nama yang digunakan untuk login.
|
||||
- **Password**: Kata sandi aman untuk akses.
|
||||
|
||||
## <Icon name="Globe" color="info" /> Konfigurasi Global
|
||||
|
||||
Sesuaikan pengaturan lingkungan untuk seluruh aplikasi:
|
||||
- **Bahasa**: Pilih bahasa antarmuka pilihan Anda (Inggris/Indonesia).
|
||||
- **Zona Waktu**: Atur waktu lokal untuk laporan dan log yang akurat.
|
||||
- **Mata Uang**: Tentukan simbol mata uang yang digunakan dalam voucher dan laporan.
|
||||
|
||||
## <Icon name="ShieldAlert" color="warning" /> Keamanan
|
||||
|
||||
MIVO menggunakan autentikasi berbasis sesi. Pastikan Anda melakukan logout saat menggunakan terminal publik.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Template Voucher
|
||||
|
||||
MIVO memiliki engine template yang mumpuni untuk membuat voucher yang cantik dan siap cetak.
|
||||
|
||||
## <Icon name="FileCode" color="primary" /> Kustomisasi Template
|
||||
|
||||
Template menggunakan HTML dan placeholder khusus untuk menampilkan data voucher.
|
||||
|
||||
- **Variabel**: `{{username}}`, `{{password}}`, `{{price}}`, dll.
|
||||
- **Preview**: Uji template Anda secara instan dari editor.
|
||||
|
||||
## <Icon name="Printer" color="info" /> Cetak Cepat
|
||||
|
||||
Setelah template disimpan, template tersebut akan tersedia di menu **Cetak Cepat** (Quick Print) di dalam sesi router Anda.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "MIVO"
|
||||
text: "Mikrotik Voucher Management"
|
||||
tagline: Modern, Lightweight, and Efficient. Built for low-end devices with premium UX.
|
||||
image:
|
||||
light: /logo-m.svg
|
||||
dark: /logo-m-dark.svg
|
||||
alt: MIVO Logo
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /guide/installation
|
||||
- theme: alt
|
||||
text: Docker Image
|
||||
link: /guide/docker
|
||||
|
||||
features:
|
||||
- title: Lightweight Core
|
||||
details: Built on a custom minimal MVC framework (~50KB core) optimized for STB/Android.
|
||||
- title: Modern UI/UX
|
||||
details: Fresh Glassmorphism design system using TailwindCSS and Alpine.js.
|
||||
- title: Docker Ready
|
||||
details: Official Alpine-based image (~50MB) with Nginx and Supervisor.
|
||||
- title: Secure
|
||||
details: Environment-based config (.env), encrypted credentials, and secure sessions.
|
||||
---
|
||||
|
||||
## Why MIVO?
|
||||
|
||||
MIVO is a next-generation **Mikrotik Voucher Management System**, engineered to deliver premium user experience even on low-end hardware.
|
||||
|
||||
### Key Highlights
|
||||
|
||||
- <Icon name="Zap" color="warning" /> **Blazing Fast**: No heavy frameworks like Laravel. Pure PHP 8.0+ performance.
|
||||
- <Icon name="Smartphone" color="info" /> **Mobile First**: Fully responsive design that feels like a native app.
|
||||
- <Icon name="Plug" color="success" /> **API First**: Built-in REST API with CORS support for 3rd party integrations.
|
||||
- <Icon name="Wrench" color="primary" /> **Developer Friendly**: Clean architecture, CLI tools (`php mivo`), and easy extension.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
title: User Manual
|
||||
---
|
||||
|
||||
# User Manual
|
||||
|
||||
Welcome to the **MIVO User Manual**. This section covers the functional aspects of using the application to manage your network.
|
||||
|
||||
## <Icon name="BookOpen" color="primary" /> Topics
|
||||
|
||||
### <Icon name="Settings" color="info" /> [Global Settings](/manual/settings/)
|
||||
Configure your system-wide settings:
|
||||
- **Managing Routers**: Connect and manage your Mikrotik devices.
|
||||
- **Voucher Templates**: Design and customize your voucher layouts.
|
||||
- **Brand Logos**: Upload custom logos for your hotspot.
|
||||
- **API & CORS**: Securely expose your router data to 3rd party apps.
|
||||
|
||||
### <Icon name="Activity" color="success" /> [Router Operations](/manual/router/)
|
||||
Manage your everyday router tasks after connecting:
|
||||
- **Dashboard**: Monitor real-time traffic and system health.
|
||||
- **Hotspot Management**: Create users, profiles, and generate vouchers.
|
||||
- **Reports**: Track your sales and view system logs.
|
||||
- **System Tools**: Reboot, scheduler, and DHCP management.
|
||||
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> This manual focuses on **using** the application. For installation and server configuration, please refer to the [Guide](/guide/installation).
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Dashboard & Monitoring
|
||||
|
||||
The Dashboard provides a real-time command center for your Mikrotik router. It aggregates critical data from the Mikrotik API to give you an immediate overview of your network's health.
|
||||
|
||||
## <Icon name="LineChart" color="primary" /> Real-time Traffic Monitor
|
||||
|
||||
MIVO features a live traffic monitor that communicates directly with your Mikrotik interfaces.
|
||||
- **Interface Selection**: Choose any physical or virtual interface (e.g., `ether1`, `wlan1`, `bridge-hotspot`).
|
||||
- **Live Graphing**: View incoming and outgoing traffic in bits/sec or bytes/sec.
|
||||
- **Peak Tracking**: Quickly identify bandwidth spikes.
|
||||
|
||||
## <Icon name="Cpu" color="warning" /> Router Resources
|
||||
|
||||
Monitor the physical health of your Mikrotik hardware:
|
||||
- **CPU Load**: Displayed as a percentage. High CPU load may indicate the need for hardware upgrades or configuration optimization.
|
||||
- **Memory**: Tracks free vs. total RAM.
|
||||
- **Uptime**: Shows how long the router has been running since the last reboot.
|
||||
- **Disk/Storage**: Monitor available space on your router's flash memory.
|
||||
|
||||
## <Icon name="Smartphone" color="success" /> Active Sessions
|
||||
|
||||
A quick summary of currently authenticated users:
|
||||
- **Total Online**: Real-time count of users currently using the hotspot.
|
||||
- **Active IP/MAC**: Monitor connected devices on a high level.
|
||||
|
||||
> [!TIP]
|
||||
> Keep the Dashboard open during peak hours to monitor for congestion or unauthorized access attempts.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Hotspot Management
|
||||
|
||||
Comprehensive tools to manage your Mikrotik Hotspot server, from user creation to advanced access control.
|
||||
|
||||
## <Icon name="Users" color="primary" /> Hotspot Users {#users}
|
||||
|
||||
The Users page (`/hotspot/users`) is the central database for all your wifi accounts.
|
||||
- **Manual Creation**: Add individual users with specific usernames and passwords.
|
||||
- **Individual Printing**: Hover over a user to see the print icon. It uses the default template assigned to their profile.
|
||||
- **Batch Printing**: Select multiple users and use the **Batch Actions** menu to print them all at once on a single page.
|
||||
- **Status Monitor**: See if a user is currently logged in (Active) directly in the list.
|
||||
|
||||
## <Icon name="Layers" color="info" /> User Profiles {#profiles}
|
||||
|
||||
User Profiles (`/hotspot/profiles`) define the rules for each type of voucher (e.g., 1 Hour, 1 Day).
|
||||
- **Rate Limit**: Control upload and download speeds (e.g., `512k/1M`).
|
||||
- **Shared Users**: Limit how many devices can use the same account simultaneously.
|
||||
- **Validity**: Set how long the account remains active after the first login.
|
||||
- **Price**: Store the selling price for reporting purposes.
|
||||
|
||||
## <Icon name="Ticket" color="success" /> Voucher Generator {#generate}
|
||||
|
||||
Generate hundreds of vouchers in seconds (`/hotspot/generate`).
|
||||
1. **Quantity**: Choose how many vouchers to create.
|
||||
2. **Server**: Select which hotspot server to target (usually `all`).
|
||||
3. **User Mode**: Choose between `Username & Password` or `Username = Password`.
|
||||
4. **Prefix**: Add a constant prefix to every generated username.
|
||||
|
||||
## <Icon name="Zap" color="warning" /> Active Sessions & Cookies {#active}
|
||||
|
||||
Monitor and control current connections (`/hotspot/active` and `/hotspot/cookies`).
|
||||
- **Kick User**: Forcefully disconnect an active user session.
|
||||
- **Cookies**: Manage 'remember me' tokens. Deleting a cookie forces the user to log in again on their next connection.
|
||||
|
||||
## <Icon name="ShieldCheck" color="danger" /> Security & Access {#security}
|
||||
|
||||
Advanced settings for network access without typical voucher requirements.
|
||||
- **IP Bindings**: Bypass the hotspot login for specific MAC or IP addresses (e.g., for office printers or servers).
|
||||
- **Walled Garden**: Allow access to specific websites or domains (e.g., your bank's portal) even before users log in.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user