Repository navigation
Releases
Releases · vidithq/vidit
v0.8.2
v0.8.1
Added
GET /feedtakes the date, media, author and location guess filters,GET /searchtakeshas_coordsandGET /eventstakesmedia;GET /search/datesandGET /feed/datescount the matching events per day (#813).GET /sites, an event's site links and the site suggestions carry each site's picture inmedia, the image of its first source in source order that is not graphic; a site search hit carries the same picture (#813).GET /search/sectionsreturns the top hits of each kind in one request, each section the first page of that kind's list with its cursor (#813).- Search finds sites by their name or by the title or note of one of their sources, and the search page's Show field picks every kind: Requested, Detected, Geolocated, Sites, Collections, Analysts and Organizations (#807).
- On the event forms, each source carries Move earlier and Move later, and readers see the sources in the order the author set (#804).
- An admin can restore a soft-deleted account from the onboarding table: the account and the events its soft delete hid come back (#793).
- On the search page, a typed coordinate pair offers to open that point on the map, and on All a query that names an analyst or an organization exactly puts a link to their page right under the search field (#791).
- When a search finds nothing, the search page suggests the closest spelling that finds results, such as Khan Yunis for Khan Younis or a handle missing a letter (#790).
- A map control beside the zoom buttons picks the basemap: Auto, which follows the theme, Dark, Light, Streets or Terrain; the browser keeps the choice (#709).
- On the Dark and Light basemaps, the same control sets the label language (Latin, local or both), the label density, and whether region borders and roads show (#709).
- The basemap control turns building footprints on or off on the Dark and Light basemaps (#709).
- The Dark and Light basemaps can highlight military areas and airports, with their runways, from zoom 10; the highlight is off by default and the basemap control turns it on (#709).
- A coordinate grid overlay, off by default, draws latitude and longitude lines with their labels on every basemap, finer as you zoom in (#709).
- The Dark and Light basemaps draw disputed country borders dashed, and the basemap control can draw them solid (#709).
- A Relief switch on the Dark and Light basemaps shades hills and mountains from open elevation data; it is off by default (#709).
- A Satellite basemap draws the cloud-free Sentinel-2 mosaic of 2016 by EOX (#709).
- Two map overlays, off by default, show NASA data of the day before on every basemap: true-colour satellite imagery and fire detections (#709).
- An instance can serve the basemap tiles, icons and relief from its own S3-compatible bucket: the backend's
update-mapcommand copies them there on a schedule, andNEXT_PUBLIC_MAP_ASSETS_URLpoints the map at them (#709). - You can attach a GIF as a source file or a proof image: each of its frames is stored as a PNG image of its own, up to 6 frames; a longer GIF is refused with a request for a video or fewer frames (#744).
- Type a latitude and a longitude, or paste a Google Maps link, to centre the map there: from a map control beside the basemap control on the map, and above the contour map of the site forms; a pair out of bounds says why (#748).
- The tag field of the event and source forms suggests your most used tags, or the platform's, in one line of one-click chips under the search;
GET /tags/suggestedlists them (#725). - When a detection's proof holds the footage its source lacks, the edit form copies that proof image into the empty source file slot in one click;
POST /uploads/from-proofstages the copy (#746). - A pulsing dot above the basemap control shows while the daily satellite imagery or the fire detections are still loading, including after you move the map (#758).
- In the basemap control, a
?beside each basemap, label setting, layer and overlay says what it shows and links to the source of its data or tiles (#757). - When the basemap's tile server does not respond, offline or with one provider down, a line over the map says so instead of leaving it blank (#792).
- Saving a site edit asks for an optional version note, like an event edit, and the site's history shows it (#800).
Changed
/paletteshows High level and Low level primitives from the import graph, the filter panel as one demo, and its check fails on a missing entry, a wrong tier or a primitive only one component uses (#814).docs/design.mdnames one primitive per job (show an item, pick from a vocabulary, enter a date, remove, delete, show an error...), and lint fails on a raw<select>,<textarea>or date input,confirm()or a retired primitive (#814)./palettelists the shared components by category, one file each, with a table of contents and a filter by name; each entry shows its live demos only, anddocs/design.mdholds the details (#814).- On the event and site forms, the coordinates field sits under the map, the title fields drop their character counter, and the suggested tags show as many as fit on two lines with no Show more (#814).
- The Filters header and the phone's filter bar show how many results match, such as 284 results, on the map, search, the feed and the Requests board;
GET /search/datesandGET /feed/datesreturn thattotal(#814). - A removal uses the cross and acts at once; a deletion or a revocation uses the trash and confirms in place: revoking an invite code shows the trash instead of a ban sign (#814).
GET /feedandGET /feed/datestake the Show filter asstatus(geolocated,requested,detected), the same parameter and values asGET /searchandGET /events, and no longer acceptkind(#814).- The Requests board keeps its filters in the page URL, like the map, search and the feed (#814).
- Every field that picks from a list, in the filter panel and on the forms (kinds, levels, reasons, effects, conflicts, tags), lists its options on focus, closes after a pick and shows the picks as removable pills (#814).
- Every field that finds an event, a site, a collection, an analyst or an organization lists its matches the same way, and Find a collection now runs the collection's own action on a pick (#814).
- One filter panel serves the map, search, the feed and the Requests board: collapsible groups What, When (the Event date and Added timelines), Source, Where and Who; Conflict, Tags and accounts search in their line; a phone opens it in a sheet (#813).
- Search, the feed and the Requests board show the filter panel under the map's Filters header, open by default and closed on the map; each surface remembers your choice, and the map header drops its point count (#813).
- The feed keeps its filters in the URL, and its Following choice is a switch in the filter panel; search lists an analyst or an organization the query names once, first in its section (#813).
- Every list shows an event, a request, a site or a collection one way, as a compact row, a list row or a feed card: its picture or its kind's icon, the title, a line with the kind, date, analyst and place, then its state (#813).
- Breaking:
GET /searchreturns one ranked list,hits, with onecursorthat pages it, in place of five groups with their totals;typerepeats to pick the kinds, andstatusnarrows only the events (#807). - The search page lists the top hits of each kind under its name, and See all opens one kind's list with Show more; the URL keeps the query and the kind, so Back and shared links work. The profile's links into search read See all (#813).
- The Links help on the source forms holds two sentences, and the methodology guide says how to link, archive and permalink a source (#804).
- The suggested tags under a tag field wrap instead of running off the card, the first 6 then one Show more (#804).
- A minor destructive action, such as dismissing a notification...
v0.8.0
Added
- An event holds up to 10 sources, each with a level (direct, indirect or context), a kind (such as drone, aircraft or satellite), a capture time and position, a graphic flag, tags, a file, and up to 10 links with their post time and archived copy (#693).
POST /uploadsstages one source file before the event write that uses it; a staged file nobody uses is deleted after 24 hours (#693).- Search matches source titles and source URLs, and the map, list, search and feed filters take
source_kindandlevel(#693). - The event page and both event forms show the sources side by side in one row with arrows, each reading title, level, kind, links, capture time, capture position, then tags; a lone source shows its file beside them on a wide screen (#693).
- In the event form, each source link is one row with its post time and archive box, the capture details fold under More details, removing a source takes a second click, and a publish attempt shows the first source with a missing field (#693).
- A satellite or aircraft source warns, without blocking, when no imagery provider lets a reader reopen the image at its date; the form names the provider, how to reopen the image and its credit, and
POST /sources/checkruns the check (#693). - Submitting a geolocation or publishing a request or a detection invites the co-geolocators you pick by the byline, in the same write;
POST /eventsandPOST /events/{id}/versionstakegeolocator_invites(#693). - The feed shows machine detections from the analysts you follow, or from everyone on All, and a Show detections switch in its Kind filter hides them;
GET /feedtakeskind=detected(#693).
Changed
- Every event write takes a
sourceslist in place of the source URL, mirror, snapshot, capture position, post time, graphic flag and file fields, and event reads servesourcesin place of the source and archive fields (#693). - Publishing needs a direct or indirect source with a file and a conflict; no tag is required. Every source needs a kind,
unknownincluded. A request needs a direct or indirect source with a file (#693). - The analyst who publishes a request can correct its sources, and batch completion takes a source kind per detection in place of a capture-source tag (#693).
- The graphic flag belongs to each source, and an event is graphic when any of its sources is; the admin moderation flag sets or clears it on every source (#693).
- A request or a detection is version 0, published from its edit form through
POST /events/{id}/versions; the published event is version 1, and the history and the version selector name version 0 Request or Detection (#693). POST /events/{id}/versionstakes an optionalversion_no, the version the form was loaded at, and answers 409version_mismatchwhen the event has moved past it (#693).- A re-import or a bot re-tag never writes over an event that has a saved version (#693).
- Profile stats count events per source kind, the source hosts read each event's first source URL, and
GET /tagslists the tags on live events or their sources (#693). - API and database fields follow one naming convention: events use
occurred_date,occurred_timeandcoords, media usekind, conflicts useis_ongoing, and foreign keys end in_id. The old field names no longer work, with no alias (#693). - An organization invitation is withdrawn by the invitee's username instead of an invitation id, and the organization and event invitation lists report
invited_atin place ofcreated_at(#693). - Approvals are final: Approve asks you to confirm first, joining an event as a co-geolocator deletes your approvals of it, and a site admin can delete an approval, after which its reviewer or organization can approve that version again (#693).
- Approvals show under the event actions as the reviewers' pictures with a check badge, and a geolocator asks for one from a Request approval action (#693).
- Notification settings are one email switch per kind, and the in-app notification list always shows every kind (#693).
- A site admin links an X handle to an account (#693).
- The worker runs the hourly bot mentions catch-up when it has the bot's X read credentials, so the backend image has no
botcommand and the bot needs no schedule of its own. Mentions queued by the webhook and the catch-up share one table (#693). - To upgrade, stop the worker, then run the database migrations, which move every event onto sources and lose data on downgrade. Give the worker the bot's X read credentials, and call
reap-password-reset-tokensin place ofreap-auth-tokens(#693). - The event page and both event forms share one layout: the title, which a form's Title field replaces, and the geolocators, the actions pinned top right over the approvals, then sources and proof beside location and Details (#693).
- Submit opens on three cards, Single event, From an X post and Bulk import, in place of the mode switch; the chosen path stays in the address, and the back arrow returns to the cards (#693).
- Both event forms pin their buttons on one line with a count of what each publish still misses, ask for a second click on every publish, and check a write in the same order with the same messages (#693).
- A submit with blank required fields turns them red in place and reads the list to a screen reader, with no box listing them (#693).
- Saving a version asks for its optional note in a small panel on the Save button, and the owner closes the event from a red trash button beside it (#693).
- Latitude and Longitude sit on one line at every width, followed by the map and copy actions as separate buttons, in the Location card and in each source's capture position (#693).
- Tags and conflicts are picked through a search field that suggests matches as you type, in the event form, each source, and the filters of the map, search, the Requests board and the feed; the form offers to create a missing tag (#693).
- A remove control that asks before it removes shows a red trash, and one that removes at once shows a red cross, on the event forms and the approval stamps (#693).
- The author filter shows among the active filter pills, as By username, so one click removes it like any other filter (#693).
- On an event page, Add to collection and Geolocators open their panel over the page, anchored to the button, so nothing on the page moves (#693).
- The event title, source titles, the version note, the close reason and report details count the characters used against their limit; link fields stop at the API's cap, and an out-of-range capture position is flagged (#693).
- A write drops the capture position of an article, an official statement or an other source, since a document has none (#693).
- Deleting a collection that an open report names withholds it from everyone at once, keeps it in the admin reports queue marked Deleted by its owner, and erases it when its last open report is resolved (#693).
- The collection forms and the profile and organization edit modes pin Save and Cancel at the top right, and a collection's form flags its missing fields and adds events from a search list (#693).
- A report opens in a dialog, and removing or leaving a geolocator takes a second click (#693).
- Sign-in, registration, password reset, Change password and the invite form outline every blank required field at once and read the list to a screen reader, and the Settings accent colour picker moves between swatches with the arrow keys (#693).
- The admin console finds analysts and events by name, confirms every destructive action in a dialog (revoking an invite code included), groups row actions in a menu, and pages the reports queue with Load more (#693).
GET /admin/meanswers every signed-in analyst with theiris_adminvalue instead of a 403 for an analyst who is not an admin (#693).
Removed
- Push notifications: email and the in-app list are the two ways to get a notification, and the push settings, devices and keys are gone (#693).
-...
v0.7.4
Changed
- The backend image runs each role with one command,
api,migrate,worker,botorsync-conflicts, so a host only chooses which command to run and when (#659). - The landing roadmap lists the planned outcomes with an In progress or Planned status instead of versions and links to the changelog for what shipped, and the contributor docs point to the issues and their epics for planning (#635).
- A frontend build loads media images from a CDN only when
NEXT_PUBLIC_MEDIA_HOSTnames it, and the config examples, comments and docs describe what any instance sets instead of one instance's hosting (#661).
Removed
- The repository no longer carries the vidit.app deploy workflow, its hosting config, the backup job or
make import-prod; the docs describe what any instance runs: the backend roles, their schedules, backups and restores (#660). - The promo video and guide screenshot tooling is maintained separately and no longer ships in this repository; the guide screenshots stay (#648).
Security
- The backend image no longer copies local
.envfiles, virtual environments, local media or datasets from the build directory (#661). - The frontend test tooling runs on Vite 8.3.5, which fixes advisories that let a page or a crafted path read files or run commands through a running Vite dev server (#654).
- The backend decodes uploaded images with Pillow 13.0.0, which fixes a heap buffer overflow in its JPEG decoder that a crafted upload could trigger (#653).
v0.7.3
Security
- The backend no longer trusts the emSign ECC Root CA - G3, whose eight eMudhra intermediates were compromised in October 2026, so a certificate forged with those keys cannot intercept its calls to email, storage, X or Sentry (#647).
v0.7.2
Changed
- The admin console no longer has the catalogue feed or maintenance panels, and one "Manage an event" panel replaces the event moderation and event delete panels (#617).
Fixed
- The archive import page asks you to confirm before you close or reload it while your archive is still being prepared or uploaded, since leaving then loses the upload (#618).
v0.7.1
Added
- From Settings, you can mint up to five invite codes a day, each bound to the invitee's X handle, and revoke an unused one; admins see who minted each code and can turn minting off for an analyst (#609).
- The Getting started page shows a new analyst around Vidit in six linkable sections of annotated screenshots and icons; the registration success screen and the confirmation email lead to it (#611).
Changed
- The test CI finishes in under 2 minutes instead of 4: the backend suite runs on three runners, the frontend checks run as parallel jobs, and the narrow-viewport smoke tests run against a production build with two workers (#613).
- The rail shows Requests only to a signed-in reader; a request page still opens from a shared link (#611).
- The import guide numbers the parts of a sample post instead of listing them, the methodology guide opens on a real annotated proof, and each guide states a fact once and links the others (#611).
- The landing roadmap lists v0.8 cleanup and restructuring, v0.9 moderation, v0.10 search and discovery, and a v1.0 that opens reader accounts while publishing stays with contributors (#607).
Fixed
- The import completion email tells you to use Start reviewing and go through detections one by one, instead of describing a page-at-a-time publish the queue no longer offers (#612).
- The account picker's suggestions show in full wherever the picker sits, such as the Request approval field in an event's Approvals card, and open above the field near the bottom of the screen (#602).
v0.6.7
Changed
- Proofs and collection descriptions use a block editor with a slash menu and are stored as sanitized HTML; existing bodies convert automatically, dropping list start numbers, code block languages and image titles (#542).
v0.6.4
Added
- Collections: two tables, eight routes, and the takedown (
backend/app/models/collection.py,backend/app/services/collections.py,backend/app/routers/collections.py,backend/app/services/event_filters.py,docs/api.md,docs/data-model.md) (#343). An analyst's named, curated set of their owngeolocatedanddetectedevents:collectionsandcollection_events, both foreign keys cascading so a GDPR erasure passes through. A collection carries two required free-text fields, a title capped like an event's and a shortdescriptionsaying what it holds, capped at 500 characters inschemas/collectionon the profile bio's figure. One validator covers the pair on the create and the update alike, so both are stripped of surrounding whitespace and a value of spaces is a 422 rather than a stored blank. Create, read, write both details in onePATCH, drop, page the items, put one on or take one off, plus the two list reads onGET /users/{username}/collectionsandGET /events/{id}/collections. Every write is owner-only, and an event joins its owner's collection alone, the one invariant the service carries rather than a SQL constraint. What a collection may show is one predicate,collectable_events, read by the item page, the count, the date range, the card mosaic and the add verb, so a row that closes or is taken down leaves all five at once with no write to the membership table. A collection stores no picture: thecoveron the read is up to four tiles taken off the items themselves in chronological order, graphic rows skipped and an image preferred over a clip, ranked for a whole page of collections in one windowed query. The items page on a cursor keyed onevent_date, event_time, created_at, id, undated last.PATCH /admin/collections/{id}/moderationmoves a reported collection along the reversiblehidden_atan event already carries, withholding it with{"hidden": true}and restoring it with{"hidden": false}, audited ascollection_hiddenandcollection_restored;DELETE /admin/collections/{id}stays as the takedown alias, and a withheld collection 404s for everyone but an admin. - Collections on the front end: the collection page, the profile section and the add-to-collection panel (
frontend/src/lib/collections.ts,frontend/src/app/collections/[id]/page.tsx,frontend/src/components/collections/,frontend/src/components/map/CoverageMap.tsx,frontend/src/app/profile/[username]/page.tsx,frontend/src/proxy.ts,docs/design.md) (#343). Three surfaces read or write one./collections/{id}opens on the title, the owner's byline, the meta line stating the count and the span, and the description under it at reading size, then the items on the shared Coverage map and the chronological list under it, both off one set so the pins and the rows cannot describe different collections. The profile card wears a mosaic of the collection's own first items, the way a playlist icon is made of the covers of what is on it: one tile fills the 16:9 slot, two split it, three stand the earliest tall on the left, four fill a 2x2, and a collection with nothing to show falls back to the site's one no-media box. Nothing is uploaded and nothing is stored, so a card cannot fall out of step with its shelf, and the page header carries no picture at all. The profile gains a Collections grid between Insights and Recent submissions, four cards with aShow morethat opens the analyst's whole shelf in/searchontype=collection&author=, the submissions list's link through the same builder; the grid is absent for a visitor with nothing to show and a first-run surface for its owner.GET /searchbrowses the collections group onauthoralone, so an empty query lists that analyst's visible, non-empty collections newest first. Every card carries the description clamped to two lines. The three writes that take a collection's details are one form, the title over the description with the sharedremaining / capcounter on each and a submit that refuses a blank or over-long value on either. The owner's two verbs ride the header cluster with their panels under it, the split the event page uses, and only dropping one is red, behind the two-click confirm, since the events it held stay as they were. An event's owner shelves it from a panel in the header's management tier whose rows toggle optimistically and roll back on a refusal./collectionsjoins the public prefixes inproxy.ts, so a shared link reads signed out like the events it holds, and the narrow-viewport suite gains the page as its owner at both phone widths. - Collections are a search result group (
backend/app/services/search.py,backend/app/schemas/search.py,backend/app/routers/search.py,backend/app/services/event_filters.py,backend/alembic/versions/q5s7u9w1y3a5_collections.py,frontend/src/app/search/page.tsx,frontend/src/components/collections/CollectionCard.tsx,docs/api.md,docs/design.md) (#343).GET /searchanswers with a fourth group. A query matches a collection's title and its description as one document, under a GIN index on the samesimpleconfiguration the events and users groups take, ranked byts_rankwithcreated_atdescending as the tie-break.type=collectionasks for the group alone andtype=allcarries it. A hit is the collection itself, the oneCollectionReadevery read surface renders, so a result opens on the same mosaic, count and date range the profile prints; a withheld collection, one whose owner is soft-deleted and one holding nothing showable are all absent, the terms a visitor already reads a profile on. The event filters empty the group, being predicates on an event, with one exception:authornames an analyst, which a collection carries, so it narrows the group to that owner's shelves. The search page gains a Collections chip between Events and Analysts and renders each hit as the profile's own card, with the byline that card hides on a profile. - Step through a collection on its own page, and a Description card for its description (
frontend/src/app/collections/[id]/page.tsx,frontend/src/components/collections/CollectionReader.tsx,frontend/src/components/collections/ReaderHeader.tsx,frontend/src/components/collections/CollectionItems.tsx,frontend/src/components/map/DetailSidePanel.tsx,frontend/src/components/map/Map.tsx,frontend/src/components/ui/EntityCard.tsx,docs/design.md) (#343). The collection page reads as three sections under its header: Description, which prints the description whole at reading size where it used to crowd the header; Coverage, now a player; and Events. The player is the map page's own two surfaces rearranged around a position, the shared map over every item's pin with the current one lit and the walked ones dimmed, and that item's event in the map's own detail panel beside it, under a header sayingN of Mwith the two moves. The panel gains aninlineplacement for it, a column of the page with no viewport insets, no bottom sheet and no height cap of its own, so the two sit side by side in a 32rem block fromsmup and stack below it with the step controls at the panel's top edge on the app's phone tap step. Nothing joins the pins: the order is said by the counter and by which pin is lit. The step rides the page's own URL as?step=, 1-based and clamped, moved withrouter.replaceso the back button leaves the page rather than walking back through every step, andArrowLeft/ArrowRightstep too unless a field has the caret. A click anywhere on a row of the Events list moves the player to it and lights that row, the newonSelect/selectedpair on the catalogue card, which keeps the way to the event's own page on the row's title. One cursor walk serves the pins, the panel and the rows, soN of Mcounts what the list shows. The narrow-viewport suite gains the player as a visitor at both phone widths. - Opening and changing a collection are pages of their own, and the profile card drops its type square (
frontend/src/app/collections/new/page.tsx,frontend/src/app/collections/[id]/edit/page.tsx,frontend/src/app/collections/[id]/page.tsx,frontend/src/components/collections/,frontend/src/lib/collections.ts,docs/design.md) (#343)./collections/newtakes the title and the description behind the wall, the way/submittakes an event, and/collections/{id}/edittakes the same two for a collection its owner already holds. Both are the one detai...
v0.6.3
Added
- @ViditBot opens a request for a coordinate-less mirror post (
backend/app/services/tweet_ingest/resolve.py,backend/app/services/tweet_ingest/urls.py,backend/app/services/detection.py,backend/app/services/events.py,backend/app/services/bot.py,backend/app/models/bot_mention.py,frontend/src/app/import/page.tsx,docs/ingestion.md,docs/data-model.md) (#342). Analysts who mirror a Telegram channel or another account's post, re-uploading the clip and linking the original, got a flatcoords_missingrefusal: the post carried everything a request needs except the geolocation, which is the one thing a request asks for. The engine now emits aRequestDraftbeside that refusal, read off the same source, media, title and date derivations the detections use, when the thread carries footage, it resolved a source, and no coordinate appears anywhere in the thread: a coordinate, own post or quoted post, is a geolocation, never a request. The source's host decides what gets fetched, never whether a request opens: an X status and a publict.mepost are chased, so the original's clip and post date fill the row, and a link on any other host, a YouTube video or a news page, opens a request over the analyst's own attached clip with the source's date left unknown. The bot writes it throughdetection.open_request, which calls the sameevents.create_requesta person's request goes through: the row is owned by and credited to the tagging analyst, stampedrequested_at, carrying the post it points at as itssource_urland the thread's footage as its onerole=sourcemedia, with no coordinate. It also carries the same five provenance columns a detection carries,detected_viareadingbot, stamped by one sharedevents.stamp_provenance. A coordinate-less re-tag, of the same post or of a repost of it, lands on that row and moves nothing, so the bot stays silent; a tag carrying the coordinate the analyst has since worked out is a geolocation, so it lands adetectedrow beside the open request, which stays its owner's to withdraw. A soft-deleted row never matches, so a takedown does not fence the owner off from mirroring that footage again. An X status and at.mepost are stored in one canonical spelling, so two spellings of one of them open one request; every other host is stored as the post wrote it. The footage is an ordered candidate list, the source's media then the analyst's own video, and the first that fetches fills the slot; a chase that failed transiently drafts nothing at all, so the next tag retries it. A clip the evidence intake refuses earns a newfootage_unusablerefusal in the reply rather than one naming a missing coordinate, and a mirror the branch can never serve, one pointing at a source with no footage to store, earns a newrequest_not_possibleone telling the analyst to attach the clip. The coordinate-less shapes a re-tag or a rewrite still answers keepcoords_missing, and so do the paste and the archive, which never ask for a request. The tag earns a linkless✅ Geolocation request opened · ref …reply under the same per-hour and per-author budget every other reply spends, and the mention is ledgeredrequested. Nothing else moves: the refusal still travels with the draft, so the paste and the archive, which read detections alone, answer exactly what they did. The reply carries the two warnings a request can earn,source_date_unknownandduplicate_media, read off the same_write_warningspass the detections run; a request is always born with footage, the analyst's own copy filling the slot when the chase served nothing, sosource_fetch_failednever reaches one. The admin reject rate (backend/app/services/admin.py) now reads oneEvent.is_machine_detectionpredicate on the model, an imported row that is not a request (requested_atNULL), so a bot-opened request never enters the cohort; the admin panel copy and the event page's provenance row, which reads Requested from on a request, say the same. No migration, sincebot_mentions.outcomecarries no database domain. - An open request is edited by its owner, and listed on their profile (
backend/app/services/events.py,backend/app/routers/events/item.py,frontend/src/app/events/[id]/edit/page.tsx,frontend/src/components/geolocations/edit/RequestEditForm.tsx,frontend/src/components/event/useEventActions.tsx,frontend/src/components/profile/OpenRequests.tsx,frontend/src/lib/events.ts,docs/api.md,docs/data-model.md,docs/ingestion.md) (#342). The bot opens requests on the analyst's behalf and answers Edit it from your profile, and neither half of that was reachable: the owner of arequestedrow could view it, answer it through the submit form or close it, and the edit address turned every other status away, while the profile listed published work and machine detections and nothing else.POST /events/{id}/requestis the owner's correction of an open request, owner-only andrequested-only, taking the create form's fields whole on the same rules: the coordinate guess and the camera point stay optional and both-or-neither, the curated floor stays unenforced until the geolocate, the mirrors and their archived copies replace what the row held, and the source media moves on theremove_media_ids+filespair the published writes take, under the same one-source cap and the same source floor. No version is filed: a version supersedes a vouched claim and a request is a question, so the row is overwritten in place and keeps its id, itsrequested_at, its requester and its provenance columns.source_posted_atis optional on this path alone, since the bot opens requests whose source date it could not read and the owner must not have to invent one. The write takes the same row lock the geolocate, the version and the close take, so an edit racing a fulfilment or a withdrawal serializes with it and the loser sees the 409. The edit address now serves three shapes, a request being the third, and the request page and the event page carry the owner's pencil beside the close. Open requests joins the profile under the submissions, built from the same compact cards, reading the public requested view filtered to that analyst and expanding into search. ThePOST /events/requestsreference rows that claimed the proof body was image-free and thatevent_timeneededevent_dateare corrected to what the endpoint does.
Changed
- The engine's warning and refusal sentences say what to do next (
backend/app/services/tweet_ingest/resolve.py,backend/app/services/bot.py,frontend/src/app/import/page.tsx,docs/ingestion.md) (#342). One table words every code for the three surfaces that show it, the bot's reply, the archive's outcome email and the paste's response, so the rewording reaches all three at once:source_missingnames the link that is absent rather than the search that failed, the two footage codes say which half came back empty,source_date_unknownandduplicate_mediaread as the row's state, andcoords_invalidgives the bounds a coordinate has to sit in. The bot's request reply opens✅ Geolocation request openedrather than restating what it did not find.
Fixed
- A reply that only inherits the tag from its parent is not a mention (
backend/app/services/tweet_ingest/extract.py,backend/app/services/bot.py,backend/app/services/x_api.py,backend/app/routers/webhooks.py,backend/app/models/bot_mention.py,frontend/src/app/import/page.tsx,docs/ingestion.md,docs/data-model.md) (#342). X opens a reply with the parent's author and the parent's mentions minus the replier, so an analyst answering a colleague whose post tagged the bot mentioned the bot without typing a character. That reached the bot on both deliveries, and since the thread under it is the replier's own and carries no coordinate, a linked analyst got a ❌ No coordinate in the post on somebody else's thread for a tag nobody made. A mention now counts as a tag only where its author typed it: on a post that is not a reply every@ViditBotis typed, and on a reply one sitting past the leading run of mentions is, the dot-mention.@ViditBotincluded, since the period is a character X never writes into a prefix. A reply whose only@ViditBotsits inside that run is settled by its parent, read once through syndication, the free path: a parent that mentions the bot, the bot's own post included, is where the tag came from, so nothing is acquired, nothing is answered and the mention is ledgeredinherited. A parent that mentions the bot nowhere leaves the tag typed, so the bare@ViditBotunder the analyst's own coordinate post and the climb it opens are untouched, and a p...