CE CE RadarCivil engineering opportunity monitor
checking…

Settings

Crawl behaviour, what counts as a match, the remote-versus-on-site rule, email delivery, and the cron URL that makes the whole thing run by itself.

Workspace

Projects

Separate workspaces. Nothing is shared between them.
Each project is a database of its own. Its sources, dictionaries, scoring rules, conditions, collected items, recipients, SMTP details and cron token all belong to that project alone — changing anything here has no effect on any other project. That also means there is no combined view: switch projects in the header to see the other one, and give each project its own cron entry.

Every project starts from the same factory dictionaries, so a new one looks identical to begin with. That is a starting point, not a shared list: edit a phrase in one and the others keep theirs. A dictionary you have never edited stays on the factory phrases, which is why two untouched projects match word for word.
Project Database Items Sources Actions
 
The database is created the first time you switch to it, so a project you never open costs nothing.
Filtering

What counts as a match

The filters every scraped block must pass.
Two numbers, and you define both. Every dictionary line is phrase = weight. When a phrase matches, its own dictionary decides what that match contributes — nothing here is fixed in the code. SCORE — a count. Add up points per match across the dictionaries you give points to. By default only the civil-engineering dictionary scores, 1 per term, so score 5 means five different civil terms appeared and score 0 means none did. MERIT — the weighted total. Each match adds sign × factor × weight, doubled when the phrase is in the title. No ceiling, and it can go negative when only noise matched. This is the figure worth ranking on. A penalty dictionary is nothing special — it is simply one with its sign set to −. Set a dictionary’s merit to off and it still gates and still decides the work mode, it just stops moving the number.

What each dictionary contributes

Dictionary Score / match Merit uses × factor Sign Title × Matches Effect

How the two numbers are worked out. Every phrase that matches is counted once, and each one moves the two numbers independently.

Score is the simpler of the two: add up the score / match column, once for every phrase that matched. Nothing else touches it — not the weights, not the title, not the penalties. That is why a noisy page cannot drag the score down, and why minimum score is the gate worth relying on.

Merit is four things multiplied together, for each phrase:

× factor straight from that column.
the weight, or 1 Merit uses decides which. “the phrase weight” takes the number you wrote beside the phrase; “a flat amount” ignores it and uses 1, so every phrase in that dictionary is worth the same. “nothing” stops the dictionary moving the merit at all — it still gates, and it still decides the work mode.
title × applied only when the phrase is in the title. Otherwise 1.
sign + adds it, − takes it away. A penalty dictionary is nothing more than one with the sign set to −.

So a phrase of weight 5, found in the title, in a dictionary set to the phrase weight with × factor 2 and title × 4, contributes 5 × 2 × 4 = 40 to the merit — and 1 to the score if that dictionary scores at all. The same phrase in the body would be 5 × 2 = 10.

Then two things happen once per listing, which is usually why the per-dictionary rows below do not appear to add up: a live-deadline bonus if the closing date is still ahead, and penalties are trimmed to at most the share of the earned merit set by penalty cap below — half, unless you change it. Noise can dent a good listing; it cannot cancel one.

Work it out on some text

Paste a real listing, or edit the example. It is run through the actual classifier with your saved rules, so the arithmetic below is the arithmetic the extractor uses — it cannot drift out of date the way a written example would.

Uses the rules as last saved, not the unsaved edits above.
Cancellation cannot affect the score. A penalty dictionary adds 0 to the score, so burying a genuine listing in cookie banners changes its merit but never its score. That is why minimum score is the gate to lean on, and why the minimum merit ships switched off.

Your own conditions

The two minimums above judge the totals. These judge what an individual dictionary contributed — “at least two civil terms”, “the noise penalty must not go below −8”. Every line in a group must hold; the block is kept if any one group holds, so groups are alternatives. With no groups written nothing here changes a verdict.

Other rules

Current rule in plain English:
…
Watching pages for changes

Page watching

Turns this project from “find me relevant things” into “tell me when this page changes”. Two different questions, so they get two different sets of rules.
off
These settings only do anything with Keep everything switched on above. Keeping everything answers “ignore the keyword filter”, which is half of watching a page — it keeps the cookie banner and the nav menu just as eagerly as the news. The settings here supply the other half.
Dictionaries

Keyword dictionaries

One phrase per line as phrase = weight. Matching is case-insensitive and ignores word boundaries, so "phd position" matches inside a longer sentence.
Every dictionary, discovered from your data. Add a group to the keywords and it appears here automatically — graded, switchable and editable, with no code change. Remove one and it simply stops appearing.

One phrase per line, phrase = weight. A line with no = gets weight 3. That weight is what a match of this phrase adds to the merit, scaled by its dictionary’s factor; the score counts the match as 1 whatever the weight. Matching is plain substring and case-insensitive, so a short phrase catches more than you expect: gis matches “registration”.

Lower-case letters, digits and underscores. It starts as a normal positive dictionary contributing its weights to the merit; change what it contributes in the table above.
What each of these contributes to the score and the merit is set in What counts as a match above — one row per dictionary, including any you add here.
After changing keywords, open the Extract page and choose Re-run on everything — already-downloaded pages are re-filtered with no new network traffic.
Email
Email moved. Addresses, transport, recipients, the schedule and the exclusion rules now live on the Email tab, together with the send log and what has already gone out. The cron settings below still drive it.

Developer — editing this project with Claude

Configuration to copy onto your own computer. Nothing here is stored or run by the app.

1. Let Claude read and edit the project

Claude Desktop → Settings → Developer → Edit Config, and paste this. Claude Code needs none of it — it already has access to the folder it is started in. Requires Node (winget install OpenJS.NodeJS.LTS), which supplies npx.

This is a folder on your own computer, never on the server. This entry reads files on your machine and has no route to this hosting at all — a server path here simply fails. To reach the live site, use section 2 below, which is a different server with a different key.

      
If the server shows as failed to connect, it is almost always because npx is not on your PATH — Node installed under a bundle like Laragon or XAMPP usually is not registered. There is no error message worth reading when this happens: the process exits immediately and the connection just closes.

Check with npx --version in a new terminal. If that fails, either add Node's folder to PATH, or give the config the full path plus an env block:
"command": "C:\path\to\node\npx.cmd"
"env": { "PATH": "C:\path\to\node;C:\Windows\system32;C:\Windows", "SystemRoot": "C:\Windows" }

All three parts are needed, and each was found by a separate failure: npx.cmd starts node by name, so a full path to npx alone still fails; and if the host replaces the environment rather than adding to it, Node aborts while starting up without SystemRoot. Adding TEMP and TMP as well lets npx populate its cache on a machine where it is empty.

2. Or let Claude edit this live site directly

With this on, changes reach the site as Claude makes them — no archive, no upload, no File Manager. It works by putting a small endpoint, mcp-bridge.php, at the top of this install, and giving your computer the only key to it.

Know what you are turning on. This is an address on the public internet that writes files into this site. The token is all that stands in front of it, and on shared hosting the files it can write reach the whole account — not just this app.

So: it is off until you switch it on, the token is generated here and never stored in the code or carried in an upload, data/ is refused outright (that is your password, your databases and your API keys), the endpoint will not rewrite itself, every write keeps a backup, and eight wrong tokens shut the door for fifteen minutes.

The alternative, if you would rather not: cPanel’s own Web Disk maps this site as a drive on your computer through cPanel’s login, and sh deploy/push.sh Z: --live then pushes to it. Same outcome, nothing new exposed on the server — but Claude cannot drive it, so you run the one command.
Checking…

3. Or upload the changes yourself

Claude edits the copy on your computer; these send it up. Pick by what your host offers — the first two need nothing at all:

sh deploy/folders.sh Builds two folders on D: — one whose contents you drag into File Manager, one that stays put. Each says which it is. Start here.
sh deploy/package.sh Builds one .tar.gz to upload and Extract in cPanel. Fewer clicks than dragging a folder tree.
sh deploy/push.sh Z: --live Copies only what changed, onto the site mapped as a drive (cPanel → Web Disk). Needs no SSH, and takes seconds.
sh deploy/deploy.sh --live Over sftp. Needs SSH, which many shared plans do not include — check before relying on it. Fill in deploy/deploy.config first.

The first two never upload anything themselves — they only assemble files on your machine, and you do the upload. The last two do upload, and both default to a dry run: leave off --live and they only report.

data/ is never uploaded by any of them. It holds the database — your sources, verdicts, settings and API keys — and once this is live, the copy that matters is the one on the server. Sending the local one up would destroy everything collected since the last deploy.

4. After uploading: what is readable over the web?

The .htaccess files shipped here are all that keep data/ceradar.sqlite — every API key, recipient and verdict — away from anyone who guesses the URL. They fail silently: a host running nginx, or Apache with AllowOverride None, ignores them and serves the database on request, with nothing in any log to say so. This asks the way an outsider would, one request per path.

If you have remote editing on, run this check after each change. A write that lands in the wrong place, or a new file created without the .htaccess that was meant to cover it, shows up here and nowhere else. The endpoint itself refuses data/, so it cannot expose the database — but it can add files, and a file added is a file served.

Worth knowing either way: some hosts run a malware scan that flags any script accepting input and writing files, and suspends it without asking. If mcp-bridge.php disappears on its own, that is what happened — it is not a bug, and re-uploading it will not help for long.

Automation

Make it run without you.

Option A — cPanel cron job. Add this command, hourly:

Option B — web cron / external pinger (cron-job.org, UptimeRobot):

Run it now
Keep that token private — anyone with the URL can trigger a run. It regenerates only if you clear it from the database.

One entry for every project

The two options above run this project only, so each project needs its own entry. Shared hosting is usually short of cron slots, so this single line runs them all instead — one after another, each in its own process, with one token for the whole install.

Option C — cPanel cron job, all projects:

Option D — web cron, all projects:

Run it now
This token is separate from the per-project one above and lives in data/projects.json, because no single project can authorise a job that spans all of them. Adding a project does not change the line — the new project is picked up on the next run.

Add &skip=archive to leave a project out, or &only=jobs,page-watch to run just some of them.
Access

Who can get in

One password for the whole install — every project shares it.
Checking…
The pages are plain files. Your host serves settings.html itself, without asking PHP, so this password cannot stop someone fetching one — what they get is an empty shell, because every value on it comes from behind this password. Nothing can be read or changed without signing in.

To put the files behind a door too, turn on your host’s own directory protection (cPanel: Directory Privacy) for the install folder — but leave cron/ out of it, or the scheduled run will be refused before PHP sees its token.
Disk space

What is taking up space

Every project in this install, and what is inside each one. Shared hosting sells disk by the gigabyte, and a scraper that keeps every raw block off every page fills it faster than anything else here.
Whole install
—
This project
—
Reclaimable now
—
Limit
—

By project

ProjectTotalWhat is inside it Wasted
Checking…

Outside the databases

Checking
…

Free up space in this project

Tick what you do not need. Nothing is removed until you press the button, and nothing here touches your sources, dictionaries or settings.

Checking…

Keep it under control automatically

Optional integrations

RSS-Bridge (optional)

Gives a feed to sites that have none. Free, open source, and it installs on this same hosting — nothing is sent to a third party.

Who else can use it

Checking…

RSS-Bridge is a separate application in this folder, and the password on this app does not reach it — this install fetches it server-side, with no browser session to hand over. Left open, anyone who finds its address can make your server fetch any page they like, which is the sort of thing a host suspends an account for.

A token closes that. It is generated here and written to rss-bridge/config.ini.php, which is where RSS-Bridge reads it from — and that is the only copy. It is not stored as a setting here, because settings belong to one project while the bridge is shared by all of them, and a per-project copy could only ever be right for one.

Your saved feeds do not change. The token is added when a feed is fetched, so it is never stored in a source URL, never in a CSV export and never on screen — and rotating it needs no edit to a single saved feed.

If a feed suddenly reports an error, clear the cache first. RSS-Bridge stores whatever a bridge returned last time — failures included — so one momentary blip keeps being served as a permanent-looking error until the entry expires.
Why bother? Feeds are dated, structured and far more stable than scraping HTML. RSS-Bridge ships 400+ ready-made bridges for sites that resist scraping (Reddit, YouTube, Telegram and so on), plus a general CSS Selector bridge for anything else. Once it is set up, add feeds from Sources → + Add sources → Via RSS-Bridge.

One limit worth knowing: RSS-Bridge is server-side PHP like this tool, so it cannot read pages that build themselves in the browser with JavaScript. For those, use the AI reading method instead.

Install it from github.com/RSS-Bridge/rss-bridge.

AI reading (optional)

Lets a source be read by Claude instead of by selectors — useful for messy pages no rule fits. Off by default, and it costs money per page.
Changes on this page are not saved until you press Save.