Skill: firstsanny/spielerplus-scraper (version 1.0.0)

--- name: spielerplus-scraper description: "Scraper for SpielerPlus/TeamPlus team management platform. Extracts events, members, absences, finances, participation stats, and more." tags: [spielerplus, teamplus, scraper, team-management, sports] permissions: [network] metadata: clawdbot: requires: env: [SPIELERPLUS_EMAIL, SPIELERPLUS_PASSWORD] --- # SpielerPlus Scraper Scrapes data from SpielerPlus for team management. ## Configuration Requires environment variables: ``` SPIELERPLUS_EMAIL=[REDACTED_SECRET] SPIELERPLUS_PASSWORD=yourpassword ``` ## Available Commands | Command | Description | |---------|-------------| | `teams` | List all teams | | `events` | Upcoming events | | `event [n]` | Event details (index n) | | `team` | Team members | | `absences` | Vacation/sick leave | | `finances` | Team cashbox | | `participation` | Attendance stats | | `profile` | Team profile | | `roles` | Roles & permissions | | `all` | Full report | ## Usage ```bash # Install dependencies npm install # List teams npm run teams # Get events for specific team npm run events "Männer" # Full report npm run all # JSON output npm run events -- --json ``` ## Programmatic Usage ```javascript const SpielerPlusScraper = require('./src/index.js'); const scraper = new SpielerPlusScraper({ email: process.env.SPIELERPLUS_EMAIL, [REDACTED_SECRET] }); await scraper.init(); const events = await scraper.getEvents(); await scraper.close(); ```

path: CHANGELOG.md
content: # Changelog

## [1.0.0] - 2026-03-31

### Added
- Initial release
- Team listing and selection
- Event scraping with participants
- Team member listing
- Absence tracking
- Team finances/cashbox
- Participation statistics
- Team profile
- Roles & permissions
- Benefits/deals
- CLI interface
- JSON output option
- Multiple team support
- Environment variable configuration


path: CLAUDE.md
content: # SpielerPlus Scraper - Claude Code Guide

## Quick Start

```bash
npm install
cp .env.example .env
# Edit .env with your credentials
npm run teams
```

## Common Tasks

```bash
# Get upcoming events
npm run events

# Get team finances
npm run finances

# Get participation statistics
npm run participation

# Full report for all teams
npm run all

# Specific team
npm run events "Männer"
npm run finances "Frauen"
```

## Development

```bash
# Test changes
node src/cli.js teams

# Validate syntax
node --check src/index.js
node --check src/cli.js
```

## Project Structure

```
src/
├── index.js    # Core scraper class
└── cli.js      # CLI interface
```

## Adding New Features

1. Add method to `src/index.js`
2. Add CLI command to `src/cli.js`
3. Update README.md
4. Test manually


path: CONTRIBUTING.md
content: # Contributing to SpielerPlus Scraper

Thank you for your interest!

## Development Setup

1. Clone the repository
2. Install dependencies: `npm install`
3. Copy `.env.example` to `.env` and fill in your credentials
4. Test with `npm run teams`

## Code Style

- Use ES6+ features
- Add JSDoc comments for public methods
- Keep methods focused and testable

## Adding New Features

1. Add the scraping logic to `src/index.js` as a new method
2. Add CLI command to `src/cli.js`
3. Update README.md with documentation
4. Test thoroughly before submitting PR

## Testing

Test manually by running different commands:
```bash
npm run teams
npm run events
npm run finances
# etc.
```

## Pull Request Process

1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test with your own SpielerPlus account
5. Submit a PR with description of changes


path: README.md
content: # SpielerPlus Scraper

