API Rate Limiter
Run ID: 69b6ee7595d85cd597625a922026-03-29Development
PantheraHive BOS
BOS Dashboard

Workflow Execution Summary

The "API Rate Limiter" workflow (category: Development) has been successfully executed with the following user-defined inputs:

This execution configures a robust rate limiting mechanism designed to manage API traffic effectively, ensuring fair usage and system stability.

Introduction to API Rate Limiting

API Rate Limiting is a critical component for managing access to your API resources. It controls the number of requests a user or client can make to an API within a defined time window.

Purpose:

Strategy Deep Dive: Sliding Window

The Sliding Window strategy is an advanced and highly accurate rate limiting algorithm chosen for its smooth enforcement and fairness.

How it Works

Unlike fixed window counters that reset at arbitrary intervals, the sliding window algorithm continuously tracks request timestamps within a rolling time window.

  1. Timestamp Collection: For each client, the system stores the timestamp of every request made.
  2. Window Definition: A time window (in this case, 60 seconds) is defined.
  3. Request Evaluation: When a new request arrives:

* All timestamps older than the current time minus the window size (e.g., current_time - 60 seconds) are removed from the client's record.

* The remaining number of timestamps in the record is counted.

* If this count is less than the specified limit (100 requests), the request is allowed, and its timestamp is added to the record.

* If the count meets or exceeds the limit, the request is denied.

Example:

If the limit is 100 req/min, and a client makes 90 requests in the first 30 seconds of a minute, then makes 10 more in the next 10 seconds, they will hit the limit (100 requests in 40 seconds). Any new requests will be denied until older requests fall outside the 60-second window.

Advantages

Disadvantages

Configuration Details

Interpretation of the Limit:

This configuration dictates that any identified client (e.g., by IP address, API key, or user ID) cannot make more than 100 requests within any continuous 60-second period. This is a moderate limit, suitable for preventing casual abuse and ensuring basic fairness for most interactive applications without being overly restrictive for legitimate users.

Impact:

Implementation Recommendations

Implementing a sliding window rate limiter, especially in a distributed environment, requires careful consideration.

1. Client Identification

Crucially, define how to uniquely identify a "client" for rate limiting purposes. Common methods include:

2. Storage Mechanism

A shared, highly available data store is essential for distributed applications.

* Data Structure: Utilize Redis Sorted Sets (ZSETs). The score of each member should be the request's timestamp (e.g., milliseconds since epoch), and the member itself can be the timestamp or a unique request ID.

* Atomic Operations (Lua Script): To prevent race conditions, all operations (removing old requests, counting, adding new requests) must be atomic. A Redis Lua script is the best way to achieve this.

lua • 847 chars
    -- Pseudocode for Redis Lua Script
    local key = KEYS[1] -- Client identifier (e.g., 'rate_limit:ip:192.168.1.1')
    local window_ms = tonumber(ARGV[1]) -- 60000 ms (1 minute)
    local limit = tonumber(ARGV[2])     -- 100
    local current_time_ms = tonumber(ARGV[3])

    local trim_time = current_time_ms - window_ms

    -- 1. Remove requests older than the window
    redis.call('ZREMRANGEBYSCORE', key, '-inf', trim_time)

    -- 2. Count remaining requests
    local count = redis.call('ZCARD', key)

    -- 3. Check limit
    if count < limit then
        -- Allow: Add current timestamp
        redis.call('ZADD', key, current_time_ms, current_time_ms)
        redis.call('EXPIRE', key, window_ms / 1000 + 1) -- Set expiration slightly longer than window
        return 1 -- Allowed
    else
        return 0 -- Denied
    end
    
Sandboxed live preview
  • In-Memory (for Single-Instance Applications):

* Can use a ConcurrentHashMap<String, Deque<Long>> where the key is the client ID and the Deque stores timestamps.

* Not suitable for horizontally scaled applications due to lack of shared state.

3. API Integration

  • Middleware/Interceptor: Integrate the rate limiter as a middleware or interceptor in your API gateway or application framework (e.g., Express.js middleware, Spring Boot interceptor, Go HTTP handler).
  • Error Response: When a limit is hit, return an HTTP 429 Too Many Requests status code.
  • Retry-After Header: Include a Retry-After header in the 429 response, indicating how long the client should wait before retrying. This can be calculated as the time until the oldest request in the window expires, or a fixed value.

