From 65cadfee69ac3418c13f39491ee93bc55908bfd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 09:02:12 +0000 Subject: [PATCH 1/6] feat: complete x0 github2stackfield application MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - static/menu.json: 3 navigation links (User Credentials, Issue/Task Mapping, Connect Stackfield Task) - static/object.json: all x0 UI objects for 3 screens (formfields, lists, buttons, service connectors, context menu) - static/skeleton.json: screen layout wiring objects to screens - python/service_implementation.py: GitHubService + StackfieldService ClassHandlers (python-micro-esb) - python/user_routing.py: ServiceRouter routing functions for all 5 backend operations - python/VerifyGitHubCredentials.py: WSGI endpoint - python/VerifyStackfieldCredentials.py: WSGI endpoint - python/SearchGitHubIssues.py: WSGI endpoint – populates issue list - python/GetGitHubIssueDetails.py: WSGI endpoint – pre-populates Screen 3 - python/CreateStackfieldTask.py: WSGI endpoint – creates Stackfield task - python/POSTData.py + StdoutLogger.py: x0 helper modules - database/01-create-schema.sql: github2sf schema, credentials + mapping tables - database/02-insert-config.sql: x0 app configuration rows - database/03-insert-text.sql: EN + DE UI text entries - docker/Dockerfile + docker-compose.yml + apache2.conf: containerised deployment - README.md: full setup and architecture documentation Agent-Logs-Url: https://github.com/WEBcodeX1/github2stackfield/sessions/9b620d8f-bd1b-45df-84a8-b2f02204213e Co-authored-by: clauspruefer <17313789+clauspruefer@users.noreply.github.com> --- README.md | 191 +++++++ database/01-create-schema.sql | 59 ++ database/02-insert-config.sql | 22 + database/03-insert-text.sql | 116 ++++ docker/Dockerfile | 31 ++ docker/apache2.conf | 35 ++ docker/docker-compose.yml | 50 ++ python/CreateStackfieldTask.py | 50 ++ python/GetGitHubIssueDetails.py | 54 ++ python/POSTData.py | 9 + python/SearchGitHubIssues.py | 54 ++ python/StdoutLogger.py | 5 + python/VerifyGitHubCredentials.py | 49 ++ python/VerifyStackfieldCredentials.py | 49 ++ python/service_implementation.py | 249 +++++++++ python/user_routing.py | 215 +++++++ static/menu.json | 20 + static/object.json | 771 ++++++++++++++++++++++++++ static/skeleton.json | 85 +++ 19 files changed, 2114 insertions(+) create mode 100644 README.md create mode 100644 database/01-create-schema.sql create mode 100644 database/02-insert-config.sql create mode 100644 database/03-insert-text.sql create mode 100644 docker/Dockerfile create mode 100644 docker/apache2.conf create mode 100644 docker/docker-compose.yml create mode 100644 python/CreateStackfieldTask.py create mode 100644 python/GetGitHubIssueDetails.py create mode 100644 python/POSTData.py create mode 100644 python/SearchGitHubIssues.py create mode 100644 python/StdoutLogger.py create mode 100644 python/VerifyGitHubCredentials.py create mode 100644 python/VerifyStackfieldCredentials.py create mode 100644 python/service_implementation.py create mode 100644 python/user_routing.py create mode 100644 static/menu.json create mode 100644 static/object.json create mode 100644 static/skeleton.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..6698eb5 --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# github2stackfield + +**github2stackfield** is an x0 web application that bridges **GitHub Issues** with **Stackfield Tasks**. +It lets you search GitHub issues, inspect their details, and create corresponding Stackfield tasks — all from a clean, Bootstrap-styled browser frontend. + +--- + +## Architecture + +| Layer | Technology | +|---|---| +| Browser frontend | [x0 JavaScript framework](https://github.com/WEBcodeX1/x0) with Bootstrap default theme | +| Backend services | Python WSGI scripts via [python-micro-esb](https://github.com/clauspruefer/python-micro-esb) | +| Database | PostgreSQL (shared x0 instance) | +| Deployment | Apache2 + mod_wsgi (Docker or bare-metal) | + +--- + +## Screens + +### Screen 1 — User Credentials +Configure and verify API access for both platforms. + +- **GitHub API Credentials** — enter your GitHub username and Personal Access Token. + Click *Verify GitHub Credentials* to validate and store them. +- **Stackfield API Credentials** — enter your Stackfield e-mail and API token. + Click *Verify Stackfield Credentials* to validate and store them. + +### Screen 2 — Issue / Task Mapping +Search GitHub issues and select one to map to Stackfield. + +1. Enter the target repository (`owner/repository`) and an optional search term. +2. Click **Search Issues** — results populate the issue list below. +3. Right-click any row and select **Connect Stackfield Task** to navigate to Screen 3. + +### Screen 3 — Connect Stackfield Task +Review the selected GitHub issue and create a Stackfield task. + +- **GitHub Issue Properties** — read-only fields: issue number, state, title, URL. +- **Stackfield Task Mapping** — editable fields pre-populated from the issue: + - *Stackfield Room ID* — the target Stackfield room / channel identifier. + - *Task Title* — editable, defaults to the GitHub issue title. + - *Description* — editable, defaults to the GitHub issue body. + - *Priority* — Low / Medium / High / Urgent. +- Click **Create New Stackfield Task** — a new task is created in Stackfield via the REST API. + +--- + +## Prerequisites + +| Requirement | Notes | +|---|---| +| x0 framework | Follow [x0 INSTALL.md](https://github.com/WEBcodeX1/x0/blob/main/INSTALL.md) | +| PostgreSQL ≥ 14 | Shared with x0 | +| Python ≥ 3.10 | `requests`, `psycopg2`, `pgdbpool`, `python-micro-esb` | +| GitHub Personal Access Token | Needs `repo` scope for private repos, `public_repo` for public | +| Stackfield API token | See Stackfield workspace settings → Integrations → API | + +--- + +## Installation + +### 1. Set up x0 + +Follow the official x0 installation guide to get the base framework running with PostgreSQL. + +### 2. Install Python dependencies + +```bash +pip install requests psycopg2-binary pgdbpool +pip install git+https://github.com/clauspruefer/python-micro-esb.git +``` + +### 3. Run database setup scripts + +Connect to your x0 PostgreSQL database and execute the scripts in order: + +```bash +psql -U postgres -d x0 -f database/01-create-schema.sql +psql -U postgres -d x0 -f database/02-insert-config.sql +psql -U postgres -d x0 -f database/03-insert-text.sql +``` + +### 4. Deploy static files + +Copy the `static/` directory so it is served at `/static/github2sf/`: + +```bash +cp -r static/ /var/www/x0/static/github2sf/ +``` + +### 5. Deploy Python backend + +Copy the `python/` directory into the x0 Python directory: + +```bash +cp python/*.py /var/www/x0/python/github2sf/ +``` + +### 6. Configure Apache2 + +Add the WSGI aliases from `docker/apache2.conf` to your Apache virtual host, then reload: + +```bash +apache2ctl graceful +``` + +### 7. Open the application + +Navigate to `http://your-server/?appid=github2sf` in your browser. + +--- + +## Docker (quick start) + +```bash +cd docker +docker compose up --build +``` + +Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf). + +> **Note:** The Docker image fetches x0 and python-micro-esb from GitHub at build time. +> You still need to run the database SQL scripts against the PostgreSQL container: +> +> ```bash +> docker exec -i github2sf-db psql -U postgres -d x0 < database/01-create-schema.sql +> docker exec -i github2sf-db psql -U postgres -d x0 < database/02-insert-config.sql +> docker exec -i github2sf-db psql -U postgres -d x0 < database/03-insert-text.sql +> ``` + +--- + +## Project structure + +``` +github2stackfield/ +├── static/ +│ ├── menu.json # x0 navigation menu definition +│ ├── object.json # x0 UI objects (formfields, lists, buttons …) +│ └── skeleton.json # x0 screen layout +├── python/ +│ ├── service_implementation.py # GitHubService + StackfieldService ClassHandlers +│ ├── user_routing.py # python-micro-esb ServiceRouter routing functions +│ ├── VerifyGitHubCredentials.py # WSGI – verify GitHub credentials +│ ├── VerifyStackfieldCredentials.py # WSGI – verify Stackfield credentials +│ ├── SearchGitHubIssues.py # WSGI – search issues, populate list +│ ├── GetGitHubIssueDetails.py # WSGI – fetch issue details for Screen 3 +│ ├── CreateStackfieldTask.py # WSGI – create Stackfield task +│ ├── POSTData.py # x0 POST body reader helper +│ └── StdoutLogger.py # logging helper +├── database/ +│ ├── 01-create-schema.sql # github2sf schema + tables +│ ├── 02-insert-config.sql # x0 app configuration rows +│ └── 03-insert-text.sql # UI text / i18n entries +├── docker/ +│ ├── Dockerfile +│ ├── docker-compose.yml +│ └── apache2.conf +└── README.md +``` + +--- + +## python-micro-esb integration + +The backend services are built on the [python-micro-esb](https://github.com/clauspruefer/python-micro-esb) framework: + +- **`service_implementation.py`** — contains `GitHubService` and `StackfieldService`, both subclassing `microesb.ClassHandler`. Each class exposes service methods (`verify`, `search_issues`, `get_issue_details`, `create_task`). +- **`user_routing.py`** — routing functions consumed by `ServiceRouter.send()`. Each function instantiates the appropriate service class, calls the relevant method, and returns the result. +- **WSGI scripts** — thin wrappers that read the x0 POST payload, call `ServiceRouter.send()`, and return JSON to the x0 frontend. + +--- + +## Stackfield API notes + +Stackfield's REST API is available at `https://www.stackfield.com/api/v1/`. +Key endpoints used: + +| Endpoint | Purpose | +|---|---| +| `GET /v1/user` | Verify credentials | +| `POST /v1/rooms/{room_id}/tasks` | Create a new task | + +The **Room ID** can be found in Stackfield under *Room settings → General → Room ID* or via the URL slug. + +--- + +## License + +See [LICENSE](LICENSE). diff --git a/database/01-create-schema.sql b/database/01-create-schema.sql new file mode 100644 index 0000000..7276a4b --- /dev/null +++ b/database/01-create-schema.sql @@ -0,0 +1,59 @@ +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . github2stackfield - Database Schema . +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . . +-- . Run against an existing x0 PostgreSQL database. . +-- . The x0 framework must be set up first (see x0 repository). . +-- . . +-- ]*[ ------------------------------------------------------------------ ]*[ + +-- Application schema +CREATE SCHEMA IF NOT EXISTS github2sf; + +-- --------------------------------------------------------------------------- +-- API Credentials storage +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS github2sf.credentials ( + credential_type VARCHAR(20) NOT NULL, + username_or_email TEXT NOT NULL, + api_token TEXT NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT pk_credentials PRIMARY KEY (credential_type) +); + +COMMENT ON TABLE github2sf.credentials IS + 'Stores GitHub and Stackfield API credentials (one row per credential_type).'; + +-- --------------------------------------------------------------------------- +-- Application configuration key-value store +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS github2sf.app_config ( + config_key VARCHAR(100) NOT NULL, + value TEXT, + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT pk_app_config PRIMARY KEY (config_key) +); + +COMMENT ON TABLE github2sf.app_config IS + 'Key-value store for github2stackfield application runtime configuration.'; + +-- --------------------------------------------------------------------------- +-- Issue / Task mapping log +-- --------------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS github2sf.issue_task_mapping ( + id SERIAL NOT NULL, + github_repo TEXT NOT NULL, + github_issue_number INTEGER NOT NULL, + github_issue_title TEXT, + stackfield_room_id TEXT NOT NULL, + stackfield_task_id TEXT, + stackfield_task_url TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT pk_issue_task_mapping PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_issue_task_mapping_repo_issue + ON github2sf.issue_task_mapping (github_repo, github_issue_number); + +COMMENT ON TABLE github2sf.issue_task_mapping IS + 'Audit log of all GitHub issue → Stackfield task mappings created by the app.'; diff --git a/database/02-insert-config.sql b/database/02-insert-config.sql new file mode 100644 index 0000000..054280f --- /dev/null +++ b/database/02-insert-config.sql @@ -0,0 +1,22 @@ +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . github2stackfield - x0 Application Configuration . +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . . +-- . Insert the x0 system.config rows for the github2stackfield app. . +-- . Adjust app_id value if your x0 setup uses a different identifier. . +-- . . +-- ]*[ ------------------------------------------------------------------ ]*[ + +-- Remove any previously inserted config for this app +DELETE FROM system.config WHERE app_id = 'github2sf'; + +INSERT INTO system.config (app_id, config_group, "value") VALUES + ('github2sf', 'index_title', 'GitHub ↔ Stackfield Connector'), + ('github2sf', 'debug_level', '0'), + ('github2sf', 'display_language', 'en'), + ('github2sf', 'default_screen', 'Screen1'), + ('github2sf', 'parent_window_url', 'null'), + ('github2sf', 'subdir', '/static/github2sf'), + ('github2sf', 'config_file_menu', 'menu.json'), + ('github2sf', 'config_file_object', 'object.json'), + ('github2sf', 'config_file_skeleton','skeleton.json'); diff --git a/database/03-insert-text.sql b/database/03-insert-text.sql new file mode 100644 index 0000000..0f4aaa8 --- /dev/null +++ b/database/03-insert-text.sql @@ -0,0 +1,116 @@ +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . github2stackfield - UI Text / Localisation . +-- ]*[ ------------------------------------------------------------------ ]*[ +-- . . +-- . Insert into the x0 webui.text table. . +-- . Both English and German translations are provided. . +-- . . +-- ]*[ ------------------------------------------------------------------ ]*[ + +-- Navigation / Menu +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task'); + +-- Screen 1 – GitHub Credentials +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication'); + +-- Screen 1 – Stackfield Credentials +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication'); + +-- Screen 2 – Search +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues'); + +-- Screen 2 – Issue List columns +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee'); + +-- Screen 2 – Context menu +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task'); + +-- Screen 3 – GitHub Issue Details +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL'); + +-- Screen 3 – Stackfield Mapping +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority'); + +-- Screen 3 – Priority options +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent'); + +-- Screen 3 – Create button +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task'); +INSERT INTO webui.text (id, "group", value_de, value_en) VALUES + ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task'); diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..353e760 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,31 @@ +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y \ + apache2 \ + libapache2-mod-wsgi-py3 \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +RUN pip install --no-cache-dir \ + requests \ + psycopg2-binary \ + pgdbpool + +# Install python-micro-esb from source +RUN pip install --no-cache-dir \ + git+https://github.com/clauspruefer/python-micro-esb.git + +# Install x0 framework Python modules +RUN pip install --no-cache-dir \ + git+https://github.com/WEBcodeX1/x0.git + +WORKDIR /var/www/x0 + +# Apache configuration +COPY docker/apache2.conf /etc/apache2/sites-available/github2sf.conf +RUN a2ensite github2sf && a2dissite 000-default && a2enmod wsgi + +EXPOSE 80 + +CMD ["apache2ctl", "-D", "FOREGROUND"] diff --git a/docker/apache2.conf b/docker/apache2.conf new file mode 100644 index 0000000..61ebf99 --- /dev/null +++ b/docker/apache2.conf @@ -0,0 +1,35 @@ + + ServerName github2sf.local + DocumentRoot /var/www/x0/www + + # Static assets: x0 framework JS/CSS + app-specific JSON config + Alias /static /var/www/x0/www/static + Alias /static/github2sf /var/www/x0/static/github2sf + + + Options -Indexes + Require all granted + + + + Options -Indexes + Require all granted + + + # x0 framework index / main entry-point + WSGIScriptAlias / /var/www/x0/python/Index.py + + # github2stackfield backend service endpoints + WSGIScriptAlias /python/VerifyGitHubCredentials.py /var/www/x0/python/github2sf/VerifyGitHubCredentials.py + WSGIScriptAlias /python/VerifyStackfieldCredentials.py /var/www/x0/python/github2sf/VerifyStackfieldCredentials.py + WSGIScriptAlias /python/SearchGitHubIssues.py /var/www/x0/python/github2sf/SearchGitHubIssues.py + WSGIScriptAlias /python/GetGitHubIssueDetails.py /var/www/x0/python/github2sf/GetGitHubIssueDetails.py + WSGIScriptAlias /python/CreateStackfieldTask.py /var/www/x0/python/github2sf/CreateStackfieldTask.py + + + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/github2sf-error.log + CustomLog ${APACHE_LOG_DIR}/github2sf-access.log combined + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..76f5829 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,50 @@ +services: + + # --------------------------------------------------------------------------- + # PostgreSQL database (shared with x0 framework) + # --------------------------------------------------------------------------- + postgres: + image: postgres:16-alpine + container_name: github2sf-db + restart: unless-stopped + environment: + POSTGRES_DB: x0 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: changeme + volumes: + - pg_data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - github2sf-net + + # --------------------------------------------------------------------------- + # x0 application server (Apache2 + mod_wsgi) + # --------------------------------------------------------------------------- + x0-app: + build: + context: .. + dockerfile: docker/Dockerfile + container_name: github2sf-app + restart: unless-stopped + depends_on: + - postgres + environment: + DB_HOST: postgres + DB_NAME: x0 + DB_USER: postgres + DB_PASSWORD: changeme + volumes: + - ../static:/var/www/x0/static/github2sf:ro + - ../python:/var/www/x0/python/github2sf:ro + ports: + - "8080:80" + networks: + - github2sf-net + +volumes: + pg_data: + +networks: + github2sf-net: + driver: bridge diff --git a/python/CreateStackfieldTask.py b/python/CreateStackfieldTask.py new file mode 100644 index 0000000..76513e8 --- /dev/null +++ b/python/CreateStackfieldTask.py @@ -0,0 +1,50 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Create Stackfield Task WSGI Endpoint . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Called by x0 CreateStackfieldTaskButton (OnClick). . +# . Creates a new Stackfield task from GitHub issue data collected on . +# . Screen 3. Stackfield credentials are loaded from the database. . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import sys +import json + +import POSTData +from StdoutLogger import logger + +from router import ServiceRouter + + +def application(environ, start_response): + + start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')]) + + if environ['REQUEST_METHOD'].upper() != 'POST': + yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8') + return + + try: + raw = POSTData.Environment.getPOSTData(environ) + service_json = json.loads(raw) + + request_data = service_json.get('RequestData', {}) + + logger.debug('CreateStackfieldTask RequestData:{}'.format(request_data)) + + router = ServiceRouter() + result = router.send('create_stackfield_task', request_data) + + logger.debug('CreateStackfieldTask result:{}'.format(result)) + + yield bytes(json.dumps(result), 'utf-8') + + except Exception as e: + logger.error('CreateStackfieldTask exception:{}'.format(e)) + error_result = { + 'error': True, + 'error_id': 500, + 'exception': str(e) + } + yield bytes(json.dumps(error_result), 'utf-8') diff --git a/python/GetGitHubIssueDetails.py b/python/GetGitHubIssueDetails.py new file mode 100644 index 0000000..d4d345f --- /dev/null +++ b/python/GetGitHubIssueDetails.py @@ -0,0 +1,54 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Get GitHub Issue Details WSGI Endpoint . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Called by x0 IssueDetailsConnector (ServiceConnector) on Screen 3. . +# . Fetches full GitHub issue details and pre-populates Screen 3 . +# . formfields (IssueDetailsForm + StackfieldMappingForm task fields). . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import sys +import json + +import POSTData +from StdoutLogger import logger + +from router import ServiceRouter + + +def application(environ, start_response): + + start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')]) + + if environ['REQUEST_METHOD'].upper() != 'POST': + yield bytes(json.dumps({}), 'utf-8') + return + + try: + raw = POSTData.Environment.getPOSTData(environ) + service_json = json.loads(raw) + + request_data = service_json.get('RequestData', {}) + + logger.debug('GetGitHubIssueDetails RequestData:{}'.format(request_data)) + + router = ServiceRouter() + result = router.send('get_github_issue_details', request_data) + + logger.debug('GetGitHubIssueDetails result:{}'.format(result)) + + # x0 FormfieldList expects {"0": {...field values...}} + if isinstance(result, dict) and 0 in result: + result = {'0': result[0]} + + yield bytes(json.dumps(result), 'utf-8') + + except Exception as e: + logger.error('GetGitHubIssueDetails exception:{}'.format(e)) + error_result = { + 'error': True, + 'error_id': 500, + 'exception': str(e) + } + yield bytes(json.dumps(error_result), 'utf-8') diff --git a/python/POSTData.py b/python/POSTData.py new file mode 100644 index 0000000..92dc6be --- /dev/null +++ b/python/POSTData.py @@ -0,0 +1,9 @@ +import json + + +class Environment: + @staticmethod + def getPOSTData(environ): + ContentLength = int(environ.get('CONTENT_LENGTH', 0)) + Data = environ['wsgi.input'].read(ContentLength) + return Data diff --git a/python/SearchGitHubIssues.py b/python/SearchGitHubIssues.py new file mode 100644 index 0000000..b67f3d1 --- /dev/null +++ b/python/SearchGitHubIssues.py @@ -0,0 +1,54 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Search GitHub Issues WSGI Endpoint . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Called by x0 IssueSearchConnector (ServiceConnector). . +# . Searches GitHub issues and returns indexed rows for the IssueList. . +# . Credentials are loaded from the database (set via Screen 1). . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import sys +import json + +import POSTData +from StdoutLogger import logger + +from router import ServiceRouter + + +def application(environ, start_response): + + start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')]) + + if environ['REQUEST_METHOD'].upper() != 'POST': + yield bytes(json.dumps({}), 'utf-8') + return + + try: + raw = POSTData.Environment.getPOSTData(environ) + service_json = json.loads(raw) + + request_data = service_json.get('RequestData', {}) + + logger.debug('SearchGitHubIssues RequestData:{}'.format(request_data)) + + router = ServiceRouter() + result = router.send('search_github_issues', request_data) + + logger.debug('SearchGitHubIssues result rows:{}'.format(len(result))) + + # Ensure result keys are strings (x0 List expects string-keyed indexed rows) + if isinstance(result, dict) and 'error' not in result: + result = {str(k): v for k, v in result.items()} + + yield bytes(json.dumps(result), 'utf-8') + + except Exception as e: + logger.error('SearchGitHubIssues exception:{}'.format(e)) + error_result = { + 'error': True, + 'error_id': 500, + 'exception': str(e) + } + yield bytes(json.dumps(error_result), 'utf-8') diff --git a/python/StdoutLogger.py b/python/StdoutLogger.py new file mode 100644 index 0000000..8618e2d --- /dev/null +++ b/python/StdoutLogger.py @@ -0,0 +1,5 @@ +import logging +import sys + +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +logger = logging.getLogger(__name__) diff --git a/python/VerifyGitHubCredentials.py b/python/VerifyGitHubCredentials.py new file mode 100644 index 0000000..4c8f791 --- /dev/null +++ b/python/VerifyGitHubCredentials.py @@ -0,0 +1,49 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Verify GitHub Credentials WSGI Endpoint . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Called by x0 GitHubVerifyButton (OnClick). . +# . Validates GitHub API user/token and stores them in the database. . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import sys +import json + +import POSTData +from StdoutLogger import logger + +from router import ServiceRouter + + +def application(environ, start_response): + + start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')]) + + if environ['REQUEST_METHOD'].upper() != 'POST': + yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8') + return + + try: + raw = POSTData.Environment.getPOSTData(environ) + service_json = json.loads(raw) + + request_data = service_json.get('RequestData', {}) + + logger.debug('VerifyGitHubCredentials RequestData:{}'.format(request_data)) + + router = ServiceRouter() + result = router.send('verify_github', request_data) + + logger.debug('VerifyGitHubCredentials result:{}'.format(result)) + + yield bytes(json.dumps(result), 'utf-8') + + except Exception as e: + logger.error('VerifyGitHubCredentials exception:{}'.format(e)) + error_result = { + 'error': True, + 'error_id': 500, + 'exception': str(e) + } + yield bytes(json.dumps(error_result), 'utf-8') diff --git a/python/VerifyStackfieldCredentials.py b/python/VerifyStackfieldCredentials.py new file mode 100644 index 0000000..f4206b2 --- /dev/null +++ b/python/VerifyStackfieldCredentials.py @@ -0,0 +1,49 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Verify Stackfield Credentials WSGI Endpoint . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Called by x0 StackfieldVerifyButton (OnClick). . +# . Validates Stackfield API email/token and stores them in the database. . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import sys +import json + +import POSTData +from StdoutLogger import logger + +from router import ServiceRouter + + +def application(environ, start_response): + + start_response('200 OK', [('Content-Type', 'application/json; charset=UTF-8')]) + + if environ['REQUEST_METHOD'].upper() != 'POST': + yield bytes(json.dumps({'error': True, 'error_id': 400}), 'utf-8') + return + + try: + raw = POSTData.Environment.getPOSTData(environ) + service_json = json.loads(raw) + + request_data = service_json.get('RequestData', {}) + + logger.debug('VerifyStackfieldCredentials RequestData:{}'.format(request_data)) + + router = ServiceRouter() + result = router.send('verify_stackfield', request_data) + + logger.debug('VerifyStackfieldCredentials result:{}'.format(result)) + + yield bytes(json.dumps(result), 'utf-8') + + except Exception as e: + logger.error('VerifyStackfieldCredentials exception:{}'.format(e)) + error_result = { + 'error': True, + 'error_id': 500, + 'exception': str(e) + } + yield bytes(json.dumps(error_result), 'utf-8') diff --git a/python/service_implementation.py b/python/service_implementation.py new file mode 100644 index 0000000..a536250 --- /dev/null +++ b/python/service_implementation.py @@ -0,0 +1,249 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - Service Implementation Classes . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Provides ClassHandler-based service classes for GitHub and Stackfield . +# . API integration, used via python-micro-esb ServiceRouter routing. . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import logging +import requests + +from microesb import microesb + +logger = logging.getLogger(__name__) + + +class GitHubService(microesb.ClassHandler): + """GitHub API service handler. + + Provides methods to interact with the GitHub REST API v3 for + credential verification and issue retrieval. + """ + + BASE_URL = 'https://api.github.com' + + def __init__(self): + super().__init__() + self.github_user = None + self.github_token = None + self.repo = None + self.issue_number = None + self.search_query = None + self.result = {} + + def _get_auth(self): + return (self.github_user, self.github_token) + + def _get_headers(self): + return { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28' + } + + def verify(self): + """Verify GitHub credentials by calling the /user endpoint.""" + logger.debug('GitHubService.verify() user:{}'.format(self.github_user)) + try: + resp = requests.get( + '{}/user'.format(self.BASE_URL), + auth=self._get_auth(), + headers=self._get_headers(), + timeout=10 + ) + if resp.status_code == 200: + data = resp.json() + self.result = { + 'success': True, + 'login': data.get('login', ''), + 'name': data.get('name', ''), + 'public_repos': str(data.get('public_repos', 0)) + } + else: + self.result = { + 'success': False, + 'error': 'Authentication failed (HTTP {})'.format(resp.status_code) + } + except requests.exceptions.RequestException as e: + logger.error('GitHubService.verify() exception:{}'.format(e)) + self.result = {'success': False, 'error': str(e)} + + def search_issues(self): + """Search GitHub issues in the configured repository.""" + logger.debug('GitHubService.search_issues() repo:{} query:{}'.format( + self.repo, self.search_query + )) + try: + params = { + 'q': '{} repo:{} is:issue'.format( + self.search_query or '', self.repo or '' + ), + 'per_page': 50, + 'sort': 'updated', + 'order': 'desc' + } + resp = requests.get( + '{}/search/issues'.format(self.BASE_URL), + params=params, + auth=self._get_auth(), + headers=self._get_headers(), + timeout=15 + ) + resp.raise_for_status() + data = resp.json() + issues = data.get('items', []) + self.result = {} + for idx, issue in enumerate(issues): + assignee = issue.get('assignee') + self.result[idx] = { + 'issue_number': str(issue.get('number', '')), + 'number': '#{}'.format(issue.get('number', '')), + 'title': issue.get('title', ''), + 'state': issue.get('state', ''), + 'created_at': str(issue.get('created_at', ''))[:10], + 'assignee': assignee.get('login', '') if assignee else '', + 'html_url': issue.get('html_url', ''), + 'body': issue.get('body', '') or '' + } + except requests.exceptions.RequestException as e: + logger.error('GitHubService.search_issues() exception:{}'.format(e)) + self.result = {'error': True, 'error_message': str(e)} + + def get_issue_details(self): + """Fetch detailed data for a specific GitHub issue.""" + logger.debug('GitHubService.get_issue_details() repo:{} issue:{}'.format( + self.repo, self.issue_number + )) + try: + resp = requests.get( + '{}/repos/{}/issues/{}'.format( + self.BASE_URL, self.repo, self.issue_number + ), + auth=self._get_auth(), + headers=self._get_headers(), + timeout=10 + ) + resp.raise_for_status() + issue = resp.json() + assignee = issue.get('assignee') + body = issue.get('body', '') or '' + self.result = { + 0: { + 'number': str(issue.get('number', '')), + 'title': issue.get('title', ''), + 'state': issue.get('state', ''), + 'html_url': issue.get('html_url', ''), + 'body': body, + 'assignee': assignee.get('login', '') if assignee else '', + 'labels': ', '.join( + lbl.get('name', '') for lbl in issue.get('labels', []) + ), + 'task_title': issue.get('title', ''), + 'task_description': body, + 'stackfield_room_id': '', + 'task_priority': 'medium' + } + } + except requests.exceptions.RequestException as e: + logger.error('GitHubService.get_issue_details() exception:{}'.format(e)) + self.result = {'error': True, 'error_message': str(e)} + + +class StackfieldService(microesb.ClassHandler): + """Stackfield API service handler. + + Provides methods to interact with the Stackfield REST API for + credential verification and task creation. + + Stackfield API reference: https://stackfield.com/rest-api + """ + + BASE_URL = 'https://www.stackfield.com/api' + + def __init__(self): + super().__init__() + self.stackfield_email = None + self.stackfield_token = None + self.room_id = None + self.task_title = None + self.task_description = None + self.task_priority = None + self.github_issue_number = None + self.github_issue_url = None + self.result = {} + + def _get_headers(self): + return { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {}'.format(self.stackfield_token) + } + + def verify(self): + """Verify Stackfield credentials by fetching user profile.""" + logger.debug('StackfieldService.verify() email:{}'.format(self.stackfield_email)) + try: + resp = requests.get( + '{}/v1/user'.format(self.BASE_URL), + headers=self._get_headers(), + timeout=10 + ) + if resp.status_code == 200: + data = resp.json() + self.result = { + 'success': True, + 'user_id': str(data.get('id', '')), + 'name': data.get('name', ''), + 'email': data.get('email', '') + } + else: + self.result = { + 'success': False, + 'error': 'Authentication failed (HTTP {})'.format(resp.status_code) + } + except requests.exceptions.RequestException as e: + logger.error('StackfieldService.verify() exception:{}'.format(e)) + self.result = {'success': False, 'error': str(e)} + + def create_task(self): + """Create a new task in a Stackfield room from GitHub issue data.""" + logger.debug('StackfieldService.create_task() room:{} title:{}'.format( + self.room_id, self.task_title + )) + try: + priority_map = { + 'low': 1, + 'medium': 2, + 'high': 3, + 'urgent': 4 + } + priority_value = priority_map.get(self.task_priority or 'medium', 2) + + description = self.task_description or '' + if self.github_issue_url: + description = '{}\n\n---\nGitHub Issue: {}'.format( + description, self.github_issue_url + ) + + payload = { + 'title': self.task_title, + 'content': description, + 'priority': priority_value + } + + resp = requests.post( + '{}/v1/rooms/{}/tasks'.format(self.BASE_URL, self.room_id), + headers=self._get_headers(), + json=payload, + timeout=15 + ) + resp.raise_for_status() + data = resp.json() + self.result = { + 'success': True, + 'task_id': str(data.get('id', '')), + 'task_url': data.get('url', '') + } + except requests.exceptions.RequestException as e: + logger.error('StackfieldService.create_task() exception:{}'.format(e)) + self.result = {'success': False, 'error': str(e)} diff --git a/python/user_routing.py b/python/user_routing.py new file mode 100644 index 0000000..b593a02 --- /dev/null +++ b/python/user_routing.py @@ -0,0 +1,215 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - python-micro-esb Service Router . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Defines routing functions called by ServiceRouter.send(). . +# . Each function receives a metadata dict from the x0 frontend and . +# . returns a result dict. . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import logging +import psycopg2 + +import DB +from pgdbpool import pool + +from service_implementation import GitHubService, StackfieldService + +logger = logging.getLogger(__name__) + +pool.Connection.init(DB.config) + +# --------------------------------------------------------------------------- +# Credential helpers +# --------------------------------------------------------------------------- + +def _load_credentials(dbcon, cred_type): + """Load stored API credentials from database.""" + try: + with dbcon.cursor() as crs: + crs.execute( + """ + SELECT username_or_email, api_token + FROM github2sf.credentials + WHERE credential_type = %s + """, + (cred_type,) + ) + row = crs.fetchone() + if row: + return {'username_or_email': row[0], 'api_token': row[1]} + except Exception as e: + logger.error('_load_credentials() error:{}'.format(e)) + return {} + + +def _store_credentials(dbcon, cred_type, username_or_email, api_token): + """Persist API credentials to the database.""" + try: + with dbcon.cursor() as crs: + crs.execute( + """ + INSERT INTO github2sf.credentials + (credential_type, username_or_email, api_token) + VALUES (%s, %s, %s) + ON CONFLICT (credential_type) DO UPDATE + SET username_or_email = EXCLUDED.username_or_email, + api_token = EXCLUDED.api_token, + updated_at = NOW() + """, + (cred_type, username_or_email, api_token) + ) + dbcon.commit() + except Exception as e: + logger.error('_store_credentials() error:{}'.format(e)) + dbcon.rollback() + + +# --------------------------------------------------------------------------- +# Routing functions +# --------------------------------------------------------------------------- + +def verify_github(metadata): + """Verify GitHub API credentials and persist them on success. + + :param dict metadata: expects keys GitHubUserInput, GitHubTokenInput + :return: result dict with success flag + """ + logger.debug('user_routing.verify_github() metadata:{}'.format(metadata)) + + svc = GitHubService() + svc.github_user = metadata.get('GitHubUserInput', '') + svc.github_token = metadata.get('GitHubTokenInput', '') + svc.verify() + + if svc.result.get('success'): + with pool.Handler('x0') as db: + _store_credentials( + db.connection, + 'github', + svc.github_user, + svc.github_token + ) + + return svc.result + + +def verify_stackfield(metadata): + """Verify Stackfield API credentials and persist them on success. + + :param dict metadata: expects keys StackfieldEmailInput, StackfieldTokenInput + :return: result dict with success flag + """ + logger.debug('user_routing.verify_stackfield() metadata:{}'.format(metadata)) + + svc = StackfieldService() + svc.stackfield_email = metadata.get('StackfieldEmailInput', '') + svc.stackfield_token = metadata.get('StackfieldTokenInput', '') + svc.verify() + + if svc.result.get('success'): + with pool.Handler('x0') as db: + _store_credentials( + db.connection, + 'stackfield', + svc.stackfield_email, + svc.stackfield_token + ) + + return svc.result + + +def search_github_issues(metadata): + """Search GitHub issues using stored credentials. + + :param dict metadata: expects keys SearchQueryInput, SearchRepoInput + :return: indexed dict of issue rows for x0 List population + """ + logger.debug('user_routing.search_github_issues() metadata:{}'.format(metadata)) + + with pool.Handler('x0') as db: + creds = _load_credentials(db.connection, 'github') + + if not creds: + return {'error': True, 'error_message': 'GitHub credentials not configured.'} + + svc = GitHubService() + svc.github_user = creds['username_or_email'] + svc.github_token = creds['api_token'] + svc.repo = metadata.get('SearchRepoInput', '') + svc.search_query = metadata.get('SearchQueryInput', '') + svc.search_issues() + + return svc.result + + +def get_github_issue_details(metadata): + """Fetch detailed GitHub issue data for Screen 3 population. + + :param dict metadata: expects key issue_number; repo loaded from DB + :return: single-row dict for x0 FormfieldList population + """ + logger.debug('user_routing.get_github_issue_details() metadata:{}'.format(metadata)) + + with pool.Handler('x0') as db: + creds = _load_credentials(db.connection, 'github') + + if not creds: + return {0: {'error': 'GitHub credentials not configured.'}} + + # Repo may be stored as a mapping state or passed via global var. + # Fall back to a stored repo config entry. + repo = metadata.get('repo', '') + if not repo: + with pool.Handler('x0') as db: + try: + with db.connection.cursor() as crs: + crs.execute( + """ + SELECT value FROM github2sf.app_config + WHERE config_key = 'last_search_repo' + """ + ) + row = crs.fetchone() + if row: + repo = row[0] + except Exception: + pass + + svc = GitHubService() + svc.github_user = creds['username_or_email'] + svc.github_token = creds['api_token'] + svc.repo = repo + svc.issue_number = metadata.get('issue_number', '') + svc.get_issue_details() + + return svc.result + + +def create_stackfield_task(metadata): + """Create a new Stackfield task from GitHub issue data. + + :param dict metadata: expects task fields and Stackfield room ID + :return: result dict with success flag and new task URL + """ + logger.debug('user_routing.create_stackfield_task() metadata:{}'.format(metadata)) + + with pool.Handler('x0') as db: + creds = _load_credentials(db.connection, 'stackfield') + + if not creds: + return {'success': False, 'error': 'Stackfield credentials not configured.'} + + svc = StackfieldService() + svc.stackfield_email = creds['username_or_email'] + svc.stackfield_token = creds['api_token'] + svc.room_id = metadata.get('StackfieldRoomInput', '') + svc.task_title = metadata.get('TaskTitleInput', '') + svc.task_description = metadata.get('TaskDescInput', '') + svc.task_priority = metadata.get('TaskPriorityPulldown', 'medium') + svc.github_issue_number = metadata.get('number', '') + svc.github_issue_url = metadata.get('html_url', '') + svc.create_task() + + return svc.result diff --git a/static/menu.json b/static/menu.json new file mode 100644 index 0000000..dc304d5 --- /dev/null +++ b/static/menu.json @@ -0,0 +1,20 @@ +[ + { + "MenuLink1": + { + "RefID": "sysMenu" + } + }, + { + "MenuLink2": + { + "RefID": "sysMenu" + } + }, + { + "MenuLink3": + { + "RefID": "sysMenu" + } + } +] diff --git a/static/object.json b/static/object.json new file mode 100644 index 0000000..e11a243 --- /dev/null +++ b/static/object.json @@ -0,0 +1,771 @@ +{ + "MenuLink1": + { + "Type": "Link", + "Attributes": + { + "Style": "btn btn-outline-secondary me-2", + "TextID": "TXT.MENU.SCREEN1", + "ScreenID": "Screen1" + } + }, + + "MenuLink2": + { + "Type": "Link", + "Attributes": + { + "Style": "btn btn-outline-secondary me-2", + "TextID": "TXT.MENU.SCREEN2", + "ScreenID": "Screen2" + } + }, + + "MenuLink3": + { + "Type": "Link", + "Attributes": + { + "Style": "btn btn-outline-secondary me-2", + "TextID": "TXT.MENU.SCREEN3", + "ScreenID": "Screen3" + } + }, + + "GitHubCredentialsForm": + { + "Type": "FormfieldList", + "Attributes": + { + "Sections": + [ + { + "ID": "GitHubSection", + "Object": "FormSectionHeader", + "ObjectAttributes": + { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-brands fa-github", + "HeaderTextID": "TXT.SCREEN1.GITHUB.SECTION.HEADER", + "SubHeaderTextID": "TXT.SCREEN1.GITHUB.SECTION.SUBHEADER" + }, + "Formfields": + [ + "GitHubUserLabel", + "GitHubUserInput", + "GitHubTokenLabel", + "GitHubTokenInput" + ], + "RowStyle": "row mb-3", + "RowAfterElements": 4, + "ColStyle": "col-md-6", + "ColAfterElements": 2 + } + ] + } + }, + + "GitHubUserLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN1.GITHUB.USER.LABEL" + } + }, + + "GitHubUserInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "Style": "form-control form-control-lg", + "Placeholder": "github-username", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "GitHubTokenLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN1.GITHUB.TOKEN.LABEL" + } + }, + + "GitHubTokenInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "password", + "Style": "form-control form-control-lg", + "Placeholder": "ghp_xxxxxxxxxxxxxxxxxxxx", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "GitHubVerifyButton": + { + "Type": "Button", + "Attributes": + { + "TextID": "TXT.SCREEN1.GITHUB.VERIFY.BUTTON", + "Style": "btn btn-outline-primary w-100 mb-4", + "IconStyle": "fa-solid fa-shield-halved", + "OnClick": "/python/VerifyGitHubCredentials.py", + "SrcDataObjects": + [ + "GitHubCredentialsForm" + ], + "Notify": + { + "ID": "VerifyGitHubCredentials", + "DisplayHeaderID": "TXT.SCREEN1.GITHUB.VERIFY.NOTIFY" + } + } + }, + + "StackfieldCredentialsForm": + { + "Type": "FormfieldList", + "Attributes": + { + "Sections": + [ + { + "ID": "StackfieldSection", + "Object": "FormSectionHeader", + "ObjectAttributes": + { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-solid fa-layer-group", + "HeaderTextID": "TXT.SCREEN1.STACKFIELD.SECTION.HEADER", + "SubHeaderTextID": "TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER" + }, + "Formfields": + [ + "StackfieldEmailLabel", + "StackfieldEmailInput", + "StackfieldTokenLabel", + "StackfieldTokenInput" + ], + "RowStyle": "row mb-3", + "RowAfterElements": 4, + "ColStyle": "col-md-6", + "ColAfterElements": 2 + } + ] + } + }, + + "StackfieldEmailLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN1.STACKFIELD.EMAIL.LABEL" + } + }, + + "StackfieldEmailInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "Style": "form-control form-control-lg", + "Placeholder": "user@example.com", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "StackfieldTokenLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN1.STACKFIELD.TOKEN.LABEL" + } + }, + + "StackfieldTokenInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "password", + "Style": "form-control form-control-lg", + "Placeholder": "stackfield-api-token", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "StackfieldVerifyButton": + { + "Type": "Button", + "Attributes": + { + "TextID": "TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON", + "Style": "btn btn-outline-success w-100 mb-4", + "IconStyle": "fa-solid fa-shield-halved", + "OnClick": "/python/VerifyStackfieldCredentials.py", + "SrcDataObjects": + [ + "StackfieldCredentialsForm" + ], + "Notify": + { + "ID": "VerifyStackfieldCredentials", + "DisplayHeaderID": "TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY" + } + } + }, + + "SearchForm": + { + "Type": "FormfieldList", + "Attributes": + { + "Sections": + [ + { + "ID": "SearchSection", + "Object": "FormSectionHeader", + "ObjectAttributes": + { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-3", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-solid fa-magnifying-glass", + "HeaderTextID": "TXT.SCREEN2.SEARCH.SECTION.HEADER", + "SubHeaderTextID": "TXT.SCREEN2.SEARCH.SECTION.SUBHEADER" + }, + "Formfields": + [ + "SearchQueryLabel", + "SearchQueryInput", + "SearchRepoLabel", + "SearchRepoInput" + ], + "RowStyle": "row mb-3", + "RowAfterElements": 4, + "ColStyle": "col-md-6", + "ColAfterElements": 2 + } + ] + } + }, + + "SearchQueryLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN2.SEARCH.QUERY.LABEL" + } + }, + + "SearchQueryInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "Style": "form-control form-control-lg", + "Placeholder": "Search GitHub issues...", + "ValidateRef": "DefaultString", + "ValidateNullable": true + } + }, + + "SearchRepoLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN2.SEARCH.REPO.LABEL" + } + }, + + "SearchRepoInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "Style": "form-control form-control-lg", + "Placeholder": "owner/repository", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "SearchButton": + { + "Type": "Button", + "Attributes": + { + "TextID": "TXT.SCREEN2.SEARCH.BUTTON", + "Style": "btn btn-primary w-100 mb-4", + "IconStyle": "fa-solid fa-magnifying-glass", + "FireEvents": [ "SearchGitHubIssues" ] + } + }, + + "IssueSearchConnector": + { + "Type": "ServiceConnector", + "Attributes": + { + "OnEvent": + { + "Events": [ "SearchGitHubIssues" ], + "ServiceCall": "/python/SearchGitHubIssues.py" + }, + "SrcDataObjects": + { + "SearchForm": + { + "Type": "FormfieldList" + } + } + } + }, + + "IssueList": + { + "Type": "List", + "Attributes": + { + "Style": "border rounded-top rounded-bottom", + "HeaderRowStyle": "row fw-bold fs-6 m-2 border-bottom bg-light", + "RowCount": 15, + "Navigation": true, + "Columns": + [ + { + "ID": "issue_number", + "VisibleState": "hidden" + }, + { + "ID": "number", + "HeaderTextID": "TXT.SCREEN2.LIST.COL.NUMBER", + "HeaderStyle": "col-md-1 p-3 border-end text-center" + }, + { + "ID": "title", + "HeaderTextID": "TXT.SCREEN2.LIST.COL.TITLE", + "HeaderStyle": "col-md-5 p-3 border-end" + }, + { + "ID": "state", + "HeaderTextID": "TXT.SCREEN2.LIST.COL.STATE", + "HeaderStyle": "col-md-2 p-3 border-end text-center" + }, + { + "ID": "created_at", + "HeaderTextID": "TXT.SCREEN2.LIST.COL.CREATED", + "HeaderStyle": "col-md-2 p-3 border-end" + }, + { + "ID": "assignee", + "HeaderTextID": "TXT.SCREEN2.LIST.COL.ASSIGNEE", + "HeaderStyle": "col-md-2 p-3" + } + ], + "ContextMenuItems": + [ + { + "ID": "ConnectStackfieldTask", + "TextID": "TXT.SCREEN2.CONTEXTMENU.CONNECT", + "IconStyle": "fa-solid fa-link", + "DstScreenID": "Screen3", + "RowColumn": "issue_number", + "FireEvents": [ "ConnectStackfieldTask" ] + } + ], + "RowStyle": "row m-2 border-bottom", + "RowAfterElements": 6, + "ColStyle": + [ + "dummy", + "col-md-1 p-3 border-end text-center fw-bold text-primary", + "col-md-5 p-3 border-end", + "col-md-2 p-3 border-end text-center", + "col-md-2 p-3 border-end text-muted small", + "col-md-2 p-3 text-muted small" + ] + } + }, + + "IssueDetailsConnector": + { + "Type": "ServiceConnector", + "Attributes": + { + "OnEvent": + { + "Events": [ "ConnectStackfieldTask" ], + "ServiceCall": "/python/GetGitHubIssueDetails.py" + }, + "SrcDataObjects": + { + "issue_number": + { + "Type": "ScreenGlobalVar" + } + } + } + }, + + "IssueDetailsForm": + { + "Type": "FormfieldList", + "Attributes": + { + "Sections": + [ + { + "ID": "IssueDetailsSection", + "Object": "FormSectionHeader", + "ObjectAttributes": + { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-brands fa-github", + "HeaderTextID": "TXT.SCREEN3.ISSUE.SECTION.HEADER", + "SubHeaderTextID": "TXT.SCREEN3.ISSUE.SECTION.SUBHEADER" + }, + "Formfields": + [ + "IssueNumberLabel", + "IssueNumberField", + "IssueStateLabel", + "IssueStateField", + "IssueTitleLabel", + "IssueTitleField", + "IssueUrlLabel", + "IssueUrlField" + ], + "RowStyle": + [ + "row mb-3", + "row mb-3" + ], + "RowAfterElements": [ 4, 4 ], + "ColStyle": + [ + "col-md-3 mb-2", + "col-md-3 mb-2", + "col-md-6 mb-2", + "col-md-6 mb-2" + ] + } + ], + "SrcDataObjects": + { + "issue_number": + { + "Type": "ScreenGlobalVar" + } + } + } + }, + + "IssueNumberLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.ISSUE.NUMBER.LABEL" + } + }, + + "IssueNumberField": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "number", + "Style": "form-control", + "Disabled": true + } + }, + + "IssueStateLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.ISSUE.STATE.LABEL" + } + }, + + "IssueStateField": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "state", + "Style": "form-control", + "Disabled": true + } + }, + + "IssueTitleLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.ISSUE.TITLE.LABEL" + } + }, + + "IssueTitleField": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "title", + "Style": "form-control", + "Disabled": true + } + }, + + "IssueUrlLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.ISSUE.URL.LABEL" + } + }, + + "IssueUrlField": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "html_url", + "Style": "form-control", + "Disabled": true + } + }, + + "StackfieldMappingForm": + { + "Type": "FormfieldList", + "Attributes": + { + "Sections": + [ + { + "ID": "StackfieldMappingSection", + "Object": "FormSectionHeader", + "ObjectAttributes": + { + "Style": "card card-line bg-body-tertiary border border-dark border-2 mb-4", + "SubStyle": "card-body mt-n6", + "HeaderIcon": "fa-solid fa-layer-group", + "HeaderTextID": "TXT.SCREEN3.STACKFIELD.SECTION.HEADER", + "SubHeaderTextID": "TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER" + }, + "Formfields": + [ + "StackfieldRoomLabel", + "StackfieldRoomInput", + "TaskTitleLabel", + "TaskTitleInput", + "TaskDescLabel", + "TaskDescInput", + "TaskPriorityLabel", + "TaskPriorityPulldown" + ], + "RowStyle": + [ + "row mb-3", + "row mb-3", + "row mb-3" + ], + "RowAfterElements": [ 4, 2, 2 ], + "ColStyle": + [ + "col-md-6 mb-2", + "col-md-6 mb-2", + "col-md-12 mb-2", + "col-md-12 mb-2" + ] + } + ] + } + }, + + "StackfieldRoomLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.STACKFIELD.ROOM.LABEL" + } + }, + + "StackfieldRoomInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "stackfield_room_id", + "Style": "form-control", + "Placeholder": "Stackfield Room ID", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "TaskTitleLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.TASK.TITLE.LABEL" + } + }, + + "TaskTitleInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "text", + "DBColumn": "task_title", + "Style": "form-control", + "ValidateRef": "DefaultString", + "ValidateNullable": false + } + }, + + "TaskDescLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.TASK.DESC.LABEL" + } + }, + + "TaskDescInput": + { + "Type": "Formfield", + "Attributes": + { + "Type": "textarea", + "DBColumn": "task_description", + "Style": "form-control", + "Rows": 5, + "ValidateNullable": true + } + }, + + "TaskPriorityLabel": + { + "Type": "Formfield", + "Attributes": + { + "Type": "label", + "Style": "form-label fw-semibold", + "TextID": "TXT.SCREEN3.TASK.PRIORITY.LABEL" + } + }, + + "TaskPriorityPulldown": + { + "Type": "Formfield", + "Attributes": + { + "Type": "pulldown", + "DBColumn": "task_priority", + "Style": "form-select", + "Options": + [ + { + "TextID": "TXT.SCREEN3.PRIORITY.LOW", + "Value": "low", + "Default": true + }, + { + "TextID": "TXT.SCREEN3.PRIORITY.MEDIUM", + "Value": "medium" + }, + { + "TextID": "TXT.SCREEN3.PRIORITY.HIGH", + "Value": "high" + }, + { + "TextID": "TXT.SCREEN3.PRIORITY.URGENT", + "Value": "urgent" + } + ] + } + }, + + "CreateStackfieldTaskButton": + { + "Type": "Button", + "Attributes": + { + "TextID": "TXT.SCREEN3.CREATE.BUTTON", + "Style": "btn btn-success w-100 mt-2 mb-4", + "IconStyle": "fa-solid fa-circle-plus", + "OnClick": "/python/CreateStackfieldTask.py", + "SrcDataObjects": + [ + "IssueDetailsForm", + "StackfieldMappingForm" + ], + "Notify": + { + "ID": "CreateStackfieldTask", + "DisplayHeaderID": "TXT.SCREEN3.CREATE.NOTIFY" + } + } + } +} diff --git a/static/skeleton.json b/static/skeleton.json new file mode 100644 index 0000000..060be60 --- /dev/null +++ b/static/skeleton.json @@ -0,0 +1,85 @@ +{ + "Screen1": + [ + { + "GitHubCredentialsForm": + { + "RefID": "Screen1" + } + }, + { + "GitHubVerifyButton": + { + "RefID": "Screen1" + } + }, + { + "StackfieldCredentialsForm": + { + "RefID": "Screen1" + } + }, + { + "StackfieldVerifyButton": + { + "RefID": "Screen1" + } + } + ], + + "Screen2": + [ + { + "SearchForm": + { + "RefID": "Screen2" + } + }, + { + "SearchButton": + { + "RefID": "Screen2" + } + }, + { + "IssueSearchConnector": + { + "RefID": "Screen2" + } + }, + { + "IssueList": + { + "RefID": "IssueSearchConnector" + } + } + ], + + "Screen3": + [ + { + "IssueDetailsConnector": + { + "RefID": "Screen3" + } + }, + { + "IssueDetailsForm": + { + "RefID": "IssueDetailsConnector" + } + }, + { + "StackfieldMappingForm": + { + "RefID": "IssueDetailsConnector" + } + }, + { + "CreateStackfieldTaskButton": + { + "RefID": "Screen3" + } + } + ] +} From a90ef1317ca578bb2bd8c19e9c8cd67420a6c645 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:13:10 +0000 Subject: [PATCH 2/6] fix: use official ghcr.io/webcodex1/x0-app and x0-db container images --- README.md | 23 ++++++++++++------- docker/Dockerfile | 30 +++++------------------- docker/apache2.conf | 48 ++++++++++++--------------------------- docker/docker-compose.yml | 45 ++++++++++++++++++------------------ 4 files changed, 58 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 6698eb5..a7c0ad8 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ psql -U postgres -d x0 -f database/03-insert-text.sql Copy the `static/` directory so it is served at `/static/github2sf/`: ```bash -cp -r static/ /var/www/x0/static/github2sf/ +cp -r static/ /var/www/vhosts/x0/static/github2sf/ ``` ### 5. Deploy Python backend @@ -94,14 +94,15 @@ cp -r static/ /var/www/x0/static/github2sf/ Copy the `python/` directory into the x0 Python directory: ```bash -cp python/*.py /var/www/x0/python/github2sf/ +cp python/*.py /var/www/vhosts/x0/python/github2sf/ ``` ### 6. Configure Apache2 -Add the WSGI aliases from `docker/apache2.conf` to your Apache virtual host, then reload: +Copy `docker/apache2.conf` to `/etc/apache2/conf-enabled/github2sf.conf` and reload: ```bash +cp docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf apache2ctl graceful ``` @@ -113,6 +114,13 @@ Navigate to `http://your-server/?appid=github2sf` in your browser. ## Docker (quick start) +The application uses the official x0 container images: + +- **`ghcr.io/webcodex1/x0-app`** — Apache2 + mod_wsgi web application server with the x0 JavaScript framework pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-app)) +- **`ghcr.io/webcodex1/x0-db`** — PostgreSQL 16 database with the x0 schema pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-db)) + +The `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and adds the github2stackfield Python backend dependencies on top. + ```bash cd docker docker compose up --build @@ -120,13 +128,12 @@ docker compose up --build Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf). -> **Note:** The Docker image fetches x0 and python-micro-esb from GitHub at build time. -> You still need to run the database SQL scripts against the PostgreSQL container: +> **Database setup:** After the containers are running, execute the SQL scripts against the x0-db container: > > ```bash -> docker exec -i github2sf-db psql -U postgres -d x0 < database/01-create-schema.sql -> docker exec -i github2sf-db psql -U postgres -d x0 < database/02-insert-config.sql -> docker exec -i github2sf-db psql -U postgres -d x0 < database/03-insert-text.sql +> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/01-create-schema.sql +> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/02-insert-config.sql +> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/03-insert-text.sql > ``` --- diff --git a/docker/Dockerfile b/docker/Dockerfile index 353e760..0a679e7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,31 +1,13 @@ -FROM python:3.12-slim +FROM ghcr.io/webcodex1/x0-app -RUN apt-get update && apt-get install -y \ - apache2 \ - libapache2-mod-wsgi-py3 \ +# Install github2stackfield Python backend dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-pip \ git \ && rm -rf /var/lib/apt/lists/* -# Install Python dependencies -RUN pip install --no-cache-dir \ +RUN pip3 install --no-cache-dir --break-system-packages \ requests \ psycopg2-binary \ - pgdbpool - -# Install python-micro-esb from source -RUN pip install --no-cache-dir \ + pgdbpool \ git+https://github.com/clauspruefer/python-micro-esb.git - -# Install x0 framework Python modules -RUN pip install --no-cache-dir \ - git+https://github.com/WEBcodeX1/x0.git - -WORKDIR /var/www/x0 - -# Apache configuration -COPY docker/apache2.conf /etc/apache2/sites-available/github2sf.conf -RUN a2ensite github2sf && a2dissite 000-default && a2enmod wsgi - -EXPOSE 80 - -CMD ["apache2ctl", "-D", "FOREGROUND"] diff --git a/docker/apache2.conf b/docker/apache2.conf index 61ebf99..b0f3854 100644 --- a/docker/apache2.conf +++ b/docker/apache2.conf @@ -1,35 +1,17 @@ - - ServerName github2sf.local - DocumentRoot /var/www/x0/www +# github2stackfield supplementary Apache configuration +# Mounted into /etc/apache2/conf-enabled/ inside the x0-app container. +# The x0-app base image already configures Apache2 + mod_wsgi and sets +# DocumentRoot /var/www/vhosts/x0 with WSGI handling for the python/ tree. +# This snippet grants explicit directory access to the app-specific paths. - # Static assets: x0 framework JS/CSS + app-specific JSON config - Alias /static /var/www/x0/www/static - Alias /static/github2sf /var/www/x0/static/github2sf + + Options -Indexes + Require all granted + - - Options -Indexes - Require all granted - - - - Options -Indexes - Require all granted - - - # x0 framework index / main entry-point - WSGIScriptAlias / /var/www/x0/python/Index.py - - # github2stackfield backend service endpoints - WSGIScriptAlias /python/VerifyGitHubCredentials.py /var/www/x0/python/github2sf/VerifyGitHubCredentials.py - WSGIScriptAlias /python/VerifyStackfieldCredentials.py /var/www/x0/python/github2sf/VerifyStackfieldCredentials.py - WSGIScriptAlias /python/SearchGitHubIssues.py /var/www/x0/python/github2sf/SearchGitHubIssues.py - WSGIScriptAlias /python/GetGitHubIssueDetails.py /var/www/x0/python/github2sf/GetGitHubIssueDetails.py - WSGIScriptAlias /python/CreateStackfieldTask.py /var/www/x0/python/github2sf/CreateStackfieldTask.py - - - Require all granted - - - ErrorLog ${APACHE_LOG_DIR}/github2sf-error.log - CustomLog ${APACHE_LOG_DIR}/github2sf-access.log combined - + + AddHandler wsgi-script .py + Options ExecCGI + AllowOverride None + Require all granted + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 76f5829..d58a2ae 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,25 +1,24 @@ services: # --------------------------------------------------------------------------- - # PostgreSQL database (shared with x0 framework) + # x0 database container (PostgreSQL with x0 schema pre-installed) + # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db # --------------------------------------------------------------------------- - postgres: - image: postgres:16-alpine + x0-db: + image: ghcr.io/webcodex1/x0-db container_name: github2sf-db restart: unless-stopped - environment: - POSTGRES_DB: x0 - POSTGRES_USER: postgres - POSTGRES_PASSWORD: changeme - volumes: - - pg_data:/var/lib/postgresql/data ports: - "5432:5432" networks: - - github2sf-net + github2sf-net: + ipv4_address: 172.20.0.20 + aliases: + - mypostgres # --------------------------------------------------------------------------- - # x0 application server (Apache2 + mod_wsgi) + # x0 application server (extends official x0-app with github2sf Python deps) + # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app # --------------------------------------------------------------------------- x0-app: build: @@ -28,23 +27,23 @@ services: container_name: github2sf-app restart: unless-stopped depends_on: - - postgres - environment: - DB_HOST: postgres - DB_NAME: x0 - DB_USER: postgres - DB_PASSWORD: changeme + - x0-db + extra_hosts: + - "mypostgres:172.20.0.20" volumes: - - ../static:/var/www/x0/static/github2sf:ro - - ../python:/var/www/x0/python/github2sf:ro + - ../static:/var/www/vhosts/x0/static/github2sf:ro + - ../python:/var/www/vhosts/x0/python/github2sf:ro + - ../docker/apache2.conf:/etc/apache2/conf-enabled/github2sf.conf:ro ports: - "8080:80" networks: - - github2sf-net - -volumes: - pg_data: + github2sf-net: + ipv4_address: 172.20.0.10 networks: github2sf-net: driver: bridge + ipam: + config: + - subnet: 172.20.0.0/24 + gateway: 172.20.0.1 From 0d8ba8a2db1eddd0511e0e1ad930c0a0c56e65fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 10:14:40 +0000 Subject: [PATCH 3/6] fix: automate full container self-initialisation; remove ## Installation from README --- README.md | 75 ++++--------------------------- database/03-insert-text.sql | 88 ++++++++++++++++++------------------- docker/Dockerfile | 9 ++++ docker/Dockerfile.db | 11 +++++ docker/db-init.sh | 35 +++++++++++++++ docker/docker-compose.yml | 18 ++++---- 6 files changed, 117 insertions(+), 119 deletions(-) create mode 100644 docker/Dockerfile.db create mode 100755 docker/db-init.sh diff --git a/README.md b/README.md index a7c0ad8..98b7b57 100644 --- a/README.md +++ b/README.md @@ -50,68 +50,12 @@ Review the selected GitHub issue and create a Stackfield task. | Requirement | Notes | |---|---| -| x0 framework | Follow [x0 INSTALL.md](https://github.com/WEBcodeX1/x0/blob/main/INSTALL.md) | -| PostgreSQL ≥ 14 | Shared with x0 | -| Python ≥ 3.10 | `requests`, `psycopg2`, `pgdbpool`, `python-micro-esb` | +| Docker Engine | With Compose V2 (`docker compose`) | | GitHub Personal Access Token | Needs `repo` scope for private repos, `public_repo` for public | | Stackfield API token | See Stackfield workspace settings → Integrations → API | --- -## Installation - -### 1. Set up x0 - -Follow the official x0 installation guide to get the base framework running with PostgreSQL. - -### 2. Install Python dependencies - -```bash -pip install requests psycopg2-binary pgdbpool -pip install git+https://github.com/clauspruefer/python-micro-esb.git -``` - -### 3. Run database setup scripts - -Connect to your x0 PostgreSQL database and execute the scripts in order: - -```bash -psql -U postgres -d x0 -f database/01-create-schema.sql -psql -U postgres -d x0 -f database/02-insert-config.sql -psql -U postgres -d x0 -f database/03-insert-text.sql -``` - -### 4. Deploy static files - -Copy the `static/` directory so it is served at `/static/github2sf/`: - -```bash -cp -r static/ /var/www/vhosts/x0/static/github2sf/ -``` - -### 5. Deploy Python backend - -Copy the `python/` directory into the x0 Python directory: - -```bash -cp python/*.py /var/www/vhosts/x0/python/github2sf/ -``` - -### 6. Configure Apache2 - -Copy `docker/apache2.conf` to `/etc/apache2/conf-enabled/github2sf.conf` and reload: - -```bash -cp docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf -apache2ctl graceful -``` - -### 7. Open the application - -Navigate to `http://your-server/?appid=github2sf` in your browser. - ---- - ## Docker (quick start) The application uses the official x0 container images: @@ -119,7 +63,10 @@ The application uses the official x0 container images: - **`ghcr.io/webcodex1/x0-app`** — Apache2 + mod_wsgi web application server with the x0 JavaScript framework pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-app)) - **`ghcr.io/webcodex1/x0-db`** — PostgreSQL 16 database with the x0 schema pre-installed ([packages page](https://github.com/WEBcodeX1/x0/pkgs/container/x0-db)) -The `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and adds the github2stackfield Python backend dependencies on top. +Both custom images are built automatically on `docker compose up --build`: + +- `docker/Dockerfile` extends `ghcr.io/webcodex1/x0-app` and copies all static files, Python WSGI scripts, and the Apache2 config snippet into the image. +- `docker/Dockerfile.db` extends `ghcr.io/webcodex1/x0-db` and runs the github2stackfield SQL init scripts (`database/0*.sql`) against the x0 database on first start. ```bash cd docker @@ -128,13 +75,7 @@ docker compose up --build Then open [http://localhost:8080/?appid=github2sf](http://localhost:8080/?appid=github2sf). -> **Database setup:** After the containers are running, execute the SQL scripts against the x0-db container: -> -> ```bash -> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/01-create-schema.sql -> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/02-insert-config.sql -> docker exec -i github2sf-db psql -U postgres -d x0 -f /dev/stdin < database/03-insert-text.sql -> ``` +No further manual configuration is required. --- @@ -161,8 +102,10 @@ github2stackfield/ │ ├── 02-insert-config.sql # x0 app configuration rows │ └── 03-insert-text.sql # UI text / i18n entries ├── docker/ -│ ├── Dockerfile +│ ├── Dockerfile # extends x0-app, bakes in static/ python/ apache2.conf +│ ├── Dockerfile.db # extends x0-db, auto-inits github2sf schema on startup │ ├── docker-compose.yml +│ ├── db-init.sh # startup script used by Dockerfile.db │ └── apache2.conf └── README.md ``` diff --git a/database/03-insert-text.sql b/database/03-insert-text.sql index 0f4aaa8..3235890 100644 --- a/database/03-insert-text.sql +++ b/database/03-insert-text.sql @@ -9,108 +9,108 @@ -- Navigation / Menu INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials'); + ('TXT.MENU.SCREEN1', 'menu', 'API-Zugangsdaten', 'User Credentials') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping'); + ('TXT.MENU.SCREEN2', 'menu', 'Issue / Aufgaben-Mapping', 'Issue / Task Mapping') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task'); + ('TXT.MENU.SCREEN3', 'menu', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task') ON CONFLICT (id) DO NOTHING; -- Screen 1 – GitHub Credentials INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials'); + ('TXT.SCREEN1.GITHUB.SECTION.HEADER', 'screen1', 'GitHub API-Zugangsdaten', 'GitHub API Credentials') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token'); + ('TXT.SCREEN1.GITHUB.SECTION.SUBHEADER', 'screen1', 'Benutzername und Personal Access Token eingeben', 'Enter your GitHub username and Personal Access Token') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username'); + ('TXT.SCREEN1.GITHUB.USER.LABEL', 'screen1', 'GitHub Benutzername', 'GitHub Username') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token'); + ('TXT.SCREEN1.GITHUB.TOKEN.LABEL', 'screen1', 'GitHub Personal Access Token', 'GitHub Personal Access Token') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials'); + ('TXT.SCREEN1.GITHUB.VERIFY.BUTTON', 'screen1', 'GitHub Zugangsdaten prüfen', 'Verify GitHub Credentials') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication'); + ('TXT.SCREEN1.GITHUB.VERIFY.NOTIFY', 'screen1', 'GitHub Authentifizierung', 'GitHub Authentication') ON CONFLICT (id) DO NOTHING; -- Screen 1 – Stackfield Credentials INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials'); + ('TXT.SCREEN1.STACKFIELD.SECTION.HEADER', 'screen1', 'Stackfield API-Zugangsdaten', 'Stackfield API Credentials') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token'); + ('TXT.SCREEN1.STACKFIELD.SECTION.SUBHEADER', 'screen1', 'E-Mail-Adresse und API-Token eingeben', 'Enter your Stackfield email and API token') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email'); + ('TXT.SCREEN1.STACKFIELD.EMAIL.LABEL', 'screen1', 'Stackfield E-Mail', 'Stackfield Email') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token'); + ('TXT.SCREEN1.STACKFIELD.TOKEN.LABEL', 'screen1', 'Stackfield API-Token', 'Stackfield API Token') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials'); + ('TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON', 'screen1', 'Stackfield Zugangsdaten prüfen', 'Verify Stackfield Credentials') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication'); + ('TXT.SCREEN1.STACKFIELD.VERIFY.NOTIFY', 'screen1', 'Stackfield Authentifizierung', 'Stackfield Authentication') ON CONFLICT (id) DO NOTHING; -- Screen 2 – Search INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues'); + ('TXT.SCREEN2.SEARCH.SECTION.HEADER', 'screen2', 'GitHub Issues suchen', 'Search GitHub Issues') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term'); + ('TXT.SCREEN2.SEARCH.SECTION.SUBHEADER', 'screen2', 'Repository und Suchbegriff eingeben', 'Enter the repository and an optional search term') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)'); + ('TXT.SCREEN2.SEARCH.QUERY.LABEL', 'screen2', 'Suchbegriff (optional)', 'Search query (optional)') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)'); + ('TXT.SCREEN2.SEARCH.REPO.LABEL', 'screen2', 'Repository (owner/repo)', 'Repository (owner/repo)') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues'); + ('TXT.SCREEN2.SEARCH.BUTTON', 'screen2', 'Issues suchen', 'Search Issues') ON CONFLICT (id) DO NOTHING; -- Screen 2 – Issue List columns INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#'); + ('TXT.SCREEN2.LIST.COL.NUMBER', 'screen2', 'Nr.', '#') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title'); + ('TXT.SCREEN2.LIST.COL.TITLE', 'screen2', 'Titel', 'Title') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State'); + ('TXT.SCREEN2.LIST.COL.STATE', 'screen2', 'Status', 'State') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created'); + ('TXT.SCREEN2.LIST.COL.CREATED', 'screen2', 'Erstellt', 'Created') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee'); + ('TXT.SCREEN2.LIST.COL.ASSIGNEE', 'screen2', 'Zugewiesen', 'Assignee') ON CONFLICT (id) DO NOTHING; -- Screen 2 – Context menu INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task'); + ('TXT.SCREEN2.CONTEXTMENU.CONNECT', 'screen2', 'Stackfield-Aufgabe verbinden', 'Connect Stackfield Task') ON CONFLICT (id) DO NOTHING; -- Screen 3 – GitHub Issue Details INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties'); + ('TXT.SCREEN3.ISSUE.SECTION.HEADER', 'screen3', 'GitHub Issue Eigenschaften', 'GitHub Issue Properties') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API'); + ('TXT.SCREEN3.ISSUE.SECTION.SUBHEADER', 'screen3', 'Daten aus der GitHub API', 'Data from the GitHub API') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number'); + ('TXT.SCREEN3.ISSUE.NUMBER.LABEL', 'screen3', 'Issue-Nummer', 'Issue Number') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State'); + ('TXT.SCREEN3.ISSUE.STATE.LABEL', 'screen3', 'Status', 'State') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title'); + ('TXT.SCREEN3.ISSUE.TITLE.LABEL', 'screen3', 'Titel', 'Title') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL'); + ('TXT.SCREEN3.ISSUE.URL.LABEL', 'screen3', 'GitHub URL', 'GitHub URL') ON CONFLICT (id) DO NOTHING; -- Screen 3 – Stackfield Mapping INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping'); + ('TXT.SCREEN3.STACKFIELD.SECTION.HEADER', 'screen3', 'Stackfield Aufgaben-Zuordnung', 'Stackfield Task Mapping') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details'); + ('TXT.SCREEN3.STACKFIELD.SECTION.SUBHEADER', 'screen3', 'Ziel-Raum und Aufgaben-Details', 'Target room and task details') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID'); + ('TXT.SCREEN3.STACKFIELD.ROOM.LABEL', 'screen3', 'Stackfield Raum-ID', 'Stackfield Room ID') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title'); + ('TXT.SCREEN3.TASK.TITLE.LABEL', 'screen3', 'Aufgaben-Titel', 'Task Title') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description'); + ('TXT.SCREEN3.TASK.DESC.LABEL', 'screen3', 'Beschreibung', 'Description') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority'); + ('TXT.SCREEN3.TASK.PRIORITY.LABEL', 'screen3', 'Priorität', 'Priority') ON CONFLICT (id) DO NOTHING; -- Screen 3 – Priority options INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low'); + ('TXT.SCREEN3.PRIORITY.LOW', 'screen3', 'Niedrig', 'Low') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium'); + ('TXT.SCREEN3.PRIORITY.MEDIUM', 'screen3', 'Mittel', 'Medium') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High'); + ('TXT.SCREEN3.PRIORITY.HIGH', 'screen3', 'Hoch', 'High') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent'); + ('TXT.SCREEN3.PRIORITY.URGENT', 'screen3', 'Dringend', 'Urgent') ON CONFLICT (id) DO NOTHING; -- Screen 3 – Create button INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task'); + ('TXT.SCREEN3.CREATE.BUTTON', 'screen3', 'Neue Stackfield-Aufgabe erstellen', 'Create New Stackfield Task') ON CONFLICT (id) DO NOTHING; INSERT INTO webui.text (id, "group", value_de, value_en) VALUES - ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task'); + ('TXT.SCREEN3.CREATE.NOTIFY', 'screen3', 'Stackfield Aufgabe erstellen', 'Create Stackfield Task') ON CONFLICT (id) DO NOTHING; diff --git a/docker/Dockerfile b/docker/Dockerfile index 0a679e7..0918fc4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,3 +11,12 @@ RUN pip3 install --no-cache-dir --break-system-packages \ psycopg2-binary \ pgdbpool \ git+https://github.com/clauspruefer/python-micro-esb.git + +# Copy static x0 config files into the web-server document root +COPY static/ /var/www/vhosts/x0/static/github2sf/ + +# Copy Python WSGI backend scripts into the x0 python directory +COPY python/ /var/www/vhosts/x0/python/github2sf/ + +# Install the supplementary Apache2 configuration snippet +COPY docker/apache2.conf /etc/apache2/conf-enabled/github2sf.conf diff --git a/docker/Dockerfile.db b/docker/Dockerfile.db new file mode 100644 index 0000000..83bea28 --- /dev/null +++ b/docker/Dockerfile.db @@ -0,0 +1,11 @@ +FROM ghcr.io/webcodex1/x0-db + +# Copy the github2stackfield SQL init scripts into the container +COPY database/ /opt/github2sf-init/ + +# Copy the custom startup script and make it executable +COPY docker/db-init.sh /root/github2sf-start.sh +RUN chmod +x /root/github2sf-start.sh + +# Override the default x0-db startup with our initialising script +CMD ["/root/github2sf-start.sh"] diff --git a/docker/db-init.sh b/docker/db-init.sh new file mode 100755 index 0000000..02172e2 --- /dev/null +++ b/docker/db-init.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# ]*[ --------------------------------------------------------------------- ]*[ +# . github2stackfield - x0-db automatic database initialisation . +# . . +# . Replaces the default x0-db start script. . +# . Starts PostgreSQL, waits until it accepts connections, applies the . +# . github2stackfield SQL scripts against the x0 database, then keeps . +# . the server running. . +# ]*[ --------------------------------------------------------------------- ]*[ +set -e + +chown -R postgres:postgres /var/lib/postgresql/16/main/ +chown -R postgres:postgres /var/run/postgresql/ + +# Start PostgreSQL in the background +su -c "/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main" postgres & +PG_PID=$! + +# Wait until PostgreSQL is ready to accept connections +echo "Waiting for PostgreSQL to be ready..." +until su -c "pg_isready -q -U postgres -d x0" postgres 2>/dev/null; do + sleep 1 +done +echo "PostgreSQL is ready." + +# Apply github2stackfield database init scripts to the x0 database. +# All scripts are idempotent and safe to re-run on container restart. +echo "Applying github2stackfield database init scripts..." +su -c "psql -d x0 -f /opt/github2sf-init/01-create-schema.sql" postgres +su -c "psql -d x0 -f /opt/github2sf-init/02-insert-config.sql" postgres +su -c "psql -d x0 -f /opt/github2sf-init/03-insert-text.sql" postgres +echo "Database initialisation complete." + +# Keep the container alive by waiting for the PostgreSQL process +wait $PG_PID diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d58a2ae..bb942dc 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,14 @@ services: # --------------------------------------------------------------------------- - # x0 database container (PostgreSQL with x0 schema pre-installed) - # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db + # x0 database container — extends the official x0-db image and + # automatically initialises the github2stackfield schema + data on startup. + # Official base: https://github.com/WEBcodeX1/x0/pkgs/container/x0-db # --------------------------------------------------------------------------- x0-db: - image: ghcr.io/webcodex1/x0-db + build: + context: .. + dockerfile: docker/Dockerfile.db container_name: github2sf-db restart: unless-stopped ports: @@ -17,8 +20,9 @@ services: - mypostgres # --------------------------------------------------------------------------- - # x0 application server (extends official x0-app with github2sf Python deps) - # Official image: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app + # x0 application server — extends the official x0-app image and + # bundles all static / Python backend / Apache config at build time. + # Official base: https://github.com/WEBcodeX1/x0/pkgs/container/x0-app # --------------------------------------------------------------------------- x0-app: build: @@ -30,10 +34,6 @@ services: - x0-db extra_hosts: - "mypostgres:172.20.0.20" - volumes: - - ../static:/var/www/vhosts/x0/static/github2sf:ro - - ../python:/var/www/vhosts/x0/python/github2sf:ro - - ../docker/apache2.conf:/etc/apache2/conf-enabled/github2sf.conf:ro ports: - "8080:80" networks: From b878e52a76d6cff93715a5fb89e7d4ff2b3204ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20Pr=C3=BCfer?= Date: Wed, 27 May 2026 12:25:19 +0200 Subject: [PATCH 4/6] Add initial AI prompt --- AI-prompt.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 AI-prompt.md diff --git a/AI-prompt.md b/AI-prompt.md new file mode 100644 index 0000000..23bec37 --- /dev/null +++ b/AI-prompt.md @@ -0,0 +1,16 @@ +Build an x0 application using the x0 framework from https://github.com/WEBcodeX1/x0, +The backend in Python should use the https://github.com/clauspruefer/python-micro-esb project +The CSS styling is done with the x0 contained Bootstrap default theme and should look modern. +The browser-frontend consists of 3 x0 screens / 3 menu entries: +1. Screen1 "User Credentials": contains 2 groups of formfields (x0 object of type `sysObjectFormfieldList`): group a) GitHub API User/Token credentials group b) Stackfield API User/Token Credentials +1.1. On each formfield group there must be a button "verify user credentials" to test if the API authentication is working +2. Screen2 "Issue / Task Mapping": contains the mapping between GitHub Issues and Stackfield Tasks +2.1. On top there should be 1 formfield / search input and a search execution button (x0 object of type `sysObjectFormfieldList`) +2.2. Under the search must be a list (x0 object of type `sysObjectList` containing the search results +2.3. The search result list from 2.2. contains a x0 object of type `sysObjContextMenu` with one entry "Connect Stackfield Task" +2.4. On button click from 2.1 the relevant python-micro-esb backend service must be called and the given result list from 2.2 must be filled with the result data from the backend service +3. Screen3 "Connect Stackfield Task" +3.1. Contains detailed GitHub Task properties from GitHub API webservice result (encapsulated from python-micro-esb backend), e.g. 3 or four formfields describing the task properties from API result +3.2. On right click of screen 2 object result display list single row, the GitHub task properties fields of Screen3 will be updated from the row data +3.3. Also there should be relevant Stackfield properties where to map the Stackfield Task (Stackfield room or similar, please find the best possible fields from the API to map) +3.4. At the bottom there must be 1 button (x0 object type `sysObjButton`), on click a NEW STACKFIELD TASK will be generated from the relevant GitHub Issue/Task data from Screen3 formfield data. From 372a76ddd613799a0d9d84c8d14c885adc222244 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 10:33:52 +0000 Subject: [PATCH 5/6] fix: resolve missing stylesheets and WSGI 404 errors --- docker/Dockerfile | 7 +++++++ python/CreateStackfieldTask.py | 4 +++- python/GetGitHubIssueDetails.py | 4 +++- python/SearchGitHubIssues.py | 4 +++- python/VerifyGitHubCredentials.py | 4 +++- python/VerifyStackfieldCredentials.py | 4 +++- static/object.json | 10 +++++----- 7 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0918fc4..6707704 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,6 +15,13 @@ RUN pip3 install --no-cache-dir --break-system-packages \ # Copy static x0 config files into the web-server document root COPY static/ /var/www/vhosts/x0/static/github2sf/ +# Link the x0 base stylesheets and fontawesome into the app subdir so that +# the x0 framework can find bootstrap.css, globalstyles.css and all.min.css +# at the {subdir}/ path it generates in the HTML tags. +RUN ln -s /var/www/vhosts/x0/static/bootstrap.css /var/www/vhosts/x0/static/github2sf/bootstrap.css \ + && ln -s /var/www/vhosts/x0/static/globalstyles.css /var/www/vhosts/x0/static/github2sf/globalstyles.css \ + && ln -s /var/www/vhosts/x0/static/fontawesome /var/www/vhosts/x0/static/github2sf/fontawesome + # Copy Python WSGI backend scripts into the x0 python directory COPY python/ /var/www/vhosts/x0/python/github2sf/ diff --git a/python/CreateStackfieldTask.py b/python/CreateStackfieldTask.py index 76513e8..795b8c0 100644 --- a/python/CreateStackfieldTask.py +++ b/python/CreateStackfieldTask.py @@ -11,10 +11,12 @@ import sys import json +sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf') + import POSTData from StdoutLogger import logger -from router import ServiceRouter +from microesb.router import ServiceRouter def application(environ, start_response): diff --git a/python/GetGitHubIssueDetails.py b/python/GetGitHubIssueDetails.py index d4d345f..f008efb 100644 --- a/python/GetGitHubIssueDetails.py +++ b/python/GetGitHubIssueDetails.py @@ -11,10 +11,12 @@ import sys import json +sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf') + import POSTData from StdoutLogger import logger -from router import ServiceRouter +from microesb.router import ServiceRouter def application(environ, start_response): diff --git a/python/SearchGitHubIssues.py b/python/SearchGitHubIssues.py index b67f3d1..f3dd07f 100644 --- a/python/SearchGitHubIssues.py +++ b/python/SearchGitHubIssues.py @@ -11,10 +11,12 @@ import sys import json +sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf') + import POSTData from StdoutLogger import logger -from router import ServiceRouter +from microesb.router import ServiceRouter def application(environ, start_response): diff --git a/python/VerifyGitHubCredentials.py b/python/VerifyGitHubCredentials.py index 4c8f791..8b5121c 100644 --- a/python/VerifyGitHubCredentials.py +++ b/python/VerifyGitHubCredentials.py @@ -10,10 +10,12 @@ import sys import json +sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf') + import POSTData from StdoutLogger import logger -from router import ServiceRouter +from microesb.router import ServiceRouter def application(environ, start_response): diff --git a/python/VerifyStackfieldCredentials.py b/python/VerifyStackfieldCredentials.py index f4206b2..b2b7eb9 100644 --- a/python/VerifyStackfieldCredentials.py +++ b/python/VerifyStackfieldCredentials.py @@ -10,10 +10,12 @@ import sys import json +sys.path.insert(0, '/var/www/vhosts/x0/python/github2sf') + import POSTData from StdoutLogger import logger -from router import ServiceRouter +from microesb.router import ServiceRouter def application(environ, start_response): diff --git a/static/object.json b/static/object.json index e11a243..ae828f7 100644 --- a/static/object.json +++ b/static/object.json @@ -122,7 +122,7 @@ "TextID": "TXT.SCREEN1.GITHUB.VERIFY.BUTTON", "Style": "btn btn-outline-primary w-100 mb-4", "IconStyle": "fa-solid fa-shield-halved", - "OnClick": "/python/VerifyGitHubCredentials.py", + "OnClick": "/python/github2sf/VerifyGitHubCredentials.py", "SrcDataObjects": [ "GitHubCredentialsForm" @@ -225,7 +225,7 @@ "TextID": "TXT.SCREEN1.STACKFIELD.VERIFY.BUTTON", "Style": "btn btn-outline-success w-100 mb-4", "IconStyle": "fa-solid fa-shield-halved", - "OnClick": "/python/VerifyStackfieldCredentials.py", + "OnClick": "/python/github2sf/VerifyStackfieldCredentials.py", "SrcDataObjects": [ "StackfieldCredentialsForm" @@ -340,7 +340,7 @@ "OnEvent": { "Events": [ "SearchGitHubIssues" ], - "ServiceCall": "/python/SearchGitHubIssues.py" + "ServiceCall": "/python/github2sf/SearchGitHubIssues.py" }, "SrcDataObjects": { @@ -426,7 +426,7 @@ "OnEvent": { "Events": [ "ConnectStackfieldTask" ], - "ServiceCall": "/python/GetGitHubIssueDetails.py" + "ServiceCall": "/python/github2sf/GetGitHubIssueDetails.py" }, "SrcDataObjects": { @@ -755,7 +755,7 @@ "TextID": "TXT.SCREEN3.CREATE.BUTTON", "Style": "btn btn-success w-100 mt-2 mb-4", "IconStyle": "fa-solid fa-circle-plus", - "OnClick": "/python/CreateStackfieldTask.py", + "OnClick": "/python/github2sf/CreateStackfieldTask.py", "SrcDataObjects": [ "IssueDetailsForm", From 55f34e51bf269d3da3d51acbb06ceb01152d114c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 10:48:03 +0000 Subject: [PATCH 6/6] fix: display menu buttons vertically (stacked) instead of side by side --- static/object.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/object.json b/static/object.json index ae828f7..2ec2c91 100644 --- a/static/object.json +++ b/static/object.json @@ -4,7 +4,7 @@ "Type": "Link", "Attributes": { - "Style": "btn btn-outline-secondary me-2", + "Style": "btn btn-outline-secondary d-block mb-2", "TextID": "TXT.MENU.SCREEN1", "ScreenID": "Screen1" } @@ -15,7 +15,7 @@ "Type": "Link", "Attributes": { - "Style": "btn btn-outline-secondary me-2", + "Style": "btn btn-outline-secondary d-block mb-2", "TextID": "TXT.MENU.SCREEN2", "ScreenID": "Screen2" } @@ -26,7 +26,7 @@ "Type": "Link", "Attributes": { - "Style": "btn btn-outline-secondary me-2", + "Style": "btn btn-outline-secondary d-block mb-2", "TextID": "TXT.MENU.SCREEN3", "ScreenID": "Screen3" }