Ini adalah kode sumber dari proyek saat ini. Gunakan ini sebagai konteks untuk melakukan perubahan yang diminta.
// FILE: vercel.json
{}
---
// FILE: tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./public/**/*.html",
],
theme: {
extend: {},
},
plugins: [],
}
---
// FILE: postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
---
// FILE: package.json
{
"name": "NextA-API",
"version": "1.0.0",
"description": "Proyek API Publik modular dengan fitur Fast Update via AI - Next.js Version.",
"scripts": {
"dev": "NODE_PATH=/root/.picoclaw/workspace/scraper-collection/node_modules next dev -p 8080",
"build": "NODE_PATH=/root/.picoclaw/workspace/scraper-collection/node_modules next build",
"build:full": "node scripts/generate-docs.js && NODE_PATH=/root/.picoclaw/workspace/scraper-collection/node_modules next build",
"vercel:build": "node scripts/generate-docs.js && next build",
"start": "NODE_PATH=/root/.picoclaw/workspace/scraper-collection/node_modules next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.7.2",
"axios-cookiejar-support": "^5.0.0",
"cheerio": "^1.0.0-rc.12",
"cloudscraper": "^4.6.0",
"crypto-js": "^4.2.0",
"fast-xml-parser": "^4.3.2",
"ffmpeg-static": "^5.2.0",
"form-data": "^4.0.0",
"fs-extra": "^11.2.0",
"glob": "^10.5.0",
"mime-types": "^2.1.35",
"next": "14.2.3",
"nprogress": "^0.2.0",
"pg": "^8.11.5",
"qs": "^6.11.0",
"react": "^18",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"react-virtuoso": "^4.7.11",
"remark-gfm": "^4.0.0",
"tough-cookie": "^4.1.3",
"uuid": "^9.0.1",
"openai": "^4.0.0"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4"
}
}
---
// FILE: next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false, // Nonaktifkan strict mode untuk menghindari double-invocation pada useEffect di dev (opsional)
serverComponentsExternalPackages: ['ffmpeg-static'],
experimental: {
serverComponentsExternalPackages: ['ffmpeg-static'],
// Fallback: beberapa versi Next.js membutuhkan outputFileTracingIncludes di dalam experimental
outputFileTracingIncludes: {
'/api/chess/stockfish': ['./lib/stockfish/**/*'],
'app/api/chess/stockfish/route': ['./lib/stockfish/**/*'],
},
},
// Pastikan stockfish.wasm dan stockfish.js ikut tercopy saat deploy (top-level, Next.js 14+)
outputFileTracingIncludes: {
// URL path pattern
'/api/chess/stockfish': ['./lib/stockfish/**/*'],
// File path pattern (untuk beberapa versi Next.js)
'app/api/chess/stockfish/route': ['./lib/stockfish/**/*'],
// Wildcard pattern
'/api/**': ['./lib/stockfish/**/*'],
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 'cdn.pixabay.com',
},
{
protocol: 'https',
hostname: 'i.pinimg.com',
},
{
protocol: 'https',
hostname: 'raw.githubusercontent.com',
},
{
protocol: 'https',
hostname: 'api.dicebear.com',
},
{
protocol: 'https',
hostname: 'picoclaw.io',
},
],
},
async headers() {
return [
{
// Terapkan header CORS dan Contact ke semua rute API
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" }, // Izinkan akses dari semua domain
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT" },
{ key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" },
{ key: "Contact", value: "https://t.me/puruboy_hub" } // Header kontak tambahan
]
}
]
},
async rewrites() {
return [
{
source: '/admin',
destination: '/admin.html',
},
{
source: '/fastupdate',
destination: '/fastupdate.html',
},
// Mapping endpoint lama agar kompatibel dengan frontend
{
source: '/download',
destination: '/api/fastupdate/download',
},
{
source: '/update',
destination: '/api/fastupdate/update',
},
// Fix Favicon untuk Search Engine (Map .ico request ke .jpg)
{
source: '/favicon.ico',
destination: '/favicon.jpg',
},
];
},
};
module.exports = nextConfig;
---
// FILE: middleware.js
/**
* Middleware Global ā Next.js 14 (Edge Runtime)
*
* Meng-intercept SEMUA response API non-200 dan mengirim report ke Telegram.
* Juga menangkap error yang terlewat dari try-catch di route handlers.
*
* Cocok untuk: route yang return non-200 tanpa throw exception
* (validasi 400, auth 401, not found 404, dll)
*
* Runtime: Edge (otomatis oleh Next.js untuk middleware)
*/
// Rate limiter sederhana (in-memory, reset tiap deploy)
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = 5 * 60 * 1000; // 5 menit
const BOT_TOKEN = '8757256180:AAEFM7NpH1eWRNsV9gTU6s2Vbsg2OdvKFfI';
const CHAT_ID = '7004559855';
/**
* Escape HTML untuk Telegram parse_mode
*/
function escapeHtml(text) {
if (!text) return '';
return String(text)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
/**
* Kirim pesan ke Telegram via fetch (Edge-compatible)
*/
async function sendTelegram(message) {
try {
const url = `https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`;
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: CHAT_ID,
text: message,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
return resp.ok;
} catch (err) {
console.error('[Middleware] Gagal kirim ke Telegram:', err.message);
return false;
}
}
/**
* Format pesan error untuk non-200 response
*/
function formatErrorMessage(status, statusText, endpoint, method, body) {
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
const emoji = status >= 500 ? 'š“' : status >= 400 ? 'š”' : 'āŖ';
let bodyStr = body ? JSON.stringify(body).substring(0, 200) : 'null';
return [
`${emoji} Non-200 Response ā Na-api`,
``,
`ā± Waktu: ${timestamp}`,
`š Endpoint: ${escapeHtml(endpoint)}`,
`š§ Method: ${escapeHtml(method)}`,
`š Status: ${status} ${escapeHtml(statusText || '')}`,
bodyStr !== 'null' ? `\nš¦ Body:\n
${escapeHtml(bodyStr)}` : '',
``,
`ā ļø Response non-200 terdeteksi oleh middleware`,
].filter(Boolean).join('\n');
}
/**
* Check rate limit untuk mencegah spam
* Key: endpoint + status code
*/
function isRateLimited(endpoint, status) {
const key = `${endpoint}:${status}`;
const now = Date.now();
const lastReport = rateLimitMap.get(key);
if (lastReport && (now - lastReport) < RATE_LIMIT_WINDOW) {
return true; // Still within rate limit window
}
rateLimitMap.set(key, now);
return false;
}
export async function middleware(request) {
const url = request.nextUrl;
const pathname = url.pathname;
// Hanya intercept API routes
if (!pathname.startsWith('/api/')) {
return;
}
// Skip route yang berdurasi panjang (download/konversi video / chess engine)
if (pathname.includes('/temp/') || pathname.includes('/media/') || pathname.includes('/chess/')) {
return;
}
// Skip jika method OPTIONS (CORS preflight)
if (request.method === 'OPTIONS') {
return;
}
// Clone request untuk dibaca bodynya
let requestBody = null;
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
try {
const cloned = request.clone();
requestBody = await cloned.json().catch(() => null);
} catch (e) {
// ignore
}
}
// Lanjutkan request ke handler
const response = await fetch(request);
// Jika response OK (2xx), tidak perlu report
if (response.status >= 200 && response.status < 300) {
return response;
}
// --- Non-200 response ---
// Rate limiting: jangan spam untuk error yang sama
if (isRateLimited(pathname, response.status)) {
return response; // Skip report, return response as-is
}
// Ambil response body untuk context
let responseBody = null;
let isHtmlResponse = false;
try {
const cloned = response.clone();
const contentType = cloned.headers.get('content-type') || '';
isHtmlResponse = contentType.includes('text/html');
if (isHtmlResponse) {
// HTML response (default Next.js error page) ā baca sebagai text
responseBody = await cloned.text().then(t => t.substring(0, 300));
} else {
responseBody = await cloned.json().catch(() => null);
}
} catch (e) {
// ignore
}
// Format & kirim ke Telegram (fire-and-forget)
const message = formatErrorMessage(
response.status,
response.statusText,
pathname,
request.method,
requestBody || responseBody
);
// Fire and forget ā jangan blokir response
sendTelegram(message).catch(() => {});
// Log ke console
console.error(`[Middleware] Non-200: ${pathname} | ${request.method} | ${response.status}`);
// šÆ Jika response berupa HTML (default Next.js error page),
// konversi ke JSON biar API selalu konsisten
if (isHtmlResponse) {
const statusTextMap = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
413: 'Payload Too Large',
415: 'Unsupported Media Type',
422: 'Unprocessable Entity',
429: 'Too Many Requests',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
};
const hint = response.status === 405
? `This endpoint does not support ${request.method}. Check the documentation for supported methods.`
: null;
return new Response(JSON.stringify({
success: false,
error: statusTextMap[response.status] || `HTTP ${response.status}`,
...(hint ? { hint } : {}),
status: response.status,
}), {
status: response.status,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
}
return response;
}
// Hanya aktif untuk /api/* routes
export const config = {
matcher: '/api/:path*',
};
---
// FILE: instrumentation.js
/**
* Instrumentation ā Next.js server-side initialization
* Auto-load error monitor untuk global error catching
*/
export async function register() {
// Only run on server
if (typeof window !== 'undefined') return;
try {
const { reportError } = await import('./lib/errorLogger');
// Unhandled Promise rejections
process.on('unhandledRejection', (reason) => {
const error = reason instanceof Error ? reason : new Error(String(reason));
reportError(error, {
endpoint: 'GLOBAL/unhandledRejection',
method: 'PROMISE',
});
});
// Uncaught exceptions
process.on('uncaughtException', (error) => {
reportError(error, {
endpoint: 'GLOBAL/uncaughtException',
method: 'UNCAUGHT',
});
});
console.log('[Instrumentation] Error monitor registered ā
');
} catch (err) {
console.error('[Instrumentation] Gagal register error monitor:', err.message);
}
}
---
// FILE: git-push.js
const { execSync } = require('child_process');
const fs = require('fs');
const dir = '/root/.picoclaw/workspace/na-api';
const memoryRaw = fs.readFileSync('/root/.picoclaw/workspace/memory/MEMORY.md', 'utf8');
const tokenMatch = memoryRaw.match(/\| github_token_na_api \| (.+?) \|/);
const token = tokenMatch ? tokenMatch[1] : null;
if (!token) { console.error('Token not found'); process.exit(1); }
execSync('git add -A', { cwd: dir });
execSync('git -c user.name=picoclaw -c user.email=picoclaw@users.noreply.github.com commit -m "feat: tambah endpoint loadtest stockfish untuk debugging"', { cwd: dir });
execSync(`git remote set-url origin "https://picoclaw:${token}@github.com/purujawa06-bot/Na-api.git"`, { cwd: dir });
execSync('git push origin main', { cwd: dir, stdio: 'inherit' });
execSync('git remote set-url origin https://github.com/purujawa06-bot/Na-api.git', { cwd: dir });
console.log('Push selesai!');
---
// FILE: scripts/rebuild-docs.js
const { scanDocs } = require('/root/.picoclaw/workspace/Na-api/lib/docsService');
const path = require('path');
const fse = require('fs-extra');
scanDocs().then(async (spec) => {
const outputPath = '/root/.picoclaw/workspace/Na-api/public/docs.json';
await fse.ensureDir(path.dirname(outputPath));
await fse.writeJson(outputPath, spec, { spaces: 2 });
console.log('ā
docs.json regenerated successfully!');
console.log('š Categories:', Object.keys(spec).length);
// Show gemini-v3 entry
for (const [cat, eps] of Object.entries(spec)) {
for (const ep of eps) {
if (ep.path && ep.path.includes('gemini-v3')) {
console.log('\n=== Gemini V3 Docs Entry ===');
console.log(JSON.stringify(ep, null, 2));
}
}
}
}).catch(e => console.error('ā Error:', e));
Updated `vercel:build` script in `Na-api/package.json` ā
---
// FILE: scripts/patch-routes.js
#!/usr/bin/env node
/**
* Patch Routes ā Auto-inject errorLogger ke semua API route files
*
* Cara pakai: node scripts/patch-routes.js
*/
const fs = require('fs');
const path = require('path');
const API_DIR = path.join(__dirname, '..', 'app', 'api');
/**
* Hitung jumlah "../" untuk import dari route.js ke lib/errorLogger
* Formula: jumlah folder dari 'app/api/' ke file + 2 (untuk app/ dan api/)
*/
function getImportDepth(routePath) {
// routePath relative to API_DIR, misal: "search/youtube/route.js" atau "blogs/route.js"
const normalized = routePath.replace(/\\/g, '/');
const parts = normalized.split('/');
// parts = ["search", "youtube", "route.js"]
// jumlah folder (exclude route.js) = parts.length - 1
const folderCount = parts.length - 1; // exclude route.js
return folderCount + 2; // +2 untuk app/ dan api/
}
function getImportPath(routePath) {
const depth = getImportDepth(routePath);
return '../'.repeat(depth) + 'lib/errorLogger';
}
/**
* Dapatkan nama endpoint dari path relatif
*/
function getEndpointName(routePath) {
let name = '/' + routePath.replace(/\/route\.js$/, '');
// Convert [param] ke :param
name = name.replace(/\[(.+?)\]/g, ':$1');
return name;
}
async function patchFile(filePath) {
let content = fs.readFileSync(filePath, 'utf-8');
const original = content;
// Skip if already patched
if (content.includes("from '") && content.includes("errorLogger")) {
console.log(` āļø Already patched: ${path.relative(API_DIR, filePath)}`);
return false;
}
const relPath = path.relative(API_DIR, filePath);
const importPath = getImportPath(relPath);
const endpointName = getEndpointName(relPath);
const importLine = `import { reportError } from '${importPath}';`;
// 1. Add import after the last import statement
const lines = content.split('\n');
let lastImportIdx = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('import ')) {
lastImportIdx = i;
}
}
if (lastImportIdx >= 0) {
lines.splice(lastImportIdx + 1, 0, importLine);
} else {
lines.unshift(importLine);
}
content = lines.join('\n');
// 2. Find all catch blocks and inject reportError
// We'll find patterns like: catch (error) { ... return NextResponse.json ...
// And inject reportError right after the opening brace
let result = '';
let remaining = content;
let catchCount = 0;
while (remaining.length > 0) {
const catchMatch = remaining.match(/catch\s*\(\s*(\w+)\s*\)\s*\{/);
if (!catchMatch) {
result += remaining;
break;
}
const catchVar = catchMatch[1];
const beforeCatch = remaining.substring(0, catchMatch.index);
// Find the HTTP method for this catch block (look backwards)
const beforeText = beforeCatch;
const methodRegex = /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/g;
let methodMatch;
let method = 'UNKNOWN';
while ((methodMatch = methodRegex.exec(beforeText)) !== null) {
method = methodMatch[1];
}
// Count braces from catch { to find the matching }
let braceCount = 1;
let pos = catchMatch.index + catchMatch[0].length;
while (braceCount > 0 && pos < remaining.length) {
if (remaining[pos] === '{') braceCount++;
else if (remaining[pos] === '}') braceCount--;
pos++;
}
const catchBlockEnd = pos - 1; // position of the closing }
const catchBlockContent = remaining.substring(catchMatch.index + catchMatch[0].length, catchBlockEnd);
// Build the modified catch block
const reportCall = `\n // Auto-report error ke Telegram\n reportError(${catchVar}, { endpoint: '${endpointName}', method: '${method}' }).catch(() => {});\n`;
const newCatchBlock = `catch (${catchVar}) {${reportCall}${catchBlockContent}}`;
result += beforeCatch + newCatchBlock;
remaining = remaining.substring(catchBlockEnd + 1);
catchCount++;
}
content = result;
// 3. Write if changed
if (content !== original) {
fs.writeFileSync(filePath, content, 'utf-8');
// Get method names
const methods = [];
const methodNames = content.match(/export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)/g);
if (methodNames) {
methodNames.forEach(m => {
const match = m.match(/(GET|POST|PUT|DELETE|PATCH)/);
if (match) methods.push(match[1]);
});
}
console.log(` ā
[${methods.join(',')}] ${relPath}`);
return true;
}
console.log(` ā ļø No changes: ${relPath}`);
return false;
}
async function main() {
console.log('š§ Na-api Error Logger Patcher\n');
console.log(`š API Directory: ${API_DIR}\n`);
// Find all route.js files
const routeFiles = [];
function walkDir(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath);
} else if (entry.name === 'route.js') {
routeFiles.push(fullPath);
}
}
}
walkDir(API_DIR);
console.log(`š¦ Found ${routeFiles.length} route files\n`);
let patched = 0;
let skipped = 0;
for (const filePath of routeFiles) {
try {
const changed = await patchFile(filePath);
if (changed) patched++;
else skipped++;
} catch (err) {
console.error(` ā Error: ${path.relative(API_DIR, filePath)} - ${err.message}`);
}
}
console.log(`\n⨠Done! ${patched} files patched, ${skipped} files skipped.`);
}
main().catch(console.error);
---
// FILE: scripts/generate-docs.js
const { scanDocs } = require('../lib/docsService');
const fse = require('fs-extra');
const path = require('path');
(async () => {
console.log('š Starting API Documentation Generation...');
try {
const spec = await scanDocs();
// Output ke folder public agar bisa diakses/dibaca di runtime production
const outputPath = path.join(process.cwd(), 'public', 'docs.json');
await fse.ensureDir(path.dirname(outputPath));
await fse.writeJson(outputPath, spec, { spaces: 2 });
console.log(`ā
Success! docs.json generated at: ${outputPath}`);
console.log(`š Stats: Found ${Object.keys(spec).length} categories.`);
} catch (err) {
console.error('ā Failed to generate docs:', err);
process.exit(1);
}
})();
---
// FILE: public/manifest.json
{
"name": "PuruBoy API",
"short_name": "PuruBoy API",
"description": "Platform REST API gratis untuk developer Indonesia dengan fitur AI, Downloader, Anime, dan Tools.",
"start_url": "/",
"display": "standalone",
"background_color": "#09090b",
"theme_color": "#ec4899",
"icons": [
{
"src": "/favicon.jpg",
"sizes": "192x192",
"type": "image/jpeg"
},
{
"src": "/favicon.jpg",
"sizes": "512x512",
"type": "image/jpeg"
}
],
"categories": ["developer tools", "web services", "api"],
"lang": "id",
"orientation": "any"
}
---
// FILE: public/google41f3f05fef8cd977.html
google-site-verification: google41f3f05fef8cd977.html
---
// FILE: public/fastupdate.html
AI-Powered Code Implementation
Ini preview HTML
',\n filename: 'promo-hari-ini'\n })\n });\n \n const data = await res.json();\n console.log(data);", "params": [ { "name": "content", "in": "body", "type": "string", "description": "(Wajib) Konten HTML yang akan diupload, bisa berupa string HTML biasa", "required": true }, { "name": "filename", "in": "body", "type": "string", "description": "(Opsional) Nama file tanpa ekstensi .html (default: \"preview\")", "required": true } ] }, { "title": "Al-Quran Digital", "summary": "Al-Quran Digital Kemenag.", "description": "Mengambil data ayat-ayat suci Al-Quran beserta terjemahan dan teks latin langsung dari sumber web-api Kemenag. Respons telah dirapikan dan dibersihkan dari properti bernilai null untuk efisiensi data.", "method": "GET", "path": "/api/tools/quran", "responseType": "json", "example": "async function getQuran() {\n const res = await fetch('/api/tools/quran?surah=10&ayah=1,2');\n const data = await res.json();\n console.log(data.result.data);\n }", "params": [ { "name": "surah", "in": "query", "type": "string", "description": "Nomor surah (1-114).", "required": true }, { "name": "ayah", "in": "query", "type": "string", "description": "Nomor ayat spesifik atau daftar ayat dipisah koma (contoh: \"1,2,3\"). Default: \"all\".", "required": false } ] }, { "title": "Remove BG", "summary": "Hapus Background Gambar.", "description": "Menghapus latar belakang gambar menggunakan API Pixelcut dan mengupload hasilnya ke penyimpanan sementara. Mengembalikan URL gambar hasil (PNG).", "method": "POST", "path": "/api/tools/removebg", "responseType": "json", "example": "// Contoh penggunaan\n async function removeBg() {\n try {\n const response = await fetch('/api/tools/removebg', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n \"url\": \"https://puruboy-api.vercel.app/example.jpg\" \n })\n });\n \n const data = await response.json();\n console.log(data); \n // Output: { status: 'success', url: 'https://domain.com/api/media/...' }\n } catch (error) {\n console.error('Error:', error.message);\n }\n }\n \n removeBg();", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL publik dari gambar yang ingin diproses.", "required": true } ] }, { "title": "Remove BG V2", "summary": "Hapus Background V2 (ILoveIMG).", "description": "Menghapus latar belakang gambar menggunakan layanan ILoveIMG. Alternatif handal untuk menghapus background dengan presisi tinggi.", "method": "POST", "path": "/api/tools/removebg-v2", "responseType": "json", "example": "// Contoh penggunaan\n async function removeBg() {\n try {\n const response = await fetch('/api/tools/removebg-v2', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n \"url\": \"https://puruboy-api.vercel.app/example.jpg\" \n })\n });\n \n const data = await response.json();\n console.log(data); \n } catch (error) {\n console.error('Error:', error.message);\n }\n }", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL publik dari gambar yang ingin diproses.", "required": true } ] }, { "title": "Colorize Image", "summary": "Colorize Image (AI Engine V2).", "description": "Mewarnai gambar hitam putih menggunakan AI Engine terbaru yang lebih akurat. Endpoint ini menggunakan Server-Sent Events (SSE). Hasil akhir disimpan sementara dalam database selama 30 menit.", "method": "POST", "path": "/api/tools/reviva", "responseType": "json", "example": "async function colorizeImage() {\n const response = await fetch('/api/tools/reviva', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \n \"url\": \"https://puruboy-api.vercel.app/example.jpg\" \n })\n });\n \n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n \n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const text = decoder.decode(value);\n if(text.includes('[true]')) {\n const retrieveUrl = text.replace('[true]', '').trim();\n console.log(\"Data tersedia di:\", retrieveUrl);\n }\n }\n }", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL gambar (hitam putih) yang ingin diwarnai.", "required": true } ] }, { "title": "Jadwal Sholat", "summary": "Jadwal Sholat Bulanan.", "description": "Mengambil jadwal sholat bulanan berdasarkan nama kota di Indonesia. Dilengkapi dengan algoritma similarity untuk menangani kesalahan penulisan (tyo) nama kota.", "method": "GET", "path": "/api/tools/sholat", "responseType": "json", "example": "async function getSholat() {\n const res = await fetch('/api/tools/sholat?q=jakarta');\n const data = await res.json();\n console.log(data);\n }", "params": [ { "name": "q", "in": "query", "type": "string", "description": "Nama kota (contoh: \"Jakarta\", \"Bandung\", \"Subang\").", "required": true } ] }, { "title": "AI Video Stabilizer", "summary": "Menstabilkan Video Shaky.", "description": "Memperbaiki guncangan (shake) pada video menggunakan teknologi AI Stabilizer. Endpoint ini akan mengembalikan job ID dan polling URL untuk memeriksa status pemrosesan.", "method": "POST", "path": "/api/tools/stabilizer", "responseType": "json", "example": "async function stabilize() {\n const res = await fetch('/api/tools/stabilizer', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \"url\": \"https://puruboy-api.vercel.app/example.mp4\" })\n });\n const data = await res.json();\n console.log(data);\n }", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL publik file video (MP4) yang ingin distabilkan.", "required": true } ] }, { "title": "AI Unblur", "summary": "AI Image Unblur (UnblurImage.ai).", "description": "Memperbaiki gambar yang buram (blur) agar menjadi lebih tajam dan jernih menggunakan teknologi AI. Endpoint ini menggunakan Server-Sent Events (SSE).", "method": "POST", "path": "/api/tools/unblur", "responseType": "json", "example": "// Contoh penggunaan SSE di client\n async function unblurImage() {\n const response = await fetch('/api/tools/unblur', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \"url\": \"https://puruboy-api.vercel.app/example.jpg\" })\n });\n \n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n const text = decoder.decode(value);\n if(text.includes('[true]')) {\n const url = text.replace('[true]', '').trim();\n console.log(\"Success:\", url);\n }\n }\n }", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL gambar yang ingin diperbaiki.", "required": true } ] }, { "title": "AI Upscale", "summary": "AI Image Upscale.", "description": "Meningkatkan resolusi dan kualitas gambar menggunakan AI (via Cloudinary). Endpoint ini menggunakan Server-Sent Events (SSE).", "method": "POST", "path": "/api/tools/upscale", "responseType": "json", "example": "// Contoh penggunaan SSE\n async function upscaleImage() {\n const response = await fetch('/api/tools/upscale', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ \"url\": \"https://puruboy-api.vercel.app/example.jpg\" })\n });\n \n const reader = response.body.getReader();\n // Read stream...\n }", "params": [ { "name": "url", "in": "body", "type": "string", "description": "URL gambar yang ingin di-upscale.", "required": true } ] } ] } --- // FILE: public/admin.htmlMasukkan kredensial untuk melanjutkan