4. Libraries and Frameworks

Consider using existing libraries that abstract away much of the complexity, often supporting Redis backends:

  • Java: resilience4j-ratelimiter, custom implementation with Jedis/Lettuce.
  • Python: limits, pyrate-limiter.
  • Node.js: express-rate-limit (with Redis store options), rate-limiter-flexible.
  • Go: go-rate, custom with go-redis.
  • Cloud API Gateways: Services like AWS API Gateway, Google Cloud Endpoints, or Azure API Management often provide built-in rate limiting capabilities (verify their algorithm for sliding window support).

Monitoring and Alerting

Effective monitoring is crucial to understand the impact of your rate limiter and detect potential issues.

Key Metrics to Monitor

  • rate_limiter_requests_total: Total number of requests processed by the rate limiter.
  • rate_limiter_allowed_total: Total number of requests allowed.
  • rate_limiter_denied_total: Total number of requests denied (resulting in 429s).
  • rate_limiter_client_hits_max: The maximum number of requests observed for a single client within a window.
  • rate_limiter_error_total: Errors encountered by the rate limiter itself (e.g., Redis connection failures).
  • rate_limiter_latency_ms: Latency introduced by the rate limiting check.

Tools

  • Prometheus & Grafana: For robust time-series data collection, aggregation, and visualization.
  • Cloud-Native Monitoring: AWS CloudWatch, Google Cloud Monitoring, Azure Monitor.
  • Centralized Logging: ELK Stack (Elasticsearch, Logstash, Kibana), Splunk, DataDog for detailed logs of denied requests, including client identifiers.

Alerting Strategies

  • High Denial Rate: Alert if rate_limiter_denied_total exceeds a predefined percentage (e.g., 5%) of rate_limiter_requests_total over a 5-minute window. This could indicate a DoS attack, a misbehaving client, or that your limits are too strict.
  • Rate Limiter Errors: Immediate alert on any increase in rate_limiter_error_total, signaling a problem with the rate limiting infrastructure itself.
  • Individual Client Abuse: Alert if a specific client_id consistently hits the rate limit excessively over a prolonged period.

Testing Strategy

Thorough testing is essential to ensure the rate limiter functions as intended and doesn't introduce unexpected bottlenecks.

  • Unit Tests:

* Isolate the core sliding window logic.

* Verify request allowance up to the limit.

* Verify request denial once the limit is reached.

* Test edge cases: requests at window boundaries, rapid bursts, long periods of inactivity.

  • Integration Tests:

* Deploy the rate limiter with your API endpoints.

* Automated tests should send requests from a simulated client and verify that 429 responses are correctly returned when limits are expected to be hit.

* Confirm the Retry-After header is correctly populated.

  • Load Testing / Stress Testing:

* Use tools like JMeter, k6, or Locust to simulate high traffic volumes.

* Validate that the rate limiter performs as expected under stress, without becoming a bottleneck itself.

* Measure the performance overhead introduced by the rate limiter.

* Test the system's resilience when large numbers of clients hit their limits simultaneously.

Further Optimization and Advanced Considerations

  • Dynamic Rate Limiting: Implement logic to adjust limits based on real-time system load, client reputation, or user tier.
  • Tiered Rate Limiting: Offer different rate limits based on subscription plans (e.g., free tier vs. premium tier), API key types, or partner agreements.
  • Burst Handling: While sliding window is smooth, it can be strict on bursts. Consider combining it with a small "token bucket" for an initial burst allowance, then falling back to the sliding window.
  • Client-Side Backoff: Strongly encourage and document that API consumers implement exponential backoff and respect the Retry-After header to avoid being continuously blocked.
  • Distributed Consistency: Ensure your chosen shared data store (e.g., Redis Cluster) is highly available and provides strong consistency guarantees for rate limiting data.
  • Caching Interactions: Understand how your API caching strategy interacts with rate limiting. Cached responses might bypass rate limiting checks, which could be desirable or undesirable depending on the context.
  • Security Enhancements:

* IP Whitelisting: Bypass rate limiting for trusted internal services or known partners.