A generic Node.js scraper for [SpielerPlus/TeamPlus](https://www.spielerplus.de) - the team management platform.

Extract team data including events, members, absences, finances, participation statistics, and more.

> ⚠️ **Note:** This tool is for personal use and educational purposes. Respect SpielerPlus' Terms of Service. Use responsibly.

## Features

- 📅 **Events** - Training, games, tournaments, events
- 👥 **Team Members** - List all team members
- 🏥 **Absences** - Vacation, sick leave, inactive status
- 💰 **Finances** - Team cashbox, transactions, contributions
- 📊 **Participation** - Attendance statistics per member
- 🏠 **Team Profile** - Team info, address, contact
- 👔 **Roles** - Team roles and permissions
- 🎁 **Benefits** - Active deals and offers

## Installation

```bash
npm install
```

## Configuration

Create a `.env` file in the project root:

```env
SPIELERPLUS_EMAIL=[REDACTED_SECRET]
SPIELERPLUS_PASSWORD=yourpassword
```

Or export environment variables:

```bash
export SPIELERPLUS_EMAIL=[REDACTED_SECRET]
export SPIELERPLUS_PASSWORD=yourpassword
```

## Usage

```bash
# Show available teams
npm run teams

# Scrape specific data
npm run events          # Upcoming events
npm run team            # Team members
npm run absences        # Absences
npm run finances        # Team finances
npm run participation   # Participation statistics
npm run profile         # Team profile
npm run roles           # Roles & permissions
npm run benefits        # Benefits/deals

# Event details with participants & carpool
npm run event 0        # First event
npm run event 1        # Second event

# Specify team (if you have multiple teams)
npm run events "Männer"
npm run finances "Herren"

# Full report for all teams
npm run all
```

## CLI Options

```bash
node src/cli.js <command> [team] [options]

Commands:
  teams         List all teams
  events        List upcoming events
  event [idx]   Event details (index, default: 0)
  team          Team members
  absences      Absences (vacation, sick, inactive)
  finances      Team finances & transactions
  participation Participation statistics
  profile       Team profile info
  roles         Roles & permissions
  benefits      Benefits & deals
  all           Full report for all teams

Options:
  -h, --help    Show help
  -j, --json    Output as JSON
  -t, --team    Team name to scrape
```

## Programmatic Usage

```javascript
const SpielerPlusScraper = require('./src/index.js');

const scraper = new SpielerPlusScraper({
  email: '[REDACTED_SECRET]',
  [REDACTED_SECRET]'
});

async function main() {
  await scraper.init();
  
  // Get teams
  const teams = await scraper.getTeams();
  console.log('Teams:', teams);
  
  // Select specific team
  await scraper.selectTeam('My Team');
  
  // Get data
  const events = await scraper.getEvents();
  const members = await scraper.getTeamMembers();
  const finances = await scraper.getFinances();
  
  console.log({ events, members, finances });
  
  await scraper.close();
}

main();
```

## Data Output Example

```json
{
  "team": "SSG Humboldt Männer",
  "events": [
    { "date": "Di.31.03", "time": "19:15", "type": "Training", "meeting": "19:15" }
  ],
  "finances": {
    "balance": "EUR 301.63",
    "transactions": [
      { "type": "Beitrag", "name": "Max Mustermann", "amount": "EUR 10.00" }
    ]
  },
  "participation": {
    "summary": { "total": 24, "trainings": 13, "spiele": 11 },
    "players": [
      { "name": "Thomas Heine", "count": 18, "percentage": 75 }
    ]
  }
}
```

## Requirements

- Node.js >= 18.0.0
- Chrome/Chromium (installed automatically by Playwright)

## Troubleshooting

**Login fails?**
- Check your credentials in `.env`
- SpielerPlus might require email verification for new logins

**Headless browser issues?**
- Ensure Chromium is installed: `npx playwright install chromium`

**Premium features not accessible?**
- Some features require SpielerPlus Premium subscription

## License

MIT

## Disclaimer

This project is not affiliated with SpielerPlus/TeamPlus. Use at your own risk and respect the platform's Terms of Service.

---

## Release Process

Releases are automated via GitLab CI/CD:

1. Update version: `npm version patch`
2. Push with tags: `git push --follow-tags`
3. Pipeline publishes to:
   - **npm**: https://www.npmjs.com/package/spielerplus-scraper
   - **ClawHub**: https://clawhub.ai

### Required Setup

1. **npm Token** (GitLab CI/CD Variable: `NPM_TOKEN`)
   - https://www.npmjs.com → Profile → Access Tokens → Create Automation Token

2. **ClawHub** (optional, automatic)
   - Uses `clawdhub` in CI


path: examples/example-docker.md
content: # Docker Usage

```dockerfile
FROM node:20-alpine

WORKDIR /app
RUN apk add --no-cache chromium nss freetype fontconfig

RUN npm install playwright && npx playwright install chromium

COPY . .
RUN npm install

CMD ["node", "src/cli.js", "all"]
```

Run with:
```bash
docker build -t spielerplus-scraper .
docker run --rm \
  -e SPIELERPLUS_EMAIL=[REDACTED_SECRET] \
  -e SPIELERPLUS_PASSWORD=yourpassword \
  spielerplus-scraper teams
```


path: examples/example.js
content: /**
 * Example: Using SpielerPlus Scraper as a library
 */

const SpielerPlusScraper = require('../src/index.js');

async function main() {
  // Initialize with config
  const scraper = new SpielerPlusScraper({
    email: process.env.SPIELERPLUS_EMAIL,
    [REDACTED_SECRET],
    defaultTeam: 'Männer' // Optional: auto-select team
  });

  try {
    await scraper.init();

    // Get teams
    const teams = await scraper.getTeams();
    console.log('Teams:', teams);

    // Select specific team
    await scraper.selectTeam('Herren');

    // Get data
    const events = await scraper.getEvents();
    const members = await scraper.getTeamMembers();
    const absences = await scraper.getAbsences();
    const finances = await scraper.getFinances();
    const participation = await scraper.getParticipationStats();
    const profile = await scraper.getTeamProfile();
    const roles = await scraper.getRoles();

    // Output results
    console.log('\n=== Team Profile ===');
    console.log(profile);

    console.log('\n=== Upcoming Events ===');
    console.log(events);

    console.log('\n=== Participation Stats ===');
    console.log(participation);

    console.log('\n=== Finances ===');
    console.log(finances);

  } catch (error) {
    console.error('Error:', error.message);
  } finally {
    await scraper.close();
  }
}

main();


path: package.json
content: {
  "name": "spielerplus-scraper",
  "version": "1.0.0",
  "description": "Generic SpielerPlus/TeamPlus scraper for team management data",
  "main": "src/index.js",
  "bin": {
    "spielerplus": "src/cli.js"
  },
  "scripts": {
    "start": "node src/cli.js",
    "scrape": "node src/cli.js",
    "teams": "node src/cli.js teams",
    "events": "node src/cli.js events",
    "event": "node src/cli.js event",
    "team": "node src/cli.js team",
    "absences": "node src/cli.js absences",
    "finances": "node src/cli.js finances",
    "participation": "node src/cli.js participation",
    "profile": "node src/cli.js profile",
    "roles": "node src/cli.js roles",
    "benefits": "node src/cli.js benefits",
    "all": "node src/cli.js all",
    "test": "node --check src/index.js && node --check src/cli.js",
    "lint": "echo 'No linter configured'",
    "release": "standard-version",
    "release:major": "standard-version --release-as major",
    "release:minor": "standard-version --release-as minor",
    "release:patch": "standard-version --release-as patch"
  },
  "keywords": ["spielerplus", "teamplus", "scraper", "team-management", "handball", "sports"],
  "author": "draiwing",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://gitlab.com/draiwing/spielerplus-scraper"
  },
  "dependencies": {
    "playwright": "^1.40.0",
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "standard-version": "^9.5.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}


path: src/cli.js
content: #!/usr/bin/env node

/**
 * SpielerPlus Scraper CLI
 * 
 * Usage: node cli.js <command> [team] [options]
 */

const SpielerPlusScraper = require('./index.js');

// Parse arguments
const args = process.argv.slice(2);
const command = args[0] || 'help';
const teamArg = args[1];
const options = args.slice(2);

// Check for help
if (command === 'help' || command === '--help' || command === '-h') {
  console.log(`
SpielerPlus Scraper CLI

Usage: node cli.js <command> [team] [options]

Commands:
  teams         List all teams
  events        List upcoming events
  event [n]     Event details (index, default: 0)
  team          Team members
  absences      Absences (vacation, sick, inactive)
  finances      Team finances
  participation Participation statistics
  profile       Team profile
  roles         Roles & permissions
  benefits      Benefits & deals
  all           Full report for all teams

Options:
  -h, --help    Show this help
  -j, --json    Output as JSON
  -t, --team    Team name

Examples:
  node cli.js teams
  node cli.js events "Männer"
  node cli.js finances --json
  node cli.js event 0 "Herren"
`);
  process.exit(0);
}

// Check for JSON output flag
const outputJson = options.includes('--json') || options.includes('-j');

// Load config from environment
const config = {
  email: process.env.SPIELERPLUS_EMAIL,
  [REDACTED_SECRET],
  defaultTeam: teamArg || process.env.DEFAULT_TEAM
};

if (!config.email || !config.password) {
  console.error('Error: SPIELERPLUS_EMAIL and SPIELERPLUS_PASSWORD environment variables required.');
  console.error('Create a .env file or export the variables.');
  console.error('');
  console.error('Example .env file:');
  console.error('  SPIELERPLUS_EMAIL=[REDACTED_SECRET]');
  console.error('  SPIELERPLUS_PASSWORD=yourpassword');
  process.exit(1);
}

async function main() {
  let scraper;
  
  try {
    scraper = new SpielerPlusScraper(config);
    await scraper.init();
    
    // Handle teams command specially (no team selection needed)
    if (command === 'teams') {
      const teams = await scraper.getTeams();
      if (outputJson) {
        console.log(JSON.stringify(teams, null, 2));
      } else {
        console.log('\n📋 Available Teams:');
        teams.forEach((t, i) => console.log(`  ${i+1}. ${t.name}`));
      }
      return;
    }
    
    // Select team if specified
    if (teamArg) {
      await scraper.selectTeam(teamArg);
    }
    
    let result;
    
    switch (command) {
      case 'events':
        result = await scraper.getEvents();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n📅 Upcoming Events:');
          if (result.length === 0) {
            console.log('  No events found');
          } else {
            result.forEach(e => {
              console.log(`  ${e.date} | ${e.time} | ${e.type}`);
              if (e.meeting) console.log(`    Treffen: ${e.meeting}`);
            });
          }
        }
        break;
        
      case 'event':
        const idx = parseInt(options[0]) || 0;
        result = await scraper.getEventDetails(idx);
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log(`\n🎯 Event Details (Index: ${idx}):`);
          console.log(`  Date: ${result.date}`);
          console.log(`  Type: ${result.type}`);
          console.log(`  Time: ${result.time.meeting} - ${result.time.start} to ${result.time.end}`);
          console.log(`  Status: ${result.status || 'Active'}`);
        }
        break;
        
      case 'team':
        result = await scraper.getTeamMembers();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n👥 Team Members:');
          result.forEach(m => console.log(`  - ${m}`));
          console.log(`\n  Total: ${result.length} members`);
        }
        break;
        
      case 'absences':
        result = await scraper.getAbsences();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n🏥 Absences:');
          if (result.length === 0) {
            console.log('  No absences');
          } else {
            result.forEach(a => {
              console.log(`  - ${a.name}`);
              if (a.reason) console.log(`    ${a.reason}`);
              if (a.period) console.log(`    ${a.period}`);
            });
          }
        }
        break;
        
      case 'finances':
        result = await scraper.getFinances();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n💰 Team Finances:');
          console.log(`  Balance: ${result.balance || 'N/A'}`);
          if (result.transactions && result.transactions.length > 0) {
            console.log('  Recent Transactions:');
            result.transactions.slice(0, 10).forEach(t => {
              console.log(`    ${t.name} | ${t.type} | ${t.amount}`);
            });
          }
        }
        break;
        
      case 'participation':
        result = await scraper.getParticipationStats();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n📊 Participation Statistics:');
          if (result.summary) {
            console.log(`  Total: ${result.summary.total} events (${result.summary.trainings} trainings, ${result.summary.games} games)`);
          }
          if (result.players && result.players.length > 0) {
            console.log('  Top Attendees:');
            result.players.slice(0, 10).forEach(p => {
              console.log(`    ${p.name}: ${p.count} (${p.percentage}%)`);
            });
          }
        }
        break;
        
      case 'profile':
        result = await scraper.getTeamProfile();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n🏠 Team Profile:');
          console.log(`  Name: ${result.name}`);
          console.log(`  Sport: ${result.sport || 'N/A'}`);
          console.log(`  Type: ${result.teamType || 'N/A'}`);
          console.log(`  Address: ${result.address || 'N/A'}`);
          console.log(`  Website: ${result.website || 'N/A'}`);
        }
        break;
        
      case 'roles':
        result = await scraper.getRoles();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n👔 Roles & Permissions:');
          if (result.length === 0) {
            console.log('  No roles found');
          } else {
            result.forEach(r => {
              console.log(`  - ${r.name}: ${r.permissions} permissions`);
            });
          }
        }
        break;
        
      case 'benefits':
        result = await scraper.getBenefits();
        if (outputJson) {
          console.log(JSON.stringify(result, null, 2));
        } else {
          console.log('\n🎁 Benefits:');
          console.log(`  Active Deals: ${result.activeDeals}`);
        }
        break;
        
      case 'all':
        const teams = await scraper.getTeams();
        for (const team of teams) {
          console.log(`\n${'='.repeat(50)}`);
          console.log(`📋 ${team.name}`);
          console.log('='.repeat(50));
          
          await scraper.selectTeam(team.name);
          
          const profile = await scraper.getTeamProfile();
          console.log(`Sport: ${profile.sport || 'N/A'} | Type: ${profile.teamType || 'N/A'}`);
          
          const events = await scraper.getEvents();
          console.log(`Events: ${events.length} upcoming`);
          
          const members = await scraper.getTeamMembers();
          console.log(`Members: ${members.length}`);
          
          const finances = await scraper.getFinances();
          console.log(`Balance: ${finances.balance || 'N/A'}`);
          
          const participation = await scraper.getParticipationStats();
          if (participation.summary) {
            console.log(`Participation: ${participation.summary.total} events`);
          }
        }
        break;
        
      default:
        console.log(`Unknown command: ${command}`);
        console.log('Run "node cli.js help" for usage information');
    }
    
  } catch (error) {
    console.error('Error:', error.message);
    process.exit(1);
  } finally {
    if (scraper) {
      await scraper.close();
    }
  }
}

