Claude
Skills
Sign in
Back

hexagone-web-feature-extractor

Included with Lifetime
$97 forever

Explore any Hexagone Web space via Playwright headless browser, capture screenshots, and produce a PO-oriented Markdown document.

Writing & Docsscripts

What this skill does


# Hexagone Web Feature Extractor

Explore a Hexagone Web functional space, capture screenshots of every page/tab, and produce a Markdown document (.md) oriented for Product Owners with functional descriptions and embedded screenshots.

## Prerequisites

- **Node.js** installed
- **Playwright** npm package (`npm install playwright`) — installs headless Chromium automatically
- Network access to the Hexagone Web server (default: `https://ws004202.dedalus.lan:8065/hexagone-01/vue/login`)

## Configuration

Default values calibrated for the standard Hexagone Web layout at 1920x1080. Adjust if the layout differs.

| Parameter | Default | Description |
|-----------|---------|-------------|
| Viewport | `1920x1080` | Browser viewport size |
| Sidebar click X coordinate | `38` | Horizontal pixel position for sidebar icon clicks (collapsed mode) |
| Sidebar max left boundary | `280` | Max `rect.left` value to identify sidebar links (expanded mode) |
| Header height offset | `55` | Min `rect.top` value to exclude header elements |
| Login wait timeout | `30s` | Max time to poll for successful login |
| Page load wait timeout | `10s` | Max time to poll for page load after navigation |
| Screenshots directory | `./screenshots` | Where screenshots are saved (relative to working directory) |

## Workflow Overview

```
1. SETUP        → Install Playwright, launch headless Chromium
2. CONNECTION   → Log in to Hexagone Web
3. NAVIGATION   → Navigate to the target space
4. DISCOVERY    → Expand sidebar, list all menu pages
5. EXPLORATION  → Visit each page, capture screenshots + metadata
6. GENERATION   → Produce the Markdown document with embedded screenshots
```

**Key advantage over Chrome extension approach**: Screenshots save directly to disk via `page.screenshot()` — no bridge server or transfer step needed.

---

## Step 1: Setup

### 1.1 Install Playwright

```bash
npm install playwright
npx playwright install chromium
```

### 1.2 Launch Browser

```javascript
const { chromium } = require('playwright');

const browser = await chromium.launch({
  headless: true,
  args: ['--ignore-certificate-errors', '--no-sandbox']
});
const context = await browser.newContext({
  viewport: { width: 1920, height: 1080 },
  ignoreHTTPSErrors: true  // Handles self-signed certs automatically
});
const page = await context.newPage();
```

**Why headless Chromium?** Eliminates the need for manual SSL certificate acceptance, Chrome extension setup, and screenshot bridge transfers. The `ignoreHTTPSErrors: true` option handles self-signed certificates programmatically.

---

## Step 2: Connection to Hexagone Web

### 2.1 Navigate to Login Page

```javascript
await page.goto(LOGIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
await sleep(3000); // Wait for Vue.js to mount
```

### 2.2 Fill the Login Form

The Hexagone Web login form has 3 fields: Username, Password, Manager code. Default credentials: username `apvhn` with a random password, unless the user provides others.

Use `page.evaluate()` with the native setter pattern — **required for Vue.js** which does not detect value changes injected directly:

```javascript
await page.evaluate(({ username, password }) => {
  const nativeSetter = Object.getOwnPropertyDescriptor(
    window.HTMLInputElement.prototype, 'value'
  ).set;

  const userInput = document.querySelector('input[type="text"]');
  if (userInput) {
    nativeSetter.call(userInput, username);
    userInput.dispatchEvent(new Event('input', { bubbles: true }));
  }

  const pwdInput = document.querySelector('input[type="password"]');
  if (pwdInput) {
    nativeSetter.call(pwdInput, password);
    pwdInput.dispatchEvent(new Event('input', { bubbles: true }));
  }

  const loginBtn = Array.from(document.querySelectorAll('button'))
    .find(b => /connect/i.test(b.textContent));
  if (loginBtn) loginBtn.click();
}, { username: USERNAME, password: PASSWORD });
```

### 2.3 Verify Connection

**Poll every 2s for up to 30s** until the URL no longer contains `/login`:

```javascript
for (let i = 0; i < 15; i++) {
  await sleep(2000);
  if (!page.url().includes('/login')) break;
}
```

**If login fails**: Take a debug screenshot with `page.screenshot()` and report the failure.

---

## Step 3: Navigation to the Target Space

### 3.1 Open the Space Selector

**CRITICAL**: Use `page.mouse.click()` — NOT `el.click()` via `page.evaluate()`.

Vue.js event handlers require native mouse events (mousedown + mouseup + click). JavaScript's `el.click()` only dispatches the `click` event and **will not trigger the space dropdown**. This was the #1 bug found during development.

The space selector is the `div` with class `bg:orange-dark` in the orange breadcrumb bar. It contains an icon `<i class="hexa-icons">changer_espaces</i>` followed by a `<span>` with the current space name.

```javascript
// Find the space selector coordinates
const selectorRect = await page.evaluate(() => {
  for (const el of document.querySelectorAll('div, span')) {
    const cls = typeof el.className === 'string' ? el.className : '';
    if (cls.includes('bg:orange-dark') && !cls.includes('uppercase') && !cls.includes('hover:')) {
      const rect = el.getBoundingClientRect();
      if (rect.top > 30 && rect.top < 70 && rect.height > 15) {
        return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
      }
    }
  }
  return null;
});

// Click with REAL mouse events (mandatory for Vue.js)
await page.mouse.click(selectorRect.x, selectorRect.y);
await sleep(3000);
```

### 3.2 Select the Space

The dropdown renders inside the sidebar area as a list of `<div>` elements with class `px:1 py:3/4 hover:bg:orange-dark cursor:pointer`. Spaces are listed alphabetically.

```javascript
// Find the target space element
const target = await page.evaluate((spaceName) => {
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT);
  while (walker.nextNode()) {
    const el = walker.currentNode;
    if (el.textContent.trim() === spaceName) {
      const rect = el.getBoundingClientRect();
      if (rect.width > 0 && rect.height > 0 && rect.top > 30) {
        return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
      }
    }
  }
  return null;
}, TARGET_SPACE);

// Click with mouse (not el.click())
await page.mouse.click(target.x, target.y);
```

### 3.3 Wait for Loading

Hexagone Web redirects via an intermediate "Connexion... Redirection..." page. **Poll every 2s for up to 24s** until the URL no longer contains `patient-portal` (the default landing space):

```javascript
for (let i = 0; i < 12; i++) {
  await sleep(2000);
  if (!page.url().includes('patient-portal')) break;
}
await sleep(3000); // Extra wait for Vue.js rendering
```

---

## Step 4: Page Discovery

### 4.1 Expand the Sidebar

The sidebar is collapsed by default (icons only, width ~65px). Click the hamburger menu to expand it and reveal text labels:

```javascript
await page.mouse.click(34, 50); // Hamburger icon position
await sleep(2000);
```

### 4.2 Identify Sidebar Menu Entries

**Primary method**: Look for elements with `cursor:pointer` class in the left 280px. Strip icon text from `<i class="hexa-icons">` children:

```javascript
const menuItems = await page.evaluate((excludeLabels) => {
  const items = [];
  const seen = new Set();
  const allEls = document.querySelectorAll('[class*="cursor:pointer"], a');
  for (const el of allEls) {
    const rect = el.getBoundingClientRect();
    if (rect.left < 280 && rect.top > 55 && rect.height > 15 && rect.height < 60) {
      let text = el.textContent.trim();
      // Strip icon prefix text
      const icon = el.querySelector('i');
      if (icon) text = text.replace(icon.textContent.trim(), '').trim();
      if (!text || text.length <= 1 || text.length >= 60 || seen.has(text)) continue;
      if (excludeLabels.includes(text)) continue;
      // Skip section headers (all-caps short text like "ACHATS")
      if (/^[A-Z ]+$/.test(text) &

Related in Writing & Docs