Bot Detection: Integrate with bot detection services or implement heuristics to identify and block malicious bots before* they consume rate limit allowances.

Actionable Summary & Next Steps

Based on the execution of the "API Rate Limiter" workflow with the sliding_window strategy and a 100 req/min limit, here are your immediate actionable steps:

  1. Define Client Identification: Clearly decide how your API will identify individual clients for rate limiting (e.g., IP address, API key, user ID). This is foundational.
  2. Choose Implementation Stack: Select the appropriate technology for your rate limiter. For distributed systems, Redis with Lua scripting for atomic operations is highly recommended.
  3. Develop Core Logic: Implement the sliding window algorithm, ensuring atomicity of operations. Leverage existing libraries where possible.
  4. Integrate into API Gateway/Application: Deploy the rate limiter as a middleware or interceptor in your API's request pipeline.
  5. Implement Error Responses: Ensure that requests exceeding the limit receive a 429 Too Many Requests HTTP status code, along with a meaningful Retry-After header.
  6. Set Up Monitoring and Alerting: Configure metrics collection (e.g., rate_limiter_denied_total) and set up alerts for high denial rates or rate limiter errors.
  7. Conduct Comprehensive Testing: Perform unit, integration, and load tests to validate functionality, performance, and resilience under various traffic conditions.
  8. Document for API Consumers: Clearly communicate your API's rate limits, the 429 response, and the importance of implementing client-side backoff strategies in your API documentation.
  9. Evaluate Advanced Features: As your system evolves, consider implementing tiered rate limits, dynamic adjustments, or specialized burst handling if your use cases demand it.

By following these steps, you will establish a robust and fair API rate limiting mechanism, significantly enhancing the stability and security of your services.

api_rate_limiter.txt
Download source file
Copy all content
Full output as text
Download ZIP
IDE-ready project ZIP
Copy share link
Permanent URL for this run
Get Embed Code
Embed this result on any website
Print / Save PDF
Use browser print dialog
\n\n\n"); var hasSrcMain=Object.keys(extracted).some(function(k){return k.indexOf("src/main")>=0;}); if(!hasSrcMain) zip.file(folder+"src/main."+ext,"import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App'\nimport './index.css'\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n \n \n \n)\n"); var hasSrcApp=Object.keys(extracted).some(function(k){return k==="src/App."+ext||k==="App."+ext;}); if(!hasSrcApp) zip.file(folder+"src/App."+ext,"import React from 'react'\nimport './App.css'\n\nfunction App(){\n return(\n
\n
\n

"+slugTitle(pn)+"

\n

Built with PantheraHive BOS