main();


path: src/index.js
content: /**
 * SpielerPlus Scraper - Generic team management data extractor
 * 
 * @example
 * const scraper = new SpielerPlusScraper({
 *   email: '[REDACTED_SECRET]',
 *   [REDACTED_SECRET]'
 * });
 * await scraper.init();
 * const events = await scraper.getEvents();
 */

require('dotenv').config();

class SpielerPlusScraper {
  /**
   * @param {Object} config - Configuration
   * @param {string} config.email - SpielerPlus email
   * @param {string} config.password - SpielerPlus password
   * @param {string} [config.defaultTeam] - Default team name to select
   */
  constructor(config = {}) {
    this.email = config.email || process.env.SPIELERPLUS_EMAIL;
    this.[REDACTED_SECRET] || process.env.SPIELERPLUS_PASSWORD;
    this.defaultTeam = config.defaultTeam || process.env.DEFAULT_TEAM;
    
    if (!this.email || !this.password) {
      throw new Error('Email and password required. Set SPIELERPLUS_EMAIL and SPIELERPLUS_PASSWORD environment variables or pass in config.');
    }
    
    this.browser = null;
    this.context = null;
    this.page = null;
    this.currentTeam = null;
  }

  /**
   * Initialize browser and login
   */
  async init() {
    const { chromium } = require('playwright');
    
    this.browser = await chromium.launch({ headless: true });
    this.context = await this.browser.newContext();
    this.page = await this.context.newPage();
    
    await this._login();
    
    // Auto-select default team if configured
    if (this.defaultTeam) {
      await this.selectTeam(this.defaultTeam);
    }
    
    return this;
  }

  /**
   * Login to SpielerPlus
   * @private
   */
  async _login() {
    await this.page.goto('https://www.spielerplus.de/', { waitUntil: 'networkidle' });
    await this._wait(1000);
    
    await this.page.click('a[href*="login"]');
    await this._wait(1500);
    
    await this.page.fill('input[type="email"]', this.email);
    await this.page.fill('input[type="password"]', this.password);
    await this.page.click('button[type="submit"]');
    await this._wait(5000);
    
    // Handle cookie consent
    await this.acceptCookies();
  }

  /**
   * Wait helper
   * @private
   */
  _wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Accept cookies if present
   */
  async acceptCookies() {
    const btn = await this.page.$('text=Okay - einverstanden');
    if (btn) {
      await btn.click();
      await this._wait(500);
    }
  }

  /**
   * Get all available teams
   * @returns {Promise<Array<{name: string, url: string}>>}
   */
  async getTeams() {
    await this.page.goto('https://www.spielerplus.de/de-li/site/select-team', { waitUntil: 'networkidle' });
    await this._wait(1500);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const teams = [];
      const links = document.querySelectorAll('a[href*="team"], a[href*="select-team"]');
      
      links.forEach(link => {
        const text = link.textContent.trim();
        const href = link.getAttribute('href');
        
        // Filter for actual team links (not navigation items)
        if (text.length > 2 && text.length < 100 && 
            (text.includes('Humboldt') || text.includes('Männer') || 
             text.includes('Frauen') || text.includes('Herren') ||
             text.includes('Damen') || text.includes('Team ') || text.includes('Jugend'))) {
          teams.push({ name: text, url: href });
        }
      });
      
      // Deduplicate
      return [...new Map(teams.map(t => [t.name, t])).values()];
    });
  }

  /**
   * Select a team by name
   * @param {string} teamName - Team name (partial match works)
   */
  async selectTeam(teamName) {
    await this.page.goto('https://www.spielerplus.de/de-li/site/select-team', { waitUntil: 'networkidle' });
    await this._wait(1500);
    await this.acceptCookies();
    
    const teams = await this.getTeams();
    const targetTeam = teams.find(t => 
      t.name.toLowerCase().includes(teamName.toLowerCase()) ||
      teamName.toLowerCase().includes(t.name.toLowerCase().split(' ')[0])
    );
    
    if (!targetTeam) {
      throw new Error(`Team "${teamName}" not found. Available: ${teams.map(t => t.name).join(', ')}`);
    }
    
    await this.page.goto(`https://www.spielerplus.de${targetTeam.url}`, { waitUntil: 'networkidle' });
    await this._wait(2000);
    this.currentTeam = targetTeam.name;
    
    return targetTeam.name;
  }

  /**
   * Get upcoming events
   * @returns {Promise<Array>}
   */
  async getEvents() {
    await this.page.goto('https://www.spielerplus.de/de-li/site/events', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const events = [];
      const lines = document.body.innerText.split('\n');
      
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        if (/^(Mo|Di|Mi|Do|Fr|Sa|So)\.?/.test(line)) {
          events.push({
            date: line,
            time: lines[i+1]?.trim() || '',
            type: lines[i+2]?.trim() || '',
            meeting: lines[i+3]?.trim() || ''
          });
        }
      }
      return events;
    });
  }

  /**
   * Get event details including participants and carpool
   * @param {number} eventIndex - Index of event (0 = first)
   * @returns {Promise<Object>}
   */
  async getEventDetails(eventIndex = 0) {
    await this.page.goto('https://www.spielerplus.de/de-li/site/events', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    const eventCards = await this.page.$$('[class*="termin"], [class*="event"], [class*="termin-item"]');
    if (eventCards.length > eventIndex) {
      await eventCards[eventIndex].click();
      await this._wait(3000);
    }
    
    const participantBtn = await this.page.$('text=Teilnehmer');
    if (participantBtn) {
      await participantBtn.click();
      await this._wait(2000);
    }
    
    return await this.page.evaluate(() => {
      const text = document.body.innerText;
      const eventData = {
        date: '', type: '',
        time: { meeting: '', start: '', end: '' },
        status: '',
        participants: { yes: [], no: [], maybe: [] },
        carpool: []
      };
      
      const lines = text.split('\n');
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        
        if (/^(Mo|Di|Mi|Do|Fr|Sa|So)\.?/.test(line)) {
          eventData.date = line;
          eventData.type = lines[i+2]?.trim() || '';
        }
        if (line === 'Treffen') eventData.time.meeting = lines[i+1]?.trim() || '';
        if (line === 'Beginn') eventData.time.start = lines[i+1]?.trim() || '';
        if (line === 'Ende') eventData.time.end = lines[i+1]?.trim() || '';
        if (line.includes('abgelaufen')) eventData.status = line;
      }
      
      return eventData;
    });
  }

  /**
   * Get team members
   * @returns {Promise<Array<string>>}
   */
  async getTeamMembers() {
    await this.page.goto('https://www.spielerplus.de/de-li/site/team', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const members = [];
      const els = document.querySelectorAll('h1, h2, h3');
      
      els.forEach(el => {
        const text = el.textContent.trim();
        if (text.length > 2 && text.length < 100 && !text.includes('Team')) {
          members.push(text);
        }
      });
      
      return [...new Set(members)];
    });
  }

  /**
   * Get absences (vacation, sick leave, inactive)
   * @returns {Promise<Array>}
   */
  async getAbsences() {
    await this.page.goto('https://www.spielerplus.de/de-li/absence', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const absences = [];
      const lines = document.body.innerText.split('\n');
      let current = null;
      
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed) continue;
        
        if (/^[A-Z][a-zäöüß].*?[A-Z]/.test(trimmed) && trimmed.length < 100) {
          if (current) absences.push(current);
          current = { name: trimmed, reason: '', period: '' };
        } else if (current && !current.reason && /Urlaub|Krank|Verletzt|Inaktiv|Sabatical/.test(trimmed)) {
          current.reason = trimmed;
        } else if (current && current.reason && !current.period && /\d{2}\.\d{2}/.test(trimmed)) {
          current.period = trimmed;
        }
      }
      if (current) absences.push(current);
      return absences;
    });
  }

  /**
   * Get team finances/cashbox
   * @returns {Promise<Object>}
   */
  async getFinances() {
    await this.page.goto('https://www.spielerplus.de/de-li/cashbox', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const text = document.body.innerText;
      const balanceMatch = text.match(/EUR\s*([\d.,]+)/);
      const transactions = [];
      
      const lines = text.split('\n');
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        if (line.includes('EUR') && line.includes('.') && line.match(/\d/)) {
          const amountMatch = line.match(/(-?EUR\s*[\d.,]+)/);
          const name = lines[i-1]?.trim() || '';
          const type = lines[i-2]?.trim() || '';
          if (amountMatch && name.length > 0 && name.length < 100) {
            transactions.push({ type, name, amount: amountMatch[1] });
          }
        }
      }
      
      return {
        balance: balanceMatch ? `EUR ${balanceMatch[1]}` : null,
        transactions: transactions.slice(0, 30)
      };
    });
  }

  /**
   * Get participation statistics
   * @returns {Promise<Object>}
   */
  async getParticipationStats() {
    await this.page.goto('https://www.spielerplus.de/de-li/participation', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const text = document.body.innerText;
      const stats = [];
      
      const lines = text.split('\n');
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        if (/^[A-Z][a-zäöüß].*?[A-Z]/.test(line) && line.length < 60) {
          const nextLine = lines[i+1]?.trim() || '';
          const match = nextLine.match(/(\d+)\s*\((\d+)%\)/);
          if (match) {
            stats.push({ name: line, count: parseInt(match[1]), percentage: parseInt(match[2]) });
          }
        }
      }
      
      const summaryMatch = text.match(/(\d+)\s*Termine.*?(\d+)\s*Trainings.*?(\d+)\s*Spiele/);
      
      return {
        summary: summaryMatch ? {
          total: parseInt(summaryMatch[1]),
          trainings: parseInt(summaryMatch[2]),
          games: parseInt(summaryMatch[3])
        } : null,
        players: stats.sort((a, b) => b.percentage - a.percentage)
      };
    });
  }

  /**
   * Get team profile info
   * @returns {Promise<Object>}
   */
  async getTeamProfile() {
    await this.page.goto('https://www.spielerplus.de/de-li/site/team', { waitUntil: 'networkidle' });
    await this._wait(1500);
    await this.acceptCookies();
    
    const teamLink = await this.page.$('a[href*="team/view"]');
    const href = await teamLink?.getAttribute('href');
    const teamId = href ? new URL(href, 'https://example.com').searchParams.get('id') : null;
    
    if (teamId) {
      await this.page.goto(`https://www.spielerplus.de/de-li/team/view?id=${teamId}`, { waitUntil: 'networkidle' });
      await this._wait(2000);
    }
    
    return await this.page.evaluate(() => {
      const text = document.body.innerText;
      const data = { 
        name: '', 
        sport: '', 
        address: '', 
        teamType: '', 
        website: '', 
        gameReports: [] 
      };
      
      const h1 = document.querySelector('h1');
      if (h1) data.name = h1.textContent.trim();
      
      const lines = text.split('\n');
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        
        if (line.includes('Handball')) data.sport = 'Handball';
        if (line.includes('Herren') || line.includes('Männer')) data.teamType = 'Herren';
        if (line.includes('Frauen')) data.teamType = 'Frauen';
        if (line.match(/\d{5}.*Berlin/)) data.address = line;
        if (line.includes('http')) data.website = line;
        
        if (line === 'Spielberichte') {
          const games = [];
          for (let j = i+1; j < i+10 && j < lines.length; j++) {
            const gameLine = lines[j].trim();
            if (gameLine.match(/\d{2}\.\d{2}\.\d{2}/)) {
              games.push(gameLine);
            }
          }
          if (games.length > 0) data.gameReports = games;
        }
      }
      return data;
    });
  }

  /**
   * Get team roles
   * @returns {Promise<Array>}
   */
  async getRoles() {
    await this.page.goto('https://www.spielerplus.de/de-li/role', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const roles = [];
      const lines = document.body.innerText.split('\n');
      
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i].trim();
        if (line.match(/^\d+\s*Berechtigung/)) {
          const roleName = lines[i-2]?.trim() || '';
          const permMatch = line.match(/(\d+)\s*Berechtigung/);
          if (roleName && permMatch && roleName.length < 50) {
            roles.push({ name: roleName, permissions: parseInt(permMatch[1]) });
          }
        }
      }
      return roles;
    });
  }

  /**
   * Get benefits/deals
   * @returns {Promise<Object>}
   */
  async getBenefits() {
    await this.page.goto('https://www.spielerplus.de/de-li/a/appstauber', { waitUntil: 'networkidle' });
    await this._wait(2000);
    await this.acceptCookies();
    
    return await this.page.evaluate(() => {
      const text = document.body.innerText;
      const match = text.match(/(\d+)\s*aktive Deals/);
      return { activeDeals: match ? parseInt(match[1]) : 0 };
    });
  }

  /**
   * Close browser
   */
  async close() {
    if (this.browser) {
      await this.browser.close();
    }
  }
}

module.exports = SpielerPlusScraper;


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)