Extract Opportunities into its own module; track Conference Scout as code

- opportunities/: opportunities.html (markup), opportunities.js (calendar,
  event list, scout list, drill-view logic), opportunities.css — loaded by
  index.html via fetch + dynamic <script>, replacing the inline copy that
  used to live directly in the monolith
- Shared shell (index.html) no longer hardcodes Opportunities-specific
  drill-view config; the module registers itself into HERO_NAV_BY_TAB /
  DRILL_CONFIG / DRILL_DEFAULT_VIEW at load time, and setDrillView now
  tolerates scopes that haven't registered yet
- agents/conference-scout/: the scheduled cloud routine's actual definition
  (schedule, model, tools, full prompt) as a tracked config.json, plus a
  README covering where it really runs (Anthropic's cloud, no server-side
  component here), how to keep it in sync, how to recreate it if deleted,
  and its current WebFetch/egress limitation

Team/Directory/Marketing/Sales remain inline in index.html for now — a
separate, larger pass given their size and interdependencies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-08-17 13:26:32 +02:00
1 parent 6afaefc251
commit 8d82a6d2ca
6 files changed
+559 -456

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# Conference Scout
A scheduled cloud agent (Claude Code "routine") that searches the web every Monday morning for conferences, trade shows, seminars, and networking events relevant to TXproducts, and produces a report for manual review. It does not write to this repo, any calendar, or any file — a person reviews its report and decides what to add to the Opportunities tab's Events list themselves.
## Where it actually runs
This agent does **not** run on TXproducts infrastructure and has no server-side component anywhere. It runs entirely as a scheduled routine on Anthropic's cloud (via the Claude Code "routines" feature), triggered by cron and executed in an isolated sandbox with `WebSearch`/`WebFetch` access only. There is no code to deploy or host — the routine's entire behavior is defined by the prompt in [`config.json`](./config.json).
Live routine: https://claude.ai/code/routines/trig_01XRb81y9pAWDube5kjRUvGT
## What's in this folder
- **`config.json`** — the routine's definition: schedule, model, allowed tools, and the full prompt that defines its scope (what counts as "relevant," what's explicitly excluded, geographic coverage, output format). This is the actual source of truth for *what the agent does* — read the prompt to understand its behavior.
**This file is a manually-maintained mirror, not a live sync.** If someone edits the routine directly on claude.ai, this file goes stale until someone updates it here to match. There is currently no automation keeping the two in sync.
## How to update it
1. Change the routine itself first (via the Claude Code `RemoteTrigger` tool's `update` action, or the claude.ai UI), and confirm the change actually took by fetching it back (`RemoteTrigger` `get` action).
2. Copy the confirmed live prompt/schedule/config into `config.json` here, update `_live_reference.last_synced_from_live`, and commit.
## How to recreate it if it's ever deleted
`config.json`'s top-level shape (`name`, `cron_expression`, `enabled`, `job_config`) matches the `RemoteTrigger` `create` action's request body directly — it can be passed to recreate the routine from scratch (you'll get a new `trigger_id`; update `_live_reference` afterward).
## Why results aren't fully automatic
Each run is a fresh, isolated cloud session with no memory of prior runs and no write access to this repo (by design — see the prompt's explicit "do NOT add anything to any file, calendar, or repository" instruction). Syncing a run's results into the Opportunities tab's Conference Scout view (`opportunities/opportunities.js`, `SCOUT_RESULTS`) is a manual step done by whoever reviews that week's report.
## Known limitation (as of 2026-08-17)
The routine's sandbox has consistently blocked all `WebFetch` calls to external sites — even neutral test domains like wikipedia.org — via a network egress policy, despite the prompt instructing it to verify facts via `WebFetch` on official event pages. In practice it falls back to cross-referencing multiple independent `WebSearch` results instead. Every synced report should be read as "search-corroborated," not "primary-source-verified," until this is resolved (if it ever can be, since it appears to be an environment-level restriction rather than a prompt issue).
+35
View File
@@ -0,0 +1,35 @@
{
"name": "TXproducts Conference Scout",
"cron_expression": "0 6 * * 1",
"cron_expression_human": "Every Monday 06:00 UTC (08:00 Europe/Berlin)",
"enabled": true,
"job_config": {
"ccr": {
"environment_id": "env_01Bfoxwtqvau2Pxk87vxAWiH",
"session_context": {
"model": "claude-sonnet-5",
"sources": [],
"allowed_tools": ["WebSearch", "WebFetch"]
},
"events": [
{
"data": {
"type": "user",
"parent_tool_use_id": null,
"session_id": "",
"message": {
"role": "user",
"content": "You are a research scout for TXproducts, a company whose business is: (1) buying synchrotron beamtime and using a device called Wavegate to cut experiment time, then selling beamtime access and X-ray data services to research and industry customers — data originates at beamline facilities such as DESY Hamburg. TXproducts' current industry focus is: semiconductors and power electronics (especially gallium nitride/GaN and silicon carbide/SiC, and other wide-bandgap semiconductor hardware), X-ray optics, and photon science / nanoscience, plus materials science more broadly. TXproducts actually attends events like SPIE Optics + Photonics (San Diego, USA) and SEMICON Europa (Munich) — use those as a sense of the right level and type of event. (2) TXproducts also builds scientific data infrastructure: a custom HDF5 spec (TXdata) for beamline XRD + Image data.\n\nTXproducts is ALSO actively looking to expand into new business — specifically defense and aerospace applications, wherever power electronics (GaN/SiC), semiconductor hardware, or X-ray/materials characterization intersects with defense or aerospace needs (e.g. radar, avionics, space hardware, RF power devices). This is prospecting for potential new business, not an established customer base yet — include it as a distinct category and flag it as such rather than implying TXproducts already has defense/aerospace customers.\n\nGEOGRAPHIC SCOPE: Search worldwide, not just Europe. Travel is not a constraint — actively include events in the US, China, other parts of Asia (Japan, South Korea, Taiwan, Singapore), and Africa, alongside Europe. Do not default to only European/DESY-adjacent events just because that's the home base — e.g. explicitly check for things like APEC, PCIM Asia, SEMICON China/Korea/Taiwan/Japan, CS International, ISPSD, IEDM, and any major power-electronics/GaN/SiC/semiconductor events in Asia or the Americas, plus any relevant events on the African continent.\n\nDo NOT include: pharma, drug discovery, or structural biology events (not a current focus); and do NOT include semantic-web / ontology / knowledge-graph / controlled-vocabulary conferences — that is an unrelated personal side project of one team member, not TXproducts business, and should never factor into this search.\n\nYour job: search the web for upcoming events relevant to this business (including the defense/aerospace expansion prospecting), across ALL relevant regions worldwide, and produce a report of candidates. Cast a wide net on FORMAT, not just headline trade shows — include large conferences and exhibitions, smaller technical seminars/workshops, industry-association networking events and mixers, and local/regional meetups, as long as the topical fit is genuine. Do NOT add anything to any file, calendar, or repository — this is a research report only, delivered as your final message text.\n\nRelevant domains to search across:\n- Synchrotron / X-ray facility science (beamline user meetings e.g. DESY, ESRF, Diamond, APS, SPring-8, SSRF; X-ray characterization methods, XRD, imaging)\n- Semiconductors and power electronics — especially gallium nitride (GaN), silicon carbide (SiC), and other wide-bandgap semiconductor hardware\n- X-ray optics (sources, components, instrumentation)\n- Photon science and nanoscience\n- Materials science conferences with a strong X-ray/characterization component\n- Defense and aerospace electronics — where power electronics (GaN/SiC), semiconductor hardware, or X-ray/materials characterization applies to defense or aerospace use cases (radar, avionics, space hardware, RF power devices); mark these clearly as \"potential new business\" rather than an existing customer relationship\n- Scientific data management, FAIR data, research data infrastructure (e.g. HDF5-based formats)\n- Semiconductor metrology / advanced packaging, particularly where it connects to power electronics, GaN, or SiC — general semiconductor manufacturing/fab trade shows are in scope only if they have a clear X-ray-metrology, GaN/SiC/power-electronics, or scientific-data angle\n\nInclude a mix of event formats where they exist: major conferences/trade shows, technical seminars/workshops, and industry networking events/meetups (e.g. regional industry-association mixers, chamber-of-commerce or trade-group networking nights in the relevant sectors). Note the format type for each entry, and note the region/country clearly.\n\nFor each candidate event, verify basic facts (dates, location, official website) using WebFetch on the event's own site before including it — do not rely on search snippets alone. Prefer events with confirmed, concrete dates over vague or unconfirmed ones.\n\nOutput a report with one entry per event, each containing:\n- Title\n- Format (conference / trade show / seminar / workshop / networking event)\n- Dates\n- Location (city, country)\n- Official website URL\n- One-line reason it's relevant to TXproductsLine truncated
}
}
}
]
}
},
"_live_reference": {
"note": "This block is a snapshot for humans, not consumed by the API. It is NOT kept in sync automatically — update it manually whenever the live routine is changed.",
"trigger_id": "trig_01XRb81y9pAWDube5kjRUvGT",
"routine_url": "https://claude.ai/code/routines/trig_01XRb81y9pAWDube5kjRUvGT",
"last_synced_from_live": "2026-08-17"
}
}
+25 -456
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>TXproducts Sales &amp; Marketing</title>
<link rel="stylesheet" href="opportunities/opportunities.css">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; }
@@ -728,86 +729,6 @@
background: #f0f4f8; border: 1px solid #e0e7ef; border-radius: 6px; padding: 4px 9px;
}
.links a:hover { border-color: #2d6a9f; }
/* ---------- Opportunities (calendar) ---------- */
.opp-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.opp-month-label { font-size: 1.15rem; font-weight: 800; color: #1b3a5c; }
.opp-nav-btn {
font-family: inherit; font-size: 0.85rem; font-weight: 700; color: #2d6a9f;
background: #fff; border: 1px solid #e0e7ef; border-radius: 6px; padding: 6px 12px; cursor: pointer;
}
.opp-nav-btn:hover { background: #e8f0fe; }
.opp-today-btn { margin-left: 8px; }
.opp-cal-grid {
border-top: 1px solid #e0e7ef; border-left: 1px solid #e0e7ef;
}
.opp-cal-weekdays, .opp-cal-week {
display: grid; grid-template-columns: repeat(7, 1fr);
}
.opp-cal-week { position: relative; }
.opp-cal-weekday {
text-align: center; font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.04em; color: #6b7a8d; padding: 8px 0;
border-right: 1px solid #e0e7ef; border-bottom: 1px solid #e0e7ef; background: #f7f9fc;
}
.opp-cal-cell {
min-height: 96px; border-right: 1px solid #e0e7ef; border-bottom: 1px solid #e0e7ef;
padding: 8px;
}
.opp-cal-cell.outside { background: #fafbfc; }
.opp-cal-cell.outside .opp-cal-daynum { color: #c3cdd8; }
.opp-cal-daynum { font-size: 0.82rem; font-weight: 700; color: #1a2533; }
.opp-cal-cell.today .opp-cal-daynum {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px; border-radius: 50%; background: #2d6a9f; color: #fff;
}
.opp-cal-event-bar {
position: absolute; height: 20px; line-height: 20px; border-radius: 4px;
background: #2d6a9f; color: #fff; font-size: 0.72rem; font-weight: 700;
padding: 0 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
cursor: default;
}
.opp-cal-event-bar .opp-cal-event-title { color: #ffd43b; font-weight: 800; text-decoration: none; }
a.opp-cal-event-title:hover { text-decoration: underline; }
.opp-cal-event-bar .opp-cal-event-loc { color: #fff; font-weight: 600; opacity: 0.82; margin-left: 6px; }
.opp-cal-event-bar.continues-left { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.opp-cal-event-bar.continues-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#tab-opportunities .drill-view { padding: 0; }
.opp-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 900px) { .opp-layout { grid-template-columns: 1fr; } }
.opp-list-card { padding: 24px; }
.opp-list-heading { margin: 0 0 16px; font-size: 1.05rem; font-weight: 800; color: #1b3a5c; }
.opp-list { display: flex; flex-direction: column; gap: 10px; }
.opp-list-empty { color: #6b7a8d; font-size: 0.9rem; }
.opp-list-row {
display: flex; align-items: flex-start; gap: 12px; padding: 12px;
border: 1px solid #e0e7ef; border-radius: 8px; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.opp-list-row:hover { border-color: #2d6a9f; background: #f7f9fc; }
.opp-list-meta {
flex: 0 0 128px; display: flex; flex-direction: column; gap: 3px;
font-size: 0.82rem; line-height: 1.5;
}
.opp-list-dates { font-weight: 700; color: #1a2533; }
.opp-list-loc, .opp-list-attendee { color: #6b7a8d; }
.opp-list-link {
display: inline-block; margin-top: 3px; font-size: 0.78rem; font-weight: 700;
color: #2d6a9f; text-decoration: underline;
}
.opp-list-about { flex: 1; border-left: 1px solid #eef1f5; padding-left: 16px; }
.opp-scout-note {
background: #fbf0df; border: 1px solid #f3ddb8; border-radius: 8px;
padding: 12px 14px; margin-bottom: 16px; font-size: 0.82rem; line-height: 1.55; color: #7a4a06;
}
.opp-scout-note strong { color: #b45f06; }
.opp-list-badge {
display: inline-block; background: #fbf0df; color: #b45f06; font-weight: 800;
font-size: 0.78rem; border-radius: 20px; padding: 5px 12px; text-decoration: none; white-space: nowrap;
}
a.opp-list-badge:hover { text-decoration: underline; }
.opp-list-abstract { margin: 8px 0 0; font-size: 0.82rem; line-height: 1.55; color: #3d4c5c; }
</style>
</head>
<body>
@@ -4562,43 +4483,7 @@
</div>
<div id="tab-opportunities" class="tab-content">
<div class="drill-crumbs" id="opp-crumbs" style="margin-bottom: 14px;"></div>
<div class="drill-view active" data-view="opportunities-root">
<div class="opp-layout">
<div class="card opp-list-card">
<h3 class="opp-list-heading">Events</h3>
<p class="narrative" style="max-width: none;">Conferences, events, and potential opportunities for TXproducts.</p>
<div class="opp-list" id="opp-event-list"></div>
</div>
<div class="card opp-cal-card" style="padding: 0;">
<div style="padding: 24px 32px 28px;">
<div class="opp-toolbar">
<div class="opp-month-label" id="opp-month-label"></div>
<div>
<button class="opp-nav-btn" id="opp-prev-btn">← Prev</button>
<button class="opp-nav-btn opp-today-btn" id="opp-today-btn">Today</button>
<button class="opp-nav-btn" id="opp-next-btn">Next →</button>
</div>
</div>
<div class="opp-cal-grid" id="opp-cal-grid"></div>
</div>
</div>
</div>
</div>
<div class="drill-view" data-view="scout">
<div class="card">
<h2>Conference Scout</h2>
<p class="narrative" style="max-width: none;">A scheduled research agent that sweeps the web every Monday morning for conferences, trade shows, seminars, and networking events relevant to TXproducts — semiconductors, power electronics (GaN/SiC), X-ray optics, photon/nanoscience, and defense/aerospace expansion prospecting. Review the candidates below, then add anything worth tracking to the Events list yourself — nothing here is added automatically.</p>
<div class="opp-scout-note"><strong>Verification note:</strong> this run's sandbox blocked direct fetches to every official event site (even neutral test domains), so dates and locations below are cross-referenced across multiple independent search sources rather than confirmed on the primary page — double-check the official site before committing budget or travel. This is a fresh worldwide sweep (12 candidates, replacing the previous Europe-only list) spanning Asia, the Americas, Africa, and Europe, tagged "Existing focus area" or "Potential new business (defense)" per TXproducts' corrected scope: semiconductors/power electronics (GaN/SiC), X-ray optics, photon/nanoscience, synchrotron/beamline science, and defense/aerospace prospecting.</div>
<div class="opp-list" id="opp-scout-list"></div>
<div class="drill-actions" style="margin-top: 18px;">
<button class="drill-back" data-target="opportunities-root">← Opportunities</button>
<a class="open-link" href="https://claude.ai/code/routines/trig_01XRb81y9pAWDube5kjRUvGT" target="_blank" rel="noopener">Open Conference Scout ↗</a>
</div>
</div>
</div>
<div class="opp-loading">Loading…</div>
</div>
</div>
@@ -4629,8 +4514,8 @@
/* ---- Drill-downs: Marketing -> Social Media (dropdown) -> LinkedIn -> Dash
Sales -> Market Intelligence -> site (goes there directly, one child today) ---- */
var HERO_NAV_BY_TAB = { marketing: ["hero-nav-social", "hero-nav-conferences"], opportunities: ["hero-nav-scout"] };
var DRILL_DEFAULT_VIEW = { marketing: "marketing-root", sales: "market-intelligence", opportunities: "opportunities-root" };
var HERO_NAV_BY_TAB = { marketing: ["hero-nav-social", "hero-nav-conferences"] };
var DRILL_DEFAULT_VIEW = { marketing: "marketing-root", sales: "market-intelligence" };
var DRILL_CONFIG = {
marketing: {
crumbsEl: "mkt-crumbs",
@@ -4646,22 +4531,20 @@
"sales-root": [["Sales", "sales-root"]],
"market-intelligence": [["Sales", "sales-root"], ["Intelligence Board", "market-intelligence"], ["Market Intelligence", "market-intelligence"]]
}
},
opportunities: {
crumbsEl: "opp-crumbs",
crumbs: {
"opportunities-root": [["Opportunities", "opportunities-root"]],
"scout": [["Opportunities", "opportunities-root"], ["Conference Scout", "scout"]]
}
}
};
/* Feature modules loaded async (see opportunities/opportunities.js) register their own
HERO_NAV_BY_TAB / DRILL_DEFAULT_VIEW / DRILL_CONFIG entries once their partial HTML
is injected — setDrillView tolerates scopes that aren't registered yet. */
function setDrillView(scope, view) {
var cfg = DRILL_CONFIG[scope];
if (!cfg) return;
document.querySelectorAll("#tab-" + scope + " .drill-view").forEach(function (v) {
v.classList.toggle("active", v.getAttribute("data-view") === view);
});
var cfg = DRILL_CONFIG[scope];
var crumbTrail = cfg.crumbs[view];
var crumbsEl = document.getElementById(cfg.crumbsEl);
if (!crumbsEl) return;
crumbsEl.innerHTML = "";
crumbTrail.forEach(function (pair, i) {
if (i > 0) {
@@ -4680,7 +4563,7 @@
crumbsEl.appendChild(el);
});
}
["marketing", "sales", "opportunities"].forEach(function (scope) {
["marketing", "sales"].forEach(function (scope) {
document.querySelectorAll("#tab-" + scope + " .drill-back, #tab-" + scope + " .drill-next").forEach(function (b) {
b.addEventListener("click", function () { setDrillView(scope, b.getAttribute("data-target")); });
});
@@ -4708,9 +4591,6 @@
document.getElementById("hero-nav-conferences-btn").addEventListener("click", function () {
setDrillView("marketing", "conferences");
});
document.getElementById("hero-nav-scout-btn").addEventListener("click", function () {
setDrillView("opportunities", "scout");
});
/* ---- Conferences (Directory pattern: conference cards on the left, drill into
its own presentation sub-directory on the right) ---- */
@@ -6006,332 +5886,21 @@
document.getElementById("ab-view-board-btn").click();
document.querySelector(".ab-filter-chip[data-source=\"Outreach\"]").click();
/* ---------- Opportunities calendar ----------
Each entry: { start: "YYYY-MM-DD", end: "YYYY-MM-DD" (defaults to start),
title, location, attendee (optional) }.
Multi-day entries render as one bar spanning their full date range;
bars continuing past a week boundary lose the rounded corner on that side. */
var OPPORTUNITIES = [
{ start: "2026-08-23", end: "2026-08-27", title: "SPIE", location: "San Diego, CA", attendee: "Peter", url: "https://spie.org/conferences-and-exhibitions/optics-and-photonics",
abstract: "SPIE's flagship annual optics and photonics conference and exhibition, held every August in San Diego since the 1960s. Brings together 10,000+ scientists, engineers, and industry professionals across optical engineering, nanoscience, quantum science, and organic photonics." },
{ start: "2026-11-10", end: "2026-11-13", title: "SEMICON Europa", location: "Munich, Germany", url: "https://www.semiconeuropa.org/",
abstract: "Semiconductor and electronics manufacturing trade show co-located with electronica in Munich. Covers fab management, advanced packaging, and smart manufacturing across the chip supply chain, drawing 8,000+ visitors from equipment vendors, fabs, and OSATs." }
];
var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var oppCursor = new Date();
oppCursor.setDate(1);
function oppKey(d) {
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}
function parseOppDate(s) {
var parts = s.split("-").map(Number);
return new Date(parts[0], parts[1] - 1, parts[2]);
}
function addOppDays(d, n) {
var r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
function buildOppEvents() {
return OPPORTUNITIES.map(function (o) {
return {
title: o.title, location: o.location, attendee: o.attendee, url: o.url, abstract: o.abstract,
start: parseOppDate(o.start), end: parseOppDate(o.end || o.start)
};
}).sort(function (a, b) { return a.start - b.start; });
}
function formatOppDateRange(start, end) {
if (start.getTime() === end.getTime()) {
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + ", " + start.getFullYear();
}
if (start.getFullYear() === end.getFullYear()) {
if (start.getMonth() === end.getMonth()) {
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + "–" + end.getDate() + ", " + start.getFullYear();
}
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + " – " + MONTH_ABBR[end.getMonth()] + " " + end.getDate() + ", " + start.getFullYear();
}
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + ", " + start.getFullYear() + " – " + MONTH_ABBR[end.getMonth()] + " " + end.getDate() + ", " + end.getFullYear();
}
function renderOppEventList() {
var listEl = document.getElementById("opp-event-list");
if (!listEl) return;
listEl.innerHTML = "";
var events = buildOppEvents();
if (!events.length) {
var empty = document.createElement("div");
empty.className = "opp-list-empty";
empty.textContent = "No events yet.";
listEl.appendChild(empty);
return;
}
events.forEach(function (e) {
var row = document.createElement("div");
row.className = "opp-list-row";
var meta = document.createElement("div");
meta.className = "opp-list-meta";
var datesEl = document.createElement("div");
datesEl.className = "opp-list-dates";
datesEl.textContent = formatOppDateRange(e.start, e.end);
meta.appendChild(datesEl);
if (e.location) {
var locEl = document.createElement("div");
locEl.className = "opp-list-loc";
locEl.textContent = e.location;
meta.appendChild(locEl);
}
if (e.attendee) {
var attEl = document.createElement("div");
attEl.className = "opp-list-attendee";
attEl.textContent = "Attending: " + e.attendee;
meta.appendChild(attEl);
}
if (e.url) {
var linkEl = document.createElement("a");
linkEl.className = "opp-list-link";
linkEl.href = e.url;
linkEl.target = "_blank";
linkEl.rel = "noopener";
linkEl.textContent = "Website";
meta.appendChild(linkEl);
}
row.appendChild(meta);
var about = document.createElement("div");
about.className = "opp-list-about";
var badge = document.createElement(e.url ? "a" : "span");
badge.className = "opp-list-badge";
badge.textContent = e.title;
if (e.url) {
badge.href = e.url;
badge.target = "_blank";
badge.rel = "noopener";
}
about.appendChild(badge);
if (e.abstract) {
var abstractEl = document.createElement("p");
abstractEl.className = "opp-list-abstract";
abstractEl.textContent = e.abstract;
about.appendChild(abstractEl);
}
row.appendChild(about);
row.addEventListener("click", function () {
oppCursor = new Date(e.start.getFullYear(), e.start.getMonth(), 1);
renderOppCalendar();
});
listEl.appendChild(row);
/* ---- Opportunities module loader ----
Fetches its markup, injects it into the empty tab container, then loads its
script as a normal <script src> (so it runs only once the DOM it needs exists). */
fetch("opportunities/opportunities.html")
.then(function (r) { return r.text(); })
.then(function (html) {
document.getElementById("tab-opportunities").innerHTML = html;
var s = document.createElement("script");
s.src = "opportunities/opportunities.js";
document.body.appendChild(s);
})
.catch(function (err) {
document.getElementById("tab-opportunities").innerHTML =
"<div class=\"opp-loading\">Couldn't load Opportunities (" + err + ").</div>";
});
}
function renderOppCalendar() {
var label = document.getElementById("opp-month-label");
var grid = document.getElementById("opp-cal-grid");
if (!label || !grid) return;
label.textContent = MONTH_NAMES[oppCursor.getMonth()] + " " + oppCursor.getFullYear();
grid.innerHTML = "";
var weekdayRow = document.createElement("div");
weekdayRow.className = "opp-cal-weekdays";
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach(function (wd) {
var wdEl = document.createElement("div");
wdEl.className = "opp-cal-weekday";
wdEl.textContent = wd;
weekdayRow.appendChild(wdEl);
});
grid.appendChild(weekdayRow);
var year = oppCursor.getFullYear(), month = oppCursor.getMonth();
var firstDow = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
var totalCells = Math.ceil((firstDow + daysInMonth) / 7) * 7;
var gridStart = new Date(year, month, 1 - firstDow);
var todayKey = oppKey(new Date());
var events = buildOppEvents();
for (var w = 0; w < totalCells / 7; w++) {
var weekStart = addOppDays(gridStart, w * 7);
var weekEnd = addOppDays(weekStart, 6);
var weekEl = document.createElement("div");
weekEl.className = "opp-cal-week";
var weekEvents = events
.filter(function (e) { return e.start <= weekEnd && e.end >= weekStart; })
.sort(function (a, b) { return a.start - b.start; });
var lanes = [];
weekEvents.forEach(function (e) {
var colStart = Math.max(0, Math.round((Math.max(e.start, weekStart) - weekStart) / 86400000));
var colEnd = Math.min(6, Math.round((Math.min(e.end, weekEnd) - weekStart) / 86400000));
var lane = 0;
while (lanes[lane] && lanes[lane].some(function (r) { return !(colEnd < r[0] || colStart > r[1]); })) lane++;
if (!lanes[lane]) lanes[lane] = [];
lanes[lane].push([colStart, colEnd]);
e._col = [colStart, colEnd];
e._lane = lane;
});
var cellMinHeight = Math.max(96, 34 + lanes.length * 24 + 10);
for (var d = 0; d < 7; d++) {
var cellDate = addOppDays(weekStart, d);
var outside = cellDate.getMonth() !== month;
var key = oppKey(cellDate);
var cell = document.createElement("div");
cell.className = "opp-cal-cell" + (outside ? " outside" : "") + (key === todayKey ? " today" : "");
cell.style.minHeight = cellMinHeight + "px";
var numEl = document.createElement("div");
numEl.className = "opp-cal-daynum";
numEl.textContent = cellDate.getDate();
cell.appendChild(numEl);
weekEl.appendChild(cell);
}
weekEvents.forEach(function (e) {
var bar = document.createElement("div");
bar.className = "opp-cal-event-bar";
if (e.start < weekStart) bar.classList.add("continues-left");
if (e.end > weekEnd) bar.classList.add("continues-right");
bar.style.left = "calc(" + (e._col[0] / 7 * 100) + "% + 4px)";
bar.style.width = "calc(" + ((e._col[1] - e._col[0] + 1) / 7 * 100) + "% - 8px)";
bar.style.top = (34 + e._lane * 24) + "px";
var titleEl = document.createElement(e.url ? "a" : "span");
titleEl.className = "opp-cal-event-title";
titleEl.textContent = e.title;
if (e.url) {
titleEl.href = e.url;
titleEl.target = "_blank";
titleEl.rel = "noopener";
}
bar.appendChild(titleEl);
if (e.location) {
var locEl = document.createElement("span");
locEl.className = "opp-cal-event-loc";
locEl.textContent = e.location;
bar.appendChild(locEl);
}
bar.title = e.title + (e.location ? " — " + e.location : "") + (e.attendee ? " (" + e.attendee + ")" : "");
weekEl.appendChild(bar);
});
grid.appendChild(weekEl);
}
}
/* ---------- Conference Scout results ----------
Snapshot of the weekly research agent's report, hand-reviewed and pasted in here.
Each entry: { title, dates (display text), location, url, reason }. */
var SCOUT_RESULTS = [
{ title: "PCIM Asia 2026", dates: "Aug 26–28, 2026", location: "Shenzhen, China",
reason: "Asia's leading power electronics / wide-bandgap semiconductor trade show — direct GaN/SiC device and materials audience. Existing focus area." },
{ title: "IMAPS Onshoring Workshop 2026", dates: "Aug 31–Sept 2, 2026", location: "Arlington, VA, USA",
url: "https://imaps.org/page/Onshoring",
reason: "Workshop on the US Defense Industrial Base's microelectronics packaging/assembly onshoring — a direct entry point for X-ray/semiconductor characterization into defense supply chains. Potential new business (defense)." },
{ title: "SEMICON Taiwan 2026", dates: "Sept 2–4, 2026", location: "Taipei, Taiwan",
url: "https://www.semicontaiwan.org/",
reason: "Major semiconductor equipment/materials/metrology trade show in a top compound-semiconductor manufacturing hub. Existing focus area." },
{ title: "WiPDA-Europe 2026", dates: "Sept 20–22, 2026", location: "Aalborg, Denmark",
url: "https://www.energy.aau.dk/wipda-europe-2026-e156452",
reason: "IEEE workshop on wide-bandgap (GaN/SiC) power devices — niche but highly targeted technical audience. Existing focus area." },
{ title: "XTOP 2026", dates: "Sept 21–25, 2026", location: "Karlsruhe, Germany",
url: "https://xtop2026.kit.edu",
reason: "16th Biennial Conference on High-Resolution X-Ray Diffraction and Imaging — direct X-ray diffraction/imaging overlap. Existing focus area." },
{ title: "NOBUGS 2026", dates: "Sept 21–25, 2026 (core Sept 22–24)", location: "Schenefeld Campus (European XFEL), near Hamburg, Germany",
url: "https://nobugs2026.xfel.eu/",
reason: "Explicitly covers metadata/data formats and FAIR data management for beamline software; co-hosted by DESY/European XFEL on TXproducts' home campus — the single best topical fit found. Existing focus area." },
{ title: "ICSCRM 2026", dates: "Sept 27–Oct 2, 2026", location: "Yokohama, Japan",
reason: "Premier international conference on silicon carbide and related materials — direct match for the SiC/X-ray metrology work. Existing focus area." },
{ title: "RDA 27th Plenary Meeting (P27)", dates: "Oct 6–8, 2026", location: "London, UK",
url: "https://www.rd-alliance.org/plenaries/p27/",
reason: "Research Data Alliance plenary — strong FAIR/research-data-infrastructure networking, though less domain-specific than NOBUGS. Existing focus area." },
{ title: "AFLS2026", dates: "Nov 16–20, 2026", location: "Harare, Zimbabwe",
url: "https://events.saip.org.za/event/255",
reason: "African Light Source / African Physical Society joint meeting — the Africa-based synchrotron-development community, with materials/X-ray characterization topics. Existing focus area." },
{ title: "MRS Fall Meeting & Exhibit 2026", dates: "Nov 29–Dec 4, 2026", location: "Boston, MA, USA",
url: "https://www.mrs.org/",
reason: "Broad materials-science conference with strong X-ray characterization tracks. Existing focus area." },
{ title: "GOMACTech 2027", dates: "March 22–25, 2027", location: "Pasadena, CA, USA",
url: "https://www.gomactech.net/",
reason: "The premier US DoD microelectronics conference — covers GaN/SiC power devices and X-ray characterization/reliability for defense microelectronics. Potential new business (defense)." },
{ title: "IEEE RadarConf'27", dates: "May 1–6, 2027", location: "Bangalore, India",
url: "https://radarconf2027.org/",
reason: "Radar hardware relies heavily on GaN/SiC RF power amplifiers — a defense-radar audience in a growing Indian defense-electronics market. Potential new business (defense)." }
];
function renderOppScoutList() {
var listEl = document.getElementById("opp-scout-list");
if (!listEl) return;
listEl.innerHTML = "";
if (!SCOUT_RESULTS.length) {
var empty = document.createElement("div");
empty.className = "opp-list-empty";
empty.textContent = "No results yet.";
listEl.appendChild(empty);
return;
}
SCOUT_RESULTS.forEach(function (s) {
var row = document.createElement("div");
row.className = "opp-list-row";
row.style.cursor = "default";
var meta = document.createElement("div");
meta.className = "opp-list-meta";
var datesEl = document.createElement("div");
datesEl.className = "opp-list-dates";
datesEl.textContent = s.dates;
meta.appendChild(datesEl);
if (s.location) {
var locEl = document.createElement("div");
locEl.className = "opp-list-loc";
locEl.textContent = s.location;
meta.appendChild(locEl);
}
if (s.url) {
var linkEl = document.createElement("a");
linkEl.className = "opp-list-link";
linkEl.href = s.url;
linkEl.target = "_blank";
linkEl.rel = "noopener";
linkEl.textContent = "Website";
meta.appendChild(linkEl);
}
row.appendChild(meta);
var about = document.createElement("div");
about.className = "opp-list-about";
var badge = document.createElement(s.url ? "a" : "span");
badge.className = "opp-list-badge";
badge.textContent = s.title;
if (s.url) {
badge.href = s.url;
badge.target = "_blank";
badge.rel = "noopener";
}
about.appendChild(badge);
if (s.reason) {
var reasonEl = document.createElement("p");
reasonEl.className = "opp-list-abstract";
reasonEl.textContent = s.reason;
about.appendChild(reasonEl);
}
row.appendChild(about);
listEl.appendChild(row);
});
}
document.getElementById("opp-prev-btn").addEventListener("click", function () {
oppCursor.setMonth(oppCursor.getMonth() - 1);
renderOppCalendar();
});
document.getElementById("opp-next-btn").addEventListener("click", function () {
oppCursor.setMonth(oppCursor.getMonth() + 1);
renderOppCalendar();
});
document.getElementById("opp-today-btn").addEventListener("click", function () {
oppCursor = new Date();
oppCursor.setDate(1);
renderOppCalendar();
});
renderOppEventList();
renderOppCalendar();
renderOppScoutList();
</script>
</body>
</html>
+80
View File
@@ -0,0 +1,80 @@
/* ---------- Opportunities (calendar) ---------- */
.opp-loading { color: #9aa7b4; font-size: 0.9rem; padding: 40px 0; text-align: center; }
.opp-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
.opp-month-label { font-size: 1.15rem; font-weight: 800; color: #1b3a5c; }
.opp-nav-btn {
font-family: inherit; font-size: 0.85rem; font-weight: 700; color: #2d6a9f;
background: #fff; border: 1px solid #e0e7ef; border-radius: 6px; padding: 6px 12px; cursor: pointer;
}
.opp-nav-btn:hover { background: #e8f0fe; }
.opp-today-btn { margin-left: 8px; }
.opp-cal-grid {
border-top: 1px solid #e0e7ef; border-left: 1px solid #e0e7ef;
}
.opp-cal-weekdays, .opp-cal-week {
display: grid; grid-template-columns: repeat(7, 1fr);
}
.opp-cal-week { position: relative; }
.opp-cal-weekday {
text-align: center; font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.04em; color: #6b7a8d; padding: 8px 0;
border-right: 1px solid #e0e7ef; border-bottom: 1px solid #e0e7ef; background: #f7f9fc;
}
.opp-cal-cell {
min-height: 96px; border-right: 1px solid #e0e7ef; border-bottom: 1px solid #e0e7ef;
padding: 8px;
}
.opp-cal-cell.outside { background: #fafbfc; }
.opp-cal-cell.outside .opp-cal-daynum { color: #c3cdd8; }
.opp-cal-daynum { font-size: 0.82rem; font-weight: 700; color: #1a2533; }
.opp-cal-cell.today .opp-cal-daynum {
display: inline-flex; align-items: center; justify-content: center;
width: 22px; height: 22px; border-radius: 50%; background: #2d6a9f; color: #fff;
}
.opp-cal-event-bar {
position: absolute; height: 20px; line-height: 20px; border-radius: 4px;
background: #2d6a9f; color: #fff; font-size: 0.72rem; font-weight: 700;
padding: 0 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
cursor: default;
}
.opp-cal-event-bar .opp-cal-event-title { color: #ffd43b; font-weight: 800; text-decoration: none; }
a.opp-cal-event-title:hover { text-decoration: underline; }
.opp-cal-event-bar .opp-cal-event-loc { color: #fff; font-weight: 600; opacity: 0.82; margin-left: 6px; }
.opp-cal-event-bar.continues-left { border-top-left-radius: 0; border-bottom-left-radius: 0; }
.opp-cal-event-bar.continues-right { border-top-right-radius: 0; border-bottom-right-radius: 0; }
#tab-opportunities .drill-view { padding: 0; }
.opp-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
@media (max-width: 900px) { .opp-layout { grid-template-columns: 1fr; } }
.opp-list-card { padding: 24px; }
.opp-list-heading { margin: 0 0 16px; font-size: 1.05rem; font-weight: 800; color: #1b3a5c; }
.opp-list { display: flex; flex-direction: column; gap: 10px; }
.opp-list-empty { color: #6b7a8d; font-size: 0.9rem; }
.opp-list-row {
display: flex; align-items: flex-start; gap: 12px; padding: 12px;
border: 1px solid #e0e7ef; border-radius: 8px; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.opp-list-row:hover { border-color: #2d6a9f; background: #f7f9fc; }
.opp-list-meta {
flex: 0 0 128px; display: flex; flex-direction: column; gap: 3px;
font-size: 0.82rem; line-height: 1.5;
}
.opp-list-dates { font-weight: 700; color: #1a2533; }
.opp-list-loc, .opp-list-attendee { color: #6b7a8d; }
.opp-list-link {
display: inline-block; margin-top: 3px; font-size: 0.78rem; font-weight: 700;
color: #2d6a9f; text-decoration: underline;
}
.opp-list-about { flex: 1; border-left: 1px solid #eef1f5; padding-left: 16px; }
.opp-scout-note {
background: #fbf0df; border: 1px solid #f3ddb8; border-radius: 8px;
padding: 12px 14px; margin-bottom: 16px; font-size: 0.82rem; line-height: 1.55; color: #7a4a06;
}
.opp-scout-note strong { color: #b45f06; }
.opp-list-badge {
display: inline-block; background: #fbf0df; color: #b45f06; font-weight: 800;
font-size: 0.78rem; border-radius: 20px; padding: 5px 12px; text-decoration: none; white-space: nowrap;
}
a.opp-list-badge:hover { text-decoration: underline; }
.opp-list-abstract { margin: 8px 0 0; font-size: 0.82rem; line-height: 1.55; color: #3d4c5c; }
+37
View File
@@ -0,0 +1,37 @@
<div class="drill-crumbs" id="opp-crumbs" style="margin-bottom: 14px;"></div>
<div class="drill-view active" data-view="opportunities-root">
<div class="opp-layout">
<div class="card opp-list-card">
<h3 class="opp-list-heading">Events</h3>
<p class="narrative" style="max-width: none;">Conferences, events, and potential opportunities for TXproducts.</p>
<div class="opp-list" id="opp-event-list"></div>
</div>
<div class="card opp-cal-card" style="padding: 0;">
<div style="padding: 24px 32px 28px;">
<div class="opp-toolbar">
<div class="opp-month-label" id="opp-month-label"></div>
<div>
<button class="opp-nav-btn" id="opp-prev-btn">← Prev</button>
<button class="opp-nav-btn opp-today-btn" id="opp-today-btn">Today</button>
<button class="opp-nav-btn" id="opp-next-btn">Next →</button>
</div>
</div>
<div class="opp-cal-grid" id="opp-cal-grid"></div>
</div>
</div>
</div>
</div>
<div class="drill-view" data-view="scout">
<div class="card">
<h2>Conference Scout</h2>
<p class="narrative" style="max-width: none;">A scheduled research agent that sweeps the web every Monday morning for conferences, trade shows, seminars, and networking events relevant to TXproducts — semiconductors, power electronics (GaN/SiC), X-ray optics, photon/nanoscience, and defense/aerospace expansion prospecting. Review the candidates below, then add anything worth tracking to the Events list yourself — nothing here is added automatically.</p>
<div class="opp-scout-note"><strong>Verification note:</strong> this run's sandbox blocked direct fetches to every official event site (even neutral test domains), so dates and locations below are cross-referenced across multiple independent search sources rather than confirmed on the primary page — double-check the official site before committing budget or travel. This is a fresh worldwide sweep (12 candidates, replacing the previous Europe-only list) spanning Asia, the Americas, Africa, and Europe, tagged "Existing focus area" or "Potential new business (defense)" per TXproducts' corrected scope: semiconductors/power electronics (GaN/SiC), X-ray optics, photon/nanoscience, synchrotron/beamline science, and defense/aerospace prospecting.</div>
<div class="opp-list" id="opp-scout-list"></div>
<div class="drill-actions" style="margin-top: 18px;">
<button class="drill-back" data-target="opportunities-root">← Opportunities</button>
<a class="open-link" href="https://claude.ai/code/routines/trig_01XRb81y9pAWDube5kjRUvGT" target="_blank" rel="noopener">Open Conference Scout ↗</a>
</div>
</div>
</div>
+350
View File
@@ -0,0 +1,350 @@
/* ---------- Opportunities calendar ----------
Each entry: { start: "YYYY-MM-DD", end: "YYYY-MM-DD" (defaults to start),
title, location, attendee (optional) }.
Multi-day entries render as one bar spanning their full date range;
bars continuing past a week boundary lose the rounded corner on that side. */
var OPPORTUNITIES = [
{ start: "2026-08-23", end: "2026-08-27", title: "SPIE", location: "San Diego, CA", attendee: "Peter", url: "https://spie.org/conferences-and-exhibitions/optics-and-photonics",
abstract: "SPIE's flagship annual optics and photonics conference and exhibition, held every August in San Diego since the 1960s. Brings together 10,000+ scientists, engineers, and industry professionals across optical engineering, nanoscience, quantum science, and organic photonics." },
{ start: "2026-11-10", end: "2026-11-13", title: "SEMICON Europa", location: "Munich, Germany", url: "https://www.semiconeuropa.org/",
abstract: "Semiconductor and electronics manufacturing trade show co-located with electronica in Munich. Covers fab management, advanced packaging, and smart manufacturing across the chip supply chain, drawing 8,000+ visitors from equipment vendors, fabs, and OSATs." }
];
var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var oppCursor = new Date();
oppCursor.setDate(1);
function oppKey(d) {
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}
function parseOppDate(s) {
var parts = s.split("-").map(Number);
return new Date(parts[0], parts[1] - 1, parts[2]);
}
function addOppDays(d, n) {
var r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
function buildOppEvents() {
return OPPORTUNITIES.map(function (o) {
return {
title: o.title, location: o.location, attendee: o.attendee, url: o.url, abstract: o.abstract,
start: parseOppDate(o.start), end: parseOppDate(o.end || o.start)
};
}).sort(function (a, b) { return a.start - b.start; });
}
function formatOppDateRange(start, end) {
if (start.getTime() === end.getTime()) {
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + ", " + start.getFullYear();
}
if (start.getFullYear() === end.getFullYear()) {
if (start.getMonth() === end.getMonth()) {
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + "–" + end.getDate() + ", " + start.getFullYear();
}
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + " – " + MONTH_ABBR[end.getMonth()] + " " + end.getDate() + ", " + start.getFullYear();
}
return MONTH_ABBR[start.getMonth()] + " " + start.getDate() + ", " + start.getFullYear() + " – " + MONTH_ABBR[end.getMonth()] + " " + end.getDate() + ", " + end.getFullYear();
}
function renderOppEventList() {
var listEl = document.getElementById("opp-event-list");
if (!listEl) return;
listEl.innerHTML = "";
var events = buildOppEvents();
if (!events.length) {
var empty = document.createElement("div");
empty.className = "opp-list-empty";
empty.textContent = "No events yet.";
listEl.appendChild(empty);
return;
}
events.forEach(function (e) {
var row = document.createElement("div");
row.className = "opp-list-row";
var meta = document.createElement("div");
meta.className = "opp-list-meta";
var datesEl = document.createElement("div");
datesEl.className = "opp-list-dates";
datesEl.textContent = formatOppDateRange(e.start, e.end);
meta.appendChild(datesEl);
if (e.location) {
var locEl = document.createElement("div");
locEl.className = "opp-list-loc";
locEl.textContent = e.location;
meta.appendChild(locEl);
}
if (e.attendee) {
var attEl = document.createElement("div");
attEl.className = "opp-list-attendee";
attEl.textContent = "Attending: " + e.attendee;
meta.appendChild(attEl);
}
if (e.url) {
var linkEl = document.createElement("a");
linkEl.className = "opp-list-link";
linkEl.href = e.url;
linkEl.target = "_blank";
linkEl.rel = "noopener";
linkEl.textContent = "Website";
meta.appendChild(linkEl);
}
row.appendChild(meta);
var about = document.createElement("div");
about.className = "opp-list-about";
var badge = document.createElement(e.url ? "a" : "span");
badge.className = "opp-list-badge";
badge.textContent = e.title;
if (e.url) {
badge.href = e.url;
badge.target = "_blank";
badge.rel = "noopener";
}
about.appendChild(badge);
if (e.abstract) {
var abstractEl = document.createElement("p");
abstractEl.className = "opp-list-abstract";
abstractEl.textContent = e.abstract;
about.appendChild(abstractEl);
}
row.appendChild(about);
row.addEventListener("click", function () {
oppCursor = new Date(e.start.getFullYear(), e.start.getMonth(), 1);
renderOppCalendar();
});
listEl.appendChild(row);
});
}
function renderOppCalendar() {
var label = document.getElementById("opp-month-label");
var grid = document.getElementById("opp-cal-grid");
if (!label || !grid) return;
label.textContent = MONTH_NAMES[oppCursor.getMonth()] + " " + oppCursor.getFullYear();
grid.innerHTML = "";
var weekdayRow = document.createElement("div");
weekdayRow.className = "opp-cal-weekdays";
["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].forEach(function (wd) {
var wdEl = document.createElement("div");
wdEl.className = "opp-cal-weekday";
wdEl.textContent = wd;
weekdayRow.appendChild(wdEl);
});
grid.appendChild(weekdayRow);
var year = oppCursor.getFullYear(), month = oppCursor.getMonth();
var firstDow = new Date(year, month, 1).getDay();
var daysInMonth = new Date(year, month + 1, 0).getDate();
var totalCells = Math.ceil((firstDow + daysInMonth) / 7) * 7;
var gridStart = new Date(year, month, 1 - firstDow);
var todayKey = oppKey(new Date());
var events = buildOppEvents();
for (var w = 0; w < totalCells / 7; w++) {
var weekStart = addOppDays(gridStart, w * 7);
var weekEnd = addOppDays(weekStart, 6);
var weekEl = document.createElement("div");
weekEl.className = "opp-cal-week";
var weekEvents = events
.filter(function (e) { return e.start <= weekEnd && e.end >= weekStart; })
.sort(function (a, b) { return a.start - b.start; });
var lanes = [];
weekEvents.forEach(function (e) {
var colStart = Math.max(0, Math.round((Math.max(e.start, weekStart) - weekStart) / 86400000));
var colEnd = Math.min(6, Math.round((Math.min(e.end, weekEnd) - weekStart) / 86400000));
var lane = 0;
while (lanes[lane] && lanes[lane].some(function (r) { return !(colEnd < r[0] || colStart > r[1]); })) lane++;
if (!lanes[lane]) lanes[lane] = [];
lanes[lane].push([colStart, colEnd]);
e._col = [colStart, colEnd];
e._lane = lane;
});
var cellMinHeight = Math.max(96, 34 + lanes.length * 24 + 10);
for (var d = 0; d < 7; d++) {
var cellDate = addOppDays(weekStart, d);
var outside = cellDate.getMonth() !== month;
var key = oppKey(cellDate);
var cell = document.createElement("div");
cell.className = "opp-cal-cell" + (outside ? " outside" : "") + (key === todayKey ? " today" : "");
cell.style.minHeight = cellMinHeight + "px";
var numEl = document.createElement("div");
numEl.className = "opp-cal-daynum";
numEl.textContent = cellDate.getDate();
cell.appendChild(numEl);
weekEl.appendChild(cell);
}
weekEvents.forEach(function (e) {
var bar = document.createElement("div");
bar.className = "opp-cal-event-bar";
if (e.start < weekStart) bar.classList.add("continues-left");
if (e.end > weekEnd) bar.classList.add("continues-right");
bar.style.left = "calc(" + (e._col[0] / 7 * 100) + "% + 4px)";
bar.style.width = "calc(" + ((e._col[1] - e._col[0] + 1) / 7 * 100) + "% - 8px)";
bar.style.top = (34 + e._lane * 24) + "px";
var titleEl = document.createElement(e.url ? "a" : "span");
titleEl.className = "opp-cal-event-title";
titleEl.textContent = e.title;
if (e.url) {
titleEl.href = e.url;
titleEl.target = "_blank";
titleEl.rel = "noopener";
}
bar.appendChild(titleEl);
if (e.location) {
var locEl = document.createElement("span");
locEl.className = "opp-cal-event-loc";
locEl.textContent = e.location;
bar.appendChild(locEl);
}
bar.title = e.title + (e.location ? " — " + e.location : "") + (e.attendee ? " (" + e.attendee + ")" : "");
weekEl.appendChild(bar);
});
grid.appendChild(weekEl);
}
}
/* ---------- Conference Scout results ----------
Snapshot of the weekly research agent's report, hand-reviewed and pasted in here.
Each entry: { title, dates (display text), location, url, reason }. */
var SCOUT_RESULTS = [
{ title: "PCIM Asia 2026", dates: "Aug 26–28, 2026", location: "Shenzhen, China",
reason: "Asia's leading power electronics / wide-bandgap semiconductor trade show — direct GaN/SiC device and materials audience. Existing focus area." },
{ title: "IMAPS Onshoring Workshop 2026", dates: "Aug 31–Sept 2, 2026", location: "Arlington, VA, USA",
url: "https://imaps.org/page/Onshoring",
reason: "Workshop on the US Defense Industrial Base's microelectronics packaging/assembly onshoring — a direct entry point for X-ray/semiconductor characterization into defense supply chains. Potential new business (defense)." },
{ title: "SEMICON Taiwan 2026", dates: "Sept 2–4, 2026", location: "Taipei, Taiwan",
url: "https://www.semicontaiwan.org/",
reason: "Major semiconductor equipment/materials/metrology trade show in a top compound-semiconductor manufacturing hub. Existing focus area." },
{ title: "WiPDA-Europe 2026", dates: "Sept 20–22, 2026", location: "Aalborg, Denmark",
url: "https://www.energy.aau.dk/wipda-europe-2026-e156452",
reason: "IEEE workshop on wide-bandgap (GaN/SiC) power devices — niche but highly targeted technical audience. Existing focus area." },
{ title: "XTOP 2026", dates: "Sept 21–25, 2026", location: "Karlsruhe, Germany",
url: "https://xtop2026.kit.edu",
reason: "16th Biennial Conference on High-Resolution X-Ray Diffraction and Imaging — direct X-ray diffraction/imaging overlap. Existing focus area." },
{ title: "NOBUGS 2026", dates: "Sept 21–25, 2026 (core Sept 22–24)", location: "Schenefeld Campus (European XFEL), near Hamburg, Germany",
url: "https://nobugs2026.xfel.eu/",
reason: "Explicitly covers metadata/data formats and FAIR data management for beamline software; co-hosted by DESY/European XFEL on TXproducts' home campus — the single best topical fit found. Existing focus area." },
{ title: "ICSCRM 2026", dates: "Sept 27–Oct 2, 2026", location: "Yokohama, Japan",
reason: "Premier international conference on silicon carbide and related materials — direct match for the SiC/X-ray metrology work. Existing focus area." },
{ title: "RDA 27th Plenary Meeting (P27)", dates: "Oct 6–8, 2026", location: "London, UK",
url: "https://www.rd-alliance.org/plenaries/p27/",
reason: "Research Data Alliance plenary — strong FAIR/research-data-infrastructure networking, though less domain-specific than NOBUGS. Existing focus area." },
{ title: "AFLS2026", dates: "Nov 16–20, 2026", location: "Harare, Zimbabwe",
url: "https://events.saip.org.za/event/255",
reason: "African Light Source / African Physical Society joint meeting — the Africa-based synchrotron-development community, with materials/X-ray characterization topics. Existing focus area." },
{ title: "MRS Fall Meeting & Exhibit 2026", dates: "Nov 29–Dec 4, 2026", location: "Boston, MA, USA",
url: "https://www.mrs.org/",
reason: "Broad materials-science conference with strong X-ray characterization tracks. Existing focus area." },
{ title: "GOMACTech 2027", dates: "March 22–25, 2027", location: "Pasadena, CA, USA",
url: "https://www.gomactech.net/",
reason: "The premier US DoD microelectronics conference — covers GaN/SiC power devices and X-ray characterization/reliability for defense microelectronics. Potential new business (defense)." },
{ title: "IEEE RadarConf'27", dates: "May 1–6, 2027", location: "Bangalore, India",
url: "https://radarconf2027.org/",
reason: "Radar hardware relies heavily on GaN/SiC RF power amplifiers — a defense-radar audience in a growing Indian defense-electronics market. Potential new business (defense)." }
];
function renderOppScoutList() {
var listEl = document.getElementById("opp-scout-list");
if (!listEl) return;
listEl.innerHTML = "";
if (!SCOUT_RESULTS.length) {
var empty = document.createElement("div");
empty.className = "opp-list-empty";
empty.textContent = "No results yet.";
listEl.appendChild(empty);
return;
}
SCOUT_RESULTS.forEach(function (s) {
var row = document.createElement("div");
row.className = "opp-list-row";
row.style.cursor = "default";
var meta = document.createElement("div");
meta.className = "opp-list-meta";
var datesEl = document.createElement("div");
datesEl.className = "opp-list-dates";
datesEl.textContent = s.dates;
meta.appendChild(datesEl);
if (s.location) {
var locEl = document.createElement("div");
locEl.className = "opp-list-loc";
locEl.textContent = s.location;
meta.appendChild(locEl);
}
if (s.url) {
var linkEl = document.createElement("a");
linkEl.className = "opp-list-link";
linkEl.href = s.url;
linkEl.target = "_blank";
linkEl.rel = "noopener";
linkEl.textContent = "Website";
meta.appendChild(linkEl);
}
row.appendChild(meta);
var about = document.createElement("div");
about.className = "opp-list-about";
var badge = document.createElement(s.url ? "a" : "span");
badge.className = "opp-list-badge";
badge.textContent = s.title;
if (s.url) {
badge.href = s.url;
badge.target = "_blank";
badge.rel = "noopener";
}
about.appendChild(badge);
if (s.reason) {
var reasonEl = document.createElement("p");
reasonEl.className = "opp-list-abstract";
reasonEl.textContent = s.reason;
about.appendChild(reasonEl);
}
row.appendChild(about);
listEl.appendChild(row);
});
}
document.getElementById("opp-prev-btn").addEventListener("click", function () {
oppCursor.setMonth(oppCursor.getMonth() - 1);
renderOppCalendar();
});
document.getElementById("opp-next-btn").addEventListener("click", function () {
oppCursor.setMonth(oppCursor.getMonth() + 1);
renderOppCalendar();
});
document.getElementById("opp-today-btn").addEventListener("click", function () {
oppCursor = new Date();
oppCursor.setDate(1);
renderOppCalendar();
});
renderOppEventList();
renderOppCalendar();
renderOppScoutList();
/* ---------- Module self-registration ----------
Everything below wires this module into the shared shell (app.js):
the drill-view registry, the hero-nav button, and the tab's default view.
This runs once, right after opportunities.html is injected into the DOM. */
HERO_NAV_BY_TAB.opportunities = ["hero-nav-scout"];
DRILL_DEFAULT_VIEW.opportunities = "opportunities-root";
DRILL_CONFIG.opportunities = {
crumbsEl: "opp-crumbs",
crumbs: {
"opportunities-root": [["Opportunities", "opportunities-root"]],
"scout": [["Opportunities", "opportunities-root"], ["Conference Scout", "scout"]]
}
};
document.querySelectorAll("#tab-opportunities .drill-back, #tab-opportunities .drill-next").forEach(function (b) {
b.addEventListener("click", function () { setDrillView("opportunities", b.getAttribute("data-target")); });
});
setDrillView("opportunities", DRILL_DEFAULT_VIEW.opportunities);
document.getElementById("hero-nav-scout-btn").addEventListener("click", function () {
setDrillView("opportunities", "scout");
});
if (document.querySelector(".sidebar-nav-btn.active[data-tab=\"opportunities\"]")) {
document.getElementById("hero-nav-scout").style.display = "flex";
}