\n
\n
\n )\n}\nexport default App\n"); zip.file(folder+"src/index.css","*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:system-ui,-apple-system,sans-serif;background:#f0f2f5;color:#1a1a2e}\n.app{min-height:100vh;display:flex;flex-direction:column}\n.app-header{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px}\nh1{font-size:2.5rem;font-weight:700}\n"); zip.file(folder+"src/App.css",""); zip.file(folder+"src/components/.gitkeep",""); zip.file(folder+"src/pages/.gitkeep",""); zip.file(folder+"src/hooks/.gitkeep",""); Object.keys(extracted).forEach(function(p){ var fp=p.startsWith("src/")?p:"src/"+p; zip.file(folder+fp,extracted[p]); }); zip.file(folder+"README.md","# "+slugTitle(pn)+"\n\nGenerated by PantheraHive BOS.\n\n## Setup\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\n## Build\n\`\`\`bash\nnpm run build\n\`\`\`\n\n## Open in IDE\nOpen the project folder in VS Code or WebStorm.\n"); zip.file(folder+".gitignore","node_modules/\ndist/\n.env\n.DS_Store\n*.local\n"); } /* --- Vue (Vite + Composition API + TypeScript) --- */ function buildVue(zip,folder,app,code,panelTxt){ var pn=pkgName(app); var C=cc(pn); var extracted=extractCode(panelTxt); zip.file(folder+"package.json",'{\n "name": "'+pn+'",\n "version": "0.0.0",\n "type": "module",\n "scripts": {\n "dev": "vite",\n "build": "vue-tsc -b && vite build",\n "preview": "vite preview"\n },\n "dependencies": {\n "vue": "^3.5.13",\n "vue-router": "^4.4.5",\n "pinia": "^2.3.0",\n "axios": "^1.7.9"\n },\n "devDependencies": {\n "@vitejs/plugin-vue": "^5.2.1",\n "typescript": "~5.7.3",\n "vite": "^6.0.5",\n "vue-tsc": "^2.2.0"\n }\n}\n'); zip.file(folder+"vite.config.ts","import { defineConfig } from 'vite'\nimport vue from '@vitejs/plugin-vue'\nimport { resolve } from 'path'\n\nexport default defineConfig({\n plugins: [vue()],\n resolve: { alias: { '@': resolve(__dirname,'src') } }\n})\n"); zip.file(folder+"tsconfig.json",'{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]}\n'); zip.file(folder+"tsconfig.app.json",'{\n "compilerOptions":{\n "target":"ES2020","useDefineForClassFields":true,"module":"ESNext","lib":["ES2020","DOM","DOM.Iterable"],\n "skipLibCheck":true,"moduleResolution":"bundler","allowImportingTsExtensions":true,\n "isolatedModules":true,"moduleDetection":"force","noEmit":true,"jsxImportSource":"vue",\n "strict":true,"paths":{"@/*":["./src/*"]}\n },\n "include":["src/**/*.ts","src/**/*.d.ts","src/**/*.tsx","src/**/*.vue"]\n}\n'); zip.file(folder+"env.d.ts","/// \n"); zip.file(folder+"index.html","\n\n\n \n \n "+slugTitle(pn)+"\n\n\n
\n \n\n\n"); var hasMain=Object.keys(extracted).some(function(k){return k==="src/main.ts"||k==="main.ts";}); if(!hasMain) zip.file(folder+"src/main.ts","import { createApp } from 'vue'\nimport { createPinia } from 'pinia'\nimport App from './App.vue'\nimport './assets/main.css'\n\nconst app = createApp(App)\napp.use(createPinia())\napp.mount('#app')\n"); var hasApp=Object.keys(extracted).some(function(k){return k.indexOf("App.vue")>=0;}); if(!hasApp) zip.file(folder+"src/App.vue","\n\n\n\n\n"); zip.file(folder+"src/assets/main.css","*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui,sans-serif;background:#fff;color:#213547}\n"); zip.file(folder+"src/components/.gitkeep",""); zip.file(folder+"src/views/.gitkeep",""); zip.file(folder+"src/stores/.gitkeep",""); Object.keys(extracted).forEach(function(p){ var fp=p.startsWith("src/")?p:"src/"+p; zip.file(folder+fp,extracted[p]); }); zip.file(folder+"README.md","# "+slugTitle(pn)+"\n\nGenerated by PantheraHive BOS.\n\n## Setup\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\n## Build\n\`\`\`bash\nnpm run build\n\`\`\`\n\nOpen in VS Code or WebStorm.\n"); zip.file(folder+".gitignore","node_modules/\ndist/\n.env\n.DS_Store\n*.local\n"); } /* --- Angular (v19 standalone) --- */ function buildAngular(zip,folder,app,code,panelTxt){ var pn=pkgName(app); var C=cc(pn); var sel=pn.replace(/_/g,"-"); var extracted=extractCode(panelTxt); zip.file(folder+"package.json",'{\n "name": "'+pn+'",\n "version": "0.0.0",\n "scripts": {\n "ng": "ng",\n "start": "ng serve",\n "build": "ng build",\n "test": "ng test"\n },\n "dependencies": {\n "@angular/animations": "^19.0.0",\n "@angular/common": "^19.0.0",\n "@angular/compiler": "^19.0.0",\n "@angular/core": "^19.0.0",\n "@angular/forms": "^19.0.0",\n "@angular/platform-browser": "^19.0.0",\n "@angular/platform-browser-dynamic": "^19.0.0",\n "@angular/router": "^19.0.0",\n "rxjs": "~7.8.0",\n "tslib": "^2.3.0",\n "zone.js": "~0.15.0"\n },\n "devDependencies": {\n "@angular-devkit/build-angular": "^19.0.0",\n "@angular/cli": "^19.0.0",\n "@angular/compiler-cli": "^19.0.0",\n "typescript": "~5.6.0"\n }\n}\n'); zip.file(folder+"angular.json",'{\n "$schema": "./node_modules/@angular/cli/lib/config/schema.json",\n "version": 1,\n "newProjectRoot": "projects",\n "projects": {\n "'+pn+'": {\n "projectType": "application",\n "root": "",\n "sourceRoot": "src",\n "prefix": "app",\n "architect": {\n "build": {\n "builder": "@angular-devkit/build-angular:application",\n "options": {\n "outputPath": "dist/'+pn+'",\n "index": "src/index.html",\n "browser": "src/main.ts",\n "tsConfig": "tsconfig.app.json",\n "styles": ["src/styles.css"],\n "scripts": []\n }\n },\n "serve": {"builder":"@angular-devkit/build-angular:dev-server","configurations":{"production":{"buildTarget":"'+pn+':build:production"},"development":{"buildTarget":"'+pn+':build:development"}},"defaultConfiguration":"development"}\n }\n }\n }\n}\n'); zip.file(folder+"tsconfig.json",'{\n "compileOnSave": false,\n "compilerOptions": {"baseUrl":"./","outDir":"./dist/out-tsc","forceConsistentCasingInFileNames":true,"strict":true,"noImplicitOverride":true,"noPropertyAccessFromIndexSignature":true,"noImplicitReturns":true,"noFallthroughCasesInSwitch":true,"paths":{"@/*":["src/*"]},"skipLibCheck":true,"esModuleInterop":true,"sourceMap":true,"declaration":false,"experimentalDecorators":true,"moduleResolution":"bundler","importHelpers":true,"target":"ES2022","module":"ES2022","useDefineForClassFields":false,"lib":["ES2022","dom"]},\n "references":[{"path":"./tsconfig.app.json"}]\n}\n'); zip.file(folder+"tsconfig.app.json",'{\n "extends":"./tsconfig.json",\n "compilerOptions":{"outDir":"./dist/out-tsc","types":[]},\n "files":["src/main.ts"],\n "include":["src/**/*.d.ts"]\n}\n'); zip.file(folder+"src/index.html","\n\n\n \n "+slugTitle(pn)+"\n \n \n \n\n\n \n\n\n"); zip.file(folder+"src/main.ts","import { bootstrapApplication } from '@angular/platform-browser';\nimport { appConfig } from './app/app.config';\nimport { AppComponent } from './app/app.component';\n\nbootstrapApplication(AppComponent, appConfig)\n .catch(err => console.error(err));\n"); zip.file(folder+"src/styles.css","* { margin: 0; padding: 0; box-sizing: border-box; }\nbody { font-family: system-ui, -apple-system, sans-serif; background: #f9fafb; color: #111827; }\n"); var hasComp=Object.keys(extracted).some(function(k){return k.indexOf("app.component")>=0;}); if(!hasComp){ zip.file(folder+"src/app/app.component.ts","import { Component } from '@angular/core';\nimport { RouterOutlet } from '@angular/router';\n\n@Component({\n selector: 'app-root',\n standalone: true,\n imports: [RouterOutlet],\n templateUrl: './app.component.html',\n styleUrl: './app.component.css'\n})\nexport class AppComponent {\n title = '"+pn+"';\n}\n"); zip.file(folder+"src/app/app.component.html","
\n
\n

