{"owner":"Countly","repo":"countly-server","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md",".github/copilot-instructions.md"],"skills":{"CLAUDE.md":"# CLAUDE.md - Countly Server\n\nThis file provides guidance for Claude (Anthropic) when working with this codebase.\n\n## Project Overview\n\nCountly is a product analytics platform. Backend: Node.js 22+, MongoDB. Frontend: Vue 2, Element UI, Backbone (legacy). Architecture is plugin-based.\n\n## Quick Commands\n\n```bash\nnpm run start:all:dev        # Start all services (API:3001, Frontend:6001)\nnpx grunt dist-all           # Build static assets (required after JS changes)\nnpx grunt locales            # Build locale files\nnpx grunt sass               # Compile SASS only\n\n# Testing\nnpm run test:unit            # Unit tests\nnpm run test:plugin -- name  # Single plugin tests\ncountly plugin lint name     # Lint plugin\ncountly shellcheck           # Validate shell scripts\n```\n\n## Critical Security Rules\n\n**ALWAYS follow these - no exceptions:**\n\n1. **API endpoints must use validation**:\n   ```javascript\n   const { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n   validateRead(params, FEATURE_NAME, () => { /* handler */ });\n   ```\n\n2. **Write operations must include app_id**:\n   ```javascript\n   // CORRECT - prevents cross-app access\n   db.collection(\"items\").deleteOne({_id: id, app_id: params.app_id + \"\"});\n   ```\n\n3. **Cast user input to strings for auth**:\n   ```javascript\n   params.username = params.username + \"\";\n   ```\n\n4. **Use spawn, not exec for shell commands**:\n   ```javascript\n   cp.spawn(\"command\", [userInput]);  // Safe\n   // exec(\"command \" + userInput);   // VULNERABLE\n   ```\n\n5. **Never use v-html with user data** in Vue templates.\n\n6. **Validate user-supplied Mongo queries — reject, never strip**. Any query/filter that comes from a request and reaches `find`/`aggregate`/`update`/`delete` must be checked with the `common` helpers at the endpoint where it is first parsed (NOT inside deep helpers or the `/drill/preprocess_query` hook). The query is run exactly as submitted or the request is rejected with `400` — it is never modified.\n   ```javascript\n   // raw query STRING from a request param → parse + validate in one step\n   var parsed = common.parseUserQuery(params.qstring.query); // accepts string OR object\n   if (parsed.error) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": \" + parsed.error);\n       return common.returnMessage(params, 400, parsed.error);\n   }\n   var query = parsed.query; // safe to run as-is\n\n   // ALREADY-parsed object — e.g. dbviewer parses with EJSON, or the query is\n   // nested in a larger saved payload. Validate that parsed object directly:\n   var parsedQuery = EJSON.parse(params.qstring.filter); // example: already parsed (EJSON / stored doc)\n   var badOp = common.findUnsafeMongoOperator(parsedQuery);\n   if (badOp) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": Query contains disallowed operator: \" + badOp);\n       return common.returnMessage(params, 400, \"Query contains disallowed operator: \" + badOp);\n   }\n   ```\n   `$expr` is allowed; `$where`/`$function`/`$accumulator` are rejected at any depth (including nested inside `$expr`). Log the rejection at the call site using the file's `log` and `common.reqInfo(params)` (which adds the endpoint path/method, without the api_key). Do NOT pass `params` into `parseUserQuery` and do NOT log inside it.\n\n## File Locations\n\n| What | Where |\n|------|-------|\n| Plugin code | `plugins/<name>/api/api.js` |\n| Vue views | `plugins/<name>/frontend/public/javascripts/countly.views.js` |\n| Templates | `plugins/<name>/frontend/public/templates/` |\n| Localization | `plugins/<name>/frontend/public/localization/<name>.properties` |\n| Tests | `plugins/<name>/tests.js` |\n\n## Creating API Endpoints\n\n```javascript\nvar plugins = require('../../pluginManager.js');\nvar common = require('../../../api/utils/common.js');\nconst { validateRead } = require('../../../api/utils/rights.js');\n\nconst FEATURE_NAME = 'myfeature';\n\nplugins.register(\"/o/myfeature\", function(ob) {\n    var params = ob.params;\n    validateRead(params, FEATURE_NAME, function() {\n        // Validate input\n        var argProps = {\n            'id': { 'required': true, 'type': 'String' }\n        };\n        var validation = common.validateArgs(params.qstring, argProps, true);\n        if (!validation.obj) {\n            common.returnMessage(params, 400, 'Error: ' + validation.errors);\n            return;\n        }\n        \n        // Query with app_id\n        common.db.collection('mydata').findOne(\n            {_id: validation.obj.id, app_id: params.app_id + \"\"},\n            function(err, result) {\n                common.returnOutput(params, result || {});\n            }\n        );\n    });\n});\n```\n\n## Creating Vue Components\n\n```javascript\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/main.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() {\n        return { items: [] };\n    },\n    computed: {\n        // Prefer computed over watchers\n    },\n    methods: {\n        refresh: function() {\n            // Called for auto-refresh\n        }\n    }\n});\n\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n## MongoDB Patterns\n\n```javascript\n// Read batcher for hot documents\ncommon.readBatcher.getOne(\"collection\", {_id: id}, callback);\n\n// Write batcher for multiple updates\ncommon.writeBatcher.add(\"collection\", id, {$inc: {count: 1}});\n\n// Always use projection\ndb.collection('x').findOne({_id: id}, {projection: {field: 1}});\n```\n\n## Plugin Lifecycle Hooks\n\n```javascript\n// Required if your plugin creates per-app collections\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('mydata' + ob.appId).createIndex({\"field\": 1});\n});\n\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('mydata' + ob.appId).drop();\n});\n\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"mydata\" + ob.app_id).deleteMany({uid: {$in: ob.uids}});\n});\n```\n\n## CSS/Styling\n\n- Use SASS with SCSS syntax\n- BEM naming with `cly-vue-` prefix\n- Bulma classes use `bu-` prefix\n- Use `@use`, never `@import`\n\n## Common Anti-Patterns to Avoid\n\n| Don't | Do Instead |\n|-------|------------|\n| `this.$parent.value = x` | Emit events: `this.$emit('update', x)` |\n| Deep watchers | Watch specific properties |\n| `v-html` with user data | Use `{{ }}` text interpolation |\n| Query without app_id | Always include `app_id` in queries |\n| `exec(cmd + userInput)` | `spawn(cmd, [userInput])` |\n| `replace(' ', '-')` | `replace(/ /g, '-')` for all occurrences |\n\n## Detailed Documentation\n\nFor comprehensive guidelines, read:\n- `CODING_GUIDELINES.md` - Full development standards\n- `docs/SECURITY.md` - Security requirements\n- `docs/VUEJS_GUIDELINES.md` - Vue patterns\n- `docs/CSS_STYLE_GUIDE.md` - SASS/BEM conventions\n- `docs/UI_TESTING.md` - Cypress testing\n- `test/README.md` - Test suite documentation\n- `plugins/empty/` - Sample plugin structure\n",".github/copilot-instructions.md":"# Countly Server - AI Coding Agent Instructions\n\n## Project Overview\nCountly is a product analytics platform built with **Node.js 22+**, **MongoDB**, and **Vue 2** (with Element UI). The architecture is plugin-based: core functionality lives in `api/` and `frontend/`, while features are implemented as plugins in `plugins/`.\n\n## Architecture\n\n### Multi-Process Architecture\nCountly runs as multiple services (start via `npm run start:all:dev`):\n- **API Server** (`api/api.js`) - SDK data ingestion on port 3001\n- **Frontend** (`frontend/express/app.js`) - Dashboard on port 6001\n- **Job Server** (`jobServer/index.js`) - Background job processing\n- **Aggregator** (`api/aggregator.js`) - Data aggregation\n- **Ingestor** (`api/ingestor.js`) - High-volume data ingestion\n\n### Plugin System\nPlugins extend Countly via event hooks. Each plugin has this structure:\n```\nplugins/<name>/\n├── api/api.js          # Backend API endpoints (required)\n├── frontend/app.js     # Express middleware/routes\n├── frontend/public/    # Static assets (JS, CSS, templates)\n├── package.json        # Plugin metadata\n├── install.js          # Installation hook\n└── tests.js            # Plugin tests\n```\n\n## Backend Development Checklist\n\n### API Endpoint Security (REQUIRED)\n**Every endpoint must use validation** from `api/utils/rights.js`:\n```javascript\nconst { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n\n// Read operations\nvalidateRead(params, FEATURE_NAME, () => { /* handler */ });\n\n// Write operations - always include app_id in queries!\nvalidateDelete(params, FEATURE_NAME, () => {\n    // CORRECT: Include app_id to prevent cross-app access\n    db.collection(\"items\").deleteOne({_id: params.qstring.id, app_id: params.app_id + \"\"});\n});\n```\n\n### Parameter Validation\nAlways validate and type-check input parameters:\n```javascript\nvar argProps = {\n    'name': { 'required': true, 'type': 'String' },\n    'count': { 'required': false, 'type': 'Number' }\n};\nvar validation = common.validateArgs(params.qstring.args, argProps, true);\nif (!validation.obj) {\n    common.returnMessage(params, 400, 'Error: ' + validation.errors);\n    return false;\n}\n\n// Parse JSON safely\nif (typeof params.qstring.data === \"string\") {\n    try {\n        params.qstring.data = JSON.parse(params.qstring.data);\n    } catch (ex) {\n        params.qstring.data = {};\n    }\n}\n```\n\n### MongoDB Performance\n```javascript\n// Use read batcher for frequently accessed documents\ncommon.readBatcher.getOne(\"events\", {'_id': params.app_id}, (err, event) => {});\n\n// Use write batcher for multiple updates to same document\ncommon.writeBatcher.add(\"users\", id, {'$inc': updateData});\n\n// Always use projection to limit returned fields\ndb.collection('plugins').findOne({_id: 'plugins'}, {projection: {'myfield': 1}});\n```\n\n### App Lifecycle Events (Required for plugins with collections)\n```javascript\n// Create indexes when new app is created\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).ensureIndex({\"field\": 1}, {background: true});\n});\n\n// Clean up when app is deleted\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).drop();\n});\n\n// Handle user data deletion (GDPR)\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"app_mydata\" + ob.app_id).remove({uid: {$in: ob.uids}});\n});\n```\n\n### Audit Logging\nLog all create/update/delete actions:\n```javascript\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_created\", data: newItem});\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_edited\", data: {before: oldItem, update: changes}});\n```\n\n## Frontend Development (Vue 2)\n\n### Component Conventions\n```javascript\n// Use PascalCase for component names\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/mytemplate.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() { return { /* state */ }; },\n    computed: { /* prefer computed over watchers */ },\n    methods: { /* handlers */ }\n});\n\n// Register route with kebab-case component names in templates\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n### Vue Best Practices\n- **DO**: Use `@event` instead of `v-on:event`, `:prop` instead of `v-bind:prop`\n- **DO**: Prefer computed properties over data + watchers\n- **DO**: Add `data-test-id` attributes for testable elements\n- **DON'T**: Use `v-html` with user input (XSS risk)\n- **DON'T**: Modify parent state directly - use props down, events up\n- **DON'T**: Use global component registration unless truly global\n\n### Data Test IDs for UI Testing\n```html\n<!-- Add data-test-id for Cypress tests -->\n<button data-test-id=\"submit-form-button\">Submit</button>\n<input data-test-id=\"username-input\" type=\"text\">\n\n<!-- Dynamic test IDs in Vue -->\n<el-tab :data-test-id=\"'tab-' + tab.name + '-link'\">\n```\n\n## Security Requirements\n\n### XSS Prevention\n- API output is auto-escaped via `common.returnOutput()` and `common.returnMessage()`\n- Frontend: Treat API data as HTML, use `{{ msg }}` for unescaped user input\n- Use `countlyCommon.encodeHtml()` for manual sanitization\n\n### MongoDB Injection Prevention\n```javascript\n// Always cast credentials to strings\nparams.username = params.username + \"\";\nparams.password = params.password + \"\";\n```\n\n### File Upload Security\n```javascript\n// Validate file types\nif (type !== \"image/png\" && type !== \"image/gif\" && type !== \"image/jpeg\") {\n    fs.unlink(tmp_path, function() {});\n    return;\n}\n// Sanitize filenames\nvar safeFileName = common.sanitizeFilename(params.qstring.filename);\n```\n\n### Command Line Security\n```javascript\n// Use spawn with array args, NOT exec with string concatenation\nvar cp = require('child_process');\ncp.spawn(\"command\", [userInput]);  // Safe\n// exec(\"command \" + userInput);   // UNSAFE - allows injection\n```\n\n## Testing\n\n```bash\nnpm run test:unit                 # Unit tests (no Docker)\nnpm run test:api-core             # Core API tests\nnpm run test:lite-plugins         # CE plugin tests\nnpm run test:plugin -- <name>     # Single plugin tests\n\n# Linting\ncountly plugin lint <pluginname>\ncountly plugin lintfix <pluginname>\n\n# Shell script validation\ncountly shellcheck\n```\n\n### Plugin Test Requirements\n- Test empty state, various inputs, and cleanup\n- Verify app lifecycle handlers work correctly\n- Include tests in `plugins/<name>/tests.js`\n\n## CSS & Styling\n\n- Use **SASS** (SCSS syntax) for stylesheets\n- Use **BEM naming** with `cly-vue-` prefix for all new classes\n- Use **Bulma** classes prefixed with `bu-` for grid/layout\n- Don't use `@import`, use `@use` in SASS files\n- Compile with `npx grunt sass` or `npx grunt dist-all`\n\n## JSDoc Comments\n\nDocument all public functions:\n```javascript\n/**\n * Calculates percent change between periods.\n * @param {number} previous - data for previous period\n * @param {number} current - data for current period\n * @returns {object} {\"percent\": \"20%\", \"trend\": \"u\"}\n */\n```\n\n## Custom Scripts\n\nScripts in `bin/scripts/` must include:\n- Header comment with description, server type, path, command\n- All configurable variables with comments\n- Dry run option for destructive operations\n- Idempotent behavior (safe to run multiple times)\n\n## Development Commands\n\n```bash\nnpm run start:all:dev        # All services with hot reload\nnpx grunt dist-all           # Build all static assets (required after JS changes)\nnpx grunt locales            # Build locale files\n\n# Plugin management\nnode bin/commands/scripts/plugin.js enable <name>\nnode bin/commands/scripts/plugin.js disable <name>\n```\n\n## Key Files Reference\n\n| Purpose | Location |\n|---------|----------|\n| Plugin manager | `plugins/pluginManager.js` |\n| Common utilities | `api/utils/common.js` |\n| Authorization | `api/utils/rights.js` |\n| Vue core | `frontend/express/public/javascripts/countly/vue/core.js` |\n| Sample plugin | `plugins/empty/` |\n| TypeScript types | `types/` |\n| Coding guidelines | `CODING_GUIDELINES.md` |\n| Vue.js guidelines | `docs/VUEJS_GUIDELINES.md` |\n| CSS style guide | `docs/CSS_STYLE_GUIDE.md` |\n| Security guidelines | `docs/SECURITY.md` |\n| UI testing guide | `docs/UI_TESTING.md` |\n"},"files":{"CLAUDE.md":"# CLAUDE.md - Countly Server\n\nThis file provides guidance for Claude (Anthropic) when working with this codebase.\n\n## Project Overview\n\nCountly is a product analytics platform. Backend: Node.js 22+, MongoDB. Frontend: Vue 2, Element UI, Backbone (legacy). Architecture is plugin-based.\n\n## Quick Commands\n\n```bash\nnpm run start:all:dev        # Start all services (API:3001, Frontend:6001)\nnpx grunt dist-all           # Build static assets (required after JS changes)\nnpx grunt locales            # Build locale files\nnpx grunt sass               # Compile SASS only\n\n# Testing\nnpm run test:unit            # Unit tests\nnpm run test:plugin -- name  # Single plugin tests\ncountly plugin lint name     # Lint plugin\ncountly shellcheck           # Validate shell scripts\n```\n\n## Critical Security Rules\n\n**ALWAYS follow these - no exceptions:**\n\n1. **API endpoints must use validation**:\n   ```javascript\n   const { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n   validateRead(params, FEATURE_NAME, () => { /* handler */ });\n   ```\n\n2. **Write operations must include app_id**:\n   ```javascript\n   // CORRECT - prevents cross-app access\n   db.collection(\"items\").deleteOne({_id: id, app_id: params.app_id + \"\"});\n   ```\n\n3. **Cast user input to strings for auth**:\n   ```javascript\n   params.username = params.username + \"\";\n   ```\n\n4. **Use spawn, not exec for shell commands**:\n   ```javascript\n   cp.spawn(\"command\", [userInput]);  // Safe\n   // exec(\"command \" + userInput);   // VULNERABLE\n   ```\n\n5. **Never use v-html with user data** in Vue templates.\n\n6. **Validate user-supplied Mongo queries — reject, never strip**. Any query/filter that comes from a request and reaches `find`/`aggregate`/`update`/`delete` must be checked with the `common` helpers at the endpoint where it is first parsed (NOT inside deep helpers or the `/drill/preprocess_query` hook). The query is run exactly as submitted or the request is rejected with `400` — it is never modified.\n   ```javascript\n   // raw query STRING from a request param → parse + validate in one step\n   var parsed = common.parseUserQuery(params.qstring.query); // accepts string OR object\n   if (parsed.error) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": \" + parsed.error);\n       return common.returnMessage(params, 400, parsed.error);\n   }\n   var query = parsed.query; // safe to run as-is\n\n   // ALREADY-parsed object — e.g. dbviewer parses with EJSON, or the query is\n   // nested in a larger saved payload. Validate that parsed object directly:\n   var parsedQuery = EJSON.parse(params.qstring.filter); // example: already parsed (EJSON / stored doc)\n   var badOp = common.findUnsafeMongoOperator(parsedQuery);\n   if (badOp) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": Query contains disallowed operator: \" + badOp);\n       return common.returnMessage(params, 400, \"Query contains disallowed operator: \" + badOp);\n   }\n   ```\n   `$expr` is allowed; `$where`/`$function`/`$accumulator` are rejected at any depth (including nested inside `$expr`). Log the rejection at the call site using the file's `log` and `common.reqInfo(params)` (which adds the endpoint path/method, without the api_key). Do NOT pass `params` into `parseUserQuery` and do NOT log inside it.\n\n## File Locations\n\n| What | Where |\n|------|-------|\n| Plugin code | `plugins/<name>/api/api.js` |\n| Vue views | `plugins/<name>/frontend/public/javascripts/countly.views.js` |\n| Templates | `plugins/<name>/frontend/public/templates/` |\n| Localization | `plugins/<name>/frontend/public/localization/<name>.properties` |\n| Tests | `plugins/<name>/tests.js` |\n\n## Creating API Endpoints\n\n```javascript\nvar plugins = require('../../pluginManager.js');\nvar common = require('../../../api/utils/common.js');\nconst { validateRead } = require('../../../api/utils/rights.js');\n\nconst FEATURE_NAME = 'myfeature';\n\nplugins.register(\"/o/myfeature\", function(ob) {\n    var params = ob.params;\n    validateRead(params, FEATURE_NAME, function() {\n        // Validate input\n        var argProps = {\n            'id': { 'required': true, 'type': 'String' }\n        };\n        var validation = common.validateArgs(params.qstring, argProps, true);\n        if (!validation.obj) {\n            common.returnMessage(params, 400, 'Error: ' + validation.errors);\n            return;\n        }\n        \n        // Query with app_id\n        common.db.collection('mydata').findOne(\n            {_id: validation.obj.id, app_id: params.app_id + \"\"},\n            function(err, result) {\n                common.returnOutput(params, result || {});\n            }\n        );\n    });\n});\n```\n\n## Creating Vue Components\n\n```javascript\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/main.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() {\n        return { items: [] };\n    },\n    computed: {\n        // Prefer computed over watchers\n    },\n    methods: {\n        refresh: function() {\n            // Called for auto-refresh\n        }\n    }\n});\n\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n## MongoDB Patterns\n\n```javascript\n// Read batcher for hot documents\ncommon.readBatcher.getOne(\"collection\", {_id: id}, callback);\n\n// Write batcher for multiple updates\ncommon.writeBatcher.add(\"collection\", id, {$inc: {count: 1}});\n\n// Always use projection\ndb.collection('x').findOne({_id: id}, {projection: {field: 1}});\n```\n\n## Plugin Lifecycle Hooks\n\n```javascript\n// Required if your plugin creates per-app collections\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('mydata' + ob.appId).createIndex({\"field\": 1});\n});\n\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('mydata' + ob.appId).drop();\n});\n\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"mydata\" + ob.app_id).deleteMany({uid: {$in: ob.uids}});\n});\n```\n\n## CSS/Styling\n\n- Use SASS with SCSS syntax\n- BEM naming with `cly-vue-` prefix\n- Bulma classes use `bu-` prefix\n- Use `@use`, never `@import`\n\n## Common Anti-Patterns to Avoid\n\n| Don't | Do Instead |\n|-------|------------|\n| `this.$parent.value = x` | Emit events: `this.$emit('update', x)` |\n| Deep watchers | Watch specific properties |\n| `v-html` with user data | Use `{{ }}` text interpolation |\n| Query without app_id | Always include `app_id` in queries |\n| `exec(cmd + userInput)` | `spawn(cmd, [userInput])` |\n| `replace(' ', '-')` | `replace(/ /g, '-')` for all occurrences |\n\n## Detailed Documentation\n\nFor comprehensive guidelines, read:\n- `CODING_GUIDELINES.md` - Full development standards\n- `docs/SECURITY.md` - Security requirements\n- `docs/VUEJS_GUIDELINES.md` - Vue patterns\n- `docs/CSS_STYLE_GUIDE.md` - SASS/BEM conventions\n- `docs/UI_TESTING.md` - Cypress testing\n- `test/README.md` - Test suite documentation\n- `plugins/empty/` - Sample plugin structure\n",".github/copilot-instructions.md":"# Countly Server - AI Coding Agent Instructions\n\n## Project Overview\nCountly is a product analytics platform built with **Node.js 22+**, **MongoDB**, and **Vue 2** (with Element UI). The architecture is plugin-based: core functionality lives in `api/` and `frontend/`, while features are implemented as plugins in `plugins/`.\n\n## Architecture\n\n### Multi-Process Architecture\nCountly runs as multiple services (start via `npm run start:all:dev`):\n- **API Server** (`api/api.js`) - SDK data ingestion on port 3001\n- **Frontend** (`frontend/express/app.js`) - Dashboard on port 6001\n- **Job Server** (`jobServer/index.js`) - Background job processing\n- **Aggregator** (`api/aggregator.js`) - Data aggregation\n- **Ingestor** (`api/ingestor.js`) - High-volume data ingestion\n\n### Plugin System\nPlugins extend Countly via event hooks. Each plugin has this structure:\n```\nplugins/<name>/\n├── api/api.js          # Backend API endpoints (required)\n├── frontend/app.js     # Express middleware/routes\n├── frontend/public/    # Static assets (JS, CSS, templates)\n├── package.json        # Plugin metadata\n├── install.js          # Installation hook\n└── tests.js            # Plugin tests\n```\n\n## Backend Development Checklist\n\n### API Endpoint Security (REQUIRED)\n**Every endpoint must use validation** from `api/utils/rights.js`:\n```javascript\nconst { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n\n// Read operations\nvalidateRead(params, FEATURE_NAME, () => { /* handler */ });\n\n// Write operations - always include app_id in queries!\nvalidateDelete(params, FEATURE_NAME, () => {\n    // CORRECT: Include app_id to prevent cross-app access\n    db.collection(\"items\").deleteOne({_id: params.qstring.id, app_id: params.app_id + \"\"});\n});\n```\n\n### Parameter Validation\nAlways validate and type-check input parameters:\n```javascript\nvar argProps = {\n    'name': { 'required': true, 'type': 'String' },\n    'count': { 'required': false, 'type': 'Number' }\n};\nvar validation = common.validateArgs(params.qstring.args, argProps, true);\nif (!validation.obj) {\n    common.returnMessage(params, 400, 'Error: ' + validation.errors);\n    return false;\n}\n\n// Parse JSON safely\nif (typeof params.qstring.data === \"string\") {\n    try {\n        params.qstring.data = JSON.parse(params.qstring.data);\n    } catch (ex) {\n        params.qstring.data = {};\n    }\n}\n```\n\n### MongoDB Performance\n```javascript\n// Use read batcher for frequently accessed documents\ncommon.readBatcher.getOne(\"events\", {'_id': params.app_id}, (err, event) => {});\n\n// Use write batcher for multiple updates to same document\ncommon.writeBatcher.add(\"users\", id, {'$inc': updateData});\n\n// Always use projection to limit returned fields\ndb.collection('plugins').findOne({_id: 'plugins'}, {projection: {'myfield': 1}});\n```\n\n### App Lifecycle Events (Required for plugins with collections)\n```javascript\n// Create indexes when new app is created\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).ensureIndex({\"field\": 1}, {background: true});\n});\n\n// Clean up when app is deleted\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).drop();\n});\n\n// Handle user data deletion (GDPR)\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"app_mydata\" + ob.app_id).remove({uid: {$in: ob.uids}});\n});\n```\n\n### Audit Logging\nLog all create/update/delete actions:\n```javascript\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_created\", data: newItem});\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_edited\", data: {before: oldItem, update: changes}});\n```\n\n## Frontend Development (Vue 2)\n\n### Component Conventions\n```javascript\n// Use PascalCase for component names\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/mytemplate.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() { return { /* state */ }; },\n    computed: { /* prefer computed over watchers */ },\n    methods: { /* handlers */ }\n});\n\n// Register route with kebab-case component names in templates\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n### Vue Best Practices\n- **DO**: Use `@event` instead of `v-on:event`, `:prop` instead of `v-bind:prop`\n- **DO**: Prefer computed properties over data + watchers\n- **DO**: Add `data-test-id` attributes for testable elements\n- **DON'T**: Use `v-html` with user input (XSS risk)\n- **DON'T**: Modify parent state directly - use props down, events up\n- **DON'T**: Use global component registration unless truly global\n\n### Data Test IDs for UI Testing\n```html\n<!-- Add data-test-id for Cypress tests -->\n<button data-test-id=\"submit-form-button\">Submit</button>\n<input data-test-id=\"username-input\" type=\"text\">\n\n<!-- Dynamic test IDs in Vue -->\n<el-tab :data-test-id=\"'tab-' + tab.name + '-link'\">\n```\n\n## Security Requirements\n\n### XSS Prevention\n- API output is auto-escaped via `common.returnOutput()` and `common.returnMessage()`\n- Frontend: Treat API data as HTML, use `{{ msg }}` for unescaped user input\n- Use `countlyCommon.encodeHtml()` for manual sanitization\n\n### MongoDB Injection Prevention\n```javascript\n// Always cast credentials to strings\nparams.username = params.username + \"\";\nparams.password = params.password + \"\";\n```\n\n### File Upload Security\n```javascript\n// Validate file types\nif (type !== \"image/png\" && type !== \"image/gif\" && type !== \"image/jpeg\") {\n    fs.unlink(tmp_path, function() {});\n    return;\n}\n// Sanitize filenames\nvar safeFileName = common.sanitizeFilename(params.qstring.filename);\n```\n\n### Command Line Security\n```javascript\n// Use spawn with array args, NOT exec with string concatenation\nvar cp = require('child_process');\ncp.spawn(\"command\", [userInput]);  // Safe\n// exec(\"command \" + userInput);   // UNSAFE - allows injection\n```\n\n## Testing\n\n```bash\nnpm run test:unit                 # Unit tests (no Docker)\nnpm run test:api-core             # Core API tests\nnpm run test:lite-plugins         # CE plugin tests\nnpm run test:plugin -- <name>     # Single plugin tests\n\n# Linting\ncountly plugin lint <pluginname>\ncountly plugin lintfix <pluginname>\n\n# Shell script validation\ncountly shellcheck\n```\n\n### Plugin Test Requirements\n- Test empty state, various inputs, and cleanup\n- Verify app lifecycle handlers work correctly\n- Include tests in `plugins/<name>/tests.js`\n\n## CSS & Styling\n\n- Use **SASS** (SCSS syntax) for stylesheets\n- Use **BEM naming** with `cly-vue-` prefix for all new classes\n- Use **Bulma** classes prefixed with `bu-` for grid/layout\n- Don't use `@import`, use `@use` in SASS files\n- Compile with `npx grunt sass` or `npx grunt dist-all`\n\n## JSDoc Comments\n\nDocument all public functions:\n```javascript\n/**\n * Calculates percent change between periods.\n * @param {number} previous - data for previous period\n * @param {number} current - data for current period\n * @returns {object} {\"percent\": \"20%\", \"trend\": \"u\"}\n */\n```\n\n## Custom Scripts\n\nScripts in `bin/scripts/` must include:\n- Header comment with description, server type, path, command\n- All configurable variables with comments\n- Dry run option for destructive operations\n- Idempotent behavior (safe to run multiple times)\n\n## Development Commands\n\n```bash\nnpm run start:all:dev        # All services with hot reload\nnpx grunt dist-all           # Build all static assets (required after JS changes)\nnpx grunt locales            # Build locale files\n\n# Plugin management\nnode bin/commands/scripts/plugin.js enable <name>\nnode bin/commands/scripts/plugin.js disable <name>\n```\n\n## Key Files Reference\n\n| Purpose | Location |\n|---------|----------|\n| Plugin manager | `plugins/pluginManager.js` |\n| Common utilities | `api/utils/common.js` |\n| Authorization | `api/utils/rights.js` |\n| Vue core | `frontend/express/public/javascripts/countly/vue/core.js` |\n| Sample plugin | `plugins/empty/` |\n| TypeScript types | `types/` |\n| Coding guidelines | `CODING_GUIDELINES.md` |\n| Vue.js guidelines | `docs/VUEJS_GUIDELINES.md` |\n| CSS style guide | `docs/CSS_STYLE_GUIDE.md` |\n| Security guidelines | `docs/SECURITY.md` |\n| UI testing guide | `docs/UI_TESTING.md` |\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md - Countly Server\n\nThis file provides guidance for Claude (Anthropic) when working with this codebase.\n\n## Project Overview\n\nCountly is a product analytics platform. Backend: Node.js 22+, MongoDB. Frontend: Vue 2, Element UI, Backbone (legacy). Architecture is plugin-based.\n\n## Quick Commands\n\n```bash\nnpm run start:all:dev        # Start all services (API:3001, Frontend:6001)\nnpx grunt dist-all           # Build static assets (required after JS changes)\nnpx grunt locales            # Build locale files\nnpx grunt sass               # Compile SASS only\n\n# Testing\nnpm run test:unit            # Unit tests\nnpm run test:plugin -- name  # Single plugin tests\ncountly plugin lint name     # Lint plugin\ncountly shellcheck           # Validate shell scripts\n```\n\n## Critical Security Rules\n\n**ALWAYS follow these - no exceptions:**\n\n1. **API endpoints must use validation**:\n   ```javascript\n   const { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n   validateRead(params, FEATURE_NAME, () => { /* handler */ });\n   ```\n\n2. **Write operations must include app_id**:\n   ```javascript\n   // CORRECT - prevents cross-app access\n   db.collection(\"items\").deleteOne({_id: id, app_id: params.app_id + \"\"});\n   ```\n\n3. **Cast user input to strings for auth**:\n   ```javascript\n   params.username = params.username + \"\";\n   ```\n\n4. **Use spawn, not exec for shell commands**:\n   ```javascript\n   cp.spawn(\"command\", [userInput]);  // Safe\n   // exec(\"command \" + userInput);   // VULNERABLE\n   ```\n\n5. **Never use v-html with user data** in Vue templates.\n\n6. **Validate user-supplied Mongo queries — reject, never strip**. Any query/filter that comes from a request and reaches `find`/`aggregate`/`update`/`delete` must be checked with the `common` helpers at the endpoint where it is first parsed (NOT inside deep helpers or the `/drill/preprocess_query` hook). The query is run exactly as submitted or the request is rejected with `400` — it is never modified.\n   ```javascript\n   // raw query STRING from a request param → parse + validate in one step\n   var parsed = common.parseUserQuery(params.qstring.query); // accepts string OR object\n   if (parsed.error) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": \" + parsed.error);\n       return common.returnMessage(params, 400, parsed.error);\n   }\n   var query = parsed.query; // safe to run as-is\n\n   // ALREADY-parsed object — e.g. dbviewer parses with EJSON, or the query is\n   // nested in a larger saved payload. Validate that parsed object directly:\n   var parsedQuery = EJSON.parse(params.qstring.filter); // example: already parsed (EJSON / stored doc)\n   var badOp = common.findUnsafeMongoOperator(parsedQuery);\n   if (badOp) {\n       log.d(\"Rejected user query\" + common.reqInfo(params) + \": Query contains disallowed operator: \" + badOp);\n       return common.returnMessage(params, 400, \"Query contains disallowed operator: \" + badOp);\n   }\n   ```\n   `$expr` is allowed; `$where`/`$function`/`$accumulator` are rejected at any depth (including nested inside `$expr`). Log the rejection at the call site using the file's `log` and `common.reqInfo(params)` (which adds the endpoint path/method, without the api_key). Do NOT pass `params` into `parseUserQuery` and do NOT log inside it.\n\n## File Locations\n\n| What | Where |\n|------|-------|\n| Plugin code | `plugins/<name>/api/api.js` |\n| Vue views | `plugins/<name>/frontend/public/javascripts/countly.views.js` |\n| Templates | `plugins/<name>/frontend/public/templates/` |\n| Localization | `plugins/<name>/frontend/public/localization/<name>.properties` |\n| Tests | `plugins/<name>/tests.js` |\n\n## Creating API Endpoints\n\n```javascript\nvar plugins = require('../../pluginManager.js');\nvar common = require('../../../api/utils/common.js');\nconst { validateRead } = require('../../../api/utils/rights.js');\n\nconst FEATURE_NAME = 'myfeature';\n\nplugins.register(\"/o/myfeature\", function(ob) {\n    var params = ob.params;\n    validateRead(params, FEATURE_NAME, function() {\n        // Validate input\n        var argProps = {\n            'id': { 'required': true, 'type': 'String' }\n        };\n        var validation = common.validateArgs(params.qstring, argProps, true);\n        if (!validation.obj) {\n            common.returnMessage(params, 400, 'Error: ' + validation.errors);\n            return;\n        }\n        \n        // Query with app_id\n        common.db.collection('mydata').findOne(\n            {_id: validation.obj.id, app_id: params.app_id + \"\"},\n            function(err, result) {\n                common.returnOutput(params, result || {});\n            }\n        );\n    });\n});\n```\n\n## Creating Vue Components\n\n```javascript\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/main.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() {\n        return { items: [] };\n    },\n    computed: {\n        // Prefer computed over watchers\n    },\n    methods: {\n        refresh: function() {\n            // Called for auto-refresh\n        }\n    }\n});\n\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n## MongoDB Patterns\n\n```javascript\n// Read batcher for hot documents\ncommon.readBatcher.getOne(\"collection\", {_id: id}, callback);\n\n// Write batcher for multiple updates\ncommon.writeBatcher.add(\"collection\", id, {$inc: {count: 1}});\n\n// Always use projection\ndb.collection('x').findOne({_id: id}, {projection: {field: 1}});\n```\n\n## Plugin Lifecycle Hooks\n\n```javascript\n// Required if your plugin creates per-app collections\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('mydata' + ob.appId).createIndex({\"field\": 1});\n});\n\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('mydata' + ob.appId).drop();\n});\n\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"mydata\" + ob.app_id).deleteMany({uid: {$in: ob.uids}});\n});\n```\n\n## CSS/Styling\n\n- Use SASS with SCSS syntax\n- BEM naming with `cly-vue-` prefix\n- Bulma classes use `bu-` prefix\n- Use `@use`, never `@import`\n\n## Common Anti-Patterns to Avoid\n\n| Don't | Do Instead |\n|-------|------------|\n| `this.$parent.value = x` | Emit events: `this.$emit('update', x)` |\n| Deep watchers | Watch specific properties |\n| `v-html` with user data | Use `{{ }}` text interpolation |\n| Query without app_id | Always include `app_id` in queries |\n| `exec(cmd + userInput)` | `spawn(cmd, [userInput])` |\n| `replace(' ', '-')` | `replace(/ /g, '-')` for all occurrences |\n\n## Detailed Documentation\n\nFor comprehensive guidelines, read:\n- `CODING_GUIDELINES.md` - Full development standards\n- `docs/SECURITY.md` - Security requirements\n- `docs/VUEJS_GUIDELINES.md` - Vue patterns\n- `docs/CSS_STYLE_GUIDE.md` - SASS/BEM conventions\n- `docs/UI_TESTING.md` - Cypress testing\n- `test/README.md` - Test suite documentation\n- `plugins/empty/` - Sample plugin structure\n","category":"root","tokens":1763},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","title":"copilot-instructions.md","content":"# Countly Server - AI Coding Agent Instructions\n\n## Project Overview\nCountly is a product analytics platform built with **Node.js 22+**, **MongoDB**, and **Vue 2** (with Element UI). The architecture is plugin-based: core functionality lives in `api/` and `frontend/`, while features are implemented as plugins in `plugins/`.\n\n## Architecture\n\n### Multi-Process Architecture\nCountly runs as multiple services (start via `npm run start:all:dev`):\n- **API Server** (`api/api.js`) - SDK data ingestion on port 3001\n- **Frontend** (`frontend/express/app.js`) - Dashboard on port 6001\n- **Job Server** (`jobServer/index.js`) - Background job processing\n- **Aggregator** (`api/aggregator.js`) - Data aggregation\n- **Ingestor** (`api/ingestor.js`) - High-volume data ingestion\n\n### Plugin System\nPlugins extend Countly via event hooks. Each plugin has this structure:\n```\nplugins/<name>/\n├── api/api.js          # Backend API endpoints (required)\n├── frontend/app.js     # Express middleware/routes\n├── frontend/public/    # Static assets (JS, CSS, templates)\n├── package.json        # Plugin metadata\n├── install.js          # Installation hook\n└── tests.js            # Plugin tests\n```\n\n## Backend Development Checklist\n\n### API Endpoint Security (REQUIRED)\n**Every endpoint must use validation** from `api/utils/rights.js`:\n```javascript\nconst { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');\n\n// Read operations\nvalidateRead(params, FEATURE_NAME, () => { /* handler */ });\n\n// Write operations - always include app_id in queries!\nvalidateDelete(params, FEATURE_NAME, () => {\n    // CORRECT: Include app_id to prevent cross-app access\n    db.collection(\"items\").deleteOne({_id: params.qstring.id, app_id: params.app_id + \"\"});\n});\n```\n\n### Parameter Validation\nAlways validate and type-check input parameters:\n```javascript\nvar argProps = {\n    'name': { 'required': true, 'type': 'String' },\n    'count': { 'required': false, 'type': 'Number' }\n};\nvar validation = common.validateArgs(params.qstring.args, argProps, true);\nif (!validation.obj) {\n    common.returnMessage(params, 400, 'Error: ' + validation.errors);\n    return false;\n}\n\n// Parse JSON safely\nif (typeof params.qstring.data === \"string\") {\n    try {\n        params.qstring.data = JSON.parse(params.qstring.data);\n    } catch (ex) {\n        params.qstring.data = {};\n    }\n}\n```\n\n### MongoDB Performance\n```javascript\n// Use read batcher for frequently accessed documents\ncommon.readBatcher.getOne(\"events\", {'_id': params.app_id}, (err, event) => {});\n\n// Use write batcher for multiple updates to same document\ncommon.writeBatcher.add(\"users\", id, {'$inc': updateData});\n\n// Always use projection to limit returned fields\ndb.collection('plugins').findOne({_id: 'plugins'}, {projection: {'myfield': 1}});\n```\n\n### App Lifecycle Events (Required for plugins with collections)\n```javascript\n// Create indexes when new app is created\nplugins.register(\"/i/apps/create\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).ensureIndex({\"field\": 1}, {background: true});\n});\n\n// Clean up when app is deleted\nplugins.register(\"/i/apps/delete\", function(ob) {\n    common.db.collection('app_mydata' + ob.appId).drop();\n});\n\n// Handle user data deletion (GDPR)\nplugins.register(\"/i/app_users/delete\", function(ob) {\n    common.db.collection(\"app_mydata\" + ob.app_id).remove({uid: {$in: ob.uids}});\n});\n```\n\n### Audit Logging\nLog all create/update/delete actions:\n```javascript\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_created\", data: newItem});\nplugins.dispatch(\"/systemlogs\", {params: params, action: \"item_edited\", data: {before: oldItem, update: changes}});\n```\n\n## Frontend Development (Vue 2)\n\n### Component Conventions\n```javascript\n// Use PascalCase for component names\nvar MyComponent = countlyVue.views.create({\n    template: countlyVue.T(\"/myplugin/templates/mytemplate.html\"),\n    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],\n    data: function() { return { /* state */ }; },\n    computed: { /* prefer computed over watchers */ },\n    methods: { /* handlers */ }\n});\n\n// Register route with kebab-case component names in templates\napp.route('/dashboard/myfeature', 'myfeature', function() {\n    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();\n});\n```\n\n### Vue Best Practices\n- **DO**: Use `@event` instead of `v-on:event`, `:prop` instead of `v-bind:prop`\n- **DO**: Prefer computed properties over data + watchers\n- **DO**: Add `data-test-id` attributes for testable elements\n- **DON'T**: Use `v-html` with user input (XSS risk)\n- **DON'T**: Modify parent state directly - use props down, events up\n- **DON'T**: Use global component registration unless truly global\n\n### Data Test IDs for UI Testing\n```html\n<!-- Add data-test-id for Cypress tests -->\n<button data-test-id=\"submit-form-button\">Submit</button>\n<input data-test-id=\"username-input\" type=\"text\">\n\n<!-- Dynamic test IDs in Vue -->\n<el-tab :data-test-id=\"'tab-' + tab.name + '-link'\">\n```\n\n## Security Requirements\n\n### XSS Prevention\n- API output is auto-escaped via `common.returnOutput()` and `common.returnMessage()`\n- Frontend: Treat API data as HTML, use `{{ msg }}` for unescaped user input\n- Use `countlyCommon.encodeHtml()` for manual sanitization\n\n### MongoDB Injection Prevention\n```javascript\n// Always cast credentials to strings\nparams.username = params.username + \"\";\nparams.password = params.password + \"\";\n```\n\n### File Upload Security\n```javascript\n// Validate file types\nif (type !== \"image/png\" && type !== \"image/gif\" && type !== \"image/jpeg\") {\n    fs.unlink(tmp_path, function() {});\n    return;\n}\n// Sanitize filenames\nvar safeFileName = common.sanitizeFilename(params.qstring.filename);\n```\n\n### Command Line Security\n```javascript\n// Use spawn with array args, NOT exec with string concatenation\nvar cp = require('child_process');\ncp.spawn(\"command\", [userInput]);  // Safe\n// exec(\"command \" + userInput);   // UNSAFE - allows injection\n```\n\n## Testing\n\n```bash\nnpm run test:unit                 # Unit tests (no Docker)\nnpm run test:api-core             # Core API tests\nnpm run test:lite-plugins         # CE plugin tests\nnpm run test:plugin -- <name>     # Single plugin tests\n\n# Linting\ncountly plugin lint <pluginname>\ncountly plugin lintfix <pluginname>\n\n# Shell script validation\ncountly shellcheck\n```\n\n### Plugin Test Requirements\n- Test empty state, various inputs, and cleanup\n- Verify app lifecycle handlers work correctly\n- Include tests in `plugins/<name>/tests.js`\n\n## CSS & Styling\n\n- Use **SASS** (SCSS syntax) for stylesheets\n- Use **BEM naming** with `cly-vue-` prefix for all new classes\n- Use **Bulma** classes prefixed with `bu-` for grid/layout\n- Don't use `@import`, use `@use` in SASS files\n- Compile with `npx grunt sass` or `npx grunt dist-all`\n\n## JSDoc Comments\n\nDocument all public functions:\n```javascript\n/**\n * Calculates percent change between periods.\n * @param {number} previous - data for previous period\n * @param {number} current - data for current period\n * @returns {object} {\"percent\": \"20%\", \"trend\": \"u\"}\n */\n```\n\n## Custom Scripts\n\nScripts in `bin/scripts/` must include:\n- Header comment with description, server type, path, command\n- All configurable variables with comments\n- Dry run option for destructive operations\n- Idempotent behavior (safe to run multiple times)\n\n## Development Commands\n\n```bash\nnpm run start:all:dev        # All services with hot reload\nnpx grunt dist-all           # Build all static assets (required after JS changes)\nnpx grunt locales            # Build locale files\n\n# Plugin management\nnode bin/commands/scripts/plugin.js enable <name>\nnode bin/commands/scripts/plugin.js disable <name>\n```\n\n## Key Files Reference\n\n| Purpose | Location |\n|---------|----------|\n| Plugin manager | `plugins/pluginManager.js` |\n| Common utilities | `api/utils/common.js` |\n| Authorization | `api/utils/rights.js` |\n| Vue core | `frontend/express/public/javascripts/countly/vue/core.js` |\n| Sample plugin | `plugins/empty/` |\n| TypeScript types | `types/` |\n| Coding guidelines | `CODING_GUIDELINES.md` |\n| Vue.js guidelines | `docs/VUEJS_GUIDELINES.md` |\n| CSS style guide | `docs/CSS_STYLE_GUIDE.md` |\n| Security guidelines | `docs/SECURITY.md` |\n| UI testing guide | `docs/UI_TESTING.md` |\n","category":".github","tokens":2083}]}