From d818abdaa705271566e59a128102b616f6183610 Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Sun, 9 Aug 2026 00:02:34 +0530 Subject: [PATCH 1/4] feat:add terms of service and privacy policy --- privacy-policy.md | 175 ++++++++++++++++++++++++++ src/components/layout/Footer.jsx | 6 +- terms-of-service.md | 210 +++++++++++++++++++++++++++++++ 3 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 privacy-policy.md create mode 100644 terms-of-service.md diff --git a/privacy-policy.md b/privacy-policy.md new file mode 100644 index 0000000..bcf96a8 --- /dev/null +++ b/privacy-policy.md @@ -0,0 +1,175 @@ +# Privacy Policy + +**Last updated: August 2026** + +## Introduction + +At OrgExplorer, we respect your privacy. + +This Privacy Policy explains what data OrgExplorer handles and how — and, just as importantly, what it does **not** do. + +OrgExplorer's architecture is different from many applications because it operates entirely in the browser. It has **no backend server and no user accounts**. + +--- + +## Information We Collect + +OrgExplorer does not have: + +- User accounts +- Signup or waitlist systems +- A backend server operated by us + +Because of this, we do not collect, receive, or store any personal information about you on any system we control. + +### What the App Handles + +#### Data fetched from GitHub + +When you search an organization, publicly available GitHub data is fetched directly from GitHub's API by your browser. + +This may include: + +- Repository information +- Contributor information +- Issues +- Pull requests +- Other publicly available GitHub metadata + +This data never passes through a server operated by OrgExplorer. + +#### Data stored locally on your device + +The following data may be stored locally in your browser: + +- GitHub Personal Access Token (if provided) +- Cached GitHub API responses +- API rate-limit status +- Recent search history +- Application preferences + +This data is stored only in your browser's: + +- `localStorage` +- `IndexedDB` + +This information never leaves your device and is never sent to OrgExplorer. + +#### Standard Hosting Logs + +OrgExplorer is hosted on GitHub Pages. + +GitHub, as the hosting provider, may collect standard infrastructure logs such as: + +- IP addresses +- Request timestamps +- Other operational metadata + +OrgExplorer maintainers do not have access to these logs. + +For more information, see [GitHub's Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). + +--- + +## How We Use Your Information + +Since OrgExplorer does not collect or receive your personal data, we do not use, analyze, sell, or process your information. + +All locally stored data exists only to make the application function properly, including: + +- Reducing repeated GitHub API requests through caching +- Preserving your preferences +- Allowing you to avoid entering your Personal Access Token repeatedly + +--- + +## Data Sharing and Disclosure + +We do not sell, share, or disclose your data. + +This is because OrgExplorer does not collect your data in the first place. + +The only external communication occurs directly between: +``` +Your Browser +| +| +v +GitHub API (api.github.com) +``` + + +This communication is governed by: + +- [GitHub Terms of Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) +- [GitHub Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement) + +--- + +## Data Security + +Because OrgExplorer has no backend server or database, most server-side data security risks do not apply. + +The relevant security considerations involve data stored on your own device. + +### GitHub Personal Access Token + +If you choose to provide a GitHub Personal Access Token: + +- It is stored only in your browser's `localStorage` +- It is sent only to GitHub's API +- It is never logged by OrgExplorer +- It is never transmitted to OrgExplorer servers + +### Token Safety Recommendations + +We recommend: + +- Using a token with the minimum permissions required +- Avoiding unnecessary scopes +- Revoking your token from GitHub settings if you suspect misuse + +Please note that browser `localStorage` is not encrypted. + +--- + +## Your Rights + +Since OrgExplorer does not collect or store personal information on our systems, there is no personal data held by us that you can request to: + +- Access +- Correct +- Delete +- Export + +You maintain complete control over locally stored application data. + +You can remove stored data by: + +- Clearing your browser storage +- Removing your Personal Access Token from the Settings page +- Resetting application data through browser settings + +--- + +## Changes to This Policy + +We may update this Privacy Policy as OrgExplorer evolves. + +Any changes will be reflected on this page with an updated **"Last updated"** date. + +We encourage users to review this policy periodically. + +--- + +## Contact Us + +If you have questions about this Privacy Policy, contact us: + +**Email:** [aossie.oss@gmail.com](mailto:aossie.oss@gmail.com) + +Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). + +--- + +*This policy describes OrgExplorer's actual technical behavior as of the date above. It is provided for transparency and is not a substitute for professional legal advice.* diff --git a/src/components/layout/Footer.jsx b/src/components/layout/Footer.jsx index 4b69ed1..ddb2200 100644 --- a/src/components/layout/Footer.jsx +++ b/src/components/layout/Footer.jsx @@ -19,11 +19,11 @@ const footerLinks = [ }, { label: "Terms of Service", - href: "/terms", + href: "https://github.com/AOSSIE-Org/OrgExplorer/blob/main/terms-of-service.md", }, { label: "Privacy Policy", - href: "/privacy", + href: "https://github.com/AOSSIE-Org/OrgExplorer/blob/main/privacy-policy.md", }, { label: "API Status", @@ -110,6 +110,8 @@ export default function Footer() { Date: Sun, 9 Aug 2026 00:19:13 +0530 Subject: [PATCH 2/4] fix: improve with the code rabbit feedback --- privacy-policy.md | 4 +++- terms-of-service.md | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/privacy-policy.md b/privacy-policy.md index bcf96a8..371ee9b 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -53,7 +53,9 @@ This data is stored only in your browser's: - `localStorage` - `IndexedDB` -This information never leaves your device and is never sent to OrgExplorer. +OrgExplorer does not transmit this data to any server operated by us. + +When a GitHub Personal Access Token is provided, your browser uses it to authenticate requests directly with GitHub's API. The token is sent only to GitHub for authorized API requests and is never sent to OrgExplorer servers. #### Standard Hosting Logs diff --git a/terms-of-service.md b/terms-of-service.md index 221a9c2..af0d4b6 100644 --- a/terms-of-service.md +++ b/terms-of-service.md @@ -110,7 +110,9 @@ This includes loss of locally cached data, as OrgExplorer does not store user da ### Contributor License -By contributing code, documentation, or other materials to the Project, you grant the maintainers a perpetual, irrevocable, non-exclusive, worldwide license to use your contributions under the terms of the repository's open-source license. +By contributing code, documentation, or other materials to the Project, you grant the maintainers a perpetual, worldwide, non-exclusive license to use, modify, distribute, and include your contributions as part of OrgExplorer under the project's open-source license. + +You retain ownership of your contributions. ### Code of Conduct @@ -160,7 +162,7 @@ Data stored locally for performance purposes, including: is stored only within your browser's local storage or browser databases. -For complete details, see our [Privacy Policy](https://github.com/AOSSIE-Org/OrgExplorer/blob/main/privacy-policy.md"). +For complete details, see our [Privacy Policy](https://github.com/AOSSIE-Org/OrgExplorer/blob/main/privacy-policy.md). ### Compliance From 8ad4b689f9448bbf266aba966f3c00ee7463cb37 Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Thu, 13 Aug 2026 18:05:00 +0530 Subject: [PATCH 3/4] improvement: add discord community in contact section --- Brand/Brand.md | 162 ----------------- MAINTAINERS.md | 13 -- privacy-policy.md | 3 +- src/assests/og-logo.svg | 61 ------- src/pages/Support.jsx | 50 ------ .../analytics.buildAnalyticalModel.test.js | 165 ------------------ .../analytics.buildTimeSeries.test.js | 120 ------------- src/services/analytics.healthMetrics.test.js | 116 ------------ src/test/setup.js | 7 - src/utils/formatNumber.js | 19 -- src/utils/formatNumber.test.js | 21 --- 11 files changed, 2 insertions(+), 735 deletions(-) delete mode 100644 Brand/Brand.md delete mode 100644 MAINTAINERS.md delete mode 100644 src/assests/og-logo.svg delete mode 100644 src/pages/Support.jsx delete mode 100644 src/services/analytics.buildAnalyticalModel.test.js delete mode 100644 src/services/analytics.buildTimeSeries.test.js delete mode 100644 src/services/analytics.healthMetrics.test.js delete mode 100644 src/test/setup.js delete mode 100644 src/utils/formatNumber.js delete mode 100644 src/utils/formatNumber.test.js diff --git a/Brand/Brand.md b/Brand/Brand.md deleted file mode 100644 index 2c6fcce..0000000 --- a/Brand/Brand.md +++ /dev/null @@ -1,162 +0,0 @@ -# Brand Guidelines - -This document defines the visual identity of the project. Please follow these guidelines when creating new pages, components, or assets to keep the UI consistent. - -## Design Principles - -- Keep the interface clean and minimal. -- Prioritize readability over decoration. -- Support both light and dark themes. -- Use spacing and contrast to create hierarchy instead of excessive colors. -- Avoid introducing new colors or fonts unless there is a strong reason. - ---- - -## Color Palette - -### Core Colors - -| Token | Dark | Light | -| ------ | ----- | ------ | -| Background | `#060606` | `#FFFFFF` | -| Surface | `#141414` | `#F5F5F5` | -| Surface Secondary | `#1A1A1A` | `#ECECEC` | -| Border | `#2A2A2A` | `#D5D5D5` | -| Text | `#FFFFFF` | `#060606` | -| Secondary Text | `#888888` | `#666666` | -| Muted Text | `#444444` | `#999999` | -| Accent | `#F5C518` | `#D4A500` | - -### Semantic Colors - -| Purpose | Color | -| -------- | ----- | -| Success | `#22C55E` | -| Error | `#EF4444` | -| Information | `#3B82F6` | - -Always reference the CSS variables instead of hardcoding colors. - -Example: - -```css -background: var(--surface); -color: var(--text); -border-color: var(--border); -``` - ---- - -## Typography - -Font stack: - -``` --apple-system, -BlinkMacSystemFont, -"Segoe UI", -Roboto, -sans-serif -``` - -### Text Sizes - -| Usage | Size | -| ------ | ---- | -| Body | 14px | -| Line Height | 1.6 | - -Use the default font stack unless there is a project-wide decision to change it. - ---- - -## Border Radius - -Standard radius: - -``` -8px -``` - -Use the `--radius` CSS variable whenever possible. - ---- - -## Theme Support - -Every UI component should work correctly in both themes. - -Prefer using CSS variables: - -```css -var(--bg) -var(--surface) -var(--surface2) -var(--border) -var(--text) -var(--text2) -var(--accent) -``` - -Avoid hardcoded colors. - ---- - -## Links - -Links use the accent color. - -```css -color: var(--accent); -``` - -Links should remain readable in both themes. - ---- - -## Icons & Logos - -- Use SVG whenever possible. -- Icons should inherit `currentColor` unless they intentionally use a fixed brand color. -- The project logo should support both light and dark themes. -- Do not stretch or distort the logo. - ---- - -## Spacing - -Keep spacing consistent across the project. - -Preferred spacing scale: - -``` -4px -8px -12px -16px -24px -32px -48px -64px -``` - ---- - -## Scrollbars - -The project uses a minimal scrollbar. - -- Width: `5px` -- Thumb: `var(--border)` -- Track: `var(--bg)` - ---- - -## Before Opening a UI Pull Request - -- Supports both light and dark themes. -- Uses existing CSS variables. -- Uses the project's typography. -- Maintains consistent spacing. -- Does not introduce unnecessary colors. -- Works across common screen sizes. diff --git a/MAINTAINERS.md b/MAINTAINERS.md deleted file mode 100644 index 4a8918d..0000000 --- a/MAINTAINERS.md +++ /dev/null @@ -1,13 +0,0 @@ -# Maintainers - -This document lists the people responsible for maintaining the project during the Google Summer of Code (GSoC) 2026 development period. - -## Maintainers - -| Name | Role | -|------|------| -| [Bruno](https://github.com/Zahnentferner) | Mentor / Maintainer | -| [Bhavik](https://github.com/bhavik-mangla) | Mentor / Maintainer | -| [Madhav](https://github.com/M4dhav) | Mentor / Maintainer | -| [Rahul](https://github.com/rahul-vyas-dev) | GSoC Maintainer (Student Developer) | -| [Ritik](https://github.com/Ri1tik) | GSoC Maintainer (Student Developer) | diff --git a/privacy-policy.md b/privacy-policy.md index 371ee9b..d74d1e1 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -168,7 +168,8 @@ We encourage users to review this policy periodically. If you have questions about this Privacy Policy, contact us: -**Email:** [aossie.oss@gmail.com](mailto:aossie.oss@gmail.com) +**Email:** [contact@aossie.org](mailto:contact@aossie.org) +**Discord** [Discord](https://discord.gg/hjUhu33uAn) Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). diff --git a/src/assests/og-logo.svg b/src/assests/og-logo.svg deleted file mode 100644 index 86165d9..0000000 --- a/src/assests/og-logo.svg +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/pages/Support.jsx b/src/pages/Support.jsx deleted file mode 100644 index d6a10a0..0000000 --- a/src/pages/Support.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import SupportUsButton from "support-us-button"; -import "support-us-button/style.css"; -import { useTheme } from "../context/ThemeContext"; -import logo from "../assests/og-logo.svg"; -function Support() { - const { theme } = useTheme(); - const supportUsButtonProps = { - Theme: theme, - projectInformation: { - name: "OrgExplorer", - image: logo, - description: - "OrgExplorer is an open-source tool that visualizes GitHub organization activity, helping teams analyze repositories, contributors, and project health through interactive insights.", - }, - - organizationInformation: { - name: "AOSSIE", - image: "/aossie-logo.svg", - link: "https://aossie.org", - desc: "AOSSIE is an Australian not-for-profit organization that supports and brings together open-source projects. We believe open source is a resource-efficient and collaborative way to share knowledge, encourage innovation, and make education more accessible through strong community participation.", - }, - - sponsors: [ - { - name: "Google", - }, - { - name: "Stable Order", - }, - ], - - ctaSection: { - sponsorLink: [ - { - name: "Support Now", - url: "https://buymeacoffee.com/aossie", - }, - ], - }, - Logo: true, - }; - - return ( -
- -
- ); -} - -export default Support; diff --git a/src/services/analytics.buildAnalyticalModel.test.js b/src/services/analytics.buildAnalyticalModel.test.js deleted file mode 100644 index 85cb58a..0000000 --- a/src/services/analytics.buildAnalyticalModel.test.js +++ /dev/null @@ -1,165 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { buildAnalyticalModel } from './analytics' - -function makeRepo(name, overrides = {}) { - return { - name, - pushed_at: new Date().toISOString(), - open_issues_count: 0, - stargazers_count: 0, - forks_count: 0, - ...overrides, - } -} - -describe('buildAnalyticalModel', () => { - it('populates totalRepos from totalReposPerOrg, including repos with no contributor data', () => { - const orgs = [{ login: 'org-a' }] - const repoA = makeRepo('repo-a') - const repoB = makeRepo('repo-b') // exists only in the "total" fetch, not scoped - - const reposPerOrg = { 'org-a': [repoA] } // scoped (e.g. top-10 without PAT) - const totalReposPerOrg = { 'org-a': [repoA, repoB] } // full fetch - const contribsPerRepo = { - 'org-a/repo-a': [{ login: 'alice', contributions: 10 }], - // no entry for repo-b - } - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.totalRepos).toHaveLength(2) - const repoBResult = result.totalRepos.find(r => r.name === 'repo-b') - expect(repoBResult.contributors).toEqual([]) - expect(repoBResult.busFactor).toEqual({ factor: 0, risk: 'unknown' }) - expect(repoBResult.orgLogin).toBe('org-a') - }) - - it('populates allRepos only from the scoped reposPerOrg, not from totalReposPerOrg', () => { - const orgs = [{ login: 'org-a' }] - const repoA = makeRepo('repo-a') - const repoB = makeRepo('repo-b') - - const reposPerOrg = { 'org-a': [repoA] } - const totalReposPerOrg = { 'org-a': [repoA, repoB] } - const contribsPerRepo = {} - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.allRepos).toHaveLength(1) - expect(result.allRepos[0].name).toBe('repo-a') - }) - - it('does NOT include a contributor whose only appearance is in a totalRepos-only repo', () => { - // repo-b has contributor data in contribsPerRepo, but repo-b is only in - // totalReposPerOrg, not reposPerOrg. The contributor map is built solely - // from the reposPerOrg loop, so this contributor must not appear. - const orgs = [{ login: 'org-a' }] - const repoA = makeRepo('repo-a') - const repoB = makeRepo('repo-b') - - const reposPerOrg = { 'org-a': [repoA] } - const totalReposPerOrg = { 'org-a': [repoA, repoB] } - const contribsPerRepo = { - 'org-a/repo-b': [{ login: 'ghost-contributor', avatar_url: '', contributions: 5 }], - } - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors.find(c => c.login === 'ghost-contributor')).toBeUndefined() - }) - - it('deduplicates a contributor appearing across multiple scoped repos, summing contributions', () => { - const orgs = [{ login: 'org-a' }] - const repoA = makeRepo('repo-a') - const repoB = makeRepo('repo-b') - - const reposPerOrg = { 'org-a': [repoA, repoB] } - const totalReposPerOrg = { 'org-a': [repoA, repoB] } - const contribsPerRepo = { - 'org-a/repo-a': [{ login: 'alice', avatar_url: '', contributions: 10 }], - 'org-a/repo-b': [{ login: 'alice', avatar_url: '', contributions: 15 }], - } - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors).toHaveLength(1) - expect(result.contributors[0].totalContribs).toBe(25) - expect(result.contributors[0].repos).toHaveLength(2) - }) - - it('marks a contributor as isConnector once they appear in 3+ scoped repos', () => { - const orgs = [{ login: 'org-a' }] - const repos = [makeRepo('r1'), makeRepo('r2'), makeRepo('r3')] - - const reposPerOrg = { 'org-a': repos } - const totalReposPerOrg = { 'org-a': repos } - const contribsPerRepo = Object.fromEntries( - repos.map(r => [`org-a/${r.name}`, [{ login: 'carol', avatar_url: '', contributions: 1 }]]) - ) - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors[0].isConnector).toBe(true) - }) - - it('does not mark a contributor as isConnector with fewer than 3 scoped repos', () => { - const orgs = [{ login: 'org-a' }] - const repos = [makeRepo('r1'), makeRepo('r2')] - - const reposPerOrg = { 'org-a': repos } - const totalReposPerOrg = { 'org-a': repos } - const contribsPerRepo = Object.fromEntries( - repos.map(r => [`org-a/${r.name}`, [{ login: 'dave', avatar_url: '', contributions: 1 }]]) - ) - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors[0].isConnector).toBe(false) - }) - - it('marks a contributor as isCrossOrg when they appear in 2+ orgs', () => { - const orgs = [{ login: 'org-a' }, { login: 'org-b' }] - const repoA = makeRepo('repo-a') - const repoB = makeRepo('repo-b') - - const reposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] } - const totalReposPerOrg = { 'org-a': [repoA], 'org-b': [repoB] } - const contribsPerRepo = { - 'org-a/repo-a': [{ login: 'erin', avatar_url: '', contributions: 5 }], - 'org-b/repo-b': [{ login: 'erin', avatar_url: '', contributions: 5 }], - } - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors[0].isCrossOrg).toBe(true) - expect(result.contributors[0].orgs.sort()).toEqual(['org-a', 'org-b']) - }) - - it('sorts contributors by totalContribs descending', () => { - const orgs = [{ login: 'org-a' }] - const repoA = makeRepo('repo-a') - - const reposPerOrg = { 'org-a': [repoA] } - const totalReposPerOrg = { 'org-a': [repoA] } - const contribsPerRepo = { - 'org-a/repo-a': [ - { login: 'low', avatar_url: '', contributions: 5 }, - { login: 'high', avatar_url: '', contributions: 50 }, - { login: 'mid', avatar_url: '', contributions: 20 }, - ], - } - - const result = buildAnalyticalModel(orgs, reposPerOrg, contribsPerRepo, totalReposPerOrg) - - expect(result.contributors.map(c => c.login)).toEqual(['high', 'mid', 'low']) - }) - - it('handles an org with zero repos without throwing', () => { - const orgs = [{ login: 'empty-org' }] - const result = buildAnalyticalModel(orgs, { 'empty-org': [] }, {}, { 'empty-org': [] }) - - expect(result.allRepos).toEqual([]) - expect(result.totalRepos).toEqual([]) - expect(result.contributors).toEqual([]) - }) -}) diff --git a/src/services/analytics.buildTimeSeries.test.js b/src/services/analytics.buildTimeSeries.test.js deleted file mode 100644 index c14e599..0000000 --- a/src/services/analytics.buildTimeSeries.test.js +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { buildTimeSeries } from './analytics' - -describe('buildTimeSeries', () => { - it('buckets a plain issue by its created_at month', () => { - const issues = [{ created_at: '2026-03-15T00:00:00Z' }] - - const result = buildTimeSeries(issues, 'monthly') - - expect(result).toHaveLength(1) - expect(result[0].date).toBe('2026-03') - expect(result[0].issues_created).toBe(1) - expect(result[0].prs_created).toBe(0) - }) - - it('counts a pull_request item as a PR, not an issue', () => { - const issues = [{ created_at: '2026-03-15T00:00:00Z', pull_request: {} }] - - const result = buildTimeSeries(issues, 'monthly') - - expect(result[0].prs_created).toBe(1) - expect(result[0].issues_created).toBe(0) - }) - - it('buckets closed_at into the correct month, separate from created_at', () => { - const issues = [{ - created_at: '2026-01-01T00:00:00Z', - closed_at: '2026-02-01T00:00:00Z', - }] - - const result = buildTimeSeries(issues, 'monthly') - - const jan = result.find(r => r.date === '2026-01') - const feb = result.find(r => r.date === '2026-02') - - expect(jan.issues_created).toBe(1) - expect(jan.issues_closed).toBe(0) - expect(feb.issues_closed).toBe(1) - expect(feb.issues_created).toBe(0) - }) - - it('buckets a merged PR into prs_merged using pull_request.merged_at, not closed_at', () => { - const issues = [{ - created_at: '2026-01-01T00:00:00Z', - closed_at: '2026-01-05T00:00:00Z', - pull_request: { merged_at: '2026-01-05T00:00:00Z' }, - }] - - const result = buildTimeSeries(issues, 'monthly') - const jan = result.find(r => r.date === '2026-01') - - expect(jan.prs_created).toBe(1) - expect(jan.prs_closed).toBe(1) - expect(jan.prs_merged).toBe(1) - }) - - it('does not count prs_merged for a closed-but-not-merged PR', () => { - const issues = [{ - created_at: '2026-01-01T00:00:00Z', - closed_at: '2026-01-05T00:00:00Z', - pull_request: { merged_at: null }, - }] - - const result = buildTimeSeries(issues, 'monthly') - const jan = result.find(r => r.date === '2026-01') - - expect(jan.prs_closed).toBe(1) - expect(jan.prs_merged).toBe(0) - }) - - it('produces one bucket per week when granularity is weekly, distinct from monthly', () => { - const issues = [ - { created_at: '2026-01-01T00:00:00Z' }, - { created_at: '2026-01-08T00:00:00Z' }, - ] - - const weekly = buildTimeSeries(issues, 'weekly') - - // Two different weeks -> two buckets, and week-format labels - expect(weekly.length).toBeGreaterThanOrEqual(1) - weekly.forEach(bucket => expect(bucket.date).toMatch(/^\d{4}-W\d{2}$/)) - }) - - it('sorts buckets chronologically', () => { - const issues = [ - { created_at: '2026-03-01T00:00:00Z' }, - { created_at: '2026-01-01T00:00:00Z' }, - { created_at: '2026-02-01T00:00:00Z' }, - ] - - const result = buildTimeSeries(issues, 'monthly') - - expect(result.map(r => r.date)).toEqual(['2026-01', '2026-02', '2026-03']) - }) - - it('returns at most the last 12 buckets when more months are present', () => { - const issues = Array.from({ length: 15 }, (_, i) => ({ - created_at: `2025-${String((i % 12) + 1).padStart(2, '0')}-01T00:00:00Z`, - })) - // Construct 15 distinct months across two years to exceed the 12-bucket cap - const spreadIssues = Array.from({ length: 15 }, (_, i) => { - const year = 2025 + Math.floor(i / 12) - const month = (i % 12) + 1 - return { created_at: `${year}-${String(month).padStart(2, '0')}-01T00:00:00Z` } - }) - - const result = buildTimeSeries(spreadIssues, 'monthly') - - expect(result.length).toBeLessThanOrEqual(12) - }) - - it('returns an empty array when given no issues', () => { - expect(buildTimeSeries([], 'monthly')).toEqual([]) - }) - - it('ignores an item with no created_at and no closed_at', () => { - const result = buildTimeSeries([{ created_at: null }], 'monthly') - expect(result).toEqual([]) - }) -}) diff --git a/src/services/analytics.healthMetrics.test.js b/src/services/analytics.healthMetrics.test.js deleted file mode 100644 index 3fdf4f6..0000000 --- a/src/services/analytics.healthMetrics.test.js +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { - computeHealthScore, - computeActivityClassification, - computeBusFactor, -} from './analytics' - -function daysAgoISO(days) { - return new Date(Date.now() - days * 86_400_000).toISOString() -} - -describe('computeHealthScore', () => { - it('scores a freshly pushed, issue-free repo with 2 contributors at 76', () => { - // activity = 100 - 0 = 100 - // issueHealth = 100 - (0 / (0+10)) * 100 = 100 - // diversity = min(100, 2 * 10) = 20 - // score = round(100*0.4 + 100*0.3 + 20*0.3) = round(40 + 30 + 6) = 76 - const repo = { pushed_at: new Date().toISOString(), open_issues_count: 0 } - - expect(computeHealthScore(repo, 2)).toBe(76) - }) - - it('lowers the score as open issue count grows relative to the +10 baseline', () => { - const freshRepo = pushed => ({ pushed_at: pushed, open_issues_count: 0 }) - - const noIssues = computeHealthScore({ ...freshRepo(new Date().toISOString()), open_issues_count: 0 }, 0) - const manyIssues = computeHealthScore({ ...freshRepo(new Date().toISOString()), open_issues_count: 90 }, 0) - - expect(manyIssues).toBeLessThan(noIssues) - }) - - it('caps diversity contribution at 100 once contributor count reaches 10', () => { - const repo = { pushed_at: new Date().toISOString(), open_issues_count: 0 } - - const at10 = computeHealthScore(repo, 10) - const at50 = computeHealthScore(repo, 50) - - // diversity is min(100, count*10) for both -> identical score - expect(at10).toBe(at50) - }) - - it('lowers activity score as days since last push increases', () => { - const recentRepo = { pushed_at: daysAgoISO(5), open_issues_count: 0 } - const staleRepo = { pushed_at: daysAgoISO(95), open_issues_count: 0 } - - expect(computeHealthScore(recentRepo, 5)).toBeGreaterThan( - computeHealthScore(staleRepo, 5) - ) - }) - - it('never returns a negative score for a very old, issue-heavy repo', () => { - const repo = { pushed_at: daysAgoISO(1000), open_issues_count: 500 } - - expect(computeHealthScore(repo, 0)).toBeGreaterThanOrEqual(0) - }) -}) - -describe('computeActivityClassification', () => { - it('classifies a repo pushed within 30 days as Thriving', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(10) })).toBe('Thriving') - }) - - it('classifies a repo pushed within 31-90 days as Active', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(60) })).toBe('Active') - }) - - it('classifies a repo pushed within 91-180 days as Dormant', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(120) })).toBe('Dormant') - }) - - it('classifies a repo pushed more than 180 days ago as Hibernating', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(200) })).toBe('Hibernating') - }) - - it('treats the exact 30-day boundary as Thriving (inclusive)', () => { - expect(computeActivityClassification({ pushed_at: daysAgoISO(30) })).toBe('Thriving') - }) -}) - -describe('computeBusFactor', () => { - it('returns unknown/0 for an empty contributor list', () => { - expect(computeBusFactor([])).toEqual({ factor: 0, risk: 'unknown' }) - }) - - it('returns unknown/0 when total contributions sum to zero', () => { - const contributors = [{ contributions: 0 }, { contributions: 0 }] - expect(computeBusFactor(contributors)).toEqual({ factor: 0, risk: 'unknown' }) - }) - - it('flags critical risk when one contributor alone exceeds 50% of contributions', () => { - const contributors = [{ contributions: 60 }, { contributions: 40 }] - // cum after contributor 1: 60/100 = 0.6 > 0.5 -> factor 1 - expect(computeBusFactor(contributors)).toEqual({ factor: 1, risk: 'critical' }) - }) - - it('flags high risk when it takes exactly 2 contributors to exceed 50%', () => { - const contributors = [{ contributions: 30 }, { contributions: 30 }, { contributions: 40 }] - // cum after 1: 30/100=0.3 (not >0.5); cum after 2: 60/100=0.6>0.5 -> factor 2 - expect(computeBusFactor(contributors)).toEqual({ factor: 2, risk: 'high' }) - }) - - it('flags healthy risk when it takes 3+ contributors to exceed 50%', () => { - const contributors = [ - { contributions: 20 }, { contributions: 20 }, { contributions: 20 }, { contributions: 40 }, - ] - // cum: 20/100=.2, 40/100=.4, 60/100=.6>0.5 -> factor 3 - expect(computeBusFactor(contributors)).toEqual({ factor: 3, risk: 'healthy' }) - }) - - it('respects contributor order — cumulative sum is positional, not sorted', () => { - // Smallest contributor listed first; still accumulates in list order - const contributors = [{ contributions: 10 }, { contributions: 90 }] - // cum after 1: 10/100=.1; cum after 2: 100/100=1.0>0.5 -> factor 2 - expect(computeBusFactor(contributors)).toEqual({ factor: 2, risk: 'high' }) - }) -}) diff --git a/src/test/setup.js b/src/test/setup.js deleted file mode 100644 index 4110b52..0000000 --- a/src/test/setup.js +++ /dev/null @@ -1,7 +0,0 @@ -import "@testing-library/jest-dom/vitest"; -import { afterEach } from "vitest"; -import { cleanup } from "@testing-library/react"; - -afterEach(() => { - cleanup(); -}); \ No newline at end of file diff --git a/src/utils/formatNumber.js b/src/utils/formatNumber.js deleted file mode 100644 index 987f3c5..0000000 --- a/src/utils/formatNumber.js +++ /dev/null @@ -1,19 +0,0 @@ -export function formatNumber(num) { - if (num >= 1_000_000_000) { - return `${(num / 1_000_000_000).toFixed(1)}B`; - } - - if (num >= 1_000_000) { - const millions = (num / 1_000_000).toFixed(1); - if (millions === '1000.0') return `${(num / 1_000_000_000).toFixed(1)}B`; - return `${millions}M`; - } - - if (num >= 1_000) { - const thousands = (num / 1_000).toFixed(1); - if (thousands === '1000.0') return `${(num / 1_000_000).toFixed(1)}M`; - return `${thousands}K`; - } - - return num.toString(); -} \ No newline at end of file diff --git a/src/utils/formatNumber.test.js b/src/utils/formatNumber.test.js deleted file mode 100644 index 91e74b2..0000000 --- a/src/utils/formatNumber.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { formatNumber } from "./formatNumber"; - -describe("formatNumber", () => { - it("return a number in a form of a string if it is lesser than 1000", () => { - expect(formatNumber(500)).toBe("500"); - }); - it("formats numbers in the thousands as K", () => { - expect(formatNumber(999_949)).toBe("999.9K") - }); - it("formats numbers in the millions as M", () => { - expect(formatNumber(999_950)).toBe("1.0M"); - }); - it("formats numbers in the billions as B", () => { - expect(formatNumber(999_950_000)).toBe("1.0B"); - }); - - it("handles exactly 1000", () => { - expect(formatNumber(1000)).toBe("1.0K"); - }); -}) \ No newline at end of file From 783239aad136b7a7f6a9046566e541ed6ff55cbf Mon Sep 17 00:00:00 2001 From: Ritikydv1 Date: Thu, 13 Aug 2026 18:10:17 +0530 Subject: [PATCH 4/4] fix: update as requested by mentor --- privacy-policy.md | 3 ++- terms-of-service.md | 30 ++++++++++-------------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/privacy-policy.md b/privacy-policy.md index d74d1e1..78a09f8 100644 --- a/privacy-policy.md +++ b/privacy-policy.md @@ -169,7 +169,8 @@ We encourage users to review this policy periodically. If you have questions about this Privacy Policy, contact us: **Email:** [contact@aossie.org](mailto:contact@aossie.org) -**Discord** [Discord](https://discord.gg/hjUhu33uAn) + +**Discord:** [Discord](https://discord.gg/hjUhu33uAn) Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). diff --git a/terms-of-service.md b/terms-of-service.md index af0d4b6..62f8767 100644 --- a/terms-of-service.md +++ b/terms-of-service.md @@ -16,7 +16,7 @@ By accessing or using the Project, you agree to be bound by these terms. If you OrgExplorer is open-source software. Its source code, license terms, and contribution guidelines are available at [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer). -Your rights to use, modify, and redistribute the underlying code are governed by that repository's license. This Agreement additionally governs your use of the *deployed, hosted instance* of the Project available at [Live Demo](https://orgexplorer.aossie.org/). +Your rights to use, modify, and redistribute the underlying code are governed by that repository's license. This Agreement additionally governs your use of the *deployed, hosted instance* of the Project available at (https://orgexplorer.aossie.org/). ### Permitted Uses @@ -106,21 +106,8 @@ This includes loss of locally cached data, as OrgExplorer does not store user da --- -## 6. Contributions and Community Conduct -### Contributor License - -By contributing code, documentation, or other materials to the Project, you grant the maintainers a perpetual, worldwide, non-exclusive license to use, modify, distribute, and include your contributions as part of OrgExplorer under the project's open-source license. - -You retain ownership of your contributions. - -### Code of Conduct - -Contributors and users are expected to maintain respectful, constructive, and professional interactions within project and community channels, consistent with AOSSIE's community guidelines. - ---- - -## 7. Third-Party Services and Integrations +## 6. Third-Party Services and Integrations OrgExplorer integrates with GitHub's public REST API. @@ -145,7 +132,7 @@ OrgExplorer is not responsible for GitHub service availability, API changes, or --- -## 8. Data Privacy and Use +## 7. Data Privacy and Use OrgExplorer has **no backend server and no user accounts**. @@ -170,7 +157,7 @@ You remain responsible for your use of any data retrieved through the Project, i --- -## 9. Modification and Termination +## 8. Modification and Termination ### Amendments @@ -191,7 +178,7 @@ The underlying open-source code remains available according to its license. --- -## 10. Governing Law and Dispute Resolution +## 9. Governing Law and Dispute Resolution This Agreement is governed by applicable law in the jurisdiction where the maintaining organization, AOSSIE, primarily operates. @@ -199,11 +186,14 @@ In case of disputes, the parties agree to first attempt resolution through infor --- -## 11. Contact Information +## 10. Contact Information If you have questions regarding these Terms of Service, contact us: -**Email:** [aossie.oss@gmail.com](mailto:aossie.oss@gmail.com) +**Email:** [contact@aossie.org](mailto:contact@aossie.org) + +**Discord:** [Discord](https://discord.gg/hjUhu33uAn) + Or open an issue on [the project's GitHub repository](https://github.com/AOSSIE-Org/OrgExplorer).