"+slugTitle(pn)+"

\n

Built with PantheraHive BOS

\n
\n \n
\n"); zip.file(folder+"src/app/app.component.css",".app-header{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;gap:16px}h1{font-size:2.5rem;font-weight:700;color:#6366f1}\n"); } zip.file(folder+"src/app/app.config.ts","import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';\nimport { provideRouter } from '@angular/router';\nimport { routes } from './app.routes';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideZoneChangeDetection({ eventCoalescing: true }),\n provideRouter(routes)\n ]\n};\n"); zip.file(folder+"src/app/app.routes.ts","import { Routes } from '@angular/router';\n\nexport const routes: Routes = [];\n"); Object.keys(extracted).forEach(function(p){ var fp=p.startsWith("src/")?p:"src/"+p; zip.file(folder+fp,extracted[p]); }); zip.file(folder+"README.md","# "+slugTitle(pn)+"\n\nGenerated by PantheraHive BOS.\n\n## Setup\n\`\`\`bash\nnpm install\nng serve\n# or: npm start\n\`\`\`\n\n## Build\n\`\`\`bash\nng build\n\`\`\`\n\nOpen in VS Code with Angular Language Service extension.\n"); zip.file(folder+".gitignore","node_modules/\ndist/\n.env\n.DS_Store\n*.local\n.angular/\n"); } /* --- Python --- */ function buildPython(zip,folder,app,code){ var title=slugTitle(app); var pn=pkgName(app); var src=code.replace(/^\`\`\`[\w]*\n?/m,"").replace(/\n?\`\`\`$/m,"").trim(); var reqMap={"numpy":"numpy","pandas":"pandas","sklearn":"scikit-learn","tensorflow":"tensorflow","torch":"torch","flask":"flask","fastapi":"fastapi","uvicorn":"uvicorn","requests":"requests","sqlalchemy":"sqlalchemy","pydantic":"pydantic","dotenv":"python-dotenv","PIL":"Pillow","cv2":"opencv-python","matplotlib":"matplotlib","seaborn":"seaborn","scipy":"scipy"}; var reqs=[]; Object.keys(reqMap).forEach(function(k){if(src.indexOf("import "+k)>=0||src.indexOf("from "+k)>=0)reqs.push(reqMap[k]);}); var reqsTxt=reqs.length?reqs.join("\n"):"# add dependencies here\n"; zip.file(folder+"main.py",src||"# "+title+"\n# Generated by PantheraHive BOS\n\nprint(title+\" loaded\")\n"); zip.file(folder+"requirements.txt",reqsTxt); zip.file(folder+".env.example","# Environment variables\n"); zip.file(folder+"README.md","# "+title+"\n\nGenerated by PantheraHive BOS.\n\n## Setup\n\`\`\`bash\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\n\`\`\`\n\n## Run\n\`\`\`bash\npython main.py\n\`\`\`\n"); zip.file(folder+".gitignore",".venv/\n__pycache__/\n*.pyc\n.env\n.DS_Store\n"); } /* --- Node.js --- */ function buildNode(zip,folder,app,code){ var title=slugTitle(app); var pn=pkgName(app); var src=code.replace(/^\`\`\`[\w]*\n?/m,"").replace(/\n?\`\`\`$/m,"").trim(); var depMap={"mongoose":"^8.0.0","dotenv":"^16.4.5","axios":"^1.7.9","cors":"^2.8.5","bcryptjs":"^2.4.3","jsonwebtoken":"^9.0.2","socket.io":"^4.7.4","uuid":"^9.0.1","zod":"^3.22.4","express":"^4.18.2"}; var deps={}; Object.keys(depMap).forEach(function(k){if(src.indexOf(k)>=0)deps[k]=depMap[k];}); if(!deps["express"])deps["express"]="^4.18.2"; var pkgJson=JSON.stringify({"name":pn,"version":"1.0.0","main":"src/index.js","scripts":{"start":"node src/index.js","dev":"nodemon src/index.js"},"dependencies":deps,"devDependencies":{"nodemon":"^3.0.3"}},null,2)+"\n"; zip.file(folder+"package.json",pkgJson); var fallback="const express=require(\"express\");\nconst app=express();\napp.use(express.json());\n\napp.get(\"/\",(req,res)=>{\n res.json({message:\""+title+" API\"});\n});\n\nconst PORT=process.env.PORT||3000;\napp.listen(PORT,()=>console.log(\"Server on port \"+PORT));\n"; zip.file(folder+"src/index.js",src||fallback); zip.file(folder+".env.example","PORT=3000\n"); zip.file(folder+".gitignore","node_modules/\n.env\n.DS_Store\n"); zip.file(folder+"README.md","# "+title+"\n\nGenerated by PantheraHive BOS.\n\n## Setup\n\`\`\`bash\nnpm install\n\`\`\`\n\n## Run\n\`\`\`bash\nnpm run dev\n\`\`\`\n"); } /* --- Vanilla HTML --- */ function buildVanillaHtml(zip,folder,app,code){ var title=slugTitle(app); var isFullDoc=code.trim().toLowerCase().indexOf("=0||code.trim().toLowerCase().indexOf("=0; var indexHtml=isFullDoc?code:"\n\n\n\n\n"+title+"\n\n\n\n"+code+"\n\n\n\n"; zip.file(folder+"index.html",indexHtml); zip.file(folder+"style.css","/* "+title+" — styles */\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:system-ui,-apple-system,sans-serif;background:#fff;color:#1a1a2e}\n"); zip.file(folder+"script.js","/* "+title+" — scripts */\n"); zip.file(folder+"assets/.gitkeep",""); zip.file(folder+"README.md","# "+title+"\n\nGenerated by PantheraHive BOS.\n\n## Open\nDouble-click \`index.html\` in your browser.\n\nOr serve locally:\n\`\`\`bash\nnpx serve .\n# or\npython3 -m http.server 3000\n\`\`\`\n"); zip.file(folder+".gitignore",".DS_Store\nnode_modules/\n.env\n"); } /* ===== MAIN ===== */ var sc=document.createElement("script"); sc.src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"; sc.onerror=function(){ if(lbl)lbl.textContent="Download ZIP"; alert("JSZip load failed — check connection."); }; sc.onload=function(){ var zip=new JSZip(); var base=(_phFname||"output").replace(/\.[^.]+$/,""); var app=base.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"my_app"; var folder=app+"/"; var vc=document.getElementById("panel-content"); var panelTxt=vc?(vc.innerText||vc.textContent||""):""; var lang=detectLang(_phCode,panelTxt); if(_phIsHtml){ buildVanillaHtml(zip,folder,app,_phCode); } else if(lang==="flutter"){ buildFlutter(zip,folder,app,_phCode,panelTxt); } else if(lang==="react-native"){ buildReactNative(zip,folder,app,_phCode,panelTxt); } else if(lang==="swift"){ buildSwift(zip,folder,app,_phCode,panelTxt); } else if(lang==="kotlin"){ buildKotlin(zip,folder,app,_phCode,panelTxt); } else if(lang==="react"){ buildReact(zip,folder,app,_phCode,panelTxt); } else if(lang==="vue"){ buildVue(zip,folder,app,_phCode,panelTxt); } else if(lang==="angular"){ buildAngular(zip,folder,app,_phCode,panelTxt); } else if(lang==="python"){ buildPython(zip,folder,app,_phCode); } else if(lang==="node"){ buildNode(zip,folder,app,_phCode); } else { /* Document/content workflow */ var title=app.replace(/_/g," "); var md=_phAll||_phCode||panelTxt||"No content"; zip.file(folder+app+".md",md); var h=""+title+""; h+="

"+title+"

"; var hc=md.replace(/&/g,"&").replace(//g,">"); hc=hc.replace(/^### (.+)$/gm,"

$1

"); hc=hc.replace(/^## (.+)$/gm,"

$1

"); hc=hc.replace(/^# (.+)$/gm,"

$1

"); hc=hc.replace(/\*\*(.+?)\*\*/g,"$1"); hc=hc.replace(/\n{2,}/g,"

"); h+="

"+hc+"

Generated by PantheraHive BOS
"; zip.file(folder+app+".html",h); zip.file(folder+"README.md","# "+title+"\n\nGenerated by PantheraHive BOS.\n\nFiles:\n- "+app+".md (Markdown)\n- "+app+".html (styled HTML)\n"); } zip.generateAsync({type:"blob"}).then(function(blob){ var a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=app+".zip"; a.click(); URL.revokeObjectURL(a.href); if(lbl)lbl.textContent="Download ZIP"; }); }; document.head.appendChild(sc); } function phShare(){navigator.clipboard.writeText(window.location.href).then(function(){var el=document.getElementById("ph-share-lbl");if(el){el.textContent="Link copied!";setTimeout(function(){el.textContent="Copy share link";},2500);}});}function phEmbed(){var runId=window.location.pathname.split("/").pop().replace(".html","");var embedUrl="https://pantherahive.com/embed/"+runId;var code='';navigator.clipboard.writeText(code).then(function(){var el=document.getElementById("ph-embed-lbl");if(el){el.textContent="Embed code copied!";setTimeout(function(){el.textContent="Get Embed Code";},2500